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(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}