Skip to main content

slopguard_core/
sanitize.rs

1//! Neutralizing terminal control characters in strings that third parties
2//! control (rule text, file paths, git output, model reasons).
3
4use std::borrow::Cow;
5
6/// Escape control characters so a string is safe to print to a terminal.
7///
8/// Every control char (C0, DEL and C1, ESC included) except `\n` and `\t` is
9/// replaced by a `\xNN` escape with two lowercase hex digits. Returns the
10/// input borrowed when nothing needs escaping.
11pub fn sanitize_control(s: &str) -> Cow<'_, str> {
12    let needs_escape = |c: char| c.is_control() && c != '\n' && c != '\t';
13    if !s.chars().any(needs_escape) {
14        return Cow::Borrowed(s);
15    }
16    let mut out = String::with_capacity(s.len() + 8);
17    for c in s.chars() {
18        if needs_escape(c) {
19            out.push_str(&format!("\\x{:02x}", u32::from(c)));
20        } else {
21            out.push(c);
22        }
23    }
24    Cow::Owned(out)
25}
26
27#[cfg(test)]
28mod tests {
29    use super::*;
30
31    #[test]
32    fn escapes_esc() {
33        assert_eq!(sanitize_control("a\x1b[2Jb"), "a\\x1b[2Jb");
34    }
35
36    #[test]
37    fn keeps_newline_and_tab() {
38        let out = sanitize_control("a\n\tb");
39        assert_eq!(out, "a\n\tb");
40        assert!(matches!(out, Cow::Borrowed(_)));
41    }
42
43    #[test]
44    fn escapes_carriage_return() {
45        assert_eq!(sanitize_control("a\rb"), "a\\x0db");
46    }
47
48    #[test]
49    fn escapes_c1() {
50        assert_eq!(sanitize_control("a\u{9b}b"), "a\\x9bb");
51    }
52
53    #[test]
54    fn leaves_plain_and_non_ascii_borrowed() {
55        for s in ["plain", "caf\u{e9}"] {
56            let out = sanitize_control(s);
57            assert_eq!(out, s);
58            assert!(matches!(out, Cow::Borrowed(_)));
59        }
60    }
61}