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