use crate::error::NameError;
use crate::METADATA_MEMBER;
pub fn check_payload_name(name: &str) -> Result<(), NameError> {
if name.is_empty() {
return Err(NameError::Empty);
}
if name == "." || name == ".." {
return Err(NameError::Relative);
}
if name.contains('/') {
return Err(NameError::Separator('/'));
}
if name.contains('\\') {
return Err(NameError::Separator('\\'));
}
if name.contains(':') {
return Err(NameError::Colon);
}
if let Some(c) = name.chars().find(|c| c.is_ascii() && c.is_control()) {
return Err(NameError::ControlCharacter(c));
}
if name == METADATA_MEMBER {
return Err(NameError::ReservedForMetadata);
}
Ok(())
}
#[must_use]
pub fn display_name(name: &str) -> std::borrow::Cow<'_, str> {
use std::fmt::Write as _;
if !name.chars().any(|c| is_bidi_formatting(c) || c == '\\') {
return std::borrow::Cow::Borrowed(name);
}
let mut out = String::with_capacity(name.len());
for c in name.chars() {
if c == '\\' {
out.push_str("\\\\");
} else if is_bidi_formatting(c) {
let _ = write!(out, "\\u{{{:04X}}}", c as u32);
} else {
out.push(c);
}
}
std::borrow::Cow::Owned(out)
}
fn is_bidi_formatting(c: char) -> bool {
matches!(c,
'\u{061C}' | '\u{200E}'..='\u{200F}' | '\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}' )
}
#[cfg(test)]
mod tests {
#[test]
fn every_bidi_formatting_character_is_escaped() {
for c in [
'\u{061C}', '\u{200E}', '\u{200F}', '\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}',
'\u{202E}', '\u{2066}', '\u{2067}', '\u{2068}', '\u{2069}',
] {
let name = format!("a{c}b");
assert_eq!(
display_name(&name),
format!("a\\u{{{:04X}}}b", c as u32),
"U+{:04X} was not escaped",
c as u32
);
}
}
#[test]
fn neighbouring_characters_are_left_alone() {
for c in [
'\u{061B}', '\u{061D}', '\u{200D}', '\u{2010}', '\u{2029}', '\u{202F}', '\u{2060}',
'\u{2065}', '\u{206A}', 'a', '.', 'é', '日',
] {
let name = format!("a{c}b");
assert_eq!(display_name(&name), name, "U+{:04X} was changed", c as u32);
}
}
#[test]
fn the_rendering_does_not_collide() {
let hostile = display_name("report\u{202E}fdp.exe").into_owned();
let innocent = display_name(r"report\u{202E}fdp.exe").into_owned();
assert_ne!(hostile, innocent, "two names rendered the same");
for shown in [&hostile, &innocent] {
assert!(shown.contains(".exe"), "{shown}");
}
}
#[test]
fn an_ordinary_name_is_not_copied() {
assert!(matches!(
display_name("Q3 report final.pdf"),
std::borrow::Cow::Borrowed(_)
));
assert!(matches!(
display_name("report\u{202E}fdp.exe"),
std::borrow::Cow::Owned(_)
));
}
#[test]
fn an_override_no_longer_hides_the_extension() {
let shown = display_name("report\u{202E}fdp.exe");
assert!(shown.ends_with(".exe"), "{shown}");
assert!(!shown.chars().any(is_bidi_formatting), "{shown}");
}
use super::*;
#[test]
fn accepts_a_plain_filename() {
for n in [
"report.pdf",
"a",
"..leading",
"trailing..",
"wide 名前.txt",
"-",
] {
assert_eq!(check_payload_name(n), Ok(()), "{n:?}");
}
}
#[test]
fn a_double_dot_is_only_excluded_as_the_whole_name() {
for n in ["a..b", "..leading", "trailing..", "...."] {
assert_eq!(check_payload_name(n), Ok(()), "{n:?}");
}
}
#[test]
fn rejects_every_control_character() {
for c in (0u8..=0x1f).chain(std::iter::once(0x7f)) {
let name = format!("rep{}ort.pdf", c as char);
assert_eq!(
check_payload_name(&name),
Err(NameError::ControlCharacter(c as char)),
"U+{c:04X}"
);
}
assert_eq!(check_payload_name("rep\u{200b}ort.pdf"), Ok(()));
}
#[test]
fn rejects_each_rule_in_2_3() {
assert_eq!(check_payload_name(""), Err(NameError::Empty));
assert_eq!(check_payload_name("."), Err(NameError::Relative));
assert_eq!(check_payload_name(".."), Err(NameError::Relative));
assert_eq!(check_payload_name("a/b"), Err(NameError::Separator('/')));
assert_eq!(check_payload_name("../b"), Err(NameError::Separator('/')));
assert_eq!(check_payload_name("a\\b"), Err(NameError::Separator('\\')));
assert_eq!(check_payload_name("C:file"), Err(NameError::Colon));
assert_eq!(
check_payload_name(METADATA_MEMBER),
Err(NameError::ReservedForMetadata)
);
}
}