bare-types 0.3.0

A zero-cost foundation for type-safe domain modeling in Rust. Implements the 'Parse, don't validate' philosophy to eliminate primitive obsession and ensure data integrity at the system boundary.
Documentation
//! System username type for system information.
//!
//! This module provides a type-safe abstraction for system usernames,
//! ensuring valid username strings following POSIX rules.
//!
//! # Username Format
//!
//! System usernames follow POSIX rules:
//!
//! - Must be 1-32 characters
//! - Must start with a letter or underscore
//! - Can contain alphanumeric characters, underscores, and hyphens
//!
//! # Examples
//!
//! ```rust
//! use bare_types::sys::Username;
//!
//! // Parse from string
//! let username: Username = "root".parse()?;
//!
//! // Access as string
//! assert_eq!(username.as_str(), "root");
//! # Ok::<(), bare_types::sys::UsernameError>(())
//! ```

use core::fmt;
use core::str::FromStr;

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// Error type for username parsing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[non_exhaustive]
pub enum UsernameError {
    /// Empty username
    ///
    /// The provided string is empty. Usernames must contain at least one character.
    Empty,
    /// Username too long (max 32 characters)
    ///
    /// According to POSIX, usernames must not exceed 32 characters.
    /// This variant contains the actual length of the provided username.
    TooLong(usize),
    /// Invalid first character (must be letter or underscore)
    ///
    /// Usernames must start with a letter (a-z, A-Z) or underscore (_).
    /// Digits and other characters are not allowed as the first character.
    InvalidFirstCharacter,
    /// Invalid character in username
    ///
    /// Usernames can only contain ASCII letters, digits, underscores, and hyphens.
    /// This variant indicates an invalid character was found.
    InvalidCharacter,
}

impl fmt::Display for UsernameError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => write!(f, "username cannot be empty"),
            Self::TooLong(len) => write!(f, "username too long (got {len}, max 32 characters)"),
            Self::InvalidFirstCharacter => {
                write!(f, "username must start with a letter or underscore")
            }
            Self::InvalidCharacter => write!(f, "username contains invalid character"),
        }
    }
}

#[cfg(feature = "std")]
impl std::error::Error for UsernameError {}

/// System username.
///
/// This type provides type-safe system usernames following POSIX rules.
/// It uses the newtype pattern with `#[repr(transparent)]` for zero-cost abstraction.
#[repr(transparent)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Username(heapless::String<32>);

impl Username {
    /// Maximum length of a username (POSIX)
    pub const MAX_LEN: usize = 32;

    /// Creates a new username from a string.
    ///
    /// # Errors
    ///
    /// Returns an error `UsernameError` if the string is not a valid username.
    pub fn new(s: &str) -> Result<Self, UsernameError> {
        Self::validate(s)?;
        let mut value = heapless::String::new();
        value
            .push_str(s)
            .map_err(|_| UsernameError::TooLong(s.len()))?;
        Ok(Self(value))
    }

    /// Validates a username string.
    fn validate(s: &str) -> Result<(), UsernameError> {
        if s.is_empty() {
            return Err(UsernameError::Empty);
        }
        if s.len() > Self::MAX_LEN {
            return Err(UsernameError::TooLong(s.len()));
        }
        let mut chars = s.chars();
        if let Some(first) = chars.next()
            && !first.is_ascii_alphabetic()
            && first != '_'
        {
            return Err(UsernameError::InvalidFirstCharacter);
        }
        for ch in chars {
            if !ch.is_ascii_alphanumeric() && ch != '_' && ch != '-' {
                return Err(UsernameError::InvalidCharacter);
            }
        }
        Ok(())
    }

    /// Returns the username as a string slice.
    #[must_use]
    #[inline]
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Returns a reference to the underlying `heapless::String`.
    #[must_use]
    #[inline]
    pub const fn as_inner(&self) -> &heapless::String<32> {
        &self.0
    }

    /// Consumes this username and returns the underlying string.
    #[must_use]
    #[inline]
    pub fn into_inner(self) -> heapless::String<32> {
        self.0
    }

    /// Returns `true` if this is a system user.
    ///
    /// System users typically start with underscore or are well-known system accounts.
    #[must_use]
    #[inline]
    pub fn is_system_user(&self) -> bool {
        self.0.starts_with('_') || self.0 == "root" || self.0 == "daemon"
    }

    /// Returns `true` if this is a service account.
    ///
    /// Service accounts typically start with "svc-" or "service-".
    #[must_use]
    #[inline]
    pub fn is_service_account(&self) -> bool {
        self.0.starts_with("svc-") || self.0.starts_with("service-")
    }
}

