use core::fmt;
use core::str::FromStr;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "arbitrary")]
use arbitrary::Arbitrary;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[non_exhaustive]
pub enum OsVersionError {
Empty,
InvalidMajor,
InvalidMinor,
InvalidPatch,
TooManyComponents,
NegativeVersion,
}
impl fmt::Display for OsVersionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "version string is empty"),
Self::InvalidMajor => write!(f, "invalid major version number"),
Self::InvalidMinor => write!(f, "invalid minor version number"),
Self::InvalidPatch => write!(f, "invalid patch version number"),
Self::TooManyComponents => write!(f, "version has too many components (max 3)"),
Self::NegativeVersion => write!(f, "version numbers cannot be negative"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for OsVersionError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
pub struct OsVersion {
major: u16,
minor: u16,
patch: Option<u16>,
}
impl OsVersion {
#[must_use]
pub const fn new(major: u16, minor: u16, patch: Option<u16>) -> Self {
Self {
major,
minor,
patch,
}
}
#[must_use]
#[inline]
pub const fn major(&self) -> u16 {
self.major
}
#[must_use]
#[inline]
pub const fn minor(&self) -> u16 {
self.minor
}
#[must_use]
#[inline]
pub const fn patch(&self) -> Option<u16> {
self.patch
}
#[must_use]
pub const fn is_major_release(&self) -> bool {
self.minor == 0
&& match self.patch {
Some(p) => p == 0,
None => true,
}
}
#[must_use]
pub const fn is_initial_release(&self) -> bool {
self.minor == 0
&& match self.patch {
Some(p) => p == 0,
None => true,
}
}
#[must_use]
pub const fn as_tuple(&self) -> (u16, u16, u16) {
(
self.major,
self.minor,
match self.patch {
Some(p) => p,
None => 0,
},
)
}
#[must_use]
pub const fn to_short(&self) -> Self {
Self::new(self.major, self.minor, None)
}
#[must_use]
pub const fn with_patch(&self, patch: u16) -> Self {
Self::new(self.major, self.minor, Some(patch))
}
#[must_use]
pub const fn new_short(major: u16, minor: u16) -> Self {
Self::new(major, minor, None)
}
}
impl FromStr for OsVersion {
type Err = OsVersionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Err(OsVersionError::Empty);
}
let parts: Vec<&str> = s.split('.').collect();
if parts.len() > 3 {
return Err(OsVersionError::TooManyComponents);
}
if parts.len() < 2 {
return Err(OsVersionError::InvalidMinor);
}
let major = parts[0]
.parse::<u16>()
.map_err(|_| OsVersionError::InvalidMajor)?;
let minor = parts[1]
.parse::<u16>()
.map_err(|_| OsVersionError::InvalidMinor)?;
let patch = if parts.len() > 2 {
Some(
parts[2]
.parse::<u16>()
.map_err(|_| OsVersionError::InvalidPatch)?,
)
} else {
None
};
Ok(Self::new(major, minor, patch))
}
}
impl TryFrom<&str> for OsVersion {
type Error = OsVersionError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
s.parse()
}
}
impl fmt::Display for OsVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(patch) = self.patch {
write!(f, "{}.{}.{}", self.major, self.minor, patch)
} else {
write!(f, "{}.{}", self.major, self.minor)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let version = OsVersion::new(14, 6, Some(1));
assert_eq!(version.major(), 14);
assert_eq!(version.minor(), 6);
assert_eq!(version.patch(), Some(1));
let version = OsVersion::new(22, 4, None);
assert_eq!(version.major(), 22);
assert_eq!(version.minor(), 4);
assert_eq!(version.patch(), None);
}
#[test]
fn test_new_short() {
let version = OsVersion::new_short(22, 4);
assert_eq!(version.major(), 22);
assert_eq!(version.minor(), 4);
assert_eq!(version.patch(), None);
}
#[test]
fn test_is_major_release() {
assert!(OsVersion::new(14, 0, Some(0)).is_major_release());
assert!(OsVersion::new(14, 0, None).is_major_release());
assert!(!OsVersion::new(14, 6, Some(0)).is_major_release());
assert!(!OsVersion::new(14, 6, None).is_major_release());
}
#[test]
fn test_is_initial_release() {
assert!(OsVersion::new(14, 0, Some(0)).is_initial_release());
assert!(!OsVersion::new(14, 6, Some(0)).is_initial_release());
assert!(OsVersion::new(14, 0, None).is_initial_release());
}
#[test]
fn test_as_tuple() {
let version = OsVersion::new(14, 6, Some(1));
assert_eq!(version.as_tuple(), (14, 6, 1));
let version = OsVersion::new(22, 4, None);
assert_eq!(version.as_tuple(), (22, 4, 0));
}
#[test]
fn test_to_short() {
let version = OsVersion::new(14, 6, Some(1));
let short = version.to_short();
assert_eq!(short.patch(), None);
assert_eq!(short.major(), 14);
assert_eq!(short.minor(), 6);
}
#[test]
fn test_with_patch() {
let version = OsVersion::new(14, 6, None);
let patched = version.with_patch(1);
assert_eq!(patched.patch(), Some(1));
assert_eq!(patched.major(), 14);
assert_eq!(patched.minor(), 6);
}
#[test]
fn test_from_str_three_components() {
let version: OsVersion = "14.6.1".parse().unwrap();
assert_eq!(version.major(), 14);
assert_eq!(version.minor(), 6);
assert_eq!(version.patch(), Some(1));
}
#[test]
fn test_from_str_two_components() {
let version: OsVersion = "22.04".parse().unwrap();
assert_eq!(version.major(), 22);
assert_eq!(version.minor(), 4);
assert_eq!(version.patch(), None);
}
#[test]
fn test_from_str_zero_padded() {
let version: OsVersion = "10.0.19041".parse().unwrap();
assert_eq!(version.major(), 10);
assert_eq!(version.minor(), 0);
assert_eq!(version.patch(), Some(19041));
}
#[test]
fn test_from_str_errors() {
assert!(matches!(
"".parse::<OsVersion>(),
Err(OsVersionError::Empty)
));
assert!(matches!(
"1.2.3.4".parse::<OsVersion>(),
Err(OsVersionError::TooManyComponents)
));
assert!(matches!(
"14".parse::<OsVersion>(),
Err(OsVersionError::InvalidMinor)
));
assert!("abc.def".parse::<OsVersion>().is_err());
assert!("14.abc".parse::<OsVersion>().is_err());
assert!("14.6.abc".parse::<OsVersion>().is_err());
}
#[test]
fn test_display() {
let version = OsVersion::new(14, 6, Some(1));
assert_eq!(format!("{}", version), "14.6.1");
let version = OsVersion::new(22, 4, None);
assert_eq!(format!("{}", version), "22.4");
}
#[test]
fn test_equality() {
let v1 = OsVersion::new(14, 6, Some(1));
let v2 = OsVersion::new(14, 6, Some(1));
let v3 = OsVersion::new(14, 6, None);
assert_eq!(v1, v2);
assert_ne!(v1, v3);
}
#[test]
fn test_ordering() {
let v1 = OsVersion::new(14, 6, Some(1));
let v2 = OsVersion::new(14, 6, Some(2));
let v3 = OsVersion::new(14, 7, None);
let v4 = OsVersion::new(15, 0, None);
assert!(v1 < v2);
assert!(v2 < v3);
assert!(v3 < v4);
let with_patch = OsVersion::new(14, 6, Some(0));
let without_patch = OsVersion::new(14, 6, None);
assert!(without_patch < with_patch);
}
#[test]
fn test_copy() {
let version = OsVersion::new(14, 6, Some(1));
let version2 = version;
assert_eq!(version, version2);
}
#[test]
fn test_clone() {
let version = OsVersion::new(14, 6, Some(1));
let version2 = version.clone();
assert_eq!(version, version2);
}
#[test]
fn test_common_versions() {
let ubuntu_2204: OsVersion = "22.04".parse().unwrap();
assert_eq!(ubuntu_2204.major(), 22);
assert_eq!(ubuntu_2204.minor(), 4);
let ubuntu_2404: OsVersion = "24.04".parse().unwrap();
assert_eq!(ubuntu_2404.major(), 24);
let macos_sonoma: OsVersion = "14.6.1".parse().unwrap();
assert_eq!(macos_sonoma.major(), 14);
assert_eq!(macos_sonoma.minor(), 6);
assert_eq!(macos_sonoma.patch(), Some(1));
let win11: OsVersion = "10.0.22000".parse().unwrap();
assert_eq!(win11.major(), 10);
assert_eq!(win11.minor(), 0);
assert_eq!(win11.patch(), Some(22000));
}
}