Skip to main content

bare_types/sys/
os_version.rs

1//! Operating system version type for system information.
2//!
3//! This module provides a type-safe abstraction for operating system versions,
4//! ensuring valid version number parsing and comparison.
5//!
6//! # Version Format
7//!
8//! OS versions follow semantic versioning principles:
9//!
10//! - **Major**: Major version number (e.g., 14 for macOS Sonoma)
11//! - **Minor**: Minor version number (e.g., 6 for macOS 14.6)
12//! - **Patch**: Patch/build number (e.g., 1 for 14.6.1)
13//!
14//! # Examples
15//!
16//! ```rust
17//! use bare_types::sys::OsVersion;
18//!
19//! // Parse from string
20//! let version: OsVersion = "14.6.1".parse()?;
21//!
22//! // Access components
23//! assert_eq!(version.major(), 14);
24//! assert_eq!(version.minor(), 6);
25//! assert_eq!(version.patch(), Some(1));
26//!
27//! // Compare versions
28//! assert!(version >= OsVersion::new(14, 0, None));
29//!
30//! // Short version (no patch)
31//! let version: OsVersion = "14.6".parse()?;
32//! assert_eq!(version.patch(), None);
33//! # Ok::<(), bare_types::sys::OsVersionError>(())
34//! ```
35
36use core::fmt;
37use core::str::FromStr;
38
39#[cfg(feature = "serde")]
40use serde::{Deserialize, Serialize};
41
42#[cfg(feature = "arbitrary")]
43use arbitrary::Arbitrary;
44
45/// Error type for OS version parsing.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
48pub enum OsVersionError {
49    /// Empty version string
50    Empty,
51    /// Invalid major version number
52    InvalidMajor,
53    /// Invalid minor version number
54    InvalidMinor,
55    /// Invalid patch version number
56    InvalidPatch,
57    /// Too many version components (max 3)
58    TooManyComponents,
59    /// Negative version number
60    NegativeVersion,
61}
62
63impl fmt::Display for OsVersionError {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self {
66            Self::Empty => write!(f, "version string is empty"),
67            Self::InvalidMajor => write!(f, "invalid major version number"),
68            Self::InvalidMinor => write!(f, "invalid minor version number"),
69            Self::InvalidPatch => write!(f, "invalid patch version number"),
70            Self::TooManyComponents => write!(f, "version has too many components (max 3)"),
71            Self::NegativeVersion => write!(f, "version numbers cannot be negative"),
72        }
73    }
74}
75
76#[cfg(feature = "std")]
77impl std::error::Error for OsVersionError {}
78
79/// Operating system version.
80///
81/// This type provides type-safe OS version numbers with up to three components:
82/// major, minor, and optional patch version.
83///
84/// # Invariants
85///
86/// - Major and minor versions are always present (u16)
87/// - Patch version is optional (Some(u16) or None)
88/// - All version numbers are non-negative
89///
90/// # Examples
91///
92/// ```rust
93/// use bare_types::sys::OsVersion;
94///
95/// // Create from components
96/// let version = OsVersion::new(14, 6, Some(1));
97///
98/// // Parse from string
99/// let version: OsVersion = "14.6.1".parse()?;
100/// assert_eq!(version.major(), 14);
101///
102/// // Two-component version
103/// let version: OsVersion = "22.04".parse()?;
104/// assert_eq!(version.patch(), None);
105/// # Ok::<(), bare_types::sys::OsVersionError>(())
106/// ```
107#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
108#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
109#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
110pub struct OsVersion {
111    /// The major version number
112    major: u16,
113    /// The minor version number
114    minor: u16,
115    /// The optional patch version number
116    patch: Option<u16>,
117}
118
119impl OsVersion {
120    /// Creates a new OS version from components.
121    ///
122    /// # Arguments
123    ///
124    /// * `major` - The major version number
125    /// * `minor` - The minor version number
126    /// * `patch` - The optional patch version number
127    ///
128    /// # Examples
129    ///
130    /// ```rust
131    /// use bare_types::sys::OsVersion;
132    ///
133    /// // Three-component version
134    /// let version = OsVersion::new(14, 6, Some(1));
135    /// assert_eq!(version.major(), 14);
136    /// assert_eq!(version.minor(), 6);
137    /// assert_eq!(version.patch(), Some(1));
138    ///
139    /// // Two-component version
140    /// let version = OsVersion::new(22, 4, None);
141    /// assert_eq!(version.patch(), None);
142    /// ```
143    #[must_use]
144    pub const fn new(major: u16, minor: u16, patch: Option<u16>) -> Self {
145        Self {
146            major,
147            minor,
148            patch,
149        }
150    }
151
152    /// Returns the major version number.
153    ///
154    /// # Examples
155    ///
156    /// ```rust
157    /// use bare_types::sys::OsVersion;
158    ///
159    /// let version = OsVersion::new(14, 6, Some(1));
160    /// assert_eq!(version.major(), 14);
161    /// ```
162    #[must_use]
163    #[inline]
164    pub const fn major(&self) -> u16 {
165        self.major
166    }
167
168    /// Returns the minor version number.
169    ///
170    /// # Examples
171    ///
172    /// ```rust
173    /// use bare_types::sys::OsVersion;
174    ///
175    /// let version = OsVersion::new(14, 6, Some(1));
176    /// assert_eq!(version.minor(), 6);
177    /// ```
178    #[must_use]
179    #[inline]
180    pub const fn minor(&self) -> u16 {
181        self.minor
182    }
183
184    /// Returns the optional patch version number.
185    ///
186    /// # Examples
187    ///
188    /// ```rust
189    /// use bare_types::sys::OsVersion;
190    ///
191    /// let version = OsVersion::new(14, 6, Some(1));
192    /// assert_eq!(version.patch(), Some(1));
193    ///
194    /// let version = OsVersion::new(22, 4, None);
195    /// assert_eq!(version.patch(), None);
196    /// ```
197    #[must_use]
198    #[inline]
199    pub const fn patch(&self) -> Option<u16> {
200        self.patch
201    }
202
203    /// Returns `true` if this is a major version (x.0.x or x.0).
204    ///
205    /// # Examples
206    ///
207    /// ```rust
208    /// use bare_types::sys::OsVersion;
209    ///
210    /// assert!(OsVersion::new(14, 0, Some(0)).is_major_release());
211    /// assert!(OsVersion::new(14, 0, None).is_major_release());
212    /// assert!(!OsVersion::new(14, 6, Some(0)).is_major_release());
213    /// ```
214    #[must_use]
215    pub const fn is_major_release(&self) -> bool {
216        self.minor == 0
217            && match self.patch {
218                Some(p) => p == 0,
219                None => true,
220            }
221    }
222
223    /// Returns `true` if this is an initial release (x.0.0 or x.0).
224    ///
225    /// # Examples
226    ///
227    /// ```rust
228    /// use bare_types::sys::OsVersion;
229    ///
230    /// assert!(OsVersion::new(14, 0, Some(0)).is_initial_release());
231    /// assert!(!OsVersion::new(14, 6, Some(0)).is_initial_release());
232    /// ```
233    #[must_use]
234    pub const fn is_initial_release(&self) -> bool {
235        self.minor == 0
236            && match self.patch {
237                Some(p) => p == 0,
238                None => true,
239            }
240    }
241
242    /// Returns a tuple of (major, minor, patch) for comparison.
243    ///
244    /// For versions without a patch, 0 is used as the patch number for comparison.
245    ///
246    /// # Examples
247    ///
248    /// ```rust
249    /// use bare_types::sys::OsVersion;
250    ///
251    /// let version = OsVersion::new(14, 6, Some(1));
252    /// assert_eq!(version.as_tuple(), (14, 6, 1));
253    ///
254    /// let version = OsVersion::new(22, 4, None);
255    /// assert_eq!(version.as_tuple(), (22, 4, 0));
256    /// ```
257    #[must_use]
258    pub const fn as_tuple(&self) -> (u16, u16, u16) {
259        (
260            self.major,
261            self.minor,
262            match self.patch {
263                Some(p) => p,
264                None => 0,
265            },
266        )
267    }
268
269    /// Returns a new version with only major and minor components.
270    ///
271    /// # Examples
272    ///
273    /// ```rust
274    /// use bare_types::sys::OsVersion;
275    ///
276    /// let version = OsVersion::new(14, 6, Some(1));
277    /// let short = version.to_short();
278    /// assert_eq!(short.patch(), None);
279    /// assert_eq!(short.major(), 14);
280    /// assert_eq!(short.minor(), 6);
281    /// ```
282    #[must_use]
283    pub const fn to_short(&self) -> Self {
284        Self::new(self.major, self.minor, None)
285    }
286
287    /// Returns a new version with the patch component set.
288    ///
289    /// # Examples
290    ///
291    /// ```rust
292    /// use bare_types::sys::OsVersion;
293    ///
294    /// let version = OsVersion::new(14, 6, None);
295    /// let patched = version.with_patch(1);
296    /// assert_eq!(patched.patch(), Some(1));
297    /// ```
298    #[must_use]
299    pub const fn with_patch(&self, patch: u16) -> Self {
300        Self::new(self.major, self.minor, Some(patch))
301    }
302
303    /// Creates a version from major and minor only.
304    ///
305    /// This is a convenience method for two-component versions.
306    ///
307    /// # Examples
308    ///
309    /// ```rust
310    /// use bare_types::sys::OsVersion;
311    ///
312    /// let version = OsVersion::new_short(22, 4);
313    /// assert_eq!(version.major(), 22);
314    /// assert_eq!(version.minor(), 4);
315    /// assert_eq!(version.patch(), None);
316    /// ```
317    #[must_use]
318    pub const fn new_short(major: u16, minor: u16) -> Self {
319        Self::new(major, minor, None)
320    }
321}
322
323impl FromStr for OsVersion {
324    type Err = OsVersionError;
325
326    fn from_str(s: &str) -> Result<Self, Self::Err> {
327        if s.is_empty() {
328            return Err(OsVersionError::Empty);
329        }
330
331        let parts: Vec<&str> = s.split('.').collect();
332
333        if parts.len() > 3 {
334            return Err(OsVersionError::TooManyComponents);
335        }
336
337        if parts.len() < 2 {
338            return Err(OsVersionError::InvalidMinor);
339        }
340
341        let major = parts[0]
342            .parse::<u16>()
343            .map_err(|_| OsVersionError::InvalidMajor)?;
344
345        let minor = parts[1]
346            .parse::<u16>()
347            .map_err(|_| OsVersionError::InvalidMinor)?;
348
349        let patch = if parts.len() > 2 {
350            Some(
351                parts[2]
352                    .parse::<u16>()
353                    .map_err(|_| OsVersionError::InvalidPatch)?,
354            )
355        } else {
356            None
357        };
358
359        Ok(Self::new(major, minor, patch))
360    }
361}
362
363impl TryFrom<&str> for OsVersion {
364    type Error = OsVersionError;
365
366    fn try_from(s: &str) -> Result<Self, Self::Error> {
367        s.parse()
368    }
369}
370
371impl fmt::Display for OsVersion {
372    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
373        if let Some(patch) = self.patch {
374            write!(f, "{}.{}.{}", self.major, self.minor, patch)
375        } else {
376            write!(f, "{}.{}", self.major, self.minor)
377        }
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    #[test]
386    fn test_new() {
387        let version = OsVersion::new(14, 6, Some(1));
388        assert_eq!(version.major(), 14);
389        assert_eq!(version.minor(), 6);
390        assert_eq!(version.patch(), Some(1));
391
392        let version = OsVersion::new(22, 4, None);
393        assert_eq!(version.major(), 22);
394        assert_eq!(version.minor(), 4);
395        assert_eq!(version.patch(), None);
396    }
397
398    #[test]
399    fn test_new_short() {
400        let version = OsVersion::new_short(22, 4);
401        assert_eq!(version.major(), 22);
402        assert_eq!(version.minor(), 4);
403        assert_eq!(version.patch(), None);
404    }
405
406    #[test]
407    fn test_is_major_release() {
408        assert!(OsVersion::new(14, 0, Some(0)).is_major_release());
409        assert!(OsVersion::new(14, 0, None).is_major_release());
410        assert!(!OsVersion::new(14, 6, Some(0)).is_major_release());
411        assert!(!OsVersion::new(14, 6, None).is_major_release());
412    }
413
414    #[test]
415    fn test_is_initial_release() {
416        assert!(OsVersion::new(14, 0, Some(0)).is_initial_release());
417        assert!(!OsVersion::new(14, 6, Some(0)).is_initial_release());
418        assert!(OsVersion::new(14, 0, None).is_initial_release());
419    }
420
421    #[test]
422    fn test_as_tuple() {
423        let version = OsVersion::new(14, 6, Some(1));
424        assert_eq!(version.as_tuple(), (14, 6, 1));
425
426        let version = OsVersion::new(22, 4, None);
427        assert_eq!(version.as_tuple(), (22, 4, 0));
428    }
429
430    #[test]
431    fn test_to_short() {
432        let version = OsVersion::new(14, 6, Some(1));
433        let short = version.to_short();
434        assert_eq!(short.patch(), None);
435        assert_eq!(short.major(), 14);
436        assert_eq!(short.minor(), 6);
437    }
438
439    #[test]
440    fn test_with_patch() {
441        let version = OsVersion::new(14, 6, None);
442        let patched = version.with_patch(1);
443        assert_eq!(patched.patch(), Some(1));
444        assert_eq!(patched.major(), 14);
445        assert_eq!(patched.minor(), 6);
446    }
447
448    #[test]
449    fn test_from_str_three_components() {
450        let version: OsVersion = "14.6.1".parse().unwrap();
451        assert_eq!(version.major(), 14);
452        assert_eq!(version.minor(), 6);
453        assert_eq!(version.patch(), Some(1));
454    }
455
456    #[test]
457    fn test_from_str_two_components() {
458        let version: OsVersion = "22.04".parse().unwrap();
459        assert_eq!(version.major(), 22);
460        assert_eq!(version.minor(), 4);
461        assert_eq!(version.patch(), None);
462    }
463
464    #[test]
465    fn test_from_str_zero_padded() {
466        let version: OsVersion = "10.0.19041".parse().unwrap();
467        assert_eq!(version.major(), 10);
468        assert_eq!(version.minor(), 0);
469        assert_eq!(version.patch(), Some(19041));
470    }
471
472    #[test]
473    fn test_from_str_errors() {
474        // Empty
475        assert!(matches!(
476            "".parse::<OsVersion>(),
477            Err(OsVersionError::Empty)
478        ));
479
480        // Too many components
481        assert!(matches!(
482            "1.2.3.4".parse::<OsVersion>(),
483            Err(OsVersionError::TooManyComponents)
484        ));
485
486        // Only one component
487        assert!(matches!(
488            "14".parse::<OsVersion>(),
489            Err(OsVersionError::InvalidMinor)
490        ));
491
492        // Invalid numbers
493        assert!("abc.def".parse::<OsVersion>().is_err());
494        assert!("14.abc".parse::<OsVersion>().is_err());
495        assert!("14.6.abc".parse::<OsVersion>().is_err());
496    }
497
498    #[test]
499    fn test_display() {
500        let version = OsVersion::new(14, 6, Some(1));
501        assert_eq!(format!("{}", version), "14.6.1");
502
503        let version = OsVersion::new(22, 4, None);
504        assert_eq!(format!("{}", version), "22.4");
505    }
506
507    #[test]
508    fn test_equality() {
509        let v1 = OsVersion::new(14, 6, Some(1));
510        let v2 = OsVersion::new(14, 6, Some(1));
511        let v3 = OsVersion::new(14, 6, None);
512
513        assert_eq!(v1, v2);
514        assert_ne!(v1, v3);
515    }
516
517    #[test]
518    fn test_ordering() {
519        let v1 = OsVersion::new(14, 6, Some(1));
520        let v2 = OsVersion::new(14, 6, Some(2));
521        let v3 = OsVersion::new(14, 7, None);
522        let v4 = OsVersion::new(15, 0, None);
523
524        assert!(v1 < v2);
525        assert!(v2 < v3);
526        assert!(v3 < v4);
527
528        // Version with patch > version without patch at same major.minor
529        let with_patch = OsVersion::new(14, 6, Some(0));
530        let without_patch = OsVersion::new(14, 6, None);
531        assert!(without_patch < with_patch);
532    }
533
534    #[test]
535    fn test_copy() {
536        let version = OsVersion::new(14, 6, Some(1));
537        let version2 = version;
538        assert_eq!(version, version2);
539    }
540
541    #[test]
542    fn test_clone() {
543        let version = OsVersion::new(14, 6, Some(1));
544        let version2 = version.clone();
545        assert_eq!(version, version2);
546    }
547
548    #[test]
549    fn test_common_versions() {
550        // Ubuntu LTS versions
551        let ubuntu_2204: OsVersion = "22.04".parse().unwrap();
552        assert_eq!(ubuntu_2204.major(), 22);
553        assert_eq!(ubuntu_2204.minor(), 4);
554
555        let ubuntu_2404: OsVersion = "24.04".parse().unwrap();
556        assert_eq!(ubuntu_2404.major(), 24);
557
558        // macOS versions
559        let macos_sonoma: OsVersion = "14.6.1".parse().unwrap();
560        assert_eq!(macos_sonoma.major(), 14);
561        assert_eq!(macos_sonoma.minor(), 6);
562        assert_eq!(macos_sonoma.patch(), Some(1));
563
564        // Windows versions
565        let win11: OsVersion = "10.0.22000".parse().unwrap();
566        assert_eq!(win11.major(), 10);
567        assert_eq!(win11.minor(), 0);
568        assert_eq!(win11.patch(), Some(22000));
569    }
570}