update_package/
version.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 reading the version file.
6
7use fidl_fuchsia_io as fio;
8use omaha_client::version::Version as SemanticVersion;
9use serde::de::{self, Visitor};
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11use std::convert::Infallible;
12use std::fmt;
13use std::str::FromStr;
14use thiserror::Error;
15
16/// An error encountered while reading the version.
17#[derive(Debug, Error)]
18#[allow(missing_docs)]
19pub enum ReadVersionError {
20    #[error("while opening the file")]
21    OpenFile(#[source] fuchsia_fs::node::OpenError),
22
23    #[error("while reading the file")]
24    ReadFile(#[source] fuchsia_fs::file::ReadError),
25}
26
27struct SystemVersionVisitor;
28
29impl Visitor<'_> for SystemVersionVisitor {
30    type Value = SystemVersion;
31
32    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
33        formatter.write_str("a string")
34    }
35
36    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
37    where
38        E: de::Error,
39    {
40        Ok(SystemVersion::from_str(v).unwrap())
41    }
42}
43
44#[derive(Clone, Debug, Eq, PartialEq)]
45/// Represents the version of an update package.
46pub enum SystemVersion {
47    /// An unrecognised version format.
48    Opaque(String),
49    /// A version of the format "a.b.c.d".
50    Semantic(SemanticVersion),
51}
52
53impl SystemVersion {
54    /// Returns true if this SystemVersion is an empty string.
55    pub fn is_empty(&self) -> bool {
56        if let SystemVersion::Opaque(value) = self {
57            value.is_empty()
58        } else {
59            false
60        }
61    }
62}
63
64impl PartialOrd for SystemVersion {
65    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
66        let my_version = match &self {
67            SystemVersion::Opaque(_) => return None,
68            SystemVersion::Semantic(ver) => ver,
69        };
70
71        let other_version = match other {
72            SystemVersion::Opaque(_) => return None,
73            SystemVersion::Semantic(ver) => ver,
74        };
75
76        my_version.partial_cmp(other_version)
77    }
78}
79
80// We have manual implementations of deserialize/serialize so that we can parse simple strings.
81impl<'de> Deserialize<'de> for SystemVersion {
82    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
83    where
84        D: Deserializer<'de>,
85    {
86        deserializer.deserialize_str(SystemVersionVisitor)
87    }
88}
89
90impl Serialize for SystemVersion {
91    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
92    where
93        S: Serializer,
94    {
95        match self {
96            SystemVersion::Semantic(ref version) => serializer.serialize_str(&version.to_string()),
97            SystemVersion::Opaque(ref string) => serializer.serialize_str(string),
98        }
99    }
100}
101
102impl FromStr for SystemVersion {
103    type Err = Infallible;
104
105    fn from_str(s: &str) -> Result<Self, Self::Err> {
106        let result = match SemanticVersion::from_str(s) {
107            Ok(version) => SystemVersion::Semantic(version),
108            Err(_) => SystemVersion::Opaque(s.trim_end().to_owned()),
109        };
110
111        Ok(result)
112    }
113}
114
115impl fmt::Display for SystemVersion {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        match self {
118            SystemVersion::Opaque(ref string) => f.write_str(string),
119            SystemVersion::Semantic(ref version) => version.fmt(f),
120        }
121    }
122}
123
124pub(crate) async fn read_version(
125    proxy: &fio::DirectoryProxy,
126) -> Result<SystemVersion, ReadVersionError> {
127    let file = fuchsia_fs::directory::open_file(proxy, "version", fio::PERM_READABLE)
128        .await
129        .map_err(ReadVersionError::OpenFile)?;
130    let version_str =
131        fuchsia_fs::file::read_to_string(&file).await.map_err(ReadVersionError::ReadFile)?;
132
133    Ok(SystemVersion::from_str(&version_str).unwrap())
134}
135
136#[cfg(test)]
137#[allow(clippy::bool_assert_comparison)]
138mod tests {
139    use super::*;
140    use crate::TestUpdatePackage;
141    use assert_matches::assert_matches;
142    use fuchsia_async as fasync;
143
144    #[fasync::run_singlethreaded(test)]
145    async fn read_version_success_file_exists() {
146        let p = TestUpdatePackage::new().add_file("version", "123").await;
147        assert_eq!(
148            p.version().await.unwrap(),
149            SystemVersion::Semantic(SemanticVersion::from([123]))
150        );
151    }
152
153    #[fasync::run_singlethreaded(test)]
154    async fn read_version_success_opaque() {
155        let p = TestUpdatePackage::new().add_file("version", "2020-09-08T10:17:00+10:00").await;
156        assert_eq!(
157            p.version().await.unwrap(),
158            SystemVersion::Opaque("2020-09-08T10:17:00+10:00".to_owned())
159        );
160    }
161
162    #[fasync::run_singlethreaded(test)]
163    async fn read_version_trims_trailing_whitespace() {
164        let p = TestUpdatePackage::new().add_file("version", "2020-09-08T10:17:00+10:00\n").await;
165        assert_eq!(
166            p.version().await.unwrap(),
167            SystemVersion::Opaque("2020-09-08T10:17:00+10:00".to_owned())
168        );
169    }
170
171    #[fasync::run_singlethreaded(test)]
172    async fn read_version_fail_file_does_not_exist() {
173        let p = TestUpdatePackage::new();
174        assert_matches!(read_version(p.proxy()).await, Err(ReadVersionError::OpenFile(_)));
175    }
176
177    #[test]
178    fn test_deserialize_version() {
179        let version: SystemVersion =
180            serde_json::from_str(r#""not a real version number""#).unwrap();
181        assert_eq!(version, SystemVersion::Opaque("not a real version number".to_owned()));
182        let version: SystemVersion = serde_json::from_str(r#""1.2.3""#).unwrap();
183        assert_eq!(version, SystemVersion::Semantic(SemanticVersion::from([1, 2, 3])));
184    }
185
186    #[test]
187    fn test_serialize_version() {
188        let version = SystemVersion::Opaque("not a real version number".to_owned());
189        assert_eq!(
190            serde_json::to_string(&version).unwrap(),
191            r#""not a real version number""#.to_owned()
192        );
193        let version = SystemVersion::Semantic(SemanticVersion::from([1, 3, 4, 5]));
194        assert_eq!(serde_json::to_string(&version).unwrap(), r#""1.3.4.5""#.to_owned());
195    }
196
197    #[test]
198    fn test_version_order_both_opaque() {
199        // We don't attempt to assign ordering to opaque versions.
200        let a = SystemVersion::Opaque("version 1".to_string());
201        let b = SystemVersion::Opaque("another version".to_string());
202        assert_eq!(a < b, false);
203        assert_eq!(a > b, false);
204        assert_eq!(a >= b, false);
205        assert_eq!(a <= b, false);
206    }
207
208    #[test]
209    fn test_version_order_one_opaque() {
210        let a = SystemVersion::Opaque("opaque".to_string());
211        let b = SystemVersion::Semantic(SemanticVersion::from([1, 2, 3, 4]));
212        assert_eq!(a < b, false);
213        assert_eq!(a > b, false);
214        assert_eq!(a >= b, false);
215        assert_eq!(a <= b, false);
216        assert_eq!(b < a, false);
217        assert_eq!(b > a, false);
218        assert_eq!(b >= a, false);
219        assert_eq!(b <= a, false);
220    }
221
222    #[test]
223    fn test_version_order_both_semantic() {
224        let a = SystemVersion::Semantic(SemanticVersion::from([1, 2, 3, 4]));
225        let b = SystemVersion::Semantic(SemanticVersion::from([1, 2, 3, 5]));
226
227        assert_eq!(a < b, true);
228        assert_eq!(a <= b, true);
229        assert_eq!(a > b, false);
230        assert_eq!(a >= b, false);
231        assert_eq!(b < a, false);
232        assert_eq!(b <= a, false);
233        assert_eq!(b >= a, true);
234        assert_eq!(b > a, true);
235    }
236}