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 DistroError {
Empty,
TooLong(usize),
InvalidFirstCharacter,
InvalidCharacter,
}
impl fmt::Display for DistroError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "distribution name cannot be empty"),
Self::TooLong(len) => write!(
f,
"distribution name too long (got {len}, max 64 characters)"
),
Self::InvalidFirstCharacter => {
write!(
f,
"distribution name must start with an alphanumeric character"
)
}
Self::InvalidCharacter => write!(f, "distribution name contains invalid character"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for DistroError {}
#[repr(transparent)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Distro(heapless::String<64>);
impl Distro {
pub const MAX_LEN: usize = 64;
pub fn new(s: &str) -> Result<Self, DistroError> {
Self::validate(s)?;
let mut value = heapless::String::new();
value
.push_str(s)
.map_err(|_| DistroError::TooLong(s.len()))?;
Ok(Self(value))
}
fn validate(s: &str) -> Result<(), DistroError> {
if s.is_empty() {
return Err(DistroError::Empty);
}
if s.len() > Self::MAX_LEN {
return Err(DistroError::TooLong(s.len()));
}
let mut chars = s.chars();
if let Some(first) = chars.next()
&& !first.is_ascii_alphanumeric()
{
return Err(DistroError::InvalidFirstCharacter);
}
for ch in chars {
if !ch.is_ascii_alphanumeric() && ch != ' ' && ch != '-' && ch != '_' {
return Err(DistroError::InvalidCharacter);
}
}
Ok(())
}
#[must_use]
#[inline]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
#[inline]
pub const fn as_inner(&self) -> &heapless::String<64> {
&self.0
}
#[must_use]
#[inline]
pub fn into_inner(self) -> heapless::String<64> {
self.0
}
#[must_use]
#[inline]
pub fn is_debian_based(&self) -> bool {
let s = self.0.to_lowercase();
s.contains("debian")
|| s.contains("ubuntu")
|| s.contains("mint")
|| s.contains("pop")
|| s.contains("kali")
}
#[must_use]
#[inline]
pub fn is_redhat_based(&self) -> bool {
let s = self.0.to_lowercase();
s.contains("red hat")
|| s.contains("redhat")
|| s.contains("fedora")
|| s.contains("centos")
|| s.contains("rhel")
|| s.contains("rocky")
|| s.contains("almalinux")
}
#[must_use]
#[inline]
pub fn is_arch_based(&self) -> bool {
let s = self.0.to_lowercase();
s.contains("arch") || s.contains("manjaro") || s.contains("endeavouros")
}
#[must_use]
#[inline]
pub fn is_rolling_release(&self) -> bool {
let s = self.0.to_lowercase();
s.contains("arch")
|| s.contains("gentoo")
|| s.contains("fedora")
|| s.contains("void")
|| s.contains("sid")
}
#[must_use]
#[inline]
pub fn is_lts(&self) -> bool {
let s = self.0.to_lowercase();
s.contains("lts")
|| s.contains("ubuntu")
|| s.contains("debian")
|| s.contains("rhel")
|| s.contains("rocky")
|| s.contains("almalinux")
}
}
impl AsRef<str> for Distro {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl TryFrom<&str> for Distro {
type Error = DistroError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::new(s)
}
}
impl FromStr for Distro {
type Err = DistroError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl fmt::Display for Distro {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for Distro {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
const DIGITS: &[u8] = b"0123456789";
let len = 1 + (u8::arbitrary(u)? % 64).min(63);
let mut inner = heapless::String::<64>::new();
let first_byte = u8::arbitrary(u)?;
if first_byte % 2 == 0 {
let first = ALPHABET[(first_byte % 26) as usize] as char;
inner
.push(first)
.map_err(|_| arbitrary::Error::IncorrectFormat)?;
} else {
let first = DIGITS[(first_byte % 10) as usize] as char;
inner
.push(first)
.map_err(|_| arbitrary::Error::IncorrectFormat)?;
}
for _ in 1..len {
let byte = u8::arbitrary(u)?;
let c = match byte % 5 {
0 => ALPHABET[((byte >> 2) % 26) as usize] as char,
1 => DIGITS[((byte >> 2) % 10) as usize] as char,
2 => ' ',
3 => '-',
_ => '_',
};
inner
.push(c)
.map_err(|_| arbitrary::Error::IncorrectFormat)?;
}
Ok(Self(inner))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_valid() {
let distro = Distro::new("Ubuntu").unwrap();
assert_eq!(distro.as_str(), "Ubuntu");
}
#[test]
fn test_new_empty() {
assert!(matches!(Distro::new(""), Err(DistroError::Empty)));
}
#[test]
fn test_new_too_long() {
let long_name = "a".repeat(65);
assert!(matches!(
Distro::new(&long_name),
Err(DistroError::TooLong(65))
));
}
#[test]
fn test_new_invalid_first_character() {
assert!(matches!(
Distro::new("-Ubuntu"),
Err(DistroError::InvalidFirstCharacter)
));
assert!(matches!(
Distro::new(" Ubuntu"),
Err(DistroError::InvalidFirstCharacter)
));
}
#[test]
fn test_new_invalid_character() {
assert!(matches!(
Distro::new("Ubuntu@"),
Err(DistroError::InvalidCharacter)
));
assert!(matches!(
Distro::new("Ubuntu.Distro"),
Err(DistroError::InvalidCharacter)
));
}
#[test]
fn test_is_debian_based() {
let ubuntu = Distro::new("Ubuntu").unwrap();
assert!(ubuntu.is_debian_based());
let debian = Distro::new("Debian").unwrap();
assert!(debian.is_debian_based());
let mint = Distro::new("Linux Mint").unwrap();
assert!(mint.is_debian_based());
let fedora = Distro::new("Fedora").unwrap();
assert!(!fedora.is_debian_based());
}
#[test]
fn test_is_redhat_based() {
let fedora = Distro::new("Fedora").unwrap();
assert!(fedora.is_redhat_based());
let centos = Distro::new("CentOS").unwrap();
assert!(centos.is_redhat_based());
let rhel = Distro::new("RHEL").unwrap();
assert!(rhel.is_redhat_based());
let ubuntu = Distro::new("Ubuntu").unwrap();
assert!(!ubuntu.is_redhat_based());
}
#[test]
fn test_is_arch_based() {
let arch = Distro::new("Arch Linux").unwrap();
assert!(arch.is_arch_based());
let manjaro = Distro::new("Manjaro").unwrap();
assert!(manjaro.is_arch_based());
let ubuntu = Distro::new("Ubuntu").unwrap();
assert!(!ubuntu.is_arch_based());
}
#[test]
fn test_is_rolling_release() {
let arch = Distro::new("Arch Linux").unwrap();
assert!(arch.is_rolling_release());
let gentoo = Distro::new("Gentoo").unwrap();
assert!(gentoo.is_rolling_release());
let fedora = Distro::new("Fedora").unwrap();
assert!(fedora.is_rolling_release());
let ubuntu = Distro::new("Ubuntu").unwrap();
assert!(!ubuntu.is_rolling_release());
}
#[test]
fn test_is_lts() {
let ubuntu = Distro::new("Ubuntu LTS").unwrap();
assert!(ubuntu.is_lts());
let debian = Distro::new("Debian").unwrap();
assert!(debian.is_lts());
let fedora = Distro::new("Fedora").unwrap();
assert!(!fedora.is_lts());
}
#[test]
fn test_from_str() {
let distro: Distro = "Ubuntu".parse().unwrap();
assert_eq!(distro.as_str(), "Ubuntu");
}
#[test]
fn test_from_str_error() {
assert!("".parse::<Distro>().is_err());
assert!("-Ubuntu".parse::<Distro>().is_err());
}
#[test]
fn test_display() {
let distro = Distro::new("Ubuntu").unwrap();
assert_eq!(format!("{}", distro), "Ubuntu");
}
#[test]
fn test_as_ref() {
let distro = Distro::new("Ubuntu").unwrap();
let s: &str = distro.as_ref();
assert_eq!(s, "Ubuntu");
}
#[test]
fn test_clone() {
let distro = Distro::new("Ubuntu").unwrap();
let distro2 = distro.clone();
assert_eq!(distro, distro2);
}
#[test]
fn test_equality() {
let d1 = Distro::new("Ubuntu").unwrap();
let d2 = Distro::new("Ubuntu").unwrap();
let d3 = Distro::new("Fedora").unwrap();
assert_eq!(d1, d2);
assert_ne!(d1, d3);
}
}