update_package/
update_mode.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Typesafe wrappers around parsing the update-mode file.
6
7use fidl_fuchsia_io as fio;
8use serde::{Deserialize, Serialize};
9use std::str::FromStr;
10use thiserror::Error;
11use zx_status::Status;
12
13/// An error encountered while parsing the update-mode file.
14#[derive(Debug, Error)]
15#[allow(missing_docs)]
16pub enum ParseUpdateModeError {
17    #[error("while opening the file")]
18    OpenFile(#[source] fuchsia_fs::node::OpenError),
19
20    #[error("while reading the file")]
21    ReadFile(#[source] fuchsia_fs::file::ReadError),
22
23    #[error("while deserializing: '{0:?}'")]
24    Deserialize(String, #[source] serde_json::Error),
25
26    #[error("update mode not supported: '{0}'")]
27    UpdateModeNotSupported(String),
28}
29
30/// Wrapper for deserializing the update-mode file.
31#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
32#[serde(tag = "version", content = "content", deny_unknown_fields)]
33enum UpdateModeFile {
34    #[serde(rename = "1")]
35    Version1 {
36        // We make this a String and not an UpdateMode so that we can seperate
37        // the unsupported mode errors from other json deserialization errors.
38        // For example,this would be considered valid json with an unsupported mode:
39        // { version: "1", content: { "mode" : "banana" } }
40        #[serde(rename = "mode")]
41        update_mode: String,
42    },
43}
44
45/// Enum to describe the supported update modes.
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub enum UpdateMode {
48    /// Follow the normal system update flow.
49    Normal,
50    /// Instead of the normal flow, write a recovery image and reboot into it.
51    ForceRecovery,
52}
53
54impl std::default::Default for UpdateMode {
55    fn default() -> Self {
56        Self::Normal
57    }
58}
59
60impl FromStr for UpdateMode {
61    type Err = ParseUpdateModeError;
62
63    fn from_str(s: &str) -> Result<Self, Self::Err> {
64        match s {
65            "normal" => Ok(UpdateMode::Normal),
66            "force-recovery" => Ok(UpdateMode::ForceRecovery),
67            other => Err(ParseUpdateModeError::UpdateModeNotSupported(other.to_string())),
68        }
69    }
70}
71
72pub(crate) async fn update_mode(
73    proxy: &fio::DirectoryProxy,
74) -> Result<Option<UpdateMode>, ParseUpdateModeError> {
75    // Open the update-mode file.
76    let fopen_res =
77        fuchsia_fs::directory::open_file(proxy, "update-mode", fio::PERM_READABLE).await;
78    if let Err(fuchsia_fs::node::OpenError::OpenError(Status::NOT_FOUND)) = fopen_res {
79        return Ok(None);
80    }
81
82    // Read the update-mode file.
83    let contents =
84        fuchsia_fs::file::read_to_string(&fopen_res.map_err(ParseUpdateModeError::OpenFile)?)
85            .await
86            .map_err(ParseUpdateModeError::ReadFile)?;
87
88    // Parse the json string to extract UpdateMode.
89    match serde_json::from_str(&contents)
90        .map_err(|e| ParseUpdateModeError::Deserialize(contents, e))?
91    {
92        UpdateModeFile::Version1 { update_mode } => update_mode.parse().map(Some),
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::TestUpdatePackage;
100    use assert_matches::assert_matches;
101    use fuchsia_async as fasync;
102    use proptest::prelude::*;
103    use serde_json::json;
104
105    fn valid_update_mode_json_value(mode: &str) -> serde_json::Value {
106        json!({
107            "version": "1",
108            "content": {
109                "mode": mode,
110            },
111        })
112    }
113
114    fn valid_update_mode_json_string(mode: &str) -> String {
115        valid_update_mode_json_value(mode).to_string()
116    }
117
118    proptest! {
119        #[test]
120        fn test_json_serialize_roundtrip(s in ".+") {
121            // Generate json and show that it successfully deserializes into the wrapper object.
122            let starting_json_value = valid_update_mode_json_value(&s);
123            let deserialized_object: UpdateModeFile =
124                serde_json::from_value(starting_json_value.clone())
125                    .expect("json to deserialize");
126            assert_eq!(deserialized_object, UpdateModeFile::Version1 { update_mode: s });
127
128            // Serialize back into serde_json::Value object & show we get same json we started with.
129            // Note: even though serialize generally means "convert to string", in this case we're
130            // serializing to a serde_json::Value to ignore ordering when we check equality.
131            let final_json_value =
132                serde_json::to_value(&deserialized_object)
133                    .expect("serialize to value");
134            assert_eq!(final_json_value, starting_json_value);
135        }
136    }
137
138    #[fasync::run_singlethreaded(test)]
139    async fn parse_update_mode_success_normal() {
140        let p = TestUpdatePackage::new()
141            .add_file("update-mode", &valid_update_mode_json_string("normal"))
142            .await;
143        assert_matches!(p.update_mode().await, Ok(Some(UpdateMode::Normal)));
144    }
145
146    #[fasync::run_singlethreaded(test)]
147    async fn parse_update_mode_success_force_recovery() {
148        let p = TestUpdatePackage::new()
149            .add_file("update-mode", &valid_update_mode_json_string("force-recovery"))
150            .await;
151        assert_matches!(p.update_mode().await, Ok(Some(UpdateMode::ForceRecovery)));
152    }
153
154    #[fasync::run_singlethreaded(test)]
155    async fn parse_update_mode_success_missing_update_mode_file() {
156        let p = TestUpdatePackage::new();
157        assert_matches!(p.update_mode().await, Ok(None));
158    }
159
160    #[fasync::run_singlethreaded(test)]
161    async fn parse_update_mode_fail_unsupported_mode() {
162        let p = TestUpdatePackage::new()
163            .add_file("update-mode", &valid_update_mode_json_string("potato"))
164            .await;
165        assert_matches!(
166            p.update_mode().await,
167            Err(ParseUpdateModeError::UpdateModeNotSupported(mode)) if mode=="potato"
168        );
169    }
170
171    #[fasync::run_singlethreaded(test)]
172    async fn parse_update_mode_fail_deserialize() {
173        let p = TestUpdatePackage::new().add_file("update-mode", "oh no! this isn't json.").await;
174        assert_matches!(
175            p.update_mode().await,
176            Err(ParseUpdateModeError::Deserialize(s,_)) if s == "oh no! this isn't json."
177        );
178    }
179}