pub fn validate(username: &str) -> anyhow::Result<&str> {
if username.is_empty() {
anyhow::bail!("Username must not be empty");
}
if username.contains("..") {
anyhow::bail!("Invalid username '{username}': must not contain '..'");
}
for (index, c) in username.chars().enumerate() {
let allowed = if index == 0 {
c.is_ascii_alphanumeric()
} else {
c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.'
};
if !allowed {
anyhow::bail!(
"Invalid username '{username}': must start with an ASCII letter or digit \
and contain only ASCII letters, digits, '-', '_' and '.'"
);
}
}
Ok(username)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_typical_usernames() {
assert!(validate("octocat").is_ok());
assert!(validate("user-name").is_ok());
assert!(validate("User_123").is_ok());
assert!(validate("first.last").is_ok());
}
#[test]
fn rejects_empty() {
assert!(validate("").is_err());
}
#[test]
fn rejects_path_traversal() {
assert!(validate("foo/../bar").is_err());
assert!(validate("../etc/passwd").is_err());
assert!(validate("foo..bar").is_err());
assert!(validate("..").is_err());
}
#[test]
fn rejects_url_metacharacters() {
assert!(validate("foo?x=1").is_err());
assert!(validate("foo#frag").is_err());
assert!(validate("foo bar").is_err());
assert!(validate("foo:bar").is_err());
assert!(validate("foo@host").is_err());
}
#[test]
fn rejects_leading_punctuation() {
assert!(validate("-foo").is_err());
assert!(validate("_foo").is_err());
assert!(validate(".foo").is_err());
}
}