use std::borrow::Cow;
pub trait EscapeNonPrintablePosixExt: AsRef<str> {
fn escape_non_printable(&self) -> Cow<'_, str> {
let escape_char = |c: char| {
let (prefix, payload): (&str, char) = if c.is_ascii_control() {
("^", (c as u8 ^ 0x40) as char)
} else if c.is_control() {
("M-^", ((c as u8 & 0x7f) ^ 0x40) as char)
} else {
("", c)
};
prefix.chars().chain(std::iter::once(payload))
};
let s = self.as_ref();
if !s.contains(|c: char| c.is_control()) {
return Cow::Borrowed(s);
}
Cow::Owned(s.chars().flat_map(escape_char).collect())
}
}
impl<T: AsRef<str>> EscapeNonPrintablePosixExt for T {}
#[cfg(test)]
mod tests {
use super::EscapeNonPrintablePosixExt;
use proptest::prelude::*;
use rstest::rstest;
use std::borrow::Cow;
#[rstest]
#[case::plain_text_unchanged("plain text", "plain text")]
#[case::two_words_unchanged("two words", "two words")]
#[case::multibyte_unicode_preserved_around_escaped_control("🐢\x1b[32m🦀", "🐢^[[32m🦀")]
#[case::esc_0x1b("\x1b[31mfoo", "^[[31mfoo")] #[case::tab_0x09("foo\tbar", "foo^Ibar")] #[case::nul_0x00("a\0b", "a^@b")] #[case::del_0x7f("a\x7fb", "a^?b")] #[case::c1_control_0x80("\u{80}", "M-^@")]
#[case::single_char_csi_0x9b("a\u{9b}b", "aM-^[b")] #[case::c1_control_0x9f("\u{9f}", "M-^_")]
fn escapes_as_cat_v(#[case] input: &str, #[case] expected: &str) {
assert_eq!(input.escape_non_printable(), expected);
}
#[test]
fn escapes_all_c0_controls_as_caret() {
for byte in 0x00u8..=0x1f {
let input = (byte as char).to_string();
let expected = format!("^{}", (byte ^ 0x40) as char);
assert_eq!(input.escape_non_printable(), expected, "byte {byte:#04x}");
}
}
proptest! {
#[test]
fn output_never_contains_control_chars(s in r"(?s).*") {
let escaped = s.escape_non_printable();
prop_assert!(!escaped.chars().any(char::is_control));
}
#[test]
fn control_free_input_is_borrowed(s in r"[^\p{Cc}]*") {
prop_assert!(matches!(s.escape_non_printable(), Cow::Borrowed(_)));
}
#[test]
fn escaping_is_idempotent(s in r"(?s).*") {
let once = s.escape_non_printable().into_owned();
let twice = once.escape_non_printable().into_owned();
prop_assert_eq!(once, twice);
}
}
}