pub type NameValidator = fn(&str) -> bool;
#[inline]
pub fn default_name_validator(name: &str) -> bool {
let bytes = name.as_bytes();
!bytes.first().is_some_and(u8::is_ascii_whitespace)
&& !bytes.last().is_some_and(u8::is_ascii_whitespace)
&& !bytes.iter().any(u8::is_ascii_control)
&& !bytes.contains(&b':')
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) enum NameValidation {
#[default]
Default,
Disabled,
Custom(NameValidator),
}
impl NameValidation {
pub(crate) fn from_validator(validator: Option<NameValidator>) -> Self {
match validator {
Some(validator) => Self::Custom(validator),
None => Self::Disabled,
}
}
#[inline]
pub(crate) fn accepts(self, name: &str) -> bool {
match self {
Self::Default => default_name_validator(name),
Self::Disabled => true,
Self::Custom(validator) => validator(name),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_validator_rejects_ascii_controls_colons_and_boundary_whitespace() {
for name in [
"",
"/absolute",
r"back\slash",
".",
"..",
"a//b",
"interior space",
"\u{a0}name\u{a0}",
] {
assert!(default_name_validator(name), "{name:?}");
}
for name in [
" leading",
"trailing ",
"\tname",
"name\n",
"inside\nname",
"name:stream",
] {
assert!(!default_name_validator(name), "{name:?}");
}
}
}