bare_types/sys/
username.rs1use 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 UsernameError {
38 Empty,
42 TooLong(usize),
47 InvalidFirstCharacter,
52 InvalidCharacter,
57}
58
59impl fmt::Display for UsernameError {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 match self {
62 Self::Empty => write!(f, "username cannot be empty"),
63 Self::TooLong(len) => write!(f, "username too long (got {len}, max 32 characters)"),
64 Self::InvalidFirstCharacter => {
65 write!(f, "username must start with a letter or underscore")
66 }
67 Self::InvalidCharacter => write!(f, "username contains invalid character"),
68 }
69 }
70}
71
72#[cfg(feature = "std")]
73impl std::error::Error for UsernameError {}
74
75#[repr(transparent)]
80#[derive(Debug, Clone, PartialEq, Eq, Hash)]
81#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
82pub struct Username(heapless::String<32>);
83
84impl Username {
85 pub const MAX_LEN: usize = 32;
87
88 pub fn new(s: &str) -> Result<Self, UsernameError> {
94 Self::validate(s)?;
95 let mut value = heapless::String::new();
96 value
97 .push_str(s)
98 .map_err(|_| UsernameError::TooLong(s.len()))?;
99 Ok(Self(value))
100 }
101
102 fn validate(s: &str) -> Result<(), UsernameError> {
104 if s.is_empty() {
105 return Err(UsernameError::Empty);
106 }
107 if s.len() > Self::MAX_LEN {
108 return Err(UsernameError::TooLong(s.len()));
109 }
110 let mut chars = s.chars();
111 if let Some(first) = chars.next()
112 && !first.is_ascii_alphabetic() && first != '_'
113 {
114 return Err(UsernameError::InvalidFirstCharacter);
115 }
116 for ch in chars {
117 if !ch.is_ascii_alphanumeric() && ch != '_' && ch != '-' {
118 return Err(UsernameError::InvalidCharacter);
119 }
120 }
121 Ok(())
122 }
123
124 #[must_use]
126 #[inline]
127 pub fn as_str(&self) -> &str {
128 &self.0
129 }
130
131 #[must_use]
133 #[inline]
134 pub const fn as_inner(&self) -> &heapless::String<32> {
135 &self.0
136 }
137
138 #[must_use]
140 #[inline]
141 pub fn into_inner(self) -> heapless::String<32> {
142 self.0
143 }
144
145 #[must_use]
149 #[inline]
150 pub fn is_system_user(&self) -> bool {
151 self.0.starts_with('_') || self.0 == "root" || self.0 == "daemon"
152 }
153
154 #[must_use]
158 #[inline]
159 pub fn is_service_account(&self) -> bool {
160 self.0.starts_with("svc-") || self.0.starts_with("service-")
161 }
162}
163
164impl AsRef<str> for Username {
165 fn as_ref(&self) -> &str {
166 self.as_str()
167 }
168}
169
170impl TryFrom<&str> for Username {
171 type Error = UsernameError;
172
173 fn try_from(s: &str) -> Result<Self, Self::Error> {
174 Self::new(s)
175 }
176}
177
178impl FromStr for Username {
179 type Err = UsernameError;
180
181 fn from_str(s: &str) -> Result<Self, Self::Err> {
182 Self::new(s)
183 }
184}
185
186impl fmt::Display for Username {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 write!(f, "{}", self.0)
189 }
190}
191
192#[cfg(feature = "arbitrary")]
193impl<'a> arbitrary::Arbitrary<'a> for Username {
194 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
195 const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
196 const DIGITS: &[u8] = b"0123456789";
197
198 let len = 1 + (u8::arbitrary(u)? % 32).min(31);
200 let mut inner = heapless::String::<32>::new();
201
202 let first_byte = u8::arbitrary(u)?;
204 if first_byte % 10 == 0 {
205 inner
207 .push('_')
208 .map_err(|_| arbitrary::Error::IncorrectFormat)?;
209 } else {
210 let first = ALPHABET[(first_byte % 26) as usize] as char;
211 inner
212 .push(first)
213 .map_err(|_| arbitrary::Error::IncorrectFormat)?;
214 }
215
216 for _ in 1..len {
218 let byte = u8::arbitrary(u)?;
219 let c = match byte % 4 {
220 0 => ALPHABET[((byte >> 2) % 26) as usize] as char,
221 1 => DIGITS[((byte >> 2) % 10) as usize] as char,
222 2 => '_',
223 _ => '-',
224 };
225 inner
226 .push(c)
227 .map_err(|_| arbitrary::Error::IncorrectFormat)?;
228 }
229
230 Ok(Self(inner))
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn test_new_valid() {
240 let username = Username::new("root").unwrap();
241 assert_eq!(username.as_str(), "root");
242 }
243
244 #[test]
245 fn test_new_empty() {
246 assert!(matches!(Username::new(""), Err(UsernameError::Empty)));
247 }
248
249 #[test]
250 fn test_new_too_long() {
251 let long_name = "a".repeat(33);
252 assert!(matches!(
253 Username::new(&long_name),
254 Err(UsernameError::TooLong(33))
255 ));
256 }
257
258 #[test]
259 fn test_new_invalid_first_character() {
260 assert!(matches!(
261 Username::new("1root"),
262 Err(UsernameError::InvalidFirstCharacter)
263 ));
264 assert!(matches!(
265 Username::new("-root"),
266 Err(UsernameError::InvalidFirstCharacter)
267 ));
268 }
269
270 #[test]
271 fn test_new_invalid_character() {
272 assert!(matches!(
273 Username::new("root@"),
274 Err(UsernameError::InvalidCharacter)
275 ));
276 assert!(matches!(
277 Username::new("root.user"),
278 Err(UsernameError::InvalidCharacter)
279 ));
280 }
281
282 #[test]
283 fn test_is_system_user() {
284 let root = Username::new("root").unwrap();
285 assert!(root.is_system_user());
286 let daemon = Username::new("daemon").unwrap();
287 assert!(daemon.is_system_user());
288 let system = Username::new("_system").unwrap();
289 assert!(system.is_system_user());
290 let user = Username::new("user").unwrap();
291 assert!(!user.is_system_user());
292 }
293
294 #[test]
295 fn test_is_service_account() {
296 let svc = Username::new("svc-api").unwrap();
297 assert!(svc.is_service_account());
298 let service = Username::new("service-api").unwrap();
299 assert!(service.is_service_account());
300 let user = Username::new("user").unwrap();
301 assert!(!user.is_service_account());
302 }
303
304 #[test]
305 fn test_from_str() {
306 let username: Username = "root".parse().unwrap();
307 assert_eq!(username.as_str(), "root");
308 }
309
310 #[test]
311 fn test_from_str_error() {
312 assert!("".parse::<Username>().is_err());
313 assert!("1root".parse::<Username>().is_err());
314 }
315
316 #[test]
317 fn test_display() {
318 let username = Username::new("root").unwrap();
319 assert_eq!(format!("{}", username), "root");
320 }
321
322 #[test]
323 fn test_as_ref() {
324 let username = Username::new("root").unwrap();
325 let s: &str = username.as_ref();
326 assert_eq!(s, "root");
327 }
328
329 #[test]
330 fn test_clone() {
331 let username = Username::new("root").unwrap();
332 let username2 = username.clone();
333 assert_eq!(username, username2);
334 }
335
336 #[test]
337 fn test_equality() {
338 let u1 = Username::new("root").unwrap();
339 let u2 = Username::new("root").unwrap();
340 let u3 = Username::new("user").unwrap();
341 assert_eq!(u1, u2);
342 assert_ne!(u1, u3);
343 }
344
345 #[test]
346 fn test_as_inner() {
347 let username = Username::new("root").unwrap();
348 let inner = username.as_inner();
349 assert_eq!(inner.as_str(), "root");
350 }
351
352 #[test]
353 fn test_into_inner() {
354 let username = Username::new("root").unwrap();
355 let inner = username.into_inner();
356 assert_eq!(inner.as_str(), "root");
357 }
358}