pub fn scrub_controls(input: &str) -> String {
scrub_inner(input, false)
}
pub fn scrub_controls_keep_sgr(input: &str) -> String {
scrub_inner(input, true)
}
fn scrub_inner(input: &str, keep_sgr: bool) -> String {
let mut out = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\t' | '\n' | '\r' => out.push(c),
'\x00'..='\x1F' => {
if c == '\x1B' {
match chars.peek() {
Some(&'[') => {
chars.next(); let mut buf = String::new();
buf.push('\x1b');
buf.push('[');
let mut final_byte: Option<char> = None;
while let Some(&p) = chars.peek() {
chars.next();
buf.push(p);
if ('\x40'..='\x7E').contains(&p) {
final_byte = Some(p);
break;
}
}
if keep_sgr && final_byte == Some('m') {
out.push_str(&buf);
}
}
Some(&']') => {
chars.next(); while let Some(&p) = chars.peek() {
chars.next();
if p == '\x07' {
break;
}
if p == '\x1B' {
if chars.peek() == Some(&'\\') {
chars.next();
}
break;
}
}
}
Some(_) => {
}
None => {} }
}
}
'\u{0080}'..='\u{009F}' => {
if c == '\u{009B}' {
while let Some(&p) = chars.peek() {
chars.next();
if ('\x40'..='\x7E').contains(&p) {
break;
}
}
}
}
_ => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plain_ascii_passes_through() {
assert_eq!(scrub_controls("hello world"), "hello world");
}
#[test]
fn newline_tab_cr_preserved() {
assert_eq!(scrub_controls("a\nb\tc\rd"), "a\nb\tc\rd");
}
#[test]
fn csi_escape_stripped() {
assert_eq!(scrub_controls("\x1b[2J\x1b[Hhello"), "hello");
}
#[test]
fn osc_escape_stripped() {
assert_eq!(scrub_controls("\x1b]0;pwned\x07safe"), "safe");
}
#[test]
fn cursor_position_query_stripped() {
assert_eq!(scrub_controls("a\x1b[6nb"), "ab");
}
#[test]
fn c0_controls_except_tnlcr_removed() {
assert_eq!(scrub_controls("a\x00b\x01c\x07d\x08e"), "abcde");
}
#[test]
fn c1_controls_removed() {
assert_eq!(scrub_controls("a\u{009b}2Jb"), "ab");
}
#[test]
fn utf8_text_preserved() {
assert_eq!(scrub_controls("你好\nworld"), "你好\nworld");
}
#[test]
fn bare_esc_removed() {
assert_eq!(scrub_controls("a\x1bb"), "ab");
}
#[test]
fn keep_sgr_lets_color_through() {
assert_eq!(
scrub_controls_keep_sgr("\x1b[31mred\x1b[39m tail"),
"\x1b[31mred\x1b[39m tail"
);
}
#[test]
fn keep_sgr_still_strips_cursor_csi() {
assert_eq!(
scrub_controls_keep_sgr("\x1b[2J\x1b[Hhi\x1b[6n"),
"hi"
);
}
#[test]
fn keep_sgr_still_strips_osc() {
assert_eq!(
scrub_controls_keep_sgr("\x1b]0;pwned\x07safe"),
"safe"
);
}
#[test]
fn keep_sgr_preserves_multi_param_sgr() {
assert_eq!(
scrub_controls_keep_sgr("\x1b[1;31mbold-red\x1b[0m"),
"\x1b[1;31mbold-red\x1b[0m"
);
}
}