Skip to main content

atuin_common/string/
escape_non_printable_posix_ext.rs

1use std::borrow::Cow;
2
3/// Extension trait for anything that can behave like a string to make it easy to
4/// escape control characters into a printable, `cat -v`-style representation.
5///
6/// Intended to help prevent control characters being printed and interpreted by
7/// the terminal when printing history as well as to ensure the commands that
8/// appear in the interactive search reflect the actual command run rather than
9/// just the printable characters.
10///
11/// The representation is the POSIX caret/meta notation used by `cat -v`:
12/// - C0 controls (`0x00..=0x1F`) and DEL (`0x7F`) become `^` followed by the
13///   character xor'd with `0x40`, so NUL is `^@`, tab is `^I`, ESC is `^[`, and
14///   DEL is `^?`.
15/// - C1 controls (`0x80..=0x9F`) become `M-^` followed by their low 7 bits
16///   xor'd with `0x40`, so U+009B (single-byte CSI) is `M-^[`.
17///
18/// Everything else — including spaces and printable multi-byte Unicode — is
19/// left untouched.
20pub trait EscapeNonPrintablePosixExt: AsRef<str> {
21    fn escape_non_printable(&self) -> Cow<'_, str> {
22        // Each character escapes to a (possibly empty) prefix followed by a
23        // single payload character, so every branch yields the same iterator
24        // type without any fixed-size padding.
25        let escape_char = |c: char| {
26            let (prefix, payload): (&str, char) = if c.is_ascii_control() {
27                // C0 controls and DEL: `^@`..`^_` and `^?`.
28                ("^", (c as u8 ^ 0x40) as char)
29            } else if c.is_control() {
30                // C1 controls (U+0080..=U+009F): `cat -v` meta+caret notation.
31                ("M-^", ((c as u8 & 0x7f) ^ 0x40) as char)
32            } else {
33                // Printable (including spaces and multi-byte Unicode): unchanged.
34                ("", c)
35            };
36
37            prefix.chars().chain(std::iter::once(payload))
38        };
39
40        let s = self.as_ref();
41        if !s.contains(|c: char| c.is_control()) {
42            return Cow::Borrowed(s);
43        }
44
45        Cow::Owned(s.chars().flat_map(escape_char).collect())
46    }
47}
48
49impl<T: AsRef<str>> EscapeNonPrintablePosixExt for T {}
50
51#[cfg(test)]
52mod tests {
53    use std::borrow::Cow;
54
55    use proptest::prelude::*;
56    use rstest::rstest;
57
58    use super::EscapeNonPrintablePosixExt;
59
60    /// Table-driven examples of the exact `cat -v` mapping. Each case is its own
61    /// test, so a failure names precisely which input broke.
62    #[rstest]
63    // Nothing to escape — returned unchanged (space is printable).
64    #[case::plain_text_unchanged("plain text", "plain text")]
65    #[case::two_words_unchanged("two words", "two words")]
66    // Printable multi-byte Unicode is preserved; only the control char changes.
67    #[case::multibyte_unicode_preserved_around_escaped_control("🐢\x1b[32m🦀", "🐢^[[32m🦀")]
68    // C0 controls and DEL → caret notation (`^` + byte ^ 0x40).
69    #[case::esc_0x1b("\x1b[31mfoo", "^[[31mfoo")] // ESC (0x1b)
70    #[case::tab_0x09("foo\tbar", "foo^Ibar")] // TAB (0x09)
71    #[case::nul_0x00("a\0b", "a^@b")] // NUL (0x00) — the core of issue #3589
72    #[case::del_0x7f("a\x7fb", "a^?b")] // DEL (0x7f ^ 0x40 == '?')
73    // C1 controls (U+0080..=U+009F) → `cat -v` meta+caret notation.
74    #[case::c1_control_0x80("\u{80}", "M-^@")]
75    #[case::single_char_csi_0x9b("a\u{9b}b", "aM-^[b")] // single-char CSI: 0x9b & 0x7f == 0x1b → ^[
76    #[case::c1_control_0x9f("\u{9f}", "M-^_")]
77    fn escapes_as_cat_v(#[case] input: &str, #[case] expected: &str) {
78        assert_eq!(input.escape_non_printable(), expected);
79    }
80
81    #[rstest]
82    fn escapes_all_c0_controls_as_caret() {
83        // Exhaustively check every C0 control 0x00..=0x1f → '^' + (byte ^ 0x40).
84        for byte in 0x00u8..=0x1f {
85            let input = (byte as char).to_string();
86            let expected = format!("^{}", (byte ^ 0x40) as char);
87            assert_eq!(input.escape_non_printable(), expected, "byte {byte:#04x}");
88        }
89    }
90
91    proptest! {
92        /// The whole point of the function: for ANY input string, the escaped
93        /// output never contains a control character.
94        #[rstest]
95        fn output_never_contains_control_chars(s in r"(?s).*") {
96            let escaped = s.escape_non_printable();
97            prop_assert!(!escaped.chars().any(char::is_control));
98        }
99
100        /// A string with no control characters is returned borrowed (zero-copy),
101        /// and is therefore byte-for-byte unchanged.
102        #[rstest]
103        fn control_free_input_is_borrowed(s in r"[^\p{Cc}]*") {
104            prop_assert!(matches!(s.escape_non_printable(), Cow::Borrowed(_)));
105        }
106
107        /// Escaping is idempotent: the output is already fully printable, so
108        /// escaping it a second time changes nothing.
109        #[rstest]
110        fn escaping_is_idempotent(s in r"(?s).*") {
111            let once = s.escape_non_printable().into_owned();
112            let twice = once.escape_non_printable().into_owned();
113            prop_assert_eq!(once, twice);
114        }
115    }
116}