const MAX_RENDERED_LEN: usize = 512;
const TRUNCATION_MARKER: &str = "… [truncated]";
const MAX_RENDERED_LINES: usize = 40;
const LINE_TRUNCATION_MARKER: &str = "… [truncated: block too long to display]";
pub(crate) fn operator_safe_block(s: &str) -> String {
let mut lines: Vec<String> = s
.lines()
.take(MAX_RENDERED_LINES)
.map(|line| {
let indent_len = line.len() - line.trim_start().len();
format!("{}{}", &line[..indent_len], operator_safe(line))
})
.collect();
if s.lines().count() > MAX_RENDERED_LINES {
lines.push(LINE_TRUNCATION_MARKER.to_string());
}
lines.join("\n")
}
pub(crate) fn operator_safe(s: &str) -> String {
let mut out = String::with_capacity(s.len().min(MAX_RENDERED_LEN));
let mut pending_space = false;
for ch in s.chars() {
let keep = match ch {
'\t' | '\n' | '\r' | ' ' => {
pending_space = true;
continue;
}
c if (c as u32) < 0x20 || c == '\u{7f}' => false,
c if ('\u{80}'..='\u{9f}').contains(&c) => false,
'\u{2028}'..='\u{202e}' | '\u{2066}'..='\u{2069}' => false,
'\u{200b}'..='\u{200f}' | '\u{feff}' => false,
'\u{061c}' => false,
'\u{00ad}' | '\u{034f}' | '\u{180b}'..='\u{180e}' | '\u{2060}'..='\u{2064}' | '\u{206a}'..='\u{206f}' | '\u{fff9}'..='\u{fffb}' | '\u{e0000}'..='\u{e007f}' => false,
_ => true,
};
if !keep {
continue;
}
if pending_space && !out.is_empty() {
out.push(' ');
}
pending_space = false;
out.push(ch);
}
if out.chars().count() > MAX_RENDERED_LEN {
let cut: String = out.chars().take(MAX_RENDERED_LEN).collect();
return format!("{cut}{TRUNCATION_MARKER}");
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_json_block_cannot_smuggle_bidi_or_zero_width_past_the_prompt() {
let hostile = serde_json::to_string_pretty(&serde_json::json!({
"path": "/etc/\u{202e}gnc.d/passwd\u{202c}",
"note": "safe\u{200b}ish",
}))
.expect("json");
assert!(
hostile.contains('\u{202e}'),
"serde_json escaped the override"
);
let safe = operator_safe_block(&hostile);
assert!(
!safe.contains('\u{202e}'),
"bidi override survived: {safe:?}"
);
assert!(!safe.contains('\u{200b}'), "zero-width survived: {safe:?}");
assert!(safe.lines().count() > 1, "block was flattened: {safe:?}");
assert!(safe.contains("passwd"), "visible text was lost: {safe:?}");
}
#[test]
fn a_tall_block_is_bounded_and_says_so() {
let tall = (0..500)
.map(|i| format!("line {i}"))
.collect::<Vec<_>>()
.join("\n");
let safe = operator_safe_block(&tall);
assert_eq!(safe.lines().count(), MAX_RENDERED_LINES + 1);
assert!(
safe.ends_with(LINE_TRUNCATION_MARKER),
"truncation unmarked: {safe:?}"
);
}
#[test]
fn every_invisible_formatting_character_is_dropped() {
for ch in [
'\u{061c}', '\u{00ad}', '\u{034f}', '\u{180e}', '\u{2060}', '\u{2064}', '\u{206f}', '\u{fffb}', '\u{e0041}', ] {
let hostile = format!("AptRemove{ch}Everything");
let safe = operator_safe(&hostile);
assert!(
!safe.chars().any(|c| c == ch),
"U+{:04X} survived operator_safe: {safe:?}",
ch as u32
);
assert_eq!(
safe, "AptRemoveEverything",
"U+{:04X} must vanish without disturbing the visible text",
ch as u32
);
}
}
#[test]
fn a_summary_cannot_rewrite_the_line_it_is_printed_on() {
let hostile = "install vim\u{1b}[2K\rremove nothing";
let safe = operator_safe(hostile);
assert!(!safe.contains('\u{1b}'), "ESC survived: {safe:?}");
assert!(!safe.contains('\r'), "CR survived: {safe:?}");
assert_eq!(safe, "install vim[2K remove nothing");
}
#[test]
fn a_summary_cannot_move_the_cursor_off_its_own_line() {
for seq in ["\u{1b}[1A", "\u{1b}[10;10H", "\u{1b}[s\u{1b}[u", "\u{1b}M"] {
let safe = operator_safe(&format!("upgrade{seq}kernel"));
assert!(
!safe.contains('\u{1b}'),
"ESC survived from {seq:?}: {safe:?}"
);
}
}
#[test]
fn c1_control_characters_are_dropped_too() {
let safe = operator_safe("upgrade\u{9b}2K\rkernel");
assert!(!safe.contains('\u{9b}'), "C1 CSI survived: {safe:?}");
}
#[test]
fn a_summary_cannot_forge_extra_lines() {
let safe = operator_safe("install vim\n\n 3 RemoveSwap LOW auto");
assert!(!safe.contains('\n'), "newline survived: {safe:?}");
assert_eq!(safe, "install vim 3 RemoveSwap LOW auto");
}
#[test]
fn bidi_overrides_and_zero_width_characters_are_dropped() {
for ch in [
'\u{202a}', '\u{202b}', '\u{202c}', '\u{202d}', '\u{202e}', '\u{2066}', '\u{2067}',
'\u{2068}', '\u{2069}', '\u{200b}', '\u{200e}', '\u{feff}',
'\u{2028}', '\u{2029}',
] {
let safe = operator_safe(&format!("remove{ch}nothing"));
assert!(!safe.contains(ch), "U+{:04X} survived: {safe:?}", ch as u32);
}
}
#[test]
fn an_overlong_summary_is_capped_and_says_so() {
let safe = operator_safe(&"A".repeat(10_000));
assert!(
safe.chars().count() <= MAX_RENDERED_LEN + TRUNCATION_MARKER.chars().count(),
"not capped: {} chars",
safe.chars().count()
);
assert!(safe.ends_with(TRUNCATION_MARKER), "truncation not marked");
}
#[test]
fn truncation_respects_char_boundaries() {
let safe = operator_safe(&"🔒".repeat(10_000));
assert!(safe.ends_with(TRUNCATION_MARKER));
assert!(safe.chars().count() <= MAX_RENDERED_LEN + TRUNCATION_MARKER.chars().count());
}
#[test]
fn ordinary_summaries_pass_through_intact() {
for s in [
"install vim and git",
"restart nginx.service",
"add 2 GiB of swap at /swapfile",
"rotate /var/log/nginx/*.log daily, keep 14",
"café — naïve — 日本語 — emoji 🔒 all survive",
] {
assert_eq!(operator_safe(s), s, "mangled an ordinary summary");
}
}
#[test]
fn whitespace_collapses_and_trims() {
assert_eq!(operator_safe(" install vim \t\n "), "install vim");
assert_eq!(operator_safe(""), "");
assert_eq!(operator_safe("\u{1b}\u{1b}\u{1b}"), "");
}
}