Skip to main content

stuffr_core/
display.rs

1//! Rendering an archive-supplied name for a person to read.
2
3use std::fmt;
4
5/// Displays `name` with every character that could rewrite or hide terminal
6/// output escaped: C0 controls and DEL (`\0` `\t` `\n` `\r` by name, the rest
7/// `\xHH`), C1 controls (`\u{HH}`) and the bidi embeddings, overrides and
8/// isolates U+202A–U+202E and U+2066–U+2069 (`\u{HHHH}`). Everything else —
9/// backslashes, quotes, spaces, other non-ASCII — is written unchanged, so a
10/// name with nothing to escape displays exactly as before. For display only:
11/// not reversible, and never used for a name stuffr writes or matches.
12pub 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}