use std::fmt;
use crate::crypto::zeroize::Zeroizing;
use crate::util::validation::is_valid_username;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UsernameError {
_private: (),
}
impl UsernameError {
const fn new() -> Self {
Self { _private: () }
}
}
impl fmt::Display for UsernameError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "username: {}", crate::util::validation::USERNAME_RULES)
}
}
impl std::error::Error for UsernameError {}
#[derive(Clone, PartialEq, Eq)]
pub struct Username(String);
impl Username {
pub fn new(s: &str) -> Result<Self, UsernameError> {
if is_valid_username(s) {
Ok(Self(s.to_owned()))
} else {
Err(UsernameError::new())
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Debug for Username {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Username").field(&self.0).finish()
}
}
impl fmt::Display for Username {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
pub struct Credentials {
username: Username,
password: Zeroizing<Vec<u8>>,
}
impl Credentials {
pub fn new(username: &str, password: Vec<u8>) -> Result<Self, UsernameError> {
let username = Username::new(username)?;
Ok(Self {
username,
password: Zeroizing::new(password),
})
}
#[must_use]
pub fn username(&self) -> &Username {
&self.username
}
#[must_use]
pub fn password(&self) -> &[u8] {
&self.password
}
}
impl fmt::Debug for Credentials {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Credentials")
.field("username", &self.username)
.field("password", &"[REDACTED]")
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn username_valid() {
let u = Username::new("alice").unwrap();
assert_eq!(u.as_str(), "alice");
}
#[test]
fn username_with_at_sign() {
let u = Username::new("alice@example.com").unwrap();
assert_eq!(u.as_str(), "alice@example.com");
}
#[test]
fn username_rejects_empty() {
assert!(Username::new("").is_err());
}
#[test]
fn username_rejects_leading_special() {
assert!(Username::new("-alice").is_err());
assert!(Username::new(".alice").is_err());
assert!(Username::new("@alice").is_err());
}
#[test]
fn username_rejects_consecutive_specials() {
assert!(Username::new("alice..bob").is_err());
}
#[test]
fn username_display() {
let u = Username::new("alice").unwrap();
assert_eq!(format!("{u}"), "alice");
}
#[test]
fn username_debug() {
let u = Username::new("alice").unwrap();
let debug = format!("{u:?}");
assert!(
debug.contains("alice"),
"debug should show username: {debug}"
);
}
#[test]
fn credentials_creation() {
let creds = Credentials::new("alice", b"secret".to_vec()).unwrap();
assert_eq!(creds.username().as_str(), "alice");
assert_eq!(creds.password(), b"secret");
}
#[test]
fn credentials_rejects_invalid_username() {
assert!(Credentials::new("", b"secret".to_vec()).is_err());
}
#[test]
fn credentials_debug_redacts_password() {
let creds = Credentials::new("alice", b"secret-password".to_vec()).unwrap();
let debug = format!("{creds:?}");
assert!(
debug.contains("[REDACTED]"),
"debug should redact password: {debug}"
);
assert!(
!debug.contains("secret-password"),
"debug must not contain password: {debug}",
);
}
#[test]
fn credentials_zeroize_on_drop() {
let creds = Credentials::new("alice", vec![0xFFu8; 16]).unwrap();
assert!(creds.password().iter().all(|&b| b == 0xFF));
}
#[test]
fn username_error_display() {
let err = Username::new("").unwrap_err();
let msg = err.to_string();
assert!(
msg.starts_with("username:"),
"error should be prefixed: {msg}"
);
}
#[test]
fn username_error_implements_std_error() {
let err: Box<dyn std::error::Error> = Box::new(Username::new("").unwrap_err());
let _ = err.to_string();
}
}