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))]
36pub enum DistroError {
37 Empty,
39 TooLong(usize),
41 InvalidFirstCharacter,
43 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#[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 pub const MAX_LEN: usize = 64;
81
82 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 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 #[must_use]
120 #[inline]
121 pub fn as_str(&self) -> &str {
122 &self.0
123 }
124
125 #[must_use]
127 #[inline]
128 pub const fn as_inner(&self) -> &heapless::String<64> {
129 &self.0
130 }
131
132 #[must_use]
134 #[inline]
135 pub fn into_inner(self) -> heapless::String<64> {
136 self.0
137 }
138
139 #[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 #[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 #[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 #[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 #[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(feature = "arbitrary")]
233impl<'a> arbitrary::Arbitrary<'a> for Distro {
234 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
235 const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
236 const DIGITS: &[u8] = b"0123456789";
237
238 let len = 1 + (u8::arbitrary(u)? % 64).min(63);
240 let mut inner = heapless::String::<64>::new();
241
242 let first_byte = u8::arbitrary(u)?;
244 if first_byte % 2 == 0 {
245 let first = ALPHABET[(first_byte % 26) as usize] as char;
246 inner
247 .push(first)
248 .map_err(|_| arbitrary::Error::IncorrectFormat)?;
249 } else {
250 let first = DIGITS[(first_byte % 10) as usize] as char;
251 inner
252 .push(first)
253 .map_err(|_| arbitrary::Error::IncorrectFormat)?;
254 }
255
256 for _ in 1..len {
258 let byte = u8::arbitrary(u)?;
259 let c = match byte % 5 {
260 0 => ALPHABET[((byte >> 2) % 26) as usize] as char,
261 1 => DIGITS[((byte >> 2) % 10) as usize] as char,
262 2 => ' ',
263 3 => '-',
264 _ => '_',
265 };
266 inner
267 .push(c)
268 .map_err(|_| arbitrary::Error::IncorrectFormat)?;
269 }
270
271 Ok(Self(inner))
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn test_new_valid() {
281 let distro = Distro::new("Ubuntu").unwrap();
282 assert_eq!(distro.as_str(), "Ubuntu");
283 }
284
285 #[test]
286 fn test_new_empty() {
287 assert!(matches!(Distro::new(""), Err(DistroError::Empty)));
288 }
289
290 #[test]
291 fn test_new_too_long() {
292 let long_name = "a".repeat(65);
293 assert!(matches!(
294 Distro::new(&long_name),
295 Err(DistroError::TooLong(65))
296 ));
297 }
298
299 #[test]
300 fn test_new_invalid_first_character() {
301 assert!(matches!(
302 Distro::new("-Ubuntu"),
303 Err(DistroError::InvalidFirstCharacter)
304 ));
305 assert!(matches!(
306 Distro::new(" Ubuntu"),
307 Err(DistroError::InvalidFirstCharacter)
308 ));
309 }
310
311 #[test]
312 fn test_new_invalid_character() {
313 assert!(matches!(
314 Distro::new("Ubuntu@"),
315 Err(DistroError::InvalidCharacter)
316 ));
317 assert!(matches!(
318 Distro::new("Ubuntu.Distro"),
319 Err(DistroError::InvalidCharacter)
320 ));
321 }
322
323 #[test]
324 fn test_is_debian_based() {
325 let ubuntu = Distro::new("Ubuntu").unwrap();
326 assert!(ubuntu.is_debian_based());
327 let debian = Distro::new("Debian").unwrap();
328 assert!(debian.is_debian_based());
329 let mint = Distro::new("Linux Mint").unwrap();
330 assert!(mint.is_debian_based());
331 let fedora = Distro::new("Fedora").unwrap();
332 assert!(!fedora.is_debian_based());
333 }
334
335 #[test]
336 fn test_is_redhat_based() {
337 let fedora = Distro::new("Fedora").unwrap();
338 assert!(fedora.is_redhat_based());
339 let centos = Distro::new("CentOS").unwrap();
340 assert!(centos.is_redhat_based());
341 let rhel = Distro::new("RHEL").unwrap();
342 assert!(rhel.is_redhat_based());
343 let ubuntu = Distro::new("Ubuntu").unwrap();
344 assert!(!ubuntu.is_redhat_based());
345 }
346
347 #[test]
348 fn test_is_arch_based() {
349 let arch = Distro::new("Arch Linux").unwrap();
350 assert!(arch.is_arch_based());
351 let manjaro = Distro::new("Manjaro").unwrap();
352 assert!(manjaro.is_arch_based());
353 let ubuntu = Distro::new("Ubuntu").unwrap();
354 assert!(!ubuntu.is_arch_based());
355 }
356
357 #[test]
358 fn test_is_rolling_release() {
359 let arch = Distro::new("Arch Linux").unwrap();
360 assert!(arch.is_rolling_release());
361 let gentoo = Distro::new("Gentoo").unwrap();
362 assert!(gentoo.is_rolling_release());
363 let fedora = Distro::new("Fedora").unwrap();
364 assert!(fedora.is_rolling_release());
365 let ubuntu = Distro::new("Ubuntu").unwrap();
366 assert!(!ubuntu.is_rolling_release());
367 }
368
369 #[test]
370 fn test_is_lts() {
371 let ubuntu = Distro::new("Ubuntu LTS").unwrap();
372 assert!(ubuntu.is_lts());
373 let debian = Distro::new("Debian").unwrap();
374 assert!(debian.is_lts());
375 let fedora = Distro::new("Fedora").unwrap();
376 assert!(!fedora.is_lts());
377 }
378
379 #[test]
380 fn test_from_str() {
381 let distro: Distro = "Ubuntu".parse().unwrap();
382 assert_eq!(distro.as_str(), "Ubuntu");
383 }
384
385 #[test]
386 fn test_from_str_error() {
387 assert!("".parse::<Distro>().is_err());
388 assert!("-Ubuntu".parse::<Distro>().is_err());
389 }
390
391 #[test]
392 fn test_display() {
393 let distro = Distro::new("Ubuntu").unwrap();
394 assert_eq!(format!("{}", distro), "Ubuntu");
395 }
396
397 #[test]
398 fn test_as_ref() {
399 let distro = Distro::new("Ubuntu").unwrap();
400 let s: &str = distro.as_ref();
401 assert_eq!(s, "Ubuntu");
402 }
403
404 #[test]
405 fn test_clone() {
406 let distro = Distro::new("Ubuntu").unwrap();
407 let distro2 = distro.clone();
408 assert_eq!(distro, distro2);
409 }
410
411 #[test]
412 fn test_equality() {
413 let d1 = Distro::new("Ubuntu").unwrap();
414 let d2 = Distro::new("Ubuntu").unwrap();
415 let d3 = Distro::new("Fedora").unwrap();
416 assert_eq!(d1, d2);
417 assert_ne!(d1, d3);
418 }
419}