blob: b546038e16a2da3c8f86c2aebc80ba0287c8fc01 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Typesafe wrappers around parsing the update-mode file.
use {
fidl_fuchsia_io::DirectoryProxy,
fuchsia_zircon::Status,
serde::{Deserialize, Serialize},
std::str::FromStr,
thiserror::Error,
};
/// An error encountered while parsing the update-mode file.
#[derive(Debug, Error)]
#[allow(missing_docs)]
pub enum ParseUpdateModeError {
#[error("while opening the file")]
OpenFile(#[source] io_util::node::OpenError),
#[error("while reading the file")]
ReadFile(#[source] io_util::file::ReadError),
#[error("while deserializing: '{0:?}'")]
Deserialize(String, #[source] serde_json::Error),
#[error("update mode not supported: '{0}'")]
UpdateModeNotSupported(String),
}
/// Wrapper for deserializing the update-mode file.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(tag = "version", content = "content", deny_unknown_fields)]
enum UpdateModeFile {
#[serde(rename = "1")]
Version1 {
// We make this a String and not an UpdateMode so that we can seperate
// the unsupported mode errors from other json deserialization errors.
// For example,this would be considered valid json with an unsupported mode:
// { version: "1", content: { "mode" : "banana" } }
#[serde(rename = "mode")]
update_mode: String,
},
}
/// Enum to describe the supported update modes.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UpdateMode {
/// Follow the normal system update flow.
Normal,
/// Instead of the normal flow, write a recovery image and reboot into it.
ForceRecovery,
}
impl std::default::Default for UpdateMode {
fn default() -> Self {
Self::Normal
}
}
impl FromStr for UpdateMode {
type Err = ParseUpdateModeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"normal" => Ok(UpdateMode::Normal),
"force-recovery" => Ok(UpdateMode::ForceRecovery),
other => Err(ParseUpdateModeError::UpdateModeNotSupported(other.to_string())),
}
}
}
pub(crate) async fn update_mode(
proxy: &DirectoryProxy,
) -> Result<Option<UpdateMode>, ParseUpdateModeError> {
// Open the update-mode file.
let fopen_res =
io_util::directory::open_file(proxy, "update-mode", fidl_fuchsia_io::OPEN_RIGHT_READABLE)
.await;
if let Err(io_util::node::OpenError::OpenError(Status::NOT_FOUND)) = fopen_res {
return Ok(None);
}
// Read the update-mode file.
let contents =
io_util::file::read_to_string(&fopen_res.map_err(ParseUpdateModeError::OpenFile)?)
.await
.map_err(ParseUpdateModeError::ReadFile)?;
// Parse the json string to extract UpdateMode.
match serde_json::from_str(&contents)
.map_err(|e| ParseUpdateModeError::Deserialize(contents, e))?
{
UpdateModeFile::Version1 { update_mode } => update_mode.parse().map(Some),
}
}
#[cfg(test)]
mod tests {
use {
super::*, crate::TestUpdatePackage, fuchsia_async as fasync, matches::assert_matches,
proptest::prelude::*, serde_json::json,
};
fn valid_update_mode_json_value(mode: &str) -> serde_json::Value {
json!({
"version": "1",
"content": {
"mode": mode,
},
})
}
fn valid_update_mode_json_string(mode: &str) -> String {
valid_update_mode_json_value(mode).to_string()
}
proptest! {
#[test]
fn test_json_serialize_roundtrip(s in ".+") {
// Generate json and show that it successfully deserializes into the wrapper object.
let starting_json_value = valid_update_mode_json_value(&s);
let deserialized_object: UpdateModeFile =
serde_json::from_value(starting_json_value.clone())
.expect("json to deserialize");
assert_eq!(deserialized_object, UpdateModeFile::Version1 { update_mode: s.clone() });
// Serialize back into serde_json::Value object & show we get same json we started with.
// Note: even though serialize generally means "convert to string", in this case we're
// serializing to a serde_json::Value to ignore ordering when we check equality.
let final_json_value =
serde_json::to_value(&deserialized_object)
.expect("serialize to value");
assert_eq!(final_json_value, starting_json_value);
}
}
#[fasync::run_singlethreaded(test)]
async fn parse_update_mode_success_normal() {
let p = TestUpdatePackage::new()
.add_file("update-mode", &valid_update_mode_json_string("normal"))
.await;
assert_matches!(p.update_mode().await, Ok(Some(UpdateMode::Normal)));
}
#[fasync::run_singlethreaded(test)]
async fn parse_update_mode_success_force_recovery() {
let p = TestUpdatePackage::new()
.add_file("update-mode", &valid_update_mode_json_string("force-recovery"))
.await;
assert_matches!(p.update_mode().await, Ok(Some(UpdateMode::ForceRecovery)));
}
#[fasync::run_singlethreaded(test)]
async fn parse_update_mode_success_missing_update_mode_file() {
let p = TestUpdatePackage::new();
assert_matches!(p.update_mode().await, Ok(None));
}
#[fasync::run_singlethreaded(test)]
async fn parse_update_mode_fail_unsupported_mode() {
let p = TestUpdatePackage::new()
.add_file("update-mode", &valid_update_mode_json_string("potato"))
.await;
assert_matches!(
p.update_mode().await,
Err(ParseUpdateModeError::UpdateModeNotSupported(mode)) if mode=="potato"
);
}
#[fasync::run_singlethreaded(test)]
async fn parse_update_mode_fail_deserialize() {
let p = TestUpdatePackage::new().add_file("update-mode", "oh no! this isn't json.").await;
assert_matches!(
p.update_mode().await,
Err(ParseUpdateModeError::Deserialize(s,_)) if s == "oh no! this isn't json."
);
}
}