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(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn test_new_valid() {
184 let username = Username::new("root").unwrap();
185 assert_eq!(username.as_str(), "root");
186 }
187
188 #[test]
189 fn test_new_empty() {
190 assert!(matches!(Username::new(""), Err(UsernameError::Empty)));
191 }
192
193 #[test]
194 fn test_new_too_long() {
195 let long_name = "a".repeat(33);
196 assert!(matches!(
197 Username::new(&long_name),
198 Err(UsernameError::TooLong(33))
199 ));
200 }
201
202 #[test]
203 fn test_new_invalid_first_character() {
204 assert!(matches!(
205 Username::new("1root"),
206 Err(UsernameError::InvalidFirstCharacter)
207 ));
208 assert!(matches!(
209 Username::new("-root"),
210 Err(UsernameError::InvalidFirstCharacter)
211 ));
212 }
213
214 #[test]
215 fn test_new_invalid_character() {
216 assert!(matches!(
217 Username::new("root@"),
218 Err(UsernameError::InvalidCharacter)
219 ));
220 assert!(matches!(
221 Username::new("root.user"),
222 Err(UsernameError::InvalidCharacter)
223 ));
224 }
225
226 #[test]
227 fn test_is_system_user() {
228 let root = Username::new("root").unwrap();
229 assert!(root.is_system_user());
230 let daemon = Username::new("daemon").unwrap();
231 assert!(daemon.is_system_user());
232 let system = Username::new("_system").unwrap();
233 assert!(system.is_system_user());
234 let user = Username::new("user").unwrap();
235 assert!(!user.is_system_user());
236 }
237
238 #[test]
239 fn test_is_service_account() {
240 let svc = Username::new("svc-api").unwrap();
241 assert!(svc.is_service_account());
242 let service = Username::new("service-api").unwrap();
243 assert!(service.is_service_account());
244 let user = Username::new("user").unwrap();
245 assert!(!user.is_service_account());
246 }
247
248 #[test]
249 fn test_from_str() {
250 let username: Username = "root".parse().unwrap();
251 assert_eq!(username.as_str(), "root");
252 }
253
254 #[test]
255 fn test_from_str_error() {
256 assert!("".parse::<Username>().is_err());
257 assert!("1root".parse::<Username>().is_err());
258 }
259
260 #[test]
261 fn test_display() {
262 let username = Username::new("root").unwrap();
263 assert_eq!(format!("{}", username), "root");
264 }
265
266 #[test]
267 fn test_as_ref() {
268 let username = Username::new("root").unwrap();
269 let s: &str = username.as_ref();
270 assert_eq!(s, "root");
271 }
272
273 #[test]
274 fn test_clone() {
275 let username = Username::new("root").unwrap();
276 let username2 = username.clone();
277 assert_eq!(username, username2);
278 }
279
280 #[test]
281 fn test_equality() {
282 let u1 = Username::new("root").unwrap();
283 let u2 = Username::new("root").unwrap();
284 let u3 = Username::new("user").unwrap();
285 assert_eq!(u1, u2);
286 assert_ne!(u1, u3);
287 }
288}