Skip to main content

bare_types/sys/
username.rs

1//! System username type for system information.
2//!
3//! This module provides a type-safe abstraction for system usernames,
4//! ensuring valid username strings following POSIX rules.
5//!
6//! # Username Format
7//!
8//! System usernames follow POSIX rules:
9//!
10//! - Must be 1-32 characters
11//! - Must start with a letter or underscore
12//! - Can contain alphanumeric characters, underscores, and hyphens
13//!
14//! # Examples
15//!
16//! ```rust
17//! use bare_types::sys::Username;
18//!
19//! // Parse from string
20//! let username: Username = "root".parse()?;
21//!
22//! // Access as string
23//! assert_eq!(username.as_str(), "root");
24//! # Ok::<(), bare_types::sys::UsernameError>(())
25//! ```
26
27use core::fmt;
28use core::str::FromStr;
29
30#[cfg(feature = "serde")]
31use serde::{Deserialize, Serialize};
32
33/// Error type for username parsing.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
36pub enum UsernameError {
37    /// Empty username
38    Empty,
39    /// Username too long (max 32 characters)
40    TooLong(usize),
41    /// Invalid first character (must be letter or underscore)
42    InvalidFirstCharacter,
43    /// Invalid character in username
44    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/// System username.
64///
65/// This type provides type-safe system usernames following POSIX rules.
66/// It uses the newtype pattern with `#[repr(transparent)]` for zero-cost abstraction.
67#[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    /// Maximum length of a username (POSIX)
74    pub const MAX_LEN: usize = 32;
75
76    /// Creates a new username from a string.
77    ///
78    /// # Errors
79    ///
80    /// Returns an error `UsernameError` if the string is not a valid username.
81    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    /// Validates a username string.
91    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    /// Returns the username as a string slice.
113    #[must_use]
114    #[inline]
115    pub fn as_str(&self) -> &str {
116        &self.0
117    }
118
119    /// Returns a reference to the underlying `heapless::String`.
120    #[must_use]
121    #[inline]
122    pub const fn as_inner(&self) -> &heapless::String<32> {
123        &self.0
124    }
125
126    /// Consumes this username and returns the underlying string.
127    #[must_use]
128    #[inline]
129    pub fn into_inner(self) -> heapless::String<32> {
130        self.0
131    }
132
133    /// Returns `true` if this is a system user.
134    ///
135    /// System users typically start with underscore or are well-known system accounts.
136    #[must_use]
137    pub fn is_system_user(&self) -> bool {
138        self.0.starts_with('_') || self.0 == "root" || self.0 == "daemon"
139    }
140
141    /// Returns `true` if this is a service account.
142    ///
143    /// Service accounts typically start with "svc-" or "service-".
144    #[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        // Generate 1-32 character username
185        let len = 1 + (u8::arbitrary(u)? % 32).min(31);
186        let mut inner = heapless::String::<32>::new();
187
188        // First character: letter or underscore
189        let first_byte = u8::arbitrary(u)?;
190        if first_byte % 10 == 0 {
191            // 10% chance of underscore
192            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        // Remaining characters: alphanumeric, underscore, or hyphen
203        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}