os_info/
version.rs

1use std::fmt::{self, Display, Formatter};
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6/// Operating system version.
7#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
8#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9pub enum Version {
10    /// Unknown version.
11    Unknown,
12    /// Semantic version (major.minor.patch).
13    Semantic(u64, u64, u64),
14    /// Rolling version. Optionally contains the release date in the string format.
15    Rolling(Option<String>),
16    /// Custom version format.
17    Custom(String),
18}
19
20impl Version {
21    /// Constructs `VersionType` from the given string.
22    ///
23    /// Returns `VersionType::Unknown` if the string is empty. If it can be parsed as a semantic
24    /// version, then `VersionType::Semantic`, otherwise `VersionType::Custom`.
25    ///
26    /// # Examples
27    ///
28    /// ```
29    /// use os_info::Version;
30    ///
31    /// let v = Version::from_string("custom");
32    /// assert_eq!(Version::Custom("custom".to_owned()), v);
33    ///
34    /// let v = Version::from_string("1.2.3");
35    /// assert_eq!(Version::Semantic(1, 2, 3), v);
36    /// ```
37    pub fn from_string<S: Into<String> + AsRef<str>>(s: S) -> Self {
38        if s.as_ref().is_empty() {
39            Self::Unknown
40        } else if let Some((major, minor, patch)) = parse_version(s.as_ref()) {
41            Self::Semantic(major, minor, patch)
42        } else {
43            Self::Custom(s.into())
44        }
45    }
46}
47
48impl Default for Version {
49    fn default() -> Self {
50        Version::Unknown
51    }
52}
53
54impl Display for Version {
55    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
56        match *self {
57            Self::Unknown => f.write_str("Unknown"),
58            Self::Semantic(major, minor, patch) => write!(f, "{major}.{minor}.{patch}"),
59            Self::Rolling(ref date) => {
60                let date = match date {
61                    Some(date) => format!(" ({date})"),
62                    None => "".to_owned(),
63                };
64                write!(f, "Rolling Release{date}")
65            }
66            Self::Custom(ref version) => write!(f, "{version}"),
67        }
68    }
69}
70
71fn parse_version(s: &str) -> Option<(u64, u64, u64)> {
72    let mut iter = s.trim().split_terminator('.').fuse();
73
74    let major = iter.next().and_then(|s| s.parse().ok())?;
75    let minor = iter.next().unwrap_or("0").parse().ok()?;
76    let patch = iter.next().unwrap_or("0").parse().ok()?;
77
78    if iter.next().is_some() {
79        return None;
80    }
81
82    Some((major, minor, patch))
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use pretty_assertions::assert_eq;
89
90    #[test]
91    fn parse_semantic_version() {
92        let data = [
93            ("", None),
94            ("version", None),
95            ("1", Some((1, 0, 0))),
96            ("1.", Some((1, 0, 0))),
97            ("1.2", Some((1, 2, 0))),
98            ("1.2.", Some((1, 2, 0))),
99            ("1.2.3", Some((1, 2, 3))),
100            ("1.2.3.", Some((1, 2, 3))),
101            ("1.2.3.  ", Some((1, 2, 3))),
102            ("   1.2.3.", Some((1, 2, 3))),
103            ("   1.2.3.  ", Some((1, 2, 3))),
104            ("1.2.3.4", None),
105            ("1.2.3.4.5.6.7.8.9", None),
106        ];
107
108        for (s, expected) in &data {
109            let result = parse_version(s);
110            assert_eq!(expected, &result);
111        }
112    }
113
114    #[test]
115    fn from_string() {
116        let custom_version = "some version";
117        let data = [
118            ("", Version::Unknown),
119            ("1.2.3", Version::Semantic(1, 2, 3)),
120            (custom_version, Version::Custom(custom_version.to_owned())),
121        ];
122
123        for (s, expected) in &data {
124            let version = Version::from_string(*s);
125            assert_eq!(expected, &version);
126        }
127    }
128
129    #[test]
130    fn default() {
131        assert_eq!(Version::Unknown, Version::default());
132    }
133
134    #[test]
135    fn display() {
136        let data = [
137            (Version::Unknown, "Unknown"),
138            (Version::Semantic(1, 5, 0), "1.5.0"),
139            (Version::Rolling(None), "Rolling Release"),
140            (
141                Version::Rolling(Some("date".to_owned())),
142                "Rolling Release (date)",
143            ),
144        ];
145
146        for (version, expected) in &data {
147            assert_eq!(expected, &version.to_string());
148        }
149    }
150}