1use std::fmt;
4
5pub fn fmt_name(name: &str) -> impl fmt::Display + '_ {
13 struct N<'a>(&'a str);
14 impl fmt::Display for N<'_> {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16 for c in self.0.chars() {
17 match c {
18 '\0' => f.write_str("\\0")?,
19 '\t' => f.write_str("\\t")?,
20 '\n' => f.write_str("\\n")?,
21 '\r' => f.write_str("\\r")?,
22 '\u{0}'..='\u{1f}' | '\u{7f}' => write!(f, "\\x{:02x}", c as u32)?,
23 '\u{80}'..='\u{9f}' => write!(f, "\\u{{{:x}}}", c as u32)?,
24 '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}' => {
25 write!(f, "\\u{{{:x}}}", c as u32)?
26 }
27 c => fmt::Write::write_char(f, c)?,
28 }
29 }
30 Ok(())
31 }
32 }
33 N(name)
34}
35
36#[cfg(test)]
37mod tests {
38 use super::fmt_name;
39 fn s(x: &str) -> String {
40 fmt_name(x).to_string()
41 }
42
43 #[test]
44 fn a_plain_name_is_unchanged() {
45 for n in [
46 "a.txt",
47 "dir/sub file.tar",
48 "C:\\x\\y.txt",
49 "\"q\" 'q'",
50 "naïve/日本.txt",
51 ] {
52 assert_eq!(s(n), n);
53 }
54 }
55 #[test]
56 fn c0_controls_and_del_are_escaped() {
57 assert_eq!(s("a\0b"), "a\\0b");
58 assert_eq!(s("a\tb\nc\rd"), "a\\tb\\nc\\rd");
59 assert_eq!(s("a\x1b[31mb"), "a\\x1b[31mb");
60 assert_eq!(s("\x01\x7f"), "\\x01\\x7f");
61 }
62 #[test]
63 fn c1_controls_are_escaped() {
64 assert_eq!(s("a\u{85}b\u{9b}"), "a\\u{85}b\\u{9b}");
65 }
66 #[test]
67 fn bidi_overrides_and_isolates_are_escaped() {
68 assert_eq!(s("x\u{202e}gpj.exe"), "x\\u{202e}gpj.exe");
69 for c in [
70 '\u{202a}', '\u{202b}', '\u{202c}', '\u{202d}', '\u{2066}', '\u{2067}', '\u{2068}',
71 '\u{2069}',
72 ] {
73 assert!(!s(&format!("a{c}b")).contains(c));
74 }
75 }
76}