impl AsRef<str> for Username {
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

impl TryFrom<&str> for Username {
    type Error = UsernameError;

    fn try_from(s: &str) -> Result<Self, Self::Error> {
        Self::new(s)
    }
}

impl FromStr for Username {
    type Err = UsernameError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::new(s)
    }
}

impl fmt::Display for Username {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for Username {
    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
        const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
        const DIGITS: &[u8] = b"0123456789";

        // Generate 1-32 character username
        let len = 1 + (u8::arbitrary(u)? % 32).min(31);
        let mut inner = heapless::String::<32>::new();

        // First character: letter or underscore
        let first_byte = u8::arbitrary(u)?;
        if first_byte % 10 == 0 {
            // 10% chance of underscore
            inner
                .push('_')
                .map_err(|_| arbitrary::Error::IncorrectFormat)?;
        } else {
            let first = ALPHABET[(first_byte % 26) as usize] as char;
            inner
                .push(first)
                .map_err(|_| arbitrary::Error::IncorrectFormat)?;
        }

        // Remaining characters: alphanumeric, underscore, or hyphen
        for _ in 1..len {
            let byte = u8::arbitrary(u)?;
            let c = match byte % 4 {
                0 => ALPHABET[((byte >> 2) % 26) as usize] as char,
                1 => DIGITS[((byte >> 2) % 10) as usize] as char,
                2 => '_',
                _ => '-',
            };
            inner
                .push(c)
                .map_err(|_| arbitrary::Error::IncorrectFormat)?;
        }

        Ok(Self(inner))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_new_valid() {
        let username = Username::new("root").unwrap();
        assert_eq!(username.as_str(), "root");
    }

    #[test]
    fn test_new_empty() {
        assert!(matches!(Username::new(""), Err(UsernameError::Empty)));
    }

    #[test]
    fn test_new_too_long() {
        let long_name = "a".repeat(33);
        assert!(matches!(
            Username::new(&long_name),
            Err(UsernameError::TooLong(33))
        ));
    }

    #[test]
    fn test_new_invalid_first_character() {
        assert!(matches!(
            Username::new("1root"),
            Err(UsernameError::InvalidFirstCharacter)
        ));
        assert!(matches!(
            Username::new("-root"),
            Err(UsernameError::InvalidFirstCharacter)
        ));
    }

    #[test]
    fn test_new_invalid_character() {
        assert!(matches!(
            Username::new("root@"),
            Err(UsernameError::InvalidCharacter)
        ));
        assert!(matches!(
            Username::new("root.user"),
            Err(UsernameError::InvalidCharacter)
        ));
    }

    #[test]
    fn test_is_system_user() {
        let root = Username::new("root").unwrap();
        assert!(root.is_system_user());
        let daemon = Username::new("daemon").unwrap();
        assert!(daemon.is_system_user());
        let system = Username::new("_system").unwrap();
        assert!(system.is_system_user());
        let user = Username::new("user").unwrap();
        assert!(!user.is_system_user());
    }

    #[test]
    fn test_is_service_account() {
        let svc = Username::new("svc-api").unwrap();
        assert!(svc.is_service_account());
        let service = Username::new("service-api").unwrap();
        assert!(service.is_service_account());
        let user = Username::new("user").unwrap();
        assert!(!user.is_service_account());
    }

    #[test]
    fn test_from_str() {
        let username: Username = "root".parse().unwrap();
        assert_eq!(username.as_str(), "root");
    }

    #[test]
    fn test_from_str_error() {
        assert!("".parse::<Username>().is_err());
        assert!("1root".parse::<Username>().is_err());
    }

    #[test]
    fn test_display() {
        let username = Username::new("root").unwrap();
        assert_eq!(format!("{}", username), "root");
    }

    #[test]
    fn test_as_ref() {
        let username = Username::new("root").unwrap();
        let s: &str = username.as_ref();
        assert_eq!(s, "root");
    }

    #[test]
    fn test_clone() {
        let username = Username::new("root").unwrap();
        let username2 = username.clone();
        assert_eq!(username, username2);
    }

    #[test]
    fn test_equality() {
        let u1 = Username::new("root").unwrap();
        let u2 = Username::new("root").unwrap();
        let u3 = Username::new("user").unwrap();
        assert_eq!(u1, u2);
        assert_ne!(u1, u3);
    }

    #[test]
    fn test_as_inner() {
        let username = Username::new("root").unwrap();
        let inner = username.as_inner();
        assert_eq!(inner.as_str(), "root");
    }

    #[test]
    fn test_into_inner() {
        let username = Username::new("root").unwrap();
        let inner = username.into_inner();
        assert_eq!(inner.as_str(), "root");
    }
}