pub(crate) const RULE_SUMMARY: &str = "not empty, not only dots, no / \\ : * ? \" < > |, no \
control characters, nothing ending in a dot or a space, no reserved device name (CON, NUL, \
PRN, AUX, COM1-9, LPT1-9 — also with an extension or a superscript digit, plus CONIN$ and \
CONOUT$), and at most 255 bytes";
const MAX_NAME_BYTES: usize = 255;
const RESERVED_DEVICE_NAMES: [&str; 30] = [
"con", "prn", "aux", "nul", "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8",
"com9", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", "com¹", "com²",
"com³", "lpt¹", "lpt²", "lpt³", "conin$", "conout$",
];
#[must_use]
pub(crate) fn folder_name_problem(name: &str) -> Option<&'static str> {
if name.is_empty() {
return Some("it is empty");
}
if name.chars().all(|c| c == '.') {
return Some("it consists only of dots");
}
if name.len() > MAX_NAME_BYTES {
return Some("it is longer than 255 bytes");
}
if name.contains(['/', '\\']) {
return Some("it contains a path separator");
}
if name.contains(|c: char| c <= '\u{1f}') {
return Some("it contains a control character");
}
if name.contains(['<', '>', ':', '"', '|', '?', '*']) {
return Some("it contains a character no folder name may hold");
}
if name.ends_with(['.', ' ']) {
return Some("it ends with a dot or a space");
}
if is_reserved_device_name(name) {
return Some("it is a name the platform reserves for a device");
}
None
}
fn is_reserved_device_name(name: &str) -> bool {
let stem = name.split_once('.').map_or(name, |(stem, _)| stem);
RESERVED_DEVICE_NAMES.contains(&stem.to_lowercase().as_str())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn folder_name_rule_is_the_cross_platform_intersection() {
let too_long = "x".repeat(MAX_NAME_BYTES + 1);
let refused: [&str; 34] = [
"",
".",
"..",
"...",
"a/b",
"a\\b",
"/etc",
"../evil",
"..\\evil",
"C:\\x",
"C:x",
"a\u{1}b",
"a\u{0}b",
"a:b",
"a?b",
"a*b",
"a\"b",
"a<b",
"a>b",
"a|b",
"trailing.",
"trailing ",
" ",
"nul",
"NUL",
"nul.txt",
"con.tar.gz",
"Com1",
"lpt9",
"com¹",
"LPT³",
"conin$",
"CONOUT$",
&too_long,
];
for name in refused {
assert!(
folder_name_problem(name).is_some(),
"must be refused: {name:?}"
);
}
let longest = "x".repeat(MAX_NAME_BYTES);
let accepted: [&str; 12] = [
"admin",
"alice",
"a b",
"café",
"a.b",
"conway",
"null",
".hidden",
"x",
"a-b_c",
"Japanese漢字",
&longest,
];
for name in accepted {
assert_eq!(
folder_name_problem(name),
None,
"must be accepted: {name:?}"
);
}
}
}