use core::fmt;
use core::str::FromStr;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[non_exhaustive]
pub enum KernelVersionError {
Empty,
InvalidMajor,
InvalidMinor,
InvalidPatch,
TooManyComponents,
NotEnoughComponents,
NegativeVersion,
}
impl fmt::Display for KernelVersionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "kernel version string is empty"),
Self::InvalidMajor => write!(f, "invalid major kernel version number"),
Self::InvalidMinor => write!(f, "invalid minor kernel version number"),
Self::InvalidPatch => write!(f, "invalid patch kernel version number"),
Self::TooManyComponents => {
write!(f, "kernel version has too many numeric components (max 3)")
}
Self::NotEnoughComponents => {
write!(f, "kernel version needs at least 2 numeric components")
}
Self::NegativeVersion => write!(f, "kernel version numbers cannot be negative"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for KernelVersionError {}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct KernelVersion {
major: u16,
minor: u16,
patch: u16,
release: Option<heapless::String<64>>,
}
impl KernelVersion {
#[must_use]
pub const fn new(major: u16, minor: u16, patch: u16) -> Self {
Self {
major,
minor,
patch,
release: None,
}
}
#[must_use]
pub fn with_release(major: u16, minor: u16, patch: u16, release: &str) -> Self {
let mut rel_str: heapless::String<64> = heapless::String::new();
let _ = rel_str.push_str(release);
Self {
major,
minor,
patch,
release: Some(rel_str),
}
}
#[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) -> u16 {
self.patch
}
#[must_use]
#[inline]
pub fn release(&self) -> Option<&str> {
self.release.as_deref()
}
#[must_use]
pub const fn is_development(&self) -> bool {
self.minor % 2 == 1
}
#[must_use]
pub const fn is_stable(&self) -> bool {
!self.is_development()
}
#[must_use]
#[inline]
pub fn is_lts(&self) -> bool {
self.release
.as_ref()
.is_some_and(|r| r.to_lowercase().contains("lts"))
}
#[must_use]
pub const fn as_tuple(&self) -> (u16, u16, u16) {
(self.major, self.minor, self.patch)
}
#[must_use]
pub const fn bump_patch(&self) -> Self {
Self::new(self.major, self.minor, self.patch.saturating_add(1))
}
#[must_use]
pub const fn bump_minor(&self) -> Self {
Self::new(self.major, self.minor.saturating_add(1), 0)
}
#[must_use]
pub const fn bump_major(&self) -> Self {
Self::new(self.major.saturating_add(1), 0, 0)
}
#[must_use]
pub const fn without_release(&self) -> Self {
Self::new(self.major, self.minor, self.patch)
}
}
impl FromStr for KernelVersion {
type Err = KernelVersionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Err(KernelVersionError::Empty);
}
let (version_part, release_part) = s
.find('-')
.map_or((s, None), |idx| (&s[..idx], Some(&s[idx + 1..])));
let parts: Vec<&str> = version_part.split('.').collect();
if parts.len() > 3 {
return Err(KernelVersionError::TooManyComponents);
}
if parts.len() < 2 {
return Err(KernelVersionError::NotEnoughComponents);
}
let major = parts[0]
.parse::<u16>()
.map_err(|_| KernelVersionError::InvalidMajor)?;
let minor = parts[1]
.parse::<u16>()
.map_err(|_| KernelVersionError::InvalidMinor)?;
let patch = if parts.len() > 2 {
parts[2]
.parse::<u16>()
.map_err(|_| KernelVersionError::InvalidPatch)?
} else {
0
};
let release = release_part.map(|r| {
let mut rel_str: heapless::String<64> = heapless::String::new();
let _ = rel_str.push_str(r);
rel_str
});
Ok(Self {
major,
minor,
patch,
release,
})
}
}
impl TryFrom<&str> for KernelVersion {
type Error = KernelVersionError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
s.parse()
}
}
impl fmt::Display for KernelVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
if let Some(release) = &self.release {
write!(f, "-{release}")?;
}
Ok(())
}
}
#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for KernelVersion {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let major = u16::arbitrary(u)?;
let minor = u16::arbitrary(u)?;
let patch = u16::arbitrary(u)?;
let has_release = bool::arbitrary(u)?;
let release = if has_release {
let len = u.int_in_range(0..=32usize)?;
let mut s: heapless::String<64> = heapless::String::new();
for _ in 0..len {
let ch: char = char::arbitrary(u)?;
if ch.is_ascii_alphanumeric() || ch == '-' || ch == '.' || ch == '_' {
let _ = s.push(ch);
}
}
Some(s)
} else {
None
};
Ok(Self {
major,
minor,
patch,
release,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let version = KernelVersion::new(6, 8, 0);
assert_eq!(version.major(), 6);
assert_eq!(version.minor(), 8);
assert_eq!(version.patch(), 0);
assert_eq!(version.release(), None);
}
#[test]
fn test_with_release() {
let version = KernelVersion::with_release(6, 8, 0, "40-generic");
assert_eq!(version.major(), 6);
assert_eq!(version.minor(), 8);
assert_eq!(version.patch(), 0);
assert_eq!(version.release(), Some("40-generic"));
}
#[test]
fn test_is_development() {
assert!(KernelVersion::new(6, 7, 0).is_development());
assert!(KernelVersion::new(6, 9, 0).is_development());
assert!(!KernelVersion::new(6, 8, 0).is_development());
assert!(!KernelVersion::new(6, 10, 0).is_development());
}
#[test]
fn test_is_stable() {
assert!(KernelVersion::new(6, 8, 0).is_stable());
assert!(!KernelVersion::new(6, 7, 0).is_stable());
}
#[test]
fn test_is_lts() {
let lts = KernelVersion::with_release(6, 1, 0, "lts");
assert!(lts.is_lts());
let lts_upper = KernelVersion::with_release(6, 1, 0, "LTS");
assert!(lts_upper.is_lts());
let regular = KernelVersion::new(6, 8, 0);
assert!(!regular.is_lts());
let generic = KernelVersion::with_release(6, 8, 0, "40-generic");
assert!(!generic.is_lts());
}
#[test]
fn test_as_tuple() {
let version = KernelVersion::new(6, 8, 5);
assert_eq!(version.as_tuple(), (6, 8, 5));
}
#[test]
fn test_bump_patch() {
let version = KernelVersion::new(6, 8, 0);
let bumped = version.bump_patch();
assert_eq!(bumped.patch(), 1);
assert_eq!(bumped.major(), 6);
assert_eq!(bumped.minor(), 8);
}
#[test]
fn test_bump_minor() {
let version = KernelVersion::new(6, 8, 5);
let bumped = version.bump_minor();
assert_eq!(bumped.major(), 6);
assert_eq!(bumped.minor(), 9);
assert_eq!(bumped.patch(), 0);
}
#[test]
fn test_bump_major() {
let version = KernelVersion::new(6, 8, 5);
let bumped = version.bump_major();
assert_eq!(bumped.major(), 7);
assert_eq!(bumped.minor(), 0);
assert_eq!(bumped.patch(), 0);
}
#[test]
fn test_without_release() {
let version = KernelVersion::with_release(6, 8, 0, "40-generic");
let without = version.without_release();
assert_eq!(without.release(), None);
assert_eq!(without.major(), 6);
assert_eq!(without.minor(), 8);
assert_eq!(without.patch(), 0);
}
#[test]
fn test_from_str_simple() {
let version: KernelVersion = "6.8.0".parse().unwrap();
assert_eq!(version.major(), 6);
assert_eq!(version.minor(), 8);
assert_eq!(version.patch(), 0);
assert_eq!(version.release(), None);
}
#[test]
fn test_from_str_with_release() {
let version: KernelVersion = "6.8.0-40-generic".parse().unwrap();
assert_eq!(version.major(), 6);
assert_eq!(version.minor(), 8);
assert_eq!(version.patch(), 0);
assert_eq!(version.release(), Some("40-generic"));
}
#[test]
fn test_from_str_two_components() {
let version: KernelVersion = "6.8".parse().unwrap();
assert_eq!(version.major(), 6);
assert_eq!(version.minor(), 8);
assert_eq!(version.patch(), 0);
}
#[test]
fn test_from_str_long_release() {
let version: KernelVersion = "5.15.0-1052-aws".parse().unwrap();
assert_eq!(version.major(), 5);
assert_eq!(version.minor(), 15);
assert_eq!(version.patch(), 0);
assert_eq!(version.release(), Some("1052-aws"));
}
#[test]
fn test_from_str_errors() {
assert!(matches!(
"".parse::<KernelVersion>(),
Err(KernelVersionError::Empty)
));
assert!(matches!(
"1.2.3.4".parse::<KernelVersion>(),
Err(KernelVersionError::TooManyComponents)
));
assert!(matches!(
"6".parse::<KernelVersion>(),
Err(KernelVersionError::NotEnoughComponents)
));
assert!("abc.def".parse::<KernelVersion>().is_err());
assert!("6.abc".parse::<KernelVersion>().is_err());
assert!("6.8.abc".parse::<KernelVersion>().is_err());
}
#[test]
fn test_display() {
let version = KernelVersion::new(6, 8, 0);
assert_eq!(format!("{}", version), "6.8.0");
let version = KernelVersion::with_release(6, 8, 0, "40-generic");
assert_eq!(format!("{}", version), "6.8.0-40-generic");
}
#[test]
fn test_equality() {
let v1 = KernelVersion::new(6, 8, 0);
let v2 = KernelVersion::new(6, 8, 0);
let v3 = KernelVersion::with_release(6, 8, 0, "40-generic");
assert_eq!(v1, v2);
assert_ne!(v1, v3);
}
#[test]
fn test_ordering() {
let v1 = KernelVersion::new(6, 8, 0);
let v2 = KernelVersion::new(6, 8, 1);
let v3 = KernelVersion::new(6, 9, 0);
let v4 = KernelVersion::new(7, 0, 0);
assert!(v1 < v2);
assert!(v2 < v3);
assert!(v3 < v4);
let with_release = KernelVersion::with_release(6, 8, 0, "40-generic");
let without_release = KernelVersion::new(6, 8, 0);
assert_ne!(with_release, without_release);
}
#[test]
fn test_clone() {
let version = KernelVersion::with_release(6, 8, 0, "40-generic");
let version2 = version.clone();
assert_eq!(version, version2);
}
#[test]
fn test_common_kernel_versions() {
let linux_6_8: KernelVersion = "6.8.0".parse().unwrap();
assert_eq!(linux_6_8.major(), 6);
assert!(linux_6_8.is_stable());
let linux_5_15: KernelVersion = "5.15.0".parse().unwrap();
assert_eq!(linux_5_15.major(), 5);
let ubuntu: KernelVersion = "6.8.0-40-generic".parse().unwrap();
assert_eq!(ubuntu.major(), 6);
assert_eq!(ubuntu.release(), Some("40-generic"));
let aws: KernelVersion = "5.15.0-1052-aws".parse().unwrap();
assert_eq!(aws.major(), 5);
assert_eq!(aws.release(), Some("1052-aws"));
}
}