Skip to main content

bare_types/sys/
kernel_version.rs

1//! Kernel version type for system information.
2//!
3//! This module provides a type-safe abstraction for kernel versions,
4//! ensuring valid version number parsing and comparison.
5//!
6//! Kernel versions typically follow semantic versioning (major.minor.patch)
7//! with an optional release/build string.
8//!
9//! # Examples
10//!
11//! ```rust
12//! use bare_types::sys::KernelVersion;
13//!
14//! // Parse from string
15//! let version: KernelVersion = "6.8.0-40-generic".parse()?;
16//!
17//! // Access components
18//! assert_eq!(version.major(), 6);
19//! assert_eq!(version.minor(), 8);
20//! assert_eq!(version.patch(), 0);
21//!
22//! // Get release string
23//! assert_eq!(version.release(), Some("40-generic"));
24//!
25//! // Compare versions
26//! assert!(version >= KernelVersion::new(6, 0, 0));
27//! # Ok::<(), bare_types::sys::KernelVersionError>(())
28//! ```
29use core::fmt;
30use core::str::FromStr;
31
32#[cfg(feature = "serde")]
33use serde::{Deserialize, Serialize};
34
35/// Error type for kernel version parsing.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
38pub enum KernelVersionError {
39    /// Empty version string
40    Empty,
41    /// Invalid major version number
42    InvalidMajor,
43    /// Invalid minor version number
44    InvalidMinor,
45    /// Invalid patch version number
46    InvalidPatch,
47    /// Too many numeric components (max 3)
48    TooManyComponents,
49    /// Not enough numeric components (need at least 2)
50    NotEnoughComponents,
51    /// Negative version number
52    NegativeVersion,
53}
54
55impl fmt::Display for KernelVersionError {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        match self {
58            Self::Empty => write!(f, "kernel version string is empty"),
59            Self::InvalidMajor => write!(f, "invalid major kernel version number"),
60            Self::InvalidMinor => write!(f, "invalid minor kernel version number"),
61            Self::InvalidPatch => write!(f, "invalid patch kernel version number"),
62            Self::TooManyComponents => {
63                write!(f, "kernel version has too many numeric components (max 3)")
64            }
65            Self::NotEnoughComponents => {
66                write!(f, "kernel version needs at least 2 numeric components")
67            }
68            Self::NegativeVersion => write!(f, "kernel version numbers cannot be negative"),
69        }
70    }
71}
72
73#[cfg(feature = "std")]
74impl std::error::Error for KernelVersionError {}
75
76/// Kernel version.
77///
78/// This type provides type-safe kernel version numbers with three numeric
79/// components (major, minor, patch) and an optional release string.
80///
81/// # Format
82///
83/// Kernel version format: `major.minor.patch[-release]`
84///
85/// - **Major**: Major version number (e.g., 6 for Linux 6.x)
86/// - **Minor**: Minor version number (e.g., 8 for Linux 6.8)
87/// - **Patch**: Patch level (e.g., 0 for Linux 6.8.0)
88/// - **Release**: Optional release string (e.g., "40-generic" for Ubuntu)
89///
90/// # Invariants
91///
92/// - Major, minor, and patch versions are always present (u16)
93/// - Release string is optional and must not contain leading '-'
94/// - All version numbers are non-negative
95///
96/// # Examples
97///
98/// ```rust
99/// use bare_types::sys::KernelVersion;
100///
101/// // Create from components
102/// let version = KernelVersion::new(6, 8, 0);
103///
104/// // With release string
105/// let version = KernelVersion::with_release(6, 8, 0, "40-generic");
106///
107/// // Parse from string
108/// let version: KernelVersion = "6.8.0-40-generic".parse()?;
109/// assert_eq!(version.major(), 6);
110/// assert_eq!(version.release(), Some("40-generic"));
111/// # Ok::<(), bare_types::sys::KernelVersionError>(())
112/// ```
113#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
114#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
115pub struct KernelVersion {
116    /// The major version number
117    major: u16,
118    /// The minor version number
119    minor: u16,
120    /// The patch version number
121    patch: u16,
122    /// The optional release/build string
123    release: Option<heapless::String<64>>,
124}
125
126impl KernelVersion {
127    /// Creates a new kernel version from components.
128    ///
129    /// # Arguments
130    ///
131    /// * `major` - The major version number
132    /// * `minor` - The minor version number
133    /// * `patch` - The patch version number
134    ///
135    /// To include a release string, use [`KernelVersion::with_release`].
136    ///
137    /// # Examples
138    ///
139    /// ```rust
140    /// use bare_types::sys::KernelVersion;
141    ///
142    /// // Simple version
143    /// let version = KernelVersion::new(6, 8, 0);
144    /// assert_eq!(version.major(), 6);
145    /// assert_eq!(version.minor(), 8);
146    /// assert_eq!(version.patch(), 0);
147    /// assert_eq!(version.release(), None);
148    ///
149    /// // With release string
150    /// let version = KernelVersion::with_release(6, 8, 0, "40-generic");
151    /// assert_eq!(version.release(), Some("40-generic"));
152    /// ```
153    #[must_use]
154    pub const fn new(major: u16, minor: u16, patch: u16) -> Self {
155        Self {
156            major,
157            minor,
158            patch,
159            release: None,
160        }
161    }
162
163    /// Creates a new kernel version with a release string.
164    ///
165    /// This is a convenience method for creating a kernel version with
166    /// a release/build string. For versions without a release string,
167    /// use [`KernelVersion::new`].
168    ///
169    /// # Examples
170    ///
171    /// ```rust
172    /// use bare_types::sys::KernelVersion;
173    ///
174    /// let version = KernelVersion::with_release(6, 8, 0, "40-generic");
175    /// assert_eq!(version.release(), Some("40-generic"));
176    /// ```
177    #[must_use]
178    pub fn with_release(major: u16, minor: u16, patch: u16, release: &str) -> Self {
179        let mut rel_str: heapless::String<64> = heapless::String::new();
180        // Ignore error, truncation is acceptable for release strings
181        let _ = rel_str.push_str(release);
182
183        Self {
184            major,
185            minor,
186            patch,
187            release: Some(rel_str),
188        }
189    }
190
191    /// Returns the major version number.
192    ///
193    /// # Examples
194    ///
195    /// ```rust
196    /// use bare_types::sys::KernelVersion;
197    ///
198    /// let version = KernelVersion::new(6, 8, 0);
199    /// assert_eq!(version.major(), 6);
200    /// ```
201    #[must_use]
202    #[inline]
203    pub const fn major(&self) -> u16 {
204        self.major
205    }
206
207    /// Returns the minor version number.
208    ///
209    /// # Examples
210    ///
211    /// ```rust
212    /// use bare_types::sys::KernelVersion;
213    ///
214    /// let version = KernelVersion::new(6, 8, 0);
215    /// assert_eq!(version.minor(), 8);
216    /// ```
217    #[must_use]
218    #[inline]
219    pub const fn minor(&self) -> u16 {
220        self.minor
221    }
222
223    /// Returns the patch version number.
224    ///
225    /// # Examples
226    ///
227    /// ```rust
228    /// use bare_types::sys::KernelVersion;
229    ///
230    /// let version = KernelVersion::new(6, 8, 0);
231    /// assert_eq!(version.patch(), 0);
232    /// ```
233    #[must_use]
234    #[inline]
235    pub const fn patch(&self) -> u16 {
236        self.patch
237    }
238
239    /// Returns the optional release string.
240    ///
241    /// # Examples
242    ///
243    /// ```rust
244    /// use bare_types::sys::KernelVersion;
245    ///
246    /// let version = KernelVersion::with_release(6, 8, 0, "40-generic");
247    /// assert_eq!(version.release(), Some("40-generic"));
248    ///
249    /// let version = KernelVersion::new(6, 8, 0);
250    /// assert_eq!(version.release(), None);
251    /// ```
252    #[must_use]
253    #[inline]
254    pub fn release(&self) -> Option<&str> {
255        self.release.as_deref()
256    }
257
258    /// Returns `true` if this is a development/pre-release kernel.
259    ///
260    /// A kernel is considered a development kernel if the minor version
261    /// is odd (Linux convention: odd minor = development, even = stable).
262    ///
263    /// # Examples
264    ///
265    /// ```rust
266    /// use bare_types::sys::KernelVersion;
267    ///
268    /// // Development kernels have odd minor versions
269    /// assert!(KernelVersion::new(6, 7, 0).is_development());
270    ///
271    /// // Stable kernels have even minor versions
272    /// assert!(!KernelVersion::new(6, 8, 0).is_development());
273    /// ```
274    #[must_use]
275    pub const fn is_development(&self) -> bool {
276        // Linux convention: odd minor = development, even = stable
277        self.minor % 2 == 1
278    }
279
280    /// Returns `true` if this is a stable kernel.
281    ///
282    /// A kernel is considered stable if the minor version is even
283    /// (Linux convention).
284    ///
285    /// # Examples
286    ///
287    /// ```rust
288    /// use bare_types::sys::KernelVersion;
289    ///
290    /// assert!(KernelVersion::new(6, 8, 0).is_stable());
291    /// assert!(!KernelVersion::new(6, 7, 0).is_stable());
292    /// ```
293    #[must_use]
294    pub const fn is_stable(&self) -> bool {
295        !self.is_development()
296    }
297
298    /// Returns `true` if this is a long-term support (LTS) kernel.
299    ///
300    /// This checks if the release string contains "lts" (case-insensitive).
301    /// Note: This is a heuristic and may not be accurate for all distros.
302    ///
303    /// # Examples
304    ///
305    /// ```rust
306    /// use bare_types::sys::KernelVersion;
307    ///
308    /// let lts = KernelVersion::with_release(6, 1, 0, "lts");
309    /// assert!(lts.is_lts());
310    ///
311    /// let regular = KernelVersion::new(6, 8, 0);
312    /// assert!(!regular.is_lts());
313    /// ```
314    #[must_use]
315    pub fn is_lts(&self) -> bool {
316        self.release
317            .as_ref()
318            .is_some_and(|r| r.to_lowercase().contains("lts"))
319    }
320
321    /// Returns a tuple of (major, minor, patch) for comparison.
322    ///
323    /// # Examples
324    ///
325    /// ```rust
326    /// use bare_types::sys::KernelVersion;
327    ///
328    /// let version = KernelVersion::new(6, 8, 0);
329    /// assert_eq!(version.as_tuple(), (6, 8, 0));
330    /// ```
331    #[must_use]
332    pub const fn as_tuple(&self) -> (u16, u16, u16) {
333        (self.major, self.minor, self.patch)
334    }
335
336    /// Returns a new version with the patch level incremented.
337    ///
338    /// # Examples
339    ///
340    /// ```rust
341    /// use bare_types::sys::KernelVersion;
342    ///
343    /// let version = KernelVersion::new(6, 8, 0);
344    /// let bumped = version.bump_patch();
345    /// assert_eq!(bumped.patch(), 1);
346    /// ```
347    #[must_use]
348    pub const fn bump_patch(&self) -> Self {
349        Self::new(self.major, self.minor, self.patch.saturating_add(1))
350    }
351
352    /// Returns a new version with the minor version incremented and patch reset.
353    ///
354    /// # Examples
355    ///
356    /// ```rust
357    /// use bare_types::sys::KernelVersion;
358    ///
359    /// let version = KernelVersion::new(6, 8, 5);
360    /// let bumped = version.bump_minor();
361    /// assert_eq!(bumped.minor(), 9);
362    /// assert_eq!(bumped.patch(), 0);
363    /// ```
364    #[must_use]
365    pub const fn bump_minor(&self) -> Self {
366        Self::new(self.major, self.minor.saturating_add(1), 0)
367    }
368
369    /// Returns a new version with the major version incremented and others reset.
370    ///
371    /// # Examples
372    ///
373    /// ```rust
374    /// use bare_types::sys::KernelVersion;
375    ///
376    /// let version = KernelVersion::new(6, 8, 5);
377    /// let bumped = version.bump_major();
378    /// assert_eq!(bumped.major(), 7);
379    /// assert_eq!(bumped.minor(), 0);
380    /// assert_eq!(bumped.patch(), 0);
381    /// ```
382    #[must_use]
383    pub const fn bump_major(&self) -> Self {
384        Self::new(self.major.saturating_add(1), 0, 0)
385    }
386
387    /// Returns a version without the release string.
388    ///
389    /// # Examples
390    ///
391    /// ```rust
392    /// use bare_types::sys::KernelVersion;
393    ///
394    /// let version = KernelVersion::with_release(6, 8, 0, "40-generic");
395    /// let without = version.without_release();
396    /// assert_eq!(without.release(), None);
397    /// assert_eq!(without.major(), 6);
398    /// ```
399    #[must_use]
400    pub const fn without_release(&self) -> Self {
401        Self::new(self.major, self.minor, self.patch)
402    }
403}
404
405impl FromStr for KernelVersion {
406    type Err = KernelVersionError;
407
408    fn from_str(s: &str) -> Result<Self, Self::Err> {
409        if s.is_empty() {
410            return Err(KernelVersionError::Empty);
411        }
412
413        // Split on '-' to separate version from release
414        let (version_part, release_part) = s
415            .find('-')
416            .map_or((s, None), |idx| (&s[..idx], Some(&s[idx + 1..])));
417
418        let parts: Vec<&str> = version_part.split('.').collect();
419
420        if parts.len() > 3 {
421            return Err(KernelVersionError::TooManyComponents);
422        }
423
424        if parts.len() < 2 {
425            return Err(KernelVersionError::NotEnoughComponents);
426        }
427
428        let major = parts[0]
429            .parse::<u16>()
430            .map_err(|_| KernelVersionError::InvalidMajor)?;
431
432        let minor = parts[1]
433            .parse::<u16>()
434            .map_err(|_| KernelVersionError::InvalidMinor)?;
435
436        let patch = if parts.len() > 2 {
437            parts[2]
438                .parse::<u16>()
439                .map_err(|_| KernelVersionError::InvalidPatch)?
440        } else {
441            0
442        };
443
444        let release = release_part.map(|r| {
445            let mut rel_str: heapless::String<64> = heapless::String::new();
446            // Truncate if necessary, ignoring errors
447            let _ = rel_str.push_str(r);
448            rel_str
449        });
450
451        Ok(Self {
452            major,
453            minor,
454            patch,
455            release,
456        })
457    }
458}
459
460impl TryFrom<&str> for KernelVersion {
461    type Error = KernelVersionError;
462
463    fn try_from(s: &str) -> Result<Self, Self::Error> {
464        s.parse()
465    }
466}
467
468impl fmt::Display for KernelVersion {
469    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
470        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
471        if let Some(release) = &self.release {
472            write!(f, "-{release}")?;
473        }
474        Ok(())
475    }
476}
477
478#[cfg(feature = "arbitrary")]
479impl<'a> arbitrary::Arbitrary<'a> for KernelVersion {
480    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
481        let major = u16::arbitrary(u)?;
482        let minor = u16::arbitrary(u)?;
483        let patch = u16::arbitrary(u)?;
484        let has_release = bool::arbitrary(u)?;
485
486        let release = if has_release {
487            // Generate a limited release string to avoid excessive allocations
488            let len = u.int_in_range(0..=32usize)?;
489            let mut s: heapless::String<64> = heapless::String::new();
490            for _ in 0..len {
491                let ch: char = char::arbitrary(u)?;
492                if ch.is_ascii_alphanumeric() || ch == '-' || ch == '.' || ch == '_' {
493                    let _ = s.push(ch);
494                }
495            }
496            Some(s)
497        } else {
498            None
499        };
500
501        Ok(Self {
502            major,
503            minor,
504            patch,
505            release,
506        })
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513
514    #[test]
515    fn test_new() {
516        let version = KernelVersion::new(6, 8, 0);
517        assert_eq!(version.major(), 6);
518        assert_eq!(version.minor(), 8);
519        assert_eq!(version.patch(), 0);
520        assert_eq!(version.release(), None);
521    }
522
523    #[test]
524    fn test_with_release() {
525        let version = KernelVersion::with_release(6, 8, 0, "40-generic");
526        assert_eq!(version.major(), 6);
527        assert_eq!(version.minor(), 8);
528        assert_eq!(version.patch(), 0);
529        assert_eq!(version.release(), Some("40-generic"));
530    }
531
532    #[test]
533    fn test_is_development() {
534        // Odd minor = development
535        assert!(KernelVersion::new(6, 7, 0).is_development());
536        assert!(KernelVersion::new(6, 9, 0).is_development());
537
538        // Even minor = stable
539        assert!(!KernelVersion::new(6, 8, 0).is_development());
540        assert!(!KernelVersion::new(6, 10, 0).is_development());
541    }
542
543    #[test]
544    fn test_is_stable() {
545        assert!(KernelVersion::new(6, 8, 0).is_stable());
546        assert!(!KernelVersion::new(6, 7, 0).is_stable());
547    }
548
549    #[test]
550    fn test_is_lts() {
551        let lts = KernelVersion::with_release(6, 1, 0, "lts");
552        assert!(lts.is_lts());
553
554        let lts_upper = KernelVersion::with_release(6, 1, 0, "LTS");
555        assert!(lts_upper.is_lts());
556
557        let regular = KernelVersion::new(6, 8, 0);
558        assert!(!regular.is_lts());
559
560        let generic = KernelVersion::with_release(6, 8, 0, "40-generic");
561        assert!(!generic.is_lts());
562    }
563
564    #[test]
565    fn test_as_tuple() {
566        let version = KernelVersion::new(6, 8, 5);
567        assert_eq!(version.as_tuple(), (6, 8, 5));
568    }
569
570    #[test]
571    fn test_bump_patch() {
572        let version = KernelVersion::new(6, 8, 0);
573        let bumped = version.bump_patch();
574        assert_eq!(bumped.patch(), 1);
575        assert_eq!(bumped.major(), 6);
576        assert_eq!(bumped.minor(), 8);
577    }
578
579    #[test]
580    fn test_bump_minor() {
581        let version = KernelVersion::new(6, 8, 5);
582        let bumped = version.bump_minor();
583        assert_eq!(bumped.major(), 6);
584        assert_eq!(bumped.minor(), 9);
585        assert_eq!(bumped.patch(), 0);
586    }
587
588    #[test]
589    fn test_bump_major() {
590        let version = KernelVersion::new(6, 8, 5);
591        let bumped = version.bump_major();
592        assert_eq!(bumped.major(), 7);
593        assert_eq!(bumped.minor(), 0);
594        assert_eq!(bumped.patch(), 0);
595    }
596
597    #[test]
598    fn test_without_release() {
599        let version = KernelVersion::with_release(6, 8, 0, "40-generic");
600        let without = version.without_release();
601        assert_eq!(without.release(), None);
602        assert_eq!(without.major(), 6);
603        assert_eq!(without.minor(), 8);
604        assert_eq!(without.patch(), 0);
605    }
606
607    #[test]
608    fn test_from_str_simple() {
609        let version: KernelVersion = "6.8.0".parse().unwrap();
610        assert_eq!(version.major(), 6);
611        assert_eq!(version.minor(), 8);
612        assert_eq!(version.patch(), 0);
613        assert_eq!(version.release(), None);
614    }
615
616    #[test]
617    fn test_from_str_with_release() {
618        let version: KernelVersion = "6.8.0-40-generic".parse().unwrap();
619        assert_eq!(version.major(), 6);
620        assert_eq!(version.minor(), 8);
621        assert_eq!(version.patch(), 0);
622        assert_eq!(version.release(), Some("40-generic"));
623    }
624
625    #[test]
626    fn test_from_str_two_components() {
627        let version: KernelVersion = "6.8".parse().unwrap();
628        assert_eq!(version.major(), 6);
629        assert_eq!(version.minor(), 8);
630        assert_eq!(version.patch(), 0);
631    }
632
633    #[test]
634    fn test_from_str_long_release() {
635        let version: KernelVersion = "5.15.0-1052-aws".parse().unwrap();
636        assert_eq!(version.major(), 5);
637        assert_eq!(version.minor(), 15);
638        assert_eq!(version.patch(), 0);
639        assert_eq!(version.release(), Some("1052-aws"));
640    }
641
642    #[test]
643    fn test_from_str_errors() {
644        // Empty
645        assert!(matches!(
646            "".parse::<KernelVersion>(),
647            Err(KernelVersionError::Empty)
648        ));
649
650        // Too many numeric components
651        assert!(matches!(
652            "1.2.3.4".parse::<KernelVersion>(),
653            Err(KernelVersionError::TooManyComponents)
654        ));
655
656        // Not enough components
657        assert!(matches!(
658            "6".parse::<KernelVersion>(),
659            Err(KernelVersionError::NotEnoughComponents)
660        ));
661
662        // Invalid numbers
663        assert!("abc.def".parse::<KernelVersion>().is_err());
664        assert!("6.abc".parse::<KernelVersion>().is_err());
665        assert!("6.8.abc".parse::<KernelVersion>().is_err());
666    }
667
668    #[test]
669    fn test_display() {
670        let version = KernelVersion::new(6, 8, 0);
671        assert_eq!(format!("{}", version), "6.8.0");
672
673        let version = KernelVersion::with_release(6, 8, 0, "40-generic");
674        assert_eq!(format!("{}", version), "6.8.0-40-generic");
675    }
676
677    #[test]
678    fn test_equality() {
679        let v1 = KernelVersion::new(6, 8, 0);
680        let v2 = KernelVersion::new(6, 8, 0);
681        let v3 = KernelVersion::with_release(6, 8, 0, "40-generic");
682
683        assert_eq!(v1, v2);
684        assert_ne!(v1, v3);
685    }
686
687    #[test]
688    fn test_ordering() {
689        let v1 = KernelVersion::new(6, 8, 0);
690        let v2 = KernelVersion::new(6, 8, 1);
691        let v3 = KernelVersion::new(6, 9, 0);
692        let v4 = KernelVersion::new(7, 0, 0);
693
694        assert!(v1 < v2);
695        assert!(v2 < v3);
696        assert!(v3 < v4);
697
698        // Release string affects ordering because KernelVersion derives PartialOrd
699        // Two versions with different release strings are not equal
700        let with_release = KernelVersion::with_release(6, 8, 0, "40-generic");
701        let without_release = KernelVersion::new(6, 8, 0);
702        // Version numbers are the same, but release strings differ
703        assert_ne!(with_release, without_release);
704    }
705
706    #[test]
707    fn test_clone() {
708        let version = KernelVersion::with_release(6, 8, 0, "40-generic");
709        let version2 = version.clone();
710        assert_eq!(version, version2);
711    }
712
713    #[test]
714    fn test_common_kernel_versions() {
715        // Linux kernel versions
716        let linux_6_8: KernelVersion = "6.8.0".parse().unwrap();
717        assert_eq!(linux_6_8.major(), 6);
718        assert!(linux_6_8.is_stable());
719
720        let linux_5_15: KernelVersion = "5.15.0".parse().unwrap();
721        assert_eq!(linux_5_15.major(), 5);
722
723        // Ubuntu kernel
724        let ubuntu: KernelVersion = "6.8.0-40-generic".parse().unwrap();
725        assert_eq!(ubuntu.major(), 6);
726        assert_eq!(ubuntu.release(), Some("40-generic"));
727
728        // AWS kernel
729        let aws: KernelVersion = "5.15.0-1052-aws".parse().unwrap();
730        assert_eq!(aws.major(), 5);
731        assert_eq!(aws.release(), Some("1052-aws"));
732    }
733}