1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
48pub enum OsVersionError {
49 Empty,
51 InvalidMajor,
53 InvalidMinor,
55 InvalidPatch,
57 TooManyComponents,
59 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#[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 major: u16,
113 minor: u16,
115 patch: Option<u16>,
117}
118
119impl OsVersion {
120 #[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 #[must_use]
163 #[inline]
164 pub const fn major(&self) -> u16 {
165 self.major
166 }
167
168 #[must_use]
179 #[inline]
180 pub const fn minor(&self) -> u16 {
181 self.minor
182 }
183
184 #[must_use]
198 #[inline]
199 pub const fn patch(&self) -> Option<u16> {
200 self.patch
201 }
202
203 #[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 #[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 #[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 #[must_use]
283 pub const fn to_short(&self) -> Self {
284 Self::new(self.major, self.minor, None)
285 }
286
287 #[must_use]
299 pub const fn with_patch(&self, patch: u16) -> Self {
300 Self::new(self.major, self.minor, Some(patch))
301 }
302
303 #[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 assert!(matches!(
476 "".parse::<OsVersion>(),
477 Err(OsVersionError::Empty)
478 ));
479
480 assert!(matches!(
482 "1.2.3.4".parse::<OsVersion>(),
483 Err(OsVersionError::TooManyComponents)
484 ));
485
486 assert!(matches!(
488 "14".parse::<OsVersion>(),
489 Err(OsVersionError::InvalidMinor)
490 ));
491
492 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 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 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 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 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}