| // 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: {0}")] | |
| OpenFile(#[from] io_util::node::OpenError), | |
| #[error("while reading the file: {0}")] | |
| ReadFile(#[from] io_util::file::ReadError), | |
| #[error("while deserializing the json: {0}")] | |
| Deserialize(#[from] 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?).await?; | |
| // Parse the json string to extract UpdateMode. | |
| match serde_json::from_str(&contents)? { | |
| 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(_))); | |
| } | |
| } |