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 name == METADATA_MEMBER {
return Err(NameError::ReservedForMetadata);
}
Ok(())
}
fn decoding_is_faithful(name: &str, raw: &[u8]) -> bool {
!(name.contains('\u{fffd}') && std::str::from_utf8(raw).is_err())
}
pub(crate) fn matches(name: &str, raw: &[u8], want: &str) -> bool {
decoding_is_faithful(name, raw) && name == want
}
#[cfg(test)]
mod tests {
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 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)
);
}
#[test]
fn an_honest_name_matches_itself() {
assert!(matches("report.pdf", b"report.pdf", "report.pdf"));
assert!(matches("caf\u{e7}.txt", b"caf\x87.txt", "caf\u{e7}.txt"));
}
#[test]
fn a_lossily_decoded_name_matches_nothing() {
let lossy = "caf\u{fffd}.txt";
assert!(!matches(lossy, b"caf\xff.txt", lossy));
}
#[test]
fn the_guard_does_not_catch_an_honest_replacement_character() {
let name = "caf\u{fffd}.txt";
assert!(matches(name, name.as_bytes(), name));
}
}