pub fn is_valid_subdomain_label(name: &str) -> bool {
let len = name.len(); (1..=63).contains(&len)
&& !name.starts_with('-')
&& !name.ends_with('-')
&& name
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
}
pub fn validate(input: &str) -> Result<String, String> {
let name = input.trim().to_ascii_lowercase();
if !name.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-') {
return Err(
"use only lowercase letters, digits, and hyphens — no spaces, dots, or accented/unicode characters"
.to_string(),
);
}
let len = name.chars().count();
if !(3..=32).contains(&len) {
return Err(format!("name must be 3–32 characters (got {len})"));
}
if name.starts_with('-') || name.ends_with('-') {
return Err("name can't start or end with a hyphen".to_string());
}
Ok(name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_valid_labels() {
assert_eq!(validate("alice").unwrap(), "alice");
assert_eq!(validate("foo-bar").unwrap(), "foo-bar");
assert_eq!(validate("a1b2c3").unwrap(), "a1b2c3");
assert_eq!(validate(&"a".repeat(32)).unwrap(), "a".repeat(32));
}
#[test]
fn normalizes_case_and_whitespace_unsurprisingly() {
assert_eq!(validate(" Alice ").unwrap(), "alice");
assert_eq!(validate("MyAgent2").unwrap(), "myagent2");
}
#[test]
fn rejects_unicode_instead_of_silently_mangling() {
assert!(validate("café-shop").is_err());
assert!(validate("日本").is_err());
assert!(validate("über").is_err());
}
#[test]
fn rejects_spaces_and_dots() {
assert!(validate("my cool app").is_err());
assert!(validate("a.b.c").is_err());
assert!(validate("under_score").is_err());
}
#[test]
fn rejects_bad_length() {
assert!(validate("ab").is_err()); assert!(validate(&"a".repeat(33)).is_err()); assert!(validate("").is_err());
}
#[test]
fn rejects_edge_hyphens() {
assert!(validate("-alice").is_err());
assert!(validate("alice-").is_err());
assert!(validate("--").is_err());
}
#[test]
fn label_rule_blocks_unroutable_names() {
assert!(is_valid_subdomain_label("alice"));
assert!(is_valid_subdomain_label("a")); assert!(is_valid_subdomain_label("a-b-c"));
assert!(is_valid_subdomain_label(&"a".repeat(63))); assert!(!is_valid_subdomain_label(&"a".repeat(64))); assert!(!is_valid_subdomain_label("")); assert!(!is_valid_subdomain_label("Alice")); assert!(!is_valid_subdomain_label("a_b")); assert!(!is_valid_subdomain_label("café")); assert!(!is_valid_subdomain_label("-foo")); assert!(!is_valid_subdomain_label("foo-")); assert!(!is_valid_subdomain_label("-")); }
#[test]
fn validate_is_strictly_tighter_than_the_label_rule() {
for ok in ["alice", "foo-bar", &"a".repeat(32)] {
let normalized = validate(ok).unwrap();
assert!(
is_valid_subdomain_label(&normalized),
"validate accepted an unroutable label: {normalized}"
);
}
assert!(is_valid_subdomain_label(&"a".repeat(40)));
assert!(validate(&"a".repeat(40)).is_err());
}
}