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 && !first.is_ascii_alphanumeric()
119 {
120 return Err(DistroError::InvalidFirstCharacter);
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 #[inline]
156 pub fn is_debian_based(&self) -> bool {
157 let s = self.0.to_lowercase();
158 s.contains("debian")
159 || s.contains("ubuntu")
160 || s.contains("mint")
161 || s.contains("pop")
162 || s.contains("kali")
163 }
164
165 #[must_use]
169 #[inline]
170 pub fn is_redhat_based(&self) -> bool {
171 let s = self.0.to_lowercase();
172 s.contains("red hat")
173 || s.contains("redhat")
174 || s.contains("fedora")
175 || s.contains("centos")
176 || s.contains("rhel")
177 || s.contains("rocky")
178 || s.contains("almalinux")
179 }
180
181 #[must_use]
185 #[inline]
186 pub fn is_arch_based(&self) -> bool {
187 let s = self.0.to_lowercase();
188 s.contains("arch") || s.contains("manjaro") || s.contains("endeavouros")
189 }
190
191 #[must_use]
195 #[inline]
196 pub fn is_rolling_release(&self) -> bool {
197 let s = self.0.to_lowercase();
198 s.contains("arch")
199 || s.contains("gentoo")
200 || s.contains("fedora")
201 || s.contains("void")
202 || s.contains("sid")
203 }
204
205 #[must_use]
209 #[inline]
210 pub fn is_lts(&self) -> bool {
211 let s = self.0.to_lowercase();
212 s.contains("lts")
213 || s.contains("ubuntu")
214 || s.contains("debian")
215 || s.contains("rhel")
216 || s.contains("rocky")
217 || s.contains("almalinux")
218 }
219}
220
221impl AsRef<str> for Distro {
222 fn as_ref(&self) -> &str {
223 self.as_str()
224 }
225}
226
227impl TryFrom<&str> for Distro {
228 type Error = DistroError;
229
230 fn try_from(s: &str) -> Result<Self, Self::Error> {
231 Self::new(s)
232 }
233}
234
235impl FromStr for Distro {
236 type Err = DistroError;
237
238 fn from_str(s: &str) -> Result<Self, Self::Err> {
239 Self::new(s)
240 }
241}
242
243impl fmt::Display for Distro {
244 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245 write!(f, "{}", self.0)
246 }
247}
248
249#[cfg(feature = "arbitrary")]
250impl<'a> arbitrary::Arbitrary<'a> for Distro {
251 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
252 const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
253 const DIGITS: &[u8] = b"0123456789";
254
255 let len = 1 + (u8::arbitrary(u)? % 64).min(63);
257 let mut inner = heapless::String::<64>::new();
258
259 let first_byte = u8::arbitrary(u)?;
261 if first_byte % 2 == 0 {
262 let first = ALPHABET[(first_byte % 26) as usize] as char;
263 inner
264 .push(first)
265 .map_err(|_| arbitrary::Error::IncorrectFormat)?;
266 } else {
267 let first = DIGITS[(first_byte % 10) as usize] as char;
268 inner
269 .push(first)
270 .map_err(|_| arbitrary::Error::IncorrectFormat)?;
271 }
272
273 for _ in 1..len {
275 let byte = u8::arbitrary(u)?;
276 let c = match byte % 5 {
277 0 => ALPHABET[((byte >> 2) % 26) as usize] as char,
278 1 => DIGITS[((byte >> 2) % 10) as usize] as char,
279 2 => ' ',
280 3 => '-',
281 _ => '_',
282 };
283 inner
284 .push(c)
285 .map_err(|_| arbitrary::Error::IncorrectFormat)?;
286 }
287
288 Ok(Self(inner))
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn test_new_valid() {
298 let distro = Distro::new("Ubuntu").unwrap();
299 assert_eq!(distro.as_str(), "Ubuntu");
300 }
301
302 #[test]
303 fn test_new_empty() {
304 assert!(matches!(Distro::new(""), Err(DistroError::Empty)));
305 }
306
307 #[test]
308 fn test_new_too_long() {
309 let long_name = "a".repeat(65);
310 assert!(matches!(
311 Distro::new(&long_name),
312 Err(DistroError::TooLong(65))
313 ));
314 }
315
316 #[test]
317 fn test_new_invalid_first_character() {
318 assert!(matches!(
319 Distro::new("-Ubuntu"),
320 Err(DistroError::InvalidFirstCharacter)
321 ));
322 assert!(matches!(
323 Distro::new(" Ubuntu"),
324 Err(DistroError::InvalidFirstCharacter)
325 ));
326 }
327
328 #[test]
329 fn test_new_invalid_character() {
330 assert!(matches!(
331 Distro::new("Ubuntu@"),
332 Err(DistroError::InvalidCharacter)
333 ));
334 assert!(matches!(
335 Distro::new("Ubuntu.Distro"),
336 Err(DistroError::InvalidCharacter)
337 ));
338 }
339
340 #[test]
341 fn test_is_debian_based() {
342 let ubuntu = Distro::new("Ubuntu").unwrap();
343 assert!(ubuntu.is_debian_based());
344 let debian = Distro::new("Debian").unwrap();
345 assert!(debian.is_debian_based());
346 let mint = Distro::new("Linux Mint").unwrap();
347 assert!(mint.is_debian_based());
348 let fedora = Distro::new("Fedora").unwrap();
349 assert!(!fedora.is_debian_based());
350 }
351
352 #[test]
353 fn test_is_redhat_based() {
354 let fedora = Distro::new("Fedora").unwrap();
355 assert!(fedora.is_redhat_based());
356 let centos = Distro::new("CentOS").unwrap();
357 assert!(centos.is_redhat_based());
358 let rhel = Distro::new("RHEL").unwrap();
359 assert!(rhel.is_redhat_based());
360 let ubuntu = Distro::new("Ubuntu").unwrap();
361 assert!(!ubuntu.is_redhat_based());
362 }
363
364 #[test]
365 fn test_is_arch_based() {
366 let arch = Distro::new("Arch Linux").unwrap();
367 assert!(arch.is_arch_based());
368 let manjaro = Distro::new("Manjaro").unwrap();
369 assert!(manjaro.is_arch_based());
370 let ubuntu = Distro::new("Ubuntu").unwrap();
371 assert!(!ubuntu.is_arch_based());
372 }
373
374 #[test]
375 fn test_is_rolling_release() {
376 let arch = Distro::new("Arch Linux").unwrap();
377 assert!(arch.is_rolling_release());
378 let gentoo = Distro::new("Gentoo").unwrap();
379 assert!(gentoo.is_rolling_release());
380 let fedora = Distro::new("Fedora").unwrap();
381 assert!(fedora.is_rolling_release());
382 let ubuntu = Distro::new("Ubuntu").unwrap();
383 assert!(!ubuntu.is_rolling_release());
384 }
385
386 #[test]
387 fn test_is_lts() {
388 let ubuntu = Distro::new("Ubuntu LTS").unwrap();
389 assert!(ubuntu.is_lts());
390 let debian = Distro::new("Debian").unwrap();
391 assert!(debian.is_lts());
392 let fedora = Distro::new("Fedora").unwrap();
393 assert!(!fedora.is_lts());
394 }
395
396 #[test]
397 fn test_from_str() {
398 let distro: Distro = "Ubuntu".parse().unwrap();
399 assert_eq!(distro.as_str(), "Ubuntu");
400 }
401
402 #[test]
403 fn test_from_str_error() {
404 assert!("".parse::<Distro>().is_err());
405 assert!("-Ubuntu".parse::<Distro>().is_err());
406 }
407
408 #[test]
409 fn test_display() {
410 let distro = Distro::new("Ubuntu").unwrap();
411 assert_eq!(format!("{}", distro), "Ubuntu");
412 }
413
414 #[test]
415 fn test_as_ref() {
416 let distro = Distro::new("Ubuntu").unwrap();
417 let s: &str = distro.as_ref();
418 assert_eq!(s, "Ubuntu");
419 }
420
421 #[test]
422 fn test_clone() {
423 let distro = Distro::new("Ubuntu").unwrap();
424 let distro2 = distro.clone();
425 assert_eq!(distro, distro2);
426 }
427
428 #[test]
429 fn test_equality() {
430 let d1 = Distro::new("Ubuntu").unwrap();
431 let d2 = Distro::new("Ubuntu").unwrap();
432 let d3 = Distro::new("Fedora").unwrap();
433 assert_eq!(d1, d2);
434 assert_ne!(d1, d3);
435 }
436}