pub fn sanitise_display(value: &str, cap: usize) -> String {
let stripped = strip_ansi_csi(value);
let chars: Vec<char> = stripped.chars().collect();
let (truncated, was_truncated) = if chars.len() > cap {
(&chars[..cap], true)
} else {
(&chars[..], false)
};
let mut out = String::with_capacity(truncated.len() + 3);
for &ch in truncated {
if ch.is_ascii_control() {
out.push('?');
} else {
out.push(ch);
}
}
if was_truncated {
out.push('\u{2026}'); }
out
}
fn strip_ansi_csi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\x1B' {
match chars.peek() {
Some(&'[') => {
chars.next(); for inner in chars.by_ref() {
let b = inner as u32;
if (0x40..=0x7E).contains(&b) {
break; }
}
}
Some(&']') => {
chars.next(); while let Some(inner) = chars.next() {
if inner == '\x07' {
break; }
if inner == '\x1B' {
if chars.peek() == Some(&'\\') {
chars.next();
}
break;
}
}
}
Some(_) => {
chars.next();
}
None => {
}
}
} else {
out.push(ch);
}
}
out
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "test-only; panics acceptable in unit tests"
)]
use super::*;
#[test]
fn no_change_on_clean_string() {
assert_eq!(sanitise_display("hello-world", 256), "hello-world");
}
#[test]
fn truncation_appends_ellipsis() {
let s = "abcde";
let result = sanitise_display(s, 3);
assert_eq!(result, "abc\u{2026}");
}
#[test]
fn truncation_at_exact_cap_no_ellipsis() {
let s = "abcde";
let result = sanitise_display(s, 5);
assert_eq!(result, "abcde");
}
#[test]
fn control_chars_replaced_with_question_mark() {
let s = "hello\nworld\x00end";
let result = sanitise_display(s, 256);
assert_eq!(result, "hello?world?end");
}
#[test]
fn ansi_csi_colour_code_stripped() {
let s = "\x1b[31mfoo\x1b[0m";
let result = sanitise_display(s, 256);
assert_eq!(result, "foo");
}
#[test]
fn ansi_csi_cursor_move_stripped() {
let s = "before\x1b[2Jafter";
let result = sanitise_display(s, 256);
assert_eq!(result, "beforeafter");
}
#[test]
fn osc_sequence_stripped() {
let s = "\x1b]0;window title\x07content";
let result = sanitise_display(s, 256);
assert_eq!(result, "content");
}
#[test]
fn ten_kb_token_truncated_and_sanitised() {
let big = "a".repeat(10_240);
let result = sanitise_display(&big, 256);
assert_eq!(result.chars().count(), 257); assert!(result.ends_with('\u{2026}'));
}
#[test]
fn newline_in_token_replaced() {
let s = "valid\nnewline";
let result = sanitise_display(s, 256);
assert!(!result.contains('\n'));
assert!(result.contains('?'));
}
#[test]
fn empty_string_unchanged() {
assert_eq!(sanitise_display("", 256), "");
}
#[test]
fn unicode_multibyte_cap_is_by_char_not_byte() {
let s = "你好world";
let result = sanitise_display(s, 4);
assert_eq!(result, "你好wo\u{2026}");
}
#[test]
fn osc_st_terminator_stripped() {
let input = "\x1b]0;t\x1b\\after";
let result = sanitise_display(input, 256);
assert!(
!result.contains('\x1b'),
"result must contain no ESC byte: {result:?}"
);
assert_eq!(result, "after", "text after ST-terminated OSC must survive");
}
#[test]
fn osc_unterminated_drained_to_eof() {
let input = "\x1b]0;unterminated";
let result = sanitise_display(input, 256);
assert!(
!result.contains('\x1b'),
"result must contain no ESC byte: {result:?}"
);
assert_eq!(
result, "",
"unterminated OSC drains all chars; output is empty"
);
}
#[test]
fn two_byte_fe_sequence_stripped() {
let input = "a\x1bcb";
let result = sanitise_display(input, 256);
assert!(
!result.contains('\x1b'),
"result must contain no ESC byte: {result:?}"
);
assert_eq!(
result, "ab",
"surrounding chars survive; ESC+c is discarded"
);
}
#[test]
fn trailing_lone_esc_stripped() {
let input = "trail\x1b";
let result = sanitise_display(input, 256);
assert!(
!result.contains('\x1b'),
"result must contain no ESC byte: {result:?}"
);
assert_eq!(result, "trail", "trailing lone ESC is discarded");
}
}