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}