refactor(tui): share osc text sanitization

This commit is contained in:
Felipe Coury
2026-06-05 15:11:34 -03:00
parent fda950e3c6
commit 38ebee8ebe
4 changed files with 54 additions and 32 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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"
);
}
}

View File

@@ -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;