1use core::fmt;
28use core::str::FromStr;
29
30#[cfg(feature = "serde")]
31use serde::{Deserialize, Serialize};
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
36#[non_exhaustive]
37pub enum DistroError {
38 Empty,
42 TooLong(usize),
47 InvalidFirstCharacter,
52 InvalidCharacter,
57}
58
59impl fmt::Display for DistroError {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 match self {
62 Self::Empty => write!(f, "distribution name cannot be empty"),
63 Self::TooLong(len) => write!(
64 f,
65 "distribution name too long (got {len}, max 64 characters)"
66 ),
67 Self::InvalidFirstCharacter => {
68 write!(
69 f,
70 "distribution name must start with an alphanumeric character"
71 )
72 }
73 Self::InvalidCharacter => write!(f, "distribution name contains invalid character"),
74 }
75 }
76}
77
78#[cfg(feature = "std")]
79impl std::error::Error for DistroError {}
80
81#[repr(transparent)]
86#[derive(Debug, Clone, PartialEq, Eq, Hash)]
87#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
88pub struct Distro(heapless::String<64>);
89
90impl Distro {
91 pub const MAX_LEN: usize = 64;
93
94 pub fn new(s: &str) -> Result<Self, DistroError> {
100 Self::validate(s)?;
101 let mut value = heapless::String::new();
102 value
103 .push_str(s)
104 .map_err(|_| DistroError::TooLong(s.len()))?;
105 Ok(Self(value))
106 }
107
108 fn validate(s: &str) -> Result<(), DistroError> {
110 if s.is_empty() {
111 return Err(DistroError::Empty);
112 }
113 if s.len() > Self::MAX_LEN {
114 return Err(DistroError::TooLong(s.len()));
115 }
116 let mut chars = s.chars();
117 if let Some(first) = chars.next() {
118 if !first.is_ascii_alphanumeric() {
119 return Err(DistroError::InvalidFirstCharacter);
120 }
121 }
122 for ch in chars {
123 if !ch.is_ascii_alphanumeric() && ch != ' ' && ch != '-' && ch != '_' {
124 return Err(DistroError::InvalidCharacter);
125 }
126 }
127 Ok(())
128 }
129
130 #[must_use]
132 #[inline]
133 pub fn as_str(&self) -> &str {
134 &self.0
135 }
136
137 #[must_use]
139 #[inline]
140 pub const fn as_inner(&self) -> &heapless::String<64> {
141 &self.0
142 }
143
144 #[must_use]
146 #[inline]
147 pub fn into_inner(self) -> heapless::String<64> {
148 self.0
149 }
150
151 #[must_use]
155 pub fn is_debian_based(&self) -> bool {
156 let s = self.0.to_lowercase();
157 s.contains("debian")
158 || s.contains("ubuntu")
159 || s.contains("mint")
160 || s.contains("pop")
161 || s.contains("kali")
162 }
163
164 #[must_use]
168 pub fn is_redhat_based(&self) -> bool {
169 let s = self.0.to_lowercase();
170 s.contains("red hat")
171 || s.contains("redhat")
172 || s.contains("fedora")
173 || s.contains("centos")
174 || s.contains("rhel")
175 || s.contains("rocky")
176 || s.contains("almalinux")
177 }
178
179 #[must_use]
183 pub fn is_arch_based(&self) -> bool {
184 let s = self.0.to_lowercase();
185 s.contains("arch") || s.contains("manjaro") || s.contains("endeavouros")
186 }
187
188 #[must_use]
192 pub fn is_rolling_release(&self) -> bool {
193 let s = self.0.to_lowercase();
194 s.contains("arch")
195 || s.contains("gentoo")
196 || s.contains("fedora")
197 || s.contains("void")
198 || s.contains("sid")
199 }
200
201 #[must_use]
205 pub fn is_lts(&self) -> bool {
206 let s = self.0.to_lowercase();
207 s.contains("lts")
208 || s.contains("ubuntu")
209 || s.contains("debian")
210 || s.contains("rhel")
211 || s.contains("rocky")
212 || s.contains("almalinux")
213 }
214}
215
216impl AsRef<str> for Distro {
217 fn as_ref(&self) -> &str {
218 self.as_str()
219 }
220}
221
222impl TryFrom<&str> for Distro {
223 type Error = DistroError;
224
225 fn try_from(s: &str) -> Result<Self, Self::Error> {
226 Self::new(s)
227 }
228}
229
230impl FromStr for Distro {
231 type Err = DistroError;
232
233 fn from_str(s: &str) -> Result<Self, Self::Err> {
234 Self::new(s)
235 }
236}
237
238impl fmt::Display for Distro {
239 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240 write!(f, "{}", self.0)
241 }
242}
243
244#[cfg(feature = "arbitrary")]
245impl<'a> arbitrary::Arbitrary<'a> for Distro {
246 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
247 const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
248 const DIGITS: &[u8] = b"0123456789";
249
250 let len = 1 + (u8::arbitrary(u)? % 64).min(63);
252 let mut inner = heapless::String::<64>::new();
253
254 let first_byte = u8::arbitrary(u)?;
256 if first_byte % 2 == 0 {
257 let first = ALPHABET[(first_byte % 26) as usize] as char;
258 inner
259 .push(first)
260 .map_err(|_| arbitrary::Error::IncorrectFormat)?;
261 } else {
262 let first = DIGITS[(first_byte % 10) as usize] as char;
263 inner
264 .push(first)
265 .map_err(|_| arbitrary::Error::IncorrectFormat)?;
266 }
267
268 for _ in 1..len {
270 let byte = u8::arbitrary(u)?;
271 let c = match byte % 5 {
272 0 => ALPHABET[((byte >> 2) % 26) as usize] as char,
273 1 => DIGITS[((byte >> 2) % 10) as usize] as char,
274 2 => ' ',
275 3 => '-',
276 _ => '_',
277 };
278 inner
279 .push(c)
280 .map_err(|_| arbitrary::Error::IncorrectFormat)?;
281 }
282
283 Ok(Self(inner))
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn test_new_valid() {
293 let distro = Distro::new("Ubuntu").unwrap();
294 assert_eq!(distro.as_str(), "Ubuntu");
295 }
296
297 #[test]
298 fn test_new_empty() {
299 assert!(matches!(Distro::new(""), Err(DistroError::Empty)));
300 }
301
302 #[test]
303 fn test_new_too_long() {
304 let long_name = "a".repeat(65);
305 assert!(matches!(
306 Distro::new(&long_name),
307 Err(DistroError::TooLong(65))
308 ));
309 }
310
311 #[test]
312 fn test_new_invalid_first_character() {
313 assert!(matches!(
314 Distro::new("-Ubuntu"),
315 Err(DistroError::InvalidFirstCharacter)
316 ));
317 assert!(matches!(
318 Distro::new(" Ubuntu"),
319 Err(DistroError::InvalidFirstCharacter)
320 ));
321 }
322
323 #[test]
324 fn test_new_invalid_character() {
325 assert!(matches!(
326 Distro::new("Ubuntu@"),
327 Err(DistroError::InvalidCharacter)
328 ));
329 assert!(matches!(
330 Distro::new("Ubuntu.Distro"),
331 Err(DistroError::InvalidCharacter)
332 ));
333 }
334
335 #[test]
336 fn test_is_debian_based() {
337 let ubuntu = Distro::new("Ubuntu").unwrap();
338 assert!(ubuntu.is_debian_based());
339 let debian = Distro::new("Debian").unwrap();
340 assert!(debian.is_debian_based());
341 let mint = Distro::new("Linux Mint").unwrap();
342 assert!(mint.is_debian_based());
343 let fedora = Distro::new("Fedora").unwrap();
344 assert!(!fedora.is_debian_based());
345 }
346
347 #[test]
348 fn test_is_redhat_based() {
349 let fedora = Distro::new("Fedora").unwrap();
350 assert!(fedora.is_redhat_based());
351 let centos = Distro::new("CentOS").unwrap();
352 assert!(centos.is_redhat_based());
353 let rhel = Distro::new("RHEL").unwrap();
354 assert!(rhel.is_redhat_based());
355 let ubuntu = Distro::new("Ubuntu").unwrap();
356 assert!(!ubuntu.is_redhat_based());
357 }
358
359 #[test]
360 fn test_is_arch_based() {
361 let arch = Distro::new("Arch Linux").unwrap();
362 assert!(arch.is_arch_based());
363 let manjaro = Distro::new("Manjaro").unwrap();
364 assert!(manjaro.is_arch_based());
365 let ubuntu = Distro::new("Ubuntu").unwrap();
366 assert!(!ubuntu.is_arch_based());
367 }
368
369 #[test]
370 fn test_is_rolling_release() {
371 let arch = Distro::new("Arch Linux").unwrap();
372 assert!(arch.is_rolling_release());
373 let gentoo = Distro::new("Gentoo").unwrap();
374 assert!(gentoo.is_rolling_release());
375 let fedora = Distro::new("Fedora").unwrap();
376 assert!(fedora.is_rolling_release());
377 let ubuntu = Distro::new("Ubuntu").unwrap();
378 assert!(!ubuntu.is_rolling_release());
379 }
380
381 #[test]
382 fn test_is_lts() {
383 let ubuntu = Distro::new("Ubuntu LTS").unwrap();
384 assert!(ubuntu.is_lts());
385 let debian = Distro::new("Debian").unwrap();
386 assert!(debian.is_lts());
387 let fedora = Distro::new("Fedora").unwrap();
388 assert!(!fedora.is_lts());
389 }
390
391 #[test]
392 fn test_from_str() {
393 let distro: Distro = "Ubuntu".parse().unwrap();
394 assert_eq!(distro.as_str(), "Ubuntu");
395 }
396
397 #[test]
398 fn test_from_str_error() {
399 assert!("".parse::<Distro>().is_err());
400 assert!("-Ubuntu".parse::<Distro>().is_err());
401 }
402
403 #[test]
404 fn test_display() {
405 let distro = Distro::new("Ubuntu").unwrap();
406 assert_eq!(format!("{}", distro), "Ubuntu");
407 }
408
409 #[test]
410 fn test_as_ref() {
411 let distro = Distro::new("Ubuntu").unwrap();
412 let s: &str = distro.as_ref();
413 assert_eq!(s, "Ubuntu");
414 }
415
416 #[test]
417 fn test_clone() {
418 let distro = Distro::new("Ubuntu").unwrap();
419 let distro2 = distro.clone();
420 assert_eq!(distro, distro2);
421 }
422
423 #[test]
424 fn test_equality() {
425 let d1 = Distro::new("Ubuntu").unwrap();
426 let d2 = Distro::new("Ubuntu").unwrap();
427 let d3 = Distro::new("Fedora").unwrap();
428 assert_eq!(d1, d2);
429 assert_ne!(d1, d3);
430 }
431}