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))]
36#[non_exhaustive]
37pub enum UsernameError {
38    /// Empty username
39    ///
40    /// The provided string is empty. Usernames must contain at least one character.
41    Empty,
42    /// Username too long (max 32 characters)
43    ///
44    /// According to POSIX, usernames must not exceed 32 characters.
45    /// This variant contains the actual length of the provided username.
46    TooLong(usize),
47    /// Invalid first character (must be letter or underscore)
48    ///
49    /// Usernames must start with a letter (a-z, A-Z) or underscore (_).
50    /// Digits and other characters are not allowed as the first character.
51    InvalidFirstCharacter,
52    /// Invalid character in username
53    ///
54    /// Usernames can only contain ASCII letters, digits, underscores, and hyphens.
55    /// This variant indicates an invalid character was found.
56    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/// System username.
76///
77/// This type provides type-safe system usernames following POSIX rules.
78/// It uses the newtype pattern with `#[repr(transparent)]` for zero-cost abstraction.
79#[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    /// Maximum length of a username (POSIX)
86    pub const MAX_LEN: usize = 32;
87
88    /// Creates a new username from a string.
89    ///
90    /// # Errors
91    ///
92    /// Returns an error `UsernameError` if the string is not a valid username.
93    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    /// Validates a username string.
103    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()
113            && first != '_'
114        {
115            return Err(UsernameError::InvalidFirstCharacter);
116        }
117        for ch in chars {
118            if !ch.is_ascii_alphanumeric() && ch != '_' && ch != '-' {
119                return Err(UsernameError::InvalidCharacter);
120            }
121        }
122        Ok(())
123    }
124
125    /// Returns the username as a string slice.
126    #[must_use]
127    #[inline]
128    pub fn as_str(&self) -> &str {
129        &self.0
130    }
131
132    /// Returns a reference to the underlying `heapless::String`.
133    #[must_use]
134    #[inline]
135    pub const fn as_inner(&self) -> &heapless::String<32> {
136        &self.0
137    }
138
139    /// Consumes this username and returns the underlying string.
140    #[must_use]
141    #[inline]
142    pub fn into_inner(self) -> heapless::String<32> {
143        self.0
144    }
145
146    /// Returns `true` if this is a system user.
147    ///
148    /// System users typically start with underscore or are well-known system accounts.
149    #[must_use]
150    #[inline]
151    pub fn is_system_user(&self) -> bool {
152        self.0.starts_with('_') || self.0 == "root" || self.0 == "daemon"
153    }
154
155    /// Returns `true` if this is a service account.
156    ///
157    /// Service accounts typically start with "svc-" or "service-".
158    #[must_use]
159    #[inline]
160    pub fn is_service_account(&self) -> bool {
161        self.0.starts_with("svc-") || self.0.starts_with("service-")
162    }
163}
164
165impl AsRef<str> for Username {
166    fn as_ref(&self) -> &str {
167        self.as_str()
168    }
169}
170
171impl TryFrom<&str> for Username {
172    type Error = UsernameError;
173
174    fn try_from(s: &str) -> Result<Self, Self::Error> {
175        Self::new(s)
176    }
177}
178
179impl FromStr for Username {
180    type Err = UsernameError;
181
182    fn from_str(s: &str) -> Result<Self, Self::Err> {
183        Self::new(s)
184    }
185}
186
187impl fmt::Display for Username {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        write!(f, "{}", self.0)
190    }
191}
192
193#[cfg(feature = "arbitrary")]
194impl<'a> arbitrary::Arbitrary<'a> for Username {
195    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
196        const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
197        const DIGITS: &[u8] = b"0123456789";
198
199        // Generate 1-32 character username
200        let len = 1 + (u8::arbitrary(u)? % 32).min(31);
201        let mut inner = heapless::String::<32>::new();
202
203        // First character: letter or underscore
204        let first_byte = u8::arbitrary(u)?;
205        if first_byte % 10 == 0 {
206            // 10% chance of underscore
207            inner
208                .push('_')
209                .map_err(|_| arbitrary::Error::IncorrectFormat)?;
210        } else {
211            let first = ALPHABET[(first_byte % 26) as usize] as char;
212            inner
213                .push(first)
214                .map_err(|_| arbitrary::Error::IncorrectFormat)?;
215        }
216
217        // Remaining characters: alphanumeric, underscore, or hyphen
218        for _ in 1..len {
219            let byte = u8::arbitrary(u)?;
220            let c = match byte % 4 {
221                0 => ALPHABET[((byte >> 2) % 26) as usize] as char,
222                1 => DIGITS[((byte >> 2) % 10) as usize] as char,
223                2 => '_',
224                _ => '-',
225            };
226            inner
227                .push(c)
228                .map_err(|_| arbitrary::Error::IncorrectFormat)?;
229        }
230
231        Ok(Self(inner))
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn test_new_valid() {
241        let username = Username::new("root").unwrap();
242        assert_eq!(username.as_str(), "root");
243    }
244
245    #[test]
246    fn test_new_empty() {
247        assert!(matches!(Username::new(""), Err(UsernameError::Empty)));
248    }
249
250    #[test]
251    fn test_new_too_long() {
252        let long_name = "a".repeat(33);
253        assert!(matches!(
254            Username::new(&long_name),
255            Err(UsernameError::TooLong(33))
256        ));
257    }
258
259    #[test]
260    fn test_new_invalid_first_character() {
261        assert!(matches!(
262            Username::new("1root"),
263            Err(UsernameError::InvalidFirstCharacter)
264        ));
265        assert!(matches!(
266            Username::new("-root"),
267            Err(UsernameError::InvalidFirstCharacter)
268        ));
269    }
270
271    #[test]
272    fn test_new_invalid_character() {
273        assert!(matches!(
274            Username::new("root@"),
275            Err(UsernameError::InvalidCharacter)
276        ));
277        assert!(matches!(
278            Username::new("root.user"),
279            Err(UsernameError::InvalidCharacter)
280        ));
281    }
282
283    #[test]
284    fn test_is_system_user() {
285        let root = Username::new("root").unwrap();
286        assert!(root.is_system_user());
287        let daemon = Username::new("daemon").unwrap();
288        assert!(daemon.is_system_user());
289        let system = Username::new("_system").unwrap();
290        assert!(system.is_system_user());
291        let user = Username::new("user").unwrap();
292        assert!(!user.is_system_user());
293    }
294
295    #[test]
296    fn test_is_service_account() {
297        let svc = Username::new("svc-api").unwrap();
298        assert!(svc.is_service_account());
299        let service = Username::new("service-api").unwrap();
300        assert!(service.is_service_account());
301        let user = Username::new("user").unwrap();
302        assert!(!user.is_service_account());
303    }
304
305    #[test]
306    fn test_from_str() {
307        let username: Username = "root".parse().unwrap();
308        assert_eq!(username.as_str(), "root");
309    }
310
311    #[test]
312    fn test_from_str_error() {
313        assert!("".parse::<Username>().is_err());
314        assert!("1root".parse::<Username>().is_err());
315    }
316
317    #[test]
318    fn test_display() {
319        let username = Username::new("root").unwrap();
320        assert_eq!(format!("{}", username), "root");
321    }
322
323    #[test]
324    fn test_as_ref() {
325        let username = Username::new("root").unwrap();
326        let s: &str = username.as_ref();
327        assert_eq!(s, "root");
328    }
329
330    #[test]
331    fn test_clone() {
332        let username = Username::new("root").unwrap();
333        let username2 = username.clone();
334        assert_eq!(username, username2);
335    }
336
337    #[test]
338    fn test_equality() {
339        let u1 = Username::new("root").unwrap();
340        let u2 = Username::new("root").unwrap();
341        let u3 = Username::new("user").unwrap();
342        assert_eq!(u1, u2);
343        assert_ne!(u1, u3);
344    }
345
346    #[test]
347    fn test_as_inner() {
348        let username = Username::new("root").unwrap();
349        let inner = username.as_inner();
350        assert_eq!(inner.as_str(), "root");
351    }
352
353    #[test]
354    fn test_into_inner() {
355        let username = Username::new("root").unwrap();
356        let inner = username.into_inner();
357        assert_eq!(inner.as_str(), "root");
358    }
359}