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#[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 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 pub fn is_lts(self) -> bool {
51 self.major % 2 == 0 && self.minor == 4
52 }
53
54 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 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}