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 { value.is_empty() } else { false }
57    }
58}
59
60impl PartialOrd for SystemVersion {
61    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
62        let my_version = match &self {
63            SystemVersion::Opaque(_) => return None,
64            SystemVersion::Semantic(ver) => ver,
65        };
66
67        let other_version = match other {
68            SystemVersion::Opaque(_) => return None,
69            SystemVersion::Semantic(ver) => ver,
70        };
71
72        my_version.partial_cmp(other_version)
73    }
74}
75
76// We have manual implementations of deserialize/serialize so that we can parse simple strings.
77impl<'de> Deserialize<'de> for SystemVersion {
78    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
79    where
80        D: Deserializer<'de>,
81    {
82        deserializer.deserialize_str(SystemVersionVisitor)
83    }
84}
85
86impl Serialize for SystemVersion {
87    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
88    where
89        S: Serializer,
90    {
91        match self {
92            SystemVersion::Semantic(version) => serializer.serialize_str(&version.to_string()),
93            SystemVersion::Opaque(string) => serializer.serialize_str(string),
94        }
95    }
96}
97
98impl FromStr for SystemVersion {
99    type Err = Infallible;
100
101    fn from_str(s: &str) -> Result<Self, Self::Err> {
102        let result = match SemanticVersion::from_str(s) {
103            Ok(version) => SystemVersion::Semantic(version),
104            Err(_) => SystemVersion::Opaque(s.trim_end().to_owned()),
105        };
106
107        Ok(result)
108    }
109}
110
111impl fmt::Display for SystemVersion {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        match self {
114            SystemVersion::Opaque(string) => f.write_str(string),
115            SystemVersion::Semantic(version) => version.fmt(f),
116        }
117    }
118}
119
120pub(crate) async fn read_version(
121    proxy: &fio::DirectoryProxy,
122) -> Result<SystemVersion, ReadVersionError> {
123    let file = fuchsia_fs::directory::open_file(proxy, "version", fio::PERM_READABLE)
124        .await
125        .map_err(ReadVersionError::OpenFile)?;
126    let version_str =
127        fuchsia_fs::file::read_to_string(&file).await.map_err(ReadVersionError::ReadFile)?;
128
129    Ok(SystemVersion::from_str(&version_str).unwrap())
130}
131
132#[cfg(test)]
133#[allow(clippy::bool_assert_comparison)]
134mod tests {
135    use super::*;
136    use crate::TestUpdatePackage;
137    use assert_matches::assert_matches;
138    use fuchsia_async as fasync;
139
140    #[fasync::run_singlethreaded(test)]
141    async fn read_version_success_file_exists() {
142        let p = TestUpdatePackage::new().add_file("version", "123").await;
143        assert_eq!(
144            p.version().await.unwrap(),
145            SystemVersion::Semantic(SemanticVersion::from([123]))
146        );
147    }
148
149    #[fasync::run_singlethreaded(test)]
150    async fn read_version_success_opaque() {
151        let p = TestUpdatePackage::new().add_file("version", "2020-09-08T10:17:00+10:00").await;
152        assert_eq!(
153            p.version().await.unwrap(),
154            SystemVersion::Opaque("2020-09-08T10:17:00+10:00".to_owned())
155        );
156    }
157
158    #[fasync::run_singlethreaded(test)]
159    async fn read_version_trims_trailing_whitespace() {
160        let p = TestUpdatePackage::new().add_file("version", "2020-09-08T10:17:00+10:00\n").await;
161        assert_eq!(
162            p.version().await.unwrap(),
163            SystemVersion::Opaque("2020-09-08T10:17:00+10:00".to_owned())
164        );
165    }
166
167    #[fasync::run_singlethreaded(test)]
168    async fn read_version_fail_file_does_not_exist() {
169        let p = TestUpdatePackage::new();
170        assert_matches!(read_version(p.proxy()).await, Err(ReadVersionError::OpenFile(_)));
171    }
172
173    #[test]
174    fn test_deserialize_version() {
175        let version: SystemVersion =
176            serde_json::from_str(r#""not a real version number""#).unwrap();
177        assert_eq!(version, SystemVersion::Opaque("not a real version number".to_owned()));
178        let version: SystemVersion = serde_json::from_str(r#""1.2.3""#).unwrap();
179        assert_eq!(version, SystemVersion::Semantic(SemanticVersion::from([1, 2, 3])));
180    }
181
182    #[test]
183    fn test_serialize_version() {
184        let version = SystemVersion::Opaque("not a real version number".to_owned());
185        assert_eq!(
186            serde_json::to_string(&version).unwrap(),
187            r#""not a real version number""#.to_owned()
188        );
189        let version = SystemVersion::Semantic(SemanticVersion::from([1, 3, 4, 5]));
190        assert_eq!(serde_json::to_string(&version).unwrap(), r#""1.3.4.5""#.to_owned());
191    }
192
193    #[test]
194    fn test_version_order_both_opaque() {
195        // We don't attempt to assign ordering to opaque versions.
196        let a = SystemVersion::Opaque("version 1".to_string());
197        let b = SystemVersion::Opaque("another version".to_string());
198        assert_eq!(a < b, false);
199        assert_eq!(a > b, false);
200        assert_eq!(a >= b, false);
201        assert_eq!(a <= b, false);
202    }
203
204    #[test]
205    fn test_version_order_one_opaque() {
206        let a = SystemVersion::Opaque("opaque".to_string());
207        let b = SystemVersion::Semantic(SemanticVersion::from([1, 2, 3, 4]));
208        assert_eq!(a < b, false);
209        assert_eq!(a > b, false);
210        assert_eq!(a >= b, false);
211        assert_eq!(a <= b, false);
212        assert_eq!(b < a, false);
213        assert_eq!(b > a, false);
214        assert_eq!(b >= a, false);
215        assert_eq!(b <= a, false);
216    }
217
218    #[test]
219    fn test_version_order_both_semantic() {
220        let a = SystemVersion::Semantic(SemanticVersion::from([1, 2, 3, 4]));
221        let b = SystemVersion::Semantic(SemanticVersion::from([1, 2, 3, 5]));
222
223        assert_eq!(a < b, true);
224        assert_eq!(a <= b, true);
225        assert_eq!(a > b, false);
226        assert_eq!(a >= b, false);
227        assert_eq!(b < a, false);
228        assert_eq!(b <= a, false);
229        assert_eq!(b >= a, true);
230        assert_eq!(b > a, true);
231    }
232}