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