1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
use os_release::OsRelease;
use std::fmt::{self, Display, Formatter};
use std::io;
use std::str::FromStr;
#[derive(Debug, Error)]
pub enum VersionError {
#[error(display = "failed to fetch /etc/os-release: {}", _0)]
OsRelease(io::Error),
#[error(display = "version parsing error: {}", _0)]
Parse(VersionParseError),
}
#[derive(Debug, Error)]
pub enum VersionParseError {
#[error(display = "release version component was not a number: found {}", _0)]
VersionNaN(String),
#[error(display = "invalid minor release version: expected 4 or 10, found {}", _0)]
InvalidMinorVersion(u8),
#[error(display = "major version does not exist")]
NoMajor,
#[error(display = "minor version does not exist")]
NoMinor,
#[error(display = "release version is empty")]
NoVersion,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Version {
pub major: u8,
pub minor: u8,
pub patch: u8,
}
impl Version {
pub fn detect() -> Result<Self, VersionError> {
let release = OsRelease::new().map_err(VersionError::OsRelease)?;
release.version.parse::<Version>().map_err(VersionError::Parse)
}
pub fn is_lts(self) -> bool {
self.major % 2 == 0 && self.minor == 4
}
pub fn next_release(self) -> Self {
let (major, minor) = if self.minor == 10 { (self.major + 1, 4) } else { (self.major, 10) };
Version { major, minor, patch: 0 }
}
}
impl Display for Version {
fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
write!(fmt, "{}.{:02}", self.major, self.minor)?;
if self.patch != 0 {
write!(fmt, "{}", self.patch)?;
}
Ok(())
}
}
impl FromStr for Version {
type Err = VersionParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let version = input.split_whitespace().next().ok_or(VersionParseError::NoVersion)?;
if version.is_empty() {
return Err(VersionParseError::NoVersion);
}
let mut iter = version.split('.');
let major = iter.next().ok_or(VersionParseError::NoMajor)?;
let major =
major.parse::<u8>().map_err(|_| VersionParseError::VersionNaN(major.to_owned()))?;
let minor = iter.next().ok_or(VersionParseError::NoMinor)?;
let minor =
minor.parse::<u8>().map_err(|_| VersionParseError::VersionNaN(minor.to_owned()))?;
let patch = iter.next().and_then(|p| p.parse::<u8>().ok()).unwrap_or(0);
Ok(Version { major, minor, patch })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
pub fn lts_check() {
assert!(Version { major: 18, minor: 4, patch: 0 }.is_lts());
assert!(!Version { major: 18, minor: 10, patch: 0 }.is_lts());
assert!(!Version { major: 19, minor: 4, patch: 0 }.is_lts());
assert!(!Version { major: 19, minor: 10, patch: 0 }.is_lts());
assert!(Version { major: 20, minor: 4, patch: 0 }.is_lts());
}
#[test]
pub fn lts_parse() {
assert_eq!(
Version { major: 18, minor: 4, patch: 1 },
"18.04.1 LTS".parse::<Version>().unwrap()
)
}
#[test]
pub fn lts_next() {
assert_eq!(
Version { major: 18, minor: 10, patch: 0 },
Version { major: 18, minor: 4, patch: 1 }.next_release()
)
}
#[test]
pub fn non_lts_parse() {
assert_eq!(Version { major: 18, minor: 10, patch: 0 }, "18.10".parse::<Version>().unwrap())
}
#[test]
pub fn non_lts_next() {
assert_eq!(
Version { major: 19, minor: 04, patch: 0 },
Version { major: 18, minor: 10, patch: 0 }.next_release()
)
}
}