os_utils/
version.rs

1//! Simple version information.
2
3use std::fmt;
4use std::num::ParseIntError;
5use std::str::FromStr;
6
7/// A simple `MAJOR.MINOR.PATCH` version.
8///
9/// Unlike [`semver::Version`](https://docs.rs/semver/0.9.*/semver/struct.Version.html),
10/// this value does not include a pre-release version identifier or build
11/// metadata.
12#[derive(Clone, Copy, Debug, Default, PartialOrd, Ord, PartialEq, Eq, Hash)]
13#[repr(C)]
14pub struct Version {
15    /// The `x.0.0` version number.
16    pub major: u64,
17    /// The `0.y.0` version number.
18    pub minor: u64,
19    /// The `0.0.z` version number.
20    pub patch: u64,
21}
22
23impl From<Version> for (u64, u64, u64) {
24    #[inline]
25    fn from(Version { major, minor, patch }: Version) -> Self {
26        (major, minor, patch)
27    }
28}
29
30impl From<(u64, u64, u64)> for Version {
31    #[inline]
32    fn from((major, minor, patch): (u64, u64, u64)) -> Self {
33        Version { major, minor, patch }
34    }
35}
36
37impl From<(u64, u64)> for Version {
38    #[inline]
39    fn from((major, minor): (u64, u64)) -> Self {
40        Version { major, minor, ..Default::default() }
41    }
42}
43
44impl From<(u64,)> for Version {
45    #[inline]
46    fn from((major,): (u64,)) -> Self {
47        major.into()
48    }
49}
50
51impl From<u64> for Version {
52    #[inline]
53    fn from(major: u64) -> Self {
54        Version { major, ..Default::default() }
55    }
56}
57
58impl From<OsVersion> for Version {
59    #[inline]
60    fn from(version: OsVersion) -> Version {
61        *version.as_version()
62    }
63}
64
65impl fmt::Display for Version {
66    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
67        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
68    }
69}
70
71impl FromStr for Version {
72    type Err = ParseVersionError;
73
74    fn from_str(s: &str) -> Result<Version, ParseVersionError> {
75        use self::ParseVersionError::*;
76
77        let mut vers = Version::default();
78        let mut iter = s.split('.');
79
80        let major = iter.next().ok_or(EmptyInput)?;
81        let minor = iter.next();
82        let patch = iter.next();
83
84        if iter.next().is_some() {
85            return Err(ExtraInput);
86        }
87
88        vers.major = major.parse().map_err(|e| MajorInt(e))?;
89
90        if let Some(minor) = minor {
91            vers.minor = minor.parse().map_err(|e| MinorInt(e))?;
92        }
93
94        if let Some(patch) = patch {
95            vers.patch = patch.parse().map_err(|e| PatchInt(e))?;
96        }
97
98        Ok(vers)
99    }
100}
101
102impl Version {
103    /// Creates a new instance from the three values.
104    #[inline]
105    pub fn new(major: u64, minor: u64, patch: u64) -> Version {
106        Version { major, minor, patch }
107    }
108
109    /// Converts the version string formatted as `MAJOR.MINOR.PATCH`.
110    #[inline]
111    pub fn parse(version: &str) -> Result<Version, ParseVersionError> {
112        version.parse()
113    }
114}
115
116/// A `MAJOR.MINOR.PATCH` version with extras for the current operating system.
117#[derive(Clone, Copy, Debug, Default, PartialOrd, Ord, PartialEq, Eq, Hash)]
118#[repr(C)]
119pub struct OsVersion {
120    /// The `x.0.0` version number.
121    pub major: u64,
122    /// The `0.y.0` version number.
123    pub minor: u64,
124    /// The `0.0.z` version number.
125    pub patch: u64,
126
127    /// The build number.
128    #[cfg(target_os = "windows")]
129    pub build: u64,
130}
131
132impl AsRef<Version> for OsVersion {
133    #[inline]
134    fn as_ref(&self) -> &Version {
135        self.as_version()
136    }
137}
138
139impl AsMut<Version> for OsVersion {
140    #[inline]
141    fn as_mut(&mut self) -> &mut Version {
142        self.as_version_mut()
143    }
144}
145
146impl OsVersion {
147    #[cfg(target_os = "macos")]
148    fn _get() -> Option<Self> {
149        use cocoa::appkit::*;
150        use cocoa::base::nil;
151        use cocoa::foundation::{NSInteger, NSProcessInfo};
152
153        let version = unsafe { NSAppKitVersionNumber };
154
155        let (major, minor, patch) = if version < NSAppKitVersionNumber10_7 {
156            return None;
157        } else if version < NSAppKitVersionNumber10_7_2 {
158            (10, 7, 0)
159        } else if version < NSAppKitVersionNumber10_7_3 {
160            (10, 7, 2)
161        } else if version < NSAppKitVersionNumber10_7_4 {
162            (10, 7, 3)
163        } else if version < NSAppKitVersionNumber10_8 {
164            (10, 7, 4)
165        } else if version < NSAppKitVersionNumber10_9 {
166            (10, 8, 0)
167        } else if version < NSAppKitVersionNumber10_10 {
168            (10, 9, 0)
169        } else {
170            // https://developer.apple.com/documentation/foundation/nsoperatingsystemversion?language=objc
171            #[repr(C)]
172            #[allow(dead_code)] // See https://github.com/rust-lang/rust/issues/56750
173            struct NSOperatingSystemVersion {
174                major: NSInteger,
175                minor: NSInteger,
176                patch: NSInteger,
177            }
178
179            // Available in Obj-C as of macOS 10.10+
180            let NSOperatingSystemVersion { major, minor, patch } = unsafe {
181                let proc_info = NSProcessInfo::processInfo(nil);
182                msg_send![proc_info, operatingSystemVersion]
183            };
184
185            (major as u64, minor as u64, patch as u64)
186        };
187        Some(OsVersion { major, minor, patch })
188    }
189
190    #[cfg(target_os = "windows")]
191    fn _get() -> Option<Self> {
192        use std::mem;
193        use winapi::um::{
194            sysinfoapi::GetVersionExA,
195            winnt::OSVERSIONINFOA,
196        };
197        unsafe {
198            let mut info = mem::zeroed::<OSVERSIONINFOA>();
199            info.dwOSVersionInfoSize = mem::size_of_val(&info) as _;
200            if GetVersionExA(&mut info) == 0 {
201                None
202            } else {
203                Some(OsVersion {
204                    major: info.dwMajorVersion as u64,
205                    minor: info.dwMinorVersion as u64,
206                    patch: 0,
207                    build: info.dwBuildNumber as u64,
208                })
209            }
210        }
211    }
212
213    #[cfg(target_os = "linux")]
214    fn _get() -> Option<Self> {
215        None
216    }
217
218    /// Queries the current operating system version.
219    pub fn get() -> Option<Self> {
220        Self::_get()
221    }
222
223    /// Returns a shared reference to `self` as a `Version`.
224    #[inline]
225    pub fn as_version(&self) -> &Version {
226        unsafe { &*(self as *const OsVersion as *const Version) }
227    }
228
229    /// Returns a mutable reference to `self` as a `Version`.
230    #[inline]
231    pub fn as_version_mut(&mut self) -> &mut Version {
232        unsafe { &mut *(self as *mut OsVersion as *mut Version) }
233    }
234}
235
236/// An error returned when parsing a version string fails.
237#[derive(Clone, Debug, PartialEq, Eq)]
238pub enum ParseVersionError {
239    /// Failed to parse major integer value.
240    MajorInt(ParseIntError),
241    /// Failed to parse minor integer value.
242    MinorInt(ParseIntError),
243    /// Failed to parse patch integer value.
244    PatchInt(ParseIntError),
245    /// Parse input is empty.
246    EmptyInput,
247    /// Parse input had an extra period and maybe more.
248    ExtraInput,
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    type VersionTriple = (u64, u64, u64);
256
257    #[test]
258    fn cmp() {
259        let pairs: &[(VersionTriple, VersionTriple)] = &[
260            ((0, 0, 1), (0, 0, 0)),
261            ((0, 1, 0), (0, 0, 1)),
262            ((0, 1, 1), (0, 1, 0)),
263            ((1, 0, 0), (0, 0, 1)),
264            ((1, 0, 0), (0, 1, 0)),
265        ];
266        for &(a, b) in pairs {
267            let a = Version::from(a);
268            let b = Version::from(b);
269            assert!(a > b);
270            assert!(a != b);
271            assert!(b < a);
272        }
273    }
274
275    #[test]
276    fn parse_success() {
277        let pairs: &[(&str, VersionTriple)] = &[
278            ("0",     Default::default()),
279            ("0.1",   ( 0, 1, 0)),
280            ("0.1.2", ( 0, 1, 2)),
281            ("16.04", (16, 4, 0)),
282        ];
283        for &(string, version) in pairs {
284            let version = Version::from(version);
285            let strings: [&str; 2] = [string, &version.to_string()];
286
287            for string in strings.iter() {
288                assert_eq!(string.parse(), Ok(version));
289            }
290        }
291    }
292
293    #[test]
294    fn parse_failure() {
295        let strings: &[&str] = &[
296            "",
297            ".",
298            "..",
299            "...",
300            "....",
301            "0.",
302            "0.0.",
303            "0.0.0.",
304            "0.0.0.0",
305        ];
306        for s in strings {
307            Version::parse(s).expect_err(&format!("parsing {:?}", s));
308        }
309    }
310}