pub const MAX_REGISTRY_NAME_LEN: usize = 255;
pub fn validate_registry_name(name: &str, context: &str) -> Result<(), String> {
if name.is_empty() {
return Err(format!("{context} name is empty"));
}
if name.trim().is_empty() {
return Err(format!("{context} name is whitespace-only"));
}
if name.len() > MAX_REGISTRY_NAME_LEN {
return Err(format!(
"{context} name exceeds {MAX_REGISTRY_NAME_LEN} bytes"
));
}
if name.starts_with('/') {
return Err(format!("{context} name starts with '/': {name}"));
}
for segment in name.split('/') {
if segment == ".." || segment.is_empty() {
return Err(format!("{context} name contains invalid segment: {name}"));
}
for ch in segment.chars() {
let ok = ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-');
if !ok {
return Err(format!(
"{context} name contains illegal character '{ch}' in '{name}'"
));
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_names_accepted() {
for good in &[
"sonarr",
"radarr",
"kids/tv",
"a-b_c",
"movies/4k",
"letters_and_123",
"a",
] {
assert!(
validate_registry_name(good, "category").is_ok(),
"expected {good:?} to validate",
);
assert!(validate_registry_name(good, "tag").is_ok());
}
}
#[test]
fn invalid_names_rejected() {
for bad in &[
"",
" ",
"/leading",
"a/../b",
"with space",
"has!bang",
"a//b",
"trail/",
"..",
"dot/.",
] {
assert!(
validate_registry_name(bad, "category").is_err(),
"expected {bad:?} to be rejected",
);
}
}
#[test]
fn length_cap_enforced() {
let ok = "a".repeat(MAX_REGISTRY_NAME_LEN);
let too_long = "a".repeat(MAX_REGISTRY_NAME_LEN + 1);
assert!(validate_registry_name(&ok, "tag").is_ok());
assert!(validate_registry_name(&too_long, "tag").is_err());
}
#[test]
fn context_flows_into_error_message() {
let err = validate_registry_name("", "tag").unwrap_err();
assert!(err.contains("tag"), "expected context in error: {err}");
let err = validate_registry_name("", "category").unwrap_err();
assert!(err.contains("category"), "expected context in error: {err}");
}
#[test]
fn case_sensitive_alphabet() {
assert!(validate_registry_name("Sonarr", "tag").is_ok());
assert!(validate_registry_name("SONARR", "tag").is_ok());
assert!(validate_registry_name("sonarr", "tag").is_ok());
}
}