diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 0ec8998d77..bd789e0c3c 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -164,6 +164,7 @@ mod notifications; #[cfg(any(not(debug_assertions), test))] mod npm_registry; pub(crate) mod onboarding; +mod osc_text; mod oss_selection; mod pager_overlay; mod permission_compat; diff --git a/codex-rs/tui/src/osc_text.rs b/codex-rs/tui/src/osc_text.rs new file mode 100644 index 0000000000..024d738d50 --- /dev/null +++ b/codex-rs/tui/src/osc_text.rs @@ -0,0 +1,28 @@ +//! Sanitization for untrusted terminal-title and tab-status OSC text. + +/// Whether a control or invisible formatting character is unsafe in OSC text. +pub(crate) fn is_disallowed_osc_text_char(ch: char) -> bool { + if ch.is_control() { + return true; + } + + matches!( + ch, + '\u{00AD}' + | '\u{034F}' + | '\u{061C}' + | '\u{180E}' + | '\u{200B}'..='\u{200F}' + | '\u{202A}'..='\u{202E}' + | '\u{2060}'..='\u{206F}' + | '\u{FE00}'..='\u{FE0F}' + | '\u{FEFF}' + | '\u{FFF9}'..='\u{FFFB}' + | '\u{1BCA0}'..='\u{1BCA3}' + | '\u{E0100}'..='\u{E01EF}' + ) +} + +#[cfg(test)] +#[path = "osc_text_tests.rs"] +mod tests; diff --git a/codex-rs/tui/src/osc_text_tests.rs b/codex-rs/tui/src/osc_text_tests.rs new file mode 100644 index 0000000000..fd67415180 --- /dev/null +++ b/codex-rs/tui/src/osc_text_tests.rs @@ -0,0 +1,24 @@ +use super::is_disallowed_osc_text_char; + +#[test] +fn rejects_controls_bidi_and_invisible_format_chars() { + for ch in [ + '\x07', '\x1b', '\u{009b}', '\n', '\u{202E}', '\u{2066}', '\u{200F}', '\u{061C}', + '\u{200B}', '\u{FEFF}', + ] { + assert!( + is_disallowed_osc_text_char(ch), + "expected {ch:?} to be disallowed" + ); + } +} + +#[test] +fn allows_ordinary_text() { + for ch in ['a', 'Z', '0', ' ', '/', '.', '\u{2026}', '\u{00E9}'] { + assert!( + !is_disallowed_osc_text_char(ch), + "expected {ch:?} to be allowed" + ); + } +} diff --git a/codex-rs/tui/src/terminal_title.rs b/codex-rs/tui/src/terminal_title.rs index c21e6b8bc4..88030f502f 100644 --- a/codex-rs/tui/src/terminal_title.rs +++ b/codex-rs/tui/src/terminal_title.rs @@ -121,7 +121,7 @@ fn sanitize_terminal_title(title: &str) -> String { continue; } - if is_disallowed_terminal_title_char(ch) { + if crate::osc_text::is_disallowed_osc_text_char(ch) { continue; } @@ -145,37 +145,6 @@ fn sanitize_terminal_title(title: &str) -> String { sanitized } -/// Returns whether `ch` should be dropped from terminal-title output. -/// -/// This includes both plain control characters and a curated set of invisible -/// formatting codepoints. The bidi entries here cover the Trojan-Source-style -/// text-reordering controls that can make a title render misleadingly relative -/// to its underlying byte sequence. -fn is_disallowed_terminal_title_char(ch: char) -> bool { - if ch.is_control() { - return true; - } - - // Strip Trojan-Source-related bidi controls plus common non-rendering - // formatting characters so title text cannot smuggle terminal control - // semantics or visually misleading content. - matches!( - ch, - '\u{00AD}' - | '\u{034F}' - | '\u{061C}' - | '\u{180E}' - | '\u{200B}'..='\u{200F}' - | '\u{202A}'..='\u{202E}' - | '\u{2060}'..='\u{206F}' - | '\u{FE00}'..='\u{FE0F}' - | '\u{FEFF}' - | '\u{FFF9}'..='\u{FFFB}' - | '\u{1BCA0}'..='\u{1BCA3}' - | '\u{E0100}'..='\u{E01EF}' - ) -} - #[cfg(test)] mod tests { use super::MAX_TERMINAL_TITLE_CHARS;