Skip to main content

bare_types/sys/
distro.rs

1//! OS distribution type for system information.
2//!
3//! This module provides a type-safe abstraction for OS distribution names,
4//! ensuring valid distribution name strings.
5//!
6//! # Distribution Name Format
7//!
8//! OS distribution names follow these rules:
9//!
10//! - Must be 1-64 characters
11//! - Must start with an alphanumeric character
12//! - Can contain alphanumeric characters, spaces, hyphens, and underscores
13//!
14//! # Examples
15//!
16//! ```rust
17//! use bare_types::sys::Distro;
18//!
19//! // Parse from string
20//! let distro: Distro = "Ubuntu".parse()?;
21//!
22//! // Access as string
23//! assert_eq!(distro.as_str(), "Ubuntu");
24//! # Ok::<(), bare_types::sys::DistroError>(())
25//! ```
26
27use core::fmt;
28use core::str::FromStr;
29
30#[cfg(feature = "serde")]
31use serde::{Deserialize, Serialize};
32
33/// Error type for distribution name parsing.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
36pub enum DistroError {
37    /// Empty distribution name
38    Empty,
39    /// Distribution name too long (max 64 characters)
40    TooLong(usize),
41    /// Invalid first character (must be alphanumeric)
42    InvalidFirstCharacter,
43    /// Invalid character in distribution name
44    InvalidCharacter,
45}
46
47impl fmt::Display for DistroError {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            Self::Empty => write!(f, "distribution name cannot be empty"),
51            Self::TooLong(len) => write!(
52                f,
53                "distribution name too long (got {len}, max 64 characters)"
54            ),
55            Self::InvalidFirstCharacter => {
56                write!(
57                    f,
58                    "distribution name must start with an alphanumeric character"
59                )
60            }
61            Self::InvalidCharacter => write!(f, "distribution name contains invalid character"),
62        }
63    }
64}
65
66#[cfg(feature = "std")]
67impl std::error::Error for DistroError {}
68
69/// OS distribution name.
70///
71/// This type provides type-safe OS distribution names.
72/// It uses the newtype pattern with `#[repr(transparent)]` for zero-cost abstraction.
73#[repr(transparent)]
74#[derive(Debug, Clone, PartialEq, Eq, Hash)]
75#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
76pub struct Distro(heapless::String<64>);
77
78impl Distro {
79    /// Maximum length of a distribution name
80    pub const MAX_LEN: usize = 64;
81
82    /// Creates a new distribution name from a string.
83    ///
84    /// # Errors
85    ///
86    /// Returns an error `DistroError` if the string is not a valid distribution name.
87    pub fn new(s: &str) -> Result<Self, DistroError> {
88        Self::validate(s)?;
89        let mut value = heapless::String::new();
90        value
91            .push_str(s)
92            .map_err(|_| DistroError::TooLong(s.len()))?;
93        Ok(Self(value))
94    }
95
96    /// Validates a distribution name string.
97    fn validate(s: &str) -> Result<(), DistroError> {
98        if s.is_empty() {
99            return Err(DistroError::Empty);
100        }
101        if s.len() > Self::MAX_LEN {
102            return Err(DistroError::TooLong(s.len()));
103        }
104        let mut chars = s.chars();
105        if let Some(first) = chars.next() {
106            if !first.is_ascii_alphanumeric() {
107                return Err(DistroError::InvalidFirstCharacter);
108            }
109        }
110        for ch in chars {
111            if !ch.is_ascii_alphanumeric() && ch != ' ' && ch != '-' && ch != '_' {
112                return Err(DistroError::InvalidCharacter);
113            }
114        }
115        Ok(())
116    }
117
118    /// Returns the distribution name as a string slice.
119    #[must_use]
120    #[inline]
121    pub fn as_str(&self) -> &str {
122        &self.0
123    }
124
125    /// Returns a reference to the underlying `heapless::String`.
126    #[must_use]
127    #[inline]
128    pub const fn as_inner(&self) -> &heapless::String<64> {
129        &self.0
130    }
131
132    /// Consumes this distribution name and returns the underlying string.
133    #[must_use]
134    #[inline]
135    pub fn into_inner(self) -> heapless::String<64> {
136        self.0
137    }
138
139    /// Returns `true` if this is a Debian-based distribution.
140    ///
141    /// This includes Debian, Ubuntu, Linux Mint, Pop!_OS, Kali Linux, etc.
142    #[must_use]
143    pub fn is_debian_based(&self) -> bool {
144        let s = self.0.to_lowercase();
145        s.contains("debian")
146            || s.contains("ubuntu")
147            || s.contains("mint")
148            || s.contains("pop")
149            || s.contains("kali")
150    }
151
152    /// Returns `true` if this is a Red Hat-based distribution.
153    ///
154    /// This includes Red Hat Enterprise Linux, Fedora, `CentOS`, Rocky Linux, `AlmaLinux`, etc.
155    #[must_use]
156    pub fn is_redhat_based(&self) -> bool {
157        let s = self.0.to_lowercase();
158        s.contains("red hat")
159            || s.contains("redhat")
160            || s.contains("fedora")
161            || s.contains("centos")
162            || s.contains("rhel")
163            || s.contains("rocky")
164            || s.contains("almalinux")
165    }
166
167    /// Returns `true` if this is an Arch-based distribution.
168    ///
169    /// This includes Arch Linux, Manjaro, `EndeavourOS`, etc.
170    #[must_use]
171    pub fn is_arch_based(&self) -> bool {
172        let s = self.0.to_lowercase();
173        s.contains("arch") || s.contains("manjaro") || s.contains("endeavouros")
174    }
175
176    /// Returns `true` if this is a rolling release distribution.
177    ///
178    /// This includes Arch Linux, Gentoo, Fedora, Void Linux, Debian Sid, etc.
179    #[must_use]
180    pub fn is_rolling_release(&self) -> bool {
181        let s = self.0.to_lowercase();
182        s.contains("arch")
183            || s.contains("gentoo")
184            || s.contains("fedora")
185            || s.contains("void")
186            || s.contains("sid")
187    }
188
189    /// Returns `true` if this is a long-term support (LTS) distribution.
190    ///
191    /// This includes Ubuntu LTS, Debian, RHEL, Rocky Linux, `AlmaLinux`, etc.
192    #[must_use]
193    pub fn is_lts(&self) -> bool {
194        let s = self.0.to_lowercase();
195        s.contains("lts")
196            || s.contains("ubuntu")
197            || s.contains("debian")
198            || s.contains("rhel")
199            || s.contains("rocky")
200            || s.contains("almalinux")
201    }
202}
203
204impl AsRef<str> for Distro {
205    fn as_ref(&self) -> &str {
206        self.as_str()
207    }
208}
209
210impl TryFrom<&str> for Distro {
211    type Error = DistroError;
212
213    fn try_from(s: &str) -> Result<Self, Self::Error> {
214        Self::new(s)
215    }
216}
217
218impl FromStr for Distro {
219    type Err = DistroError;
220
221    fn from_str(s: &str) -> Result<Self, Self::Err> {
222        Self::new(s)
223    }
224}
225
226impl fmt::Display for Distro {
227    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228        write!(f, "{}", self.0)
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn test_new_valid() {
238        let distro = Distro::new("Ubuntu").unwrap();
239        assert_eq!(distro.as_str(), "Ubuntu");
240    }
241
242    #[test]
243    fn test_new_empty() {
244        assert!(matches!(Distro::new(""), Err(DistroError::Empty)));
245    }
246
247    #[test]
248    fn test_new_too_long() {
249        let long_name = "a".repeat(65);
250        assert!(matches!(
251            Distro::new(&long_name),
252            Err(DistroError::TooLong(65))
253        ));
254    }
255
256    #[test]
257    fn test_new_invalid_first_character() {
258        assert!(matches!(
259            Distro::new("-Ubuntu"),
260            Err(DistroError::InvalidFirstCharacter)
261        ));
262        assert!(matches!(
263            Distro::new(" Ubuntu"),
264            Err(DistroError::InvalidFirstCharacter)
265        ));
266    }
267
268    #[test]
269    fn test_new_invalid_character() {
270        assert!(matches!(
271            Distro::new("Ubuntu@"),
272            Err(DistroError::InvalidCharacter)
273        ));
274        assert!(matches!(
275            Distro::new("Ubuntu.Distro"),
276            Err(DistroError::InvalidCharacter)
277        ));
278    }
279
280    #[test]
281    fn test_is_debian_based() {
282        let ubuntu = Distro::new("Ubuntu").unwrap();
283        assert!(ubuntu.is_debian_based());
284        let debian = Distro::new("Debian").unwrap();
285        assert!(debian.is_debian_based());
286        let mint = Distro::new("Linux Mint").unwrap();
287        assert!(mint.is_debian_based());
288        let fedora = Distro::new("Fedora").unwrap();
289        assert!(!fedora.is_debian_based());
290    }
291
292    #[test]
293    fn test_is_redhat_based() {
294        let fedora = Distro::new("Fedora").unwrap();
295        assert!(fedora.is_redhat_based());
296        let centos = Distro::new("CentOS").unwrap();
297        assert!(centos.is_redhat_based());
298        let rhel = Distro::new("RHEL").unwrap();
299        assert!(rhel.is_redhat_based());
300        let ubuntu = Distro::new("Ubuntu").unwrap();
301        assert!(!ubuntu.is_redhat_based());
302    }
303
304    #[test]
305    fn test_is_arch_based() {
306        let arch = Distro::new("Arch Linux").unwrap();
307        assert!(arch.is_arch_based());
308        let manjaro = Distro::new("Manjaro").unwrap();
309        assert!(manjaro.is_arch_based());
310        let ubuntu = Distro::new("Ubuntu").unwrap();
311        assert!(!ubuntu.is_arch_based());
312    }
313
314    #[test]
315    fn test_is_rolling_release() {
316        let arch = Distro::new("Arch Linux").unwrap();
317        assert!(arch.is_rolling_release());
318        let gentoo = Distro::new("Gentoo").unwrap();
319        assert!(gentoo.is_rolling_release());
320        let fedora = Distro::new("Fedora").unwrap();
321        assert!(fedora.is_rolling_release());
322        let ubuntu = Distro::new("Ubuntu").unwrap();
323        assert!(!ubuntu.is_rolling_release());
324    }
325
326    #[test]
327    fn test_is_lts() {
328        let ubuntu = Distro::new("Ubuntu LTS").unwrap();
329        assert!(ubuntu.is_lts());
330        let debian = Distro::new("Debian").unwrap();
331        assert!(debian.is_lts());
332        let fedora = Distro::new("Fedora").unwrap();
333        assert!(!fedora.is_lts());
334    }
335
336    #[test]
337    fn test_from_str() {
338        let distro: Distro = "Ubuntu".parse().unwrap();
339        assert_eq!(distro.as_str(), "Ubuntu");
340    }
341
342    #[test]
343    fn test_from_str_error() {
344        assert!("".parse::<Distro>().is_err());
345        assert!("-Ubuntu".parse::<Distro>().is_err());
346    }
347
348    #[test]
349    fn test_display() {
350        let distro = Distro::new("Ubuntu").unwrap();
351        assert_eq!(format!("{}", distro), "Ubuntu");
352    }
353
354    #[test]
355    fn test_as_ref() {
356        let distro = Distro::new("Ubuntu").unwrap();
357        let s: &str = distro.as_ref();
358        assert_eq!(s, "Ubuntu");
359    }
360
361    #[test]
362    fn test_clone() {
363        let distro = Distro::new("Ubuntu").unwrap();
364        let distro2 = distro.clone();
365        assert_eq!(distro, distro2);
366    }
367
368    #[test]
369    fn test_equality() {
370        let d1 = Distro::new("Ubuntu").unwrap();
371        let d2 = Distro::new("Ubuntu").unwrap();
372        let d3 = Distro::new("Fedora").unwrap();
373        assert_eq!(d1, d2);
374        assert_ne!(d1, d3);
375    }
376}