use std::borrow::Cow;
const PUA_BASE: u32 = 0xF000;
const PUA_LAST: u32 = 0xF029;
const WIRE_TRAILING_SPACE: char = '\u{F028}';
const WIRE_TRAILING_PERIOD: char = '\u{F029}';
fn encode_char(c: char) -> Option<char> {
match c {
'\u{01}'..='\u{1F}' => char::from_u32(PUA_BASE + c as u32),
'"' => Some('\u{F020}'),
'*' => Some('\u{F021}'),
':' => Some('\u{F022}'),
'<' => Some('\u{F023}'),
'>' => Some('\u{F024}'),
'?' => Some('\u{F025}'),
'\\' => Some('\u{F026}'),
'|' => Some('\u{F027}'),
_ => None,
}
}
fn decode_char(c: char) -> Option<char> {
match c {
'\u{F001}'..='\u{F01F}' => char::from_u32(c as u32 - PUA_BASE),
'\u{F020}' => Some('"'),
'\u{F021}' => Some('*'),
'\u{F022}' => Some(':'),
'\u{F023}' => Some('<'),
'\u{F024}' => Some('>'),
'\u{F025}' => Some('?'),
'\u{F026}' => Some('\\'),
'\u{F027}' => Some('|'),
'\u{F028}' => Some(' '),
'\u{F029}' => Some('.'),
_ => None,
}
}
pub fn encode_name(name: &str) -> Cow<'_, str> {
let ends_illegally = matches!(name.chars().next_back(), Some(' ') | Some('.'));
if !ends_illegally && !name.chars().any(|c| encode_char(c).is_some()) {
return Cow::Borrowed(name);
}
let mut out = String::with_capacity(name.len());
let mut chars = name.chars().peekable();
while let Some(c) = chars.next() {
let is_last = chars.peek().is_none();
let mapped = match c {
' ' if is_last => Some(WIRE_TRAILING_SPACE),
'.' if is_last => Some(WIRE_TRAILING_PERIOD),
_ => encode_char(c),
};
out.push(mapped.unwrap_or(c));
}
Cow::Owned(out)
}
pub fn decode_name(name: &str) -> Cow<'_, str> {
if !name.chars().any(is_mapped) {
return Cow::Borrowed(name);
}
Cow::Owned(
name.chars()
.map(|c| decode_char(c).unwrap_or(c))
.collect::<String>(),
)
}
fn is_mapped(c: char) -> bool {
let v = c as u32;
v > PUA_BASE && v <= PUA_LAST
}
pub fn encode_path(path: &str) -> String {
let path = path.trim_start_matches('/');
let mut out = String::with_capacity(path.len());
for (i, component) in path.split('/').enumerate() {
if i > 0 {
out.push('\\');
}
if component == "." || component == ".." {
out.push_str(component);
} else {
out.push_str(&encode_name(component));
}
}
out
}
pub fn decode_path(path: &str) -> String {
let mut out = String::with_capacity(path.len());
for (i, component) in path.split('\\').enumerate() {
if i > 0 {
out.push('/');
}
out.push_str(&decode_name(component));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn wire_bytes(s: &str) -> Vec<u8> {
s.encode_utf16().flat_map(u16::to_le_bytes).collect()
}
#[test]
fn every_illegal_character_maps_to_its_documented_code_point() {
let table = [
('"', '\u{F020}'),
('*', '\u{F021}'),
(':', '\u{F022}'),
('<', '\u{F023}'),
('>', '\u{F024}'),
('?', '\u{F025}'),
('\\', '\u{F026}'),
('|', '\u{F027}'),
];
for (local, wire) in table {
assert_eq!(
encode_name(&format!("a{local}b")),
format!("a{wire}b"),
"encoding {local:?}"
);
assert_eq!(
decode_name(&format!("a{wire}b")),
format!("a{local}b"),
"decoding {wire:?}"
);
}
}
#[test]
fn control_characters_map_to_the_matching_offset() {
for code in 0x01u32..=0x1F {
let local = char::from_u32(code).expect("ASCII control is a char");
let wire = char::from_u32(PUA_BASE + code).expect("private use is a char");
assert_eq!(encode_name(&format!("a{local}b")), format!("a{wire}b"));
assert_eq!(decode_name(&format!("a{wire}b")), format!("a{local}b"));
}
}
#[test]
fn nul_is_not_part_of_the_table() {
assert_eq!(encode_name("a\u{0}b"), "a\u{0}b");
assert_eq!(decode_name("a\u{F000}b"), "a\u{F000}b");
}
#[test]
fn private_use_code_points_outside_the_table_are_left_alone() {
for c in ['\u{F02A}', '\u{F0FF}', '\u{F8FF}'] {
assert_eq!(decode_name(&format!("a{c}b")), format!("a{c}b"));
assert_eq!(encode_name(&format!("a{c}b")), format!("a{c}b"));
}
}
#[test]
fn only_the_final_space_or_period_of_a_component_is_mapped() {
assert_eq!(encode_name("ab "), "ab\u{F028}");
assert_eq!(encode_name("ab "), "ab \u{F028}");
assert_eq!(encode_name("ab."), "ab\u{F029}");
assert_eq!(encode_name("ab..."), "ab..\u{F029}");
assert_eq!(encode_name("ab. "), "ab.\u{F028}");
assert_eq!(encode_name("ab ."), "ab \u{F029}");
assert_eq!(encode_name(" ab"), " ab");
assert_eq!(encode_name(".ab"), ".ab");
assert_eq!(encode_name("a b"), "a b");
assert_eq!(encode_name("a.b"), "a.b");
assert_eq!(encode_name(" "), "\u{F028}");
assert_eq!(encode_name("."), "\u{F029}");
}
#[test]
fn a_trailing_marker_decodes_wherever_it_appears() {
assert_eq!(decode_name("a\u{F028}b"), "a b");
assert_eq!(decode_name("a\u{F029}b"), "a.b");
}
#[test]
fn ordinary_names_are_returned_borrowed_and_unchanged() {
for name in [
"report.pdf",
"",
"日本語テスト.txt",
"café",
"café",
"📁 folder",
"a-name_with (punctuation)!#$%&'+,;=[]{}~`^@",
"документ.txt",
] {
assert!(
matches!(encode_name(name), Cow::Borrowed(_)),
"{name:?} should not allocate"
);
assert_eq!(encode_name(name), name);
assert!(matches!(decode_name(name), Cow::Borrowed(_)));
assert_eq!(decode_name(name), name);
}
}
#[test]
fn combining_marks_and_astral_planes_survive_a_name_that_is_mapped() {
let local = "e\u{301}mo\u{1F600}ji?\u{1F469}\u{200D}\u{1F4BB}";
let wire = "e\u{301}mo\u{1F600}ji\u{F025}\u{1F469}\u{200D}\u{1F4BB}";
assert_eq!(encode_name(local), wire);
assert_eq!(decode_name(wire), local);
}
#[test]
fn each_component_is_mapped_on_its_own() {
assert_eq!(encode_path("a?b/c*d"), "a\u{F025}b\\c\u{F021}d");
assert_eq!(encode_path("dir /file. "), "dir\u{F028}\\file.\u{F028}");
assert_eq!(encode_path("dir./sub /x"), "dir\u{F029}\\sub\u{F028}\\x");
}
#[test]
fn a_backslash_in_a_name_is_a_name_character_not_a_separator() {
assert_eq!(encode_path("a\\b"), "a\u{F026}b");
assert_eq!(encode_path("dir/a\\b"), "dir\\a\u{F026}b");
assert_eq!(decode_path("dir\\a\u{F026}b"), "dir/a\\b");
}
#[test]
fn leading_slashes_are_dropped_and_other_separators_are_kept() {
assert_eq!(encode_path("/leading/slash"), "leading\\slash");
assert_eq!(encode_path("///leading"), "leading");
assert_eq!(encode_path("foo/bar/baz"), "foo\\bar\\baz");
assert_eq!(encode_path("no_change"), "no_change");
assert_eq!(encode_path(""), "");
assert_eq!(encode_path("projects/"), "projects\\");
}
#[test]
fn relative_components_pass_through() {
assert_eq!(encode_path("a/../b"), "a\\..\\b");
assert_eq!(encode_path("./a"), ".\\a");
assert_eq!(encode_name(".."), ".\u{F029}");
}
#[test]
fn decode_path_hands_back_something_the_tree_api_accepts() {
let wire = "sub\u{F025}dir\\lea\u{F021}f. ";
let caller = decode_path(wire);
assert_eq!(caller, "sub?dir/lea*f. ");
assert_eq!(
encode_path(&caller),
"sub\u{F025}dir\\lea\u{F021}f.\u{F028}"
);
}
#[test]
fn the_real_failing_name_matches_the_bytes_macos_wrote() {
let encoded = encode_name("\"how_are_you_feeling?\"_emojis.json");
let mut expected = Vec::new();
expected.extend_from_slice(&[0xEF, 0x80, 0xA0]);
expected.extend_from_slice(b"how_are_you_feeling");
expected.extend_from_slice(&[0xEF, 0x80, 0xA5]);
expected.extend_from_slice(&[0xEF, 0x80, 0xA0]);
expected.extend_from_slice(b"_emojis.json");
assert_eq!(encoded.as_bytes(), expected.as_slice());
}
#[test]
fn the_wire_bytes_of_every_mapped_character_are_pinned() {
assert_eq!(wire_bytes(&encode_name("\"")), [0x20, 0xF0]);
assert_eq!(wire_bytes(&encode_name("*")), [0x21, 0xF0]);
assert_eq!(wire_bytes(&encode_name(":")), [0x22, 0xF0]);
assert_eq!(wire_bytes(&encode_name("<")), [0x23, 0xF0]);
assert_eq!(wire_bytes(&encode_name(">")), [0x24, 0xF0]);
assert_eq!(wire_bytes(&encode_name("?")), [0x25, 0xF0]);
assert_eq!(wire_bytes(&encode_name("\\")), [0x26, 0xF0]);
assert_eq!(wire_bytes(&encode_name("|")), [0x27, 0xF0]);
assert_eq!(wire_bytes(&encode_name(" ")), [0x28, 0xF0]);
assert_eq!(wire_bytes(&encode_name(".")), [0x29, 0xF0]);
assert_eq!(wire_bytes(&encode_name("\u{01}")), [0x01, 0xF0]);
assert_eq!(wire_bytes(&encode_name("\u{1F}")), [0x1F, 0xF0]);
}
#[test]
fn the_utf8_bytes_samba_stores_are_pinned() {
let cases: [(&str, &[u8]); 10] = [
("\"", &[0xEF, 0x80, 0xA0]),
("*", &[0xEF, 0x80, 0xA1]),
(":", &[0xEF, 0x80, 0xA2]),
("<", &[0xEF, 0x80, 0xA3]),
(">", &[0xEF, 0x80, 0xA4]),
("?", &[0xEF, 0x80, 0xA5]),
("\\", &[0xEF, 0x80, 0xA6]),
("|", &[0xEF, 0x80, 0xA7]),
(" ", &[0xEF, 0x80, 0xA8]),
(".", &[0xEF, 0x80, 0xA9]),
];
for (local, utf8) in cases {
assert_eq!(encode_name(local).as_bytes(), utf8, "for {local:?}");
}
}
#[test]
fn every_name_without_private_use_code_points_round_trips_exactly() {
for name in [
"a\"b*c:d<e>f?g\\h|i",
"trailing ",
"trailing.",
"\u{01}\u{1F}leading control",
"日本語?テスト. ",
"",
"?",
"\u{7F}",
] {
assert_eq!(decode_name(&encode_name(name)), name, "for {name:?}");
}
}
#[test]
fn a_name_that_already_holds_a_mapped_code_point_is_not_round_trip_stable() {
assert_eq!(encode_name("a\u{F025}b"), "a\u{F025}b");
assert_eq!(decode_name(&encode_name("a\u{F025}b")), "a?b");
}
#[test]
fn paths_round_trip_through_both_helpers() {
for path in [
"a?b/c*d",
"dir /file. ",
"plain/path/file.txt",
"a\\b/c",
"one",
] {
assert_eq!(decode_path(&encode_path(path)), path, "for {path:?}");
}
}
}
#[cfg(test)]
mod round_trip_props {
use super::*;
use proptest::prelude::*;
fn arb_name() -> impl Strategy<Value = String> {
proptest::collection::vec(
any::<char>().prop_filter("reserved by the scheme", |c| {
let v = *c as u32;
*c != '/' && *c != '\u{0}' && !(PUA_BASE..=PUA_LAST).contains(&v)
}),
0..24,
)
.prop_map(|chars| chars.into_iter().collect())
}
fn is_mapped_control(c: char) -> bool {
('\u{01}'..='\u{1F}').contains(&c)
}
fn arb_component() -> impl Strategy<Value = String> {
arb_name().prop_filter("a component has a name", |s| !s.is_empty())
}
proptest! {
#[test]
fn decode_undoes_encode_for_any_name(name in arb_name()) {
let encoded = encode_name(&name);
let decoded = decode_name(&encoded).into_owned();
prop_assert_eq!(decoded, name);
}
#[test]
fn an_encoded_name_carries_nothing_smb2_rejects(name in arb_name()) {
let encoded = encode_name(&name);
let has_illegal = encoded.contains(['"', '*', ':', '<', '>', '?', '\\', '|']);
let has_control = encoded.chars().any(is_mapped_control);
let ends_illegally = matches!(encoded.chars().next_back(), Some(' ') | Some('.'));
prop_assert!(!has_illegal);
prop_assert!(!has_control);
prop_assert!(!ends_illegally);
}
#[test]
fn a_path_survives_both_directions(
components in proptest::collection::vec(arb_component(), 1..4),
) {
let path = components.join("/");
let encoded = encode_path(&path);
prop_assert_eq!(decode_path(&encoded), path);
}
}
}