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() && 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    /// Returns the username as a string slice.
125    #[must_use]
126    #[inline]
127    pub fn as_str(&self) -> &str {
128        &self.0
129    }
130
131    /// Returns a reference to the underlying `heapless::String`.
132    #[must_use]
133    #[inline]
134    pub const fn as_inner(&self) -> &heapless::String<32> {
135        &self.0
136    }
137
138    /// Consumes this username and returns the underlying string.
139    #[must_use]
140    #[inline]
141    pub fn into_inner(self) -> heapless::String<32> {
142        self.0
143    }
144
145    /// Returns `true` if this is a system user.
146    ///
147    /// System users typically start with underscore or are well-known system accounts.
148    #[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    /// Returns `true` if this is a service account.
155    ///
156    /// Service accounts typically start with "svc-" or "service-".
157    #[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        // Generate 1-32 character username
199        let len = 1 + (u8::arbitrary(u)? % 32).min(31);
200        let mut inner = heapless::String::<32>::new();
201
202        // First character: letter or underscore
203        let first_byte = u8::arbitrary(u)?;
204        if first_byte % 10 == 0 {
205            // 10% chance of underscore
206            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        // Remaining characters: alphanumeric, underscore, or hyphen
217        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}