use std::borrow::Cow;
use std::fmt::Write;
#[inline]
fn needs_escape(c: char) -> bool {
if c == '\t' {
return false;
}
c.is_control()
|| matches!(c,
'\u{00AD}' | '\u{180E}' | '\u{200B}'..='\u{200F}' | '\u{202A}'..='\u{202E}' | '\u{2060}'..='\u{206F}' | '\u{FEFF}' | '\u{FFF9}'..='\u{FFFB}' | '\u{E0000}'..='\u{E007F}' )
}
pub fn sanitize_for_terminal(s: &str) -> Cow<'_, str> {
if !s.chars().any(needs_escape) {
return Cow::Borrowed(s);
}
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if needs_escape(c) {
let v = c as u32;
if v <= 0xFF {
let _ = write!(out, "\\x{v:02X}");
} else {
let _ = write!(out, "\\u{{{v:04X}}}");
}
} else {
out.push(c);
}
}
Cow::Owned(out)
}
pub fn maybe_sanitize<'a>(s: &'a str, is_terminal: bool) -> Cow<'a, str> {
if is_terminal {
sanitize_for_terminal(s)
} else {
Cow::Borrowed(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn preserves_safe_content() {
for s in [
"hello.txt",
"résumé.pdf",
"文档.txt",
"🦀.rs",
"a\tb",
"a\u{FFFD}b",
] {
assert!(
matches!(sanitize_for_terminal(s), Cow::Borrowed(_)),
"{s:?}"
);
assert_eq!(sanitize_for_terminal(s), s);
}
}
#[test]
fn strips_osc52_clipboard_payload() {
let attack = "innocent\x1b]52;c;cHduZWQ=\x1b\\.txt";
let safe = sanitize_for_terminal(attack);
assert!(!safe.contains('\x1b'));
assert_eq!(safe, "innocent\\x1B]52;c;cHduZWQ=\\x1B\\.txt");
}
#[test]
fn strips_cr_output_forgery() {
assert_eq!(sanitize_for_terminal("A\rFAKE OUTPUT"), "A\\x0DFAKE OUTPUT");
}
#[test]
fn strips_osc8_hyperlink_injection() {
let attack = "phish\x1b]8;;https:evil.example\x1b\\phony.txt";
assert!(!sanitize_for_terminal(attack).contains('\x1b'));
}
#[test]
fn strips_del() {
assert_eq!(sanitize_for_terminal("a\x7fb"), "a\\x7Fb");
}
#[test]
fn strips_bel_and_null() {
assert_eq!(sanitize_for_terminal("a\x07b"), "a\\x07b");
assert_eq!(sanitize_for_terminal("a\0b"), "a\\x00b");
}
#[test]
fn strips_newline() {
assert_eq!(sanitize_for_terminal("a\nb"), "a\\x0Ab");
}
#[test]
fn escape_preserves_information() {
let s = "name\x1bX\x07Y.txt";
assert_eq!(sanitize_for_terminal(s), "name\\x1BX\\x07Y.txt");
}
#[test]
fn strips_c1_csi_and_osc_initiators() {
assert_eq!(sanitize_for_terminal("\u{9b}31m"), "\\x9B31m");
assert_eq!(
sanitize_for_terminal("\u{9d}0;pwned\u{9c}"),
"\\x9D0;pwned\\x9C"
);
}
#[test]
fn strips_bidi_overrides_and_zero_width() {
assert_eq!(
sanitize_for_terminal("safe\u{202E}fil\u{202D}gnp.exe"),
"safe\\u{202E}fil\\u{202D}gnp.exe"
);
assert_eq!(sanitize_for_terminal("a\u{200B}b"), "a\\u{200B}b");
assert_eq!(sanitize_for_terminal("\u{FEFF}name"), "\\u{FEFF}name");
}
#[test]
fn keeps_legitimate_unicode_features() {
for s in [
"heart\u{2764}\u{FE0F}.txt",
"icon\u{E000}.cfg",
"cjk\u{6F22}\u{E0101}.txt",
] {
assert!(
matches!(sanitize_for_terminal(s), Cow::Borrowed(_)),
"{s:?}"
);
}
}
#[test]
fn maybe_sanitize_passthrough_when_not_terminal() {
let attack = "x\x1by";
let out = maybe_sanitize(attack, false);
assert!(matches!(out, Cow::Borrowed(_)));
assert_eq!(out, attack);
assert_eq!(maybe_sanitize(attack, true), "x\\x1By");
}
}