1pub type NameValidator = fn(&str) -> bool;
3
4#[inline]
11pub fn default_name_validator(name: &str) -> bool {
12 let bytes = name.as_bytes();
13 !bytes.first().is_some_and(u8::is_ascii_whitespace)
14 && !bytes.last().is_some_and(u8::is_ascii_whitespace)
15 && !bytes.iter().any(u8::is_ascii_control)
16 && !bytes.contains(&b':')
17}
18
19#[derive(Clone, Copy, Debug, Default)]
20pub(crate) enum NameValidation {
21 #[default]
22 Default,
23 Disabled,
24 Custom(NameValidator),
25}
26
27impl NameValidation {
28 pub(crate) fn from_validator(validator: Option<NameValidator>) -> Self {
29 match validator {
30 Some(validator) => Self::Custom(validator),
31 None => Self::Disabled,
32 }
33 }
34
35 #[inline]
36 pub(crate) fn accepts(self, name: &str) -> bool {
37 match self {
38 Self::Default => default_name_validator(name),
39 Self::Disabled => true,
40 Self::Custom(validator) => validator(name),
41 }
42 }
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48
49 #[test]
50 fn default_validator_rejects_ascii_controls_colons_and_boundary_whitespace() {
51 for name in [
52 "",
53 "/absolute",
54 r"back\slash",
55 ".",
56 "..",
57 "a//b",
58 "interior space",
59 "\u{a0}name\u{a0}",
60 ] {
61 assert!(default_name_validator(name), "{name:?}");
62 }
63 for name in [
64 " leading",
65 "trailing ",
66 "\tname",
67 "name\n",
68 "inside\nname",
69 "name:stream",
70 ] {
71 assert!(!default_name_validator(name), "{name:?}");
72 }
73 }
74}