ubuntu_version/
version.rs

1use chrono::{Datelike, Utc};
2use os_release::OsRelease;
3use std::{
4    fmt::{self, Display, Formatter},
5    io,
6    str::FromStr,
7};
8
9#[derive(Debug, Error)]
10pub enum VersionError {
11    #[error("failed to fetch /etc/os-release: {}", _0)]
12    OsRelease(io::Error),
13    #[error("version parsing error: {}", _0)]
14    Parse(VersionParseError),
15}
16
17#[derive(Debug, Error)]
18pub enum VersionParseError {
19    #[error("release version component was not a number: found {}", _0)]
20    VersionNaN(String),
21    #[error("invalid minor release version: expected 4 or 10, found {}", _0)]
22    InvalidMinorVersion(u8),
23    #[error("major version does not exist")]
24    NoMajor,
25    #[error("minor version does not exist")]
26    NoMinor,
27    #[error("release version is empty")]
28    NoVersion,
29}
30
31/// The version of an Ubuntu release, which is based on the date of release.
32#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
33pub struct Version {
34    pub major: u8,
35    pub minor: u8,
36    pub patch: u8,
37}
38
39impl Version {
40    /// Reads the `/etc/os-release` file and determines which version of Ubuntu is in use.
41    pub fn detect() -> Result<Self, VersionError> {
42        let release = OsRelease::new().map_err(VersionError::OsRelease)?;
43        release
44            .version
45            .parse::<Version>()
46            .map_err(VersionError::Parse)
47    }
48
49    /// Returns `true` if this is a LTS release.
50    pub fn is_lts(self) -> bool {
51        self.major % 2 == 0 && self.minor == 4
52    }
53
54    /// The number of months that have passed since this version was released.
55    pub fn months_since(self) -> i32 {
56        let today = Utc::today();
57
58        let major = 2000 - today.year() as u32;
59        let minor = today.month() as u32;
60
61        months_since(self, major, minor)
62    }
63
64    /// Increments the major / minor version to the next expected release version.
65    pub fn next_release(self) -> Self {
66        let (major, minor) = if self.minor == 10 {
67            (self.major + 1, 4)
68        } else {
69            (self.major, 10)
70        };
71
72        Version {
73            major,
74            minor,
75            patch: 0,
76        }
77    }
78}
79
80impl Display for Version {
81    fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
82        write!(fmt, "{}.{:02}", self.major, self.minor)?;
83
84        if self.patch != 0 {
85            write!(fmt, "{}", self.patch)?;
86        }
87
88        Ok(())
89    }
90}
91
92impl FromStr for Version {
93    type Err = VersionParseError;
94
95    fn from_str(input: &str) -> Result<Self, Self::Err> {
96        let version = input
97            .split_whitespace()
98            .next()
99            .ok_or(VersionParseError::NoVersion)?;
100        if version.is_empty() {
101            return Err(VersionParseError::NoVersion);
102        }
103
104        let mut iter = version.split('.');
105
106        let major = iter.next().ok_or(VersionParseError::NoMajor)?;
107        let major = major
108            .parse::<u8>()
109            .map_err(|_| VersionParseError::VersionNaN(major.to_owned()))?;
110        let minor = iter.next().ok_or(VersionParseError::NoMinor)?;
111        let minor = minor
112            .parse::<u8>()
113            .map_err(|_| VersionParseError::VersionNaN(minor.to_owned()))?;
114        let patch = iter.next().and_then(|p| p.parse::<u8>().ok()).unwrap_or(0);
115
116        Ok(Version {
117            major,
118            minor,
119            patch,
120        })
121    }
122}
123
124fn months_since(version: Version, major: u32, minor: u32) -> i32 {
125    ((major as i32 - version.major as i32) * 12) + minor as i32 - version.minor as i32
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn months_since_release() {
134        assert_eq!(
135            18,
136            months_since(
137                Version {
138                    major: 18,
139                    minor: 4,
140                    patch: 0
141                },
142                19,
143                10
144            )
145        );
146        assert_eq!(
147            3,
148            months_since(
149                Version {
150                    major: 19,
151                    minor: 10,
152                    patch: 0
153                },
154                20,
155                1
156            )
157        );
158        assert_eq!(
159            -3,
160            months_since(
161                Version {
162                    major: 18,
163                    minor: 4,
164                    patch: 0
165                },
166                18,
167                1
168            )
169        )
170    }
171
172    #[test]
173    pub fn lts_check() {
174        assert!(Version {
175            major: 18,
176            minor: 4,
177            patch: 0
178        }
179        .is_lts());
180        assert!(!Version {
181            major: 18,
182            minor: 10,
183            patch: 0
184        }
185        .is_lts());
186        assert!(!Version {
187            major: 19,
188            minor: 4,
189            patch: 0
190        }
191        .is_lts());
192        assert!(!Version {
193            major: 19,
194            minor: 10,
195            patch: 0
196        }
197        .is_lts());
198        assert!(Version {
199            major: 20,
200            minor: 4,
201            patch: 0
202        }
203        .is_lts());
204    }
205
206    #[test]
207    pub fn lts_parse() {
208        assert_eq!(
209            Version {
210                major: 18,
211                minor: 4,
212                patch: 1
213            },
214            "18.04.1 LTS".parse::<Version>().unwrap()
215        )
216    }
217
218    #[test]
219    pub fn lts_next() {
220        assert_eq!(
221            Version {
222                major: 18,
223                minor: 10,
224                patch: 0
225            },
226            Version {
227                major: 18,
228                minor: 4,
229                patch: 1
230            }
231            .next_release()
232        )
233    }
234
235    #[test]
236    pub fn non_lts_parse() {
237        assert_eq!(
238            Version {
239                major: 18,
240                minor: 10,
241                patch: 0
242            },
243            "18.10".parse::<Version>().unwrap()
244        )
245    }
246
247    #[test]
248    pub fn non_lts_next() {
249        assert_eq!(
250            Version {
251                major: 19,
252                minor: 4,
253                patch: 0
254            },
255            Version {
256                major: 18,
257                minor: 10,
258                patch: 0
259            }
260            .next_release()
261        )
262    }
263}