use opentui::ansi::escape_url_for_osc8;
use opentui::input::{InputParser, ParseError};
use opentui::terminal::Terminal;
use opentui_rust as opentui;
#[test]
fn security_title_filters_c0_controls() {
let mut output = Vec::new();
{
let mut term = Terminal::new(&mut output);
term.set_title("Hello\x1bWorld").unwrap();
}
let s = String::from_utf8_lossy(&output);
let title_start = s.find("\x1b]0;").expect("Should have OSC prefix");
let title_end = s.find("\x1b\\").expect("Should have ST terminator");
let title_content = &s[title_start + 4..title_end];
assert!(
!title_content.contains('\x1b'),
"Title content should not contain ESC: {title_content:?}"
);
}
#[test]
fn security_title_filters_bel() {
let mut output = Vec::new();
{
let mut term = Terminal::new(&mut output);
term.set_title("Hello\x07World").unwrap();
}
let s = String::from_utf8_lossy(&output);
let title_start = s.find("\x1b]0;").unwrap();
let title_end = s.find("\x1b\\").unwrap();
let title_content = &s[title_start + 4..title_end];
assert!(
!title_content.contains('\x07'),
"Title content should not contain BEL"
);
}
#[test]
fn security_title_filters_c1_controls() {
let mut output = Vec::new();
{
let mut term = Terminal::new(&mut output);
term.set_title("Hello\u{009B}2JWorld").unwrap();
}
let s = String::from_utf8_lossy(&output);
let title_start = s.find("\x1b]0;").unwrap();
let title_end = s.find("\x1b\\").unwrap();
let title_content = &s[title_start + 4..title_end];
assert!(
!title_content.contains('\u{009B}'),
"Title should not contain CSI (U+009B)"
);
}
#[test]
fn security_title_filters_osc_c1() {
let mut output = Vec::new();
{
let mut term = Terminal::new(&mut output);
term.set_title("Hello\u{009D}0;EvilTitle\u{009C}World")
.unwrap();
}
let s = String::from_utf8_lossy(&output);
let title_start = s.find("\x1b]0;").unwrap();
let title_end = s.find("\x1b\\").unwrap();
let title_content = &s[title_start + 4..title_end];
assert!(
!title_content.contains('\u{009D}'),
"Title should not contain OSC (U+009D)"
);
assert!(
!title_content.contains('\u{009C}'),
"Title should not contain ST (U+009C)"
);
}
#[test]
fn security_title_preserves_normal_unicode() {
let mut output = Vec::new();
{
let mut term = Terminal::new(&mut output);
term.set_title("日本語タイトル 🎉 Émojis").unwrap();
}
let s = String::from_utf8_lossy(&output);
assert!(s.contains("日本語タイトル"), "Japanese should be preserved");
assert!(s.contains("🎉"), "Emoji should be preserved");
assert!(s.contains("Émojis"), "Accented chars should be preserved");
}
#[test]
fn security_osc8_escapes_esc() {
let malicious = "http://evil.com/\x1b]0;Pwned\x1b\\";
let escaped = escape_url_for_osc8(malicious);
assert!(!escaped.contains('\x1b'), "ESC should be percent-encoded");
assert!(escaped.contains("%1B"), "ESC should become %1B");
}
#[test]
fn security_osc8_escapes_bel() {
let malicious = "http://evil.com/\x07";
let escaped = escape_url_for_osc8(malicious);
assert!(!escaped.contains('\x07'), "BEL should be percent-encoded");
assert!(escaped.contains("%07"), "BEL should become %07");
}
#[test]
fn security_osc8_escapes_c1_controls() {
let url_with_csi = "http://evil.com/\u{009B}2J";
let escaped = escape_url_for_osc8(url_with_csi);
assert!(
!escaped.contains('\u{009B}'),
"CSI should be percent-encoded"
);
let url_with_st = "http://evil.com/\u{009C}";
let escaped = escape_url_for_osc8(url_with_st);
assert!(
!escaped.contains('\u{009C}'),
"ST should be percent-encoded"
);
let url_with_osc = "http://evil.com/\u{009D}";
let escaped = escape_url_for_osc8(url_with_osc);
assert!(
!escaped.contains('\u{009D}'),
"OSC should be percent-encoded"
);
}
#[test]
fn security_osc8_injection_attempt() {
let malicious = "http://x\x1b\\\x1b[2J\x1b]0;Pwned";
let escaped = escape_url_for_osc8(malicious);
let esc_count = escaped.chars().filter(|&c| c == '\x1b').count();
assert_eq!(esc_count, 0, "All ESC should be percent-encoded");
assert!(escaped.contains("%1B"), "ESC should be %1B");
}
#[test]
fn security_osc8_preserves_valid_urls() {
let valid_url = "https://example.com/path?query=value&other=123#anchor";
let escaped = escape_url_for_osc8(valid_url);
assert_eq!(escaped, valid_url, "Valid URL should not be modified");
}
#[test]
fn security_osc8_preserves_unicode() {
let unicode_url = "https://example.com/路径/файл?q=日本語";
let escaped = escape_url_for_osc8(unicode_url);
assert_eq!(escaped, unicode_url, "Unicode should be preserved");
}
const MAX_PASTE_SIZE: usize = 10 * 1024 * 1024;
#[test]
fn security_paste_overflow_returns_error() {
let mut parser = InputParser::new();
let start_paste = b"\x1b[200~";
let result = parser.parse(start_paste);
assert!(result.is_err());
let large_chunk = vec![b'x'; MAX_PASTE_SIZE + 100];
let result = parser.parse(&large_chunk);
assert!(
matches!(result, Err(ParseError::PasteBufferOverflow)),
"Should return PasteBufferOverflow error, got: {result:?}"
);
}
#[test]
fn security_paste_overflow_resets_state() {
let mut parser = InputParser::new();
let _ = parser.parse(b"\x1b[200~");
let large_chunk = vec![b'x'; MAX_PASTE_SIZE + 100];
let overflow_result = parser.parse(&large_chunk);
assert!(matches!(
overflow_result,
Err(ParseError::PasteBufferOverflow)
));
let start_result = parser.parse(b"\x1b[200~");
assert!(
matches!(start_result, Err(ParseError::Incomplete)),
"Should enter paste mode: {start_result:?}"
);
let content_result = parser.parse(b"hello\x1b[201~");
assert!(
content_result.is_ok(),
"Parser should accept normal paste after overflow: {content_result:?}"
);
}
#[test]
fn security_paste_incremental_overflow() {
let mut parser = InputParser::new();
let _ = parser.parse(b"\x1b[200~");
let chunk_size = MAX_PASTE_SIZE / 4;
let chunk = vec![b'a'; chunk_size];
for i in 0..4 {
let result = parser.parse(&chunk);
assert!(
matches!(result, Err(ParseError::Incomplete)),
"Chunk {i} should return Incomplete"
);
}
let result = parser.parse(&chunk);
assert!(
matches!(result, Err(ParseError::PasteBufferOverflow)),
"5th chunk should trigger overflow: {result:?}"
);
}
#[test]
fn security_paste_at_limit_succeeds() {
let mut parser = InputParser::new();
let _ = parser.parse(b"\x1b[200~");
let content = vec![b'x'; MAX_PASTE_SIZE];
let mut paste_data = content;
paste_data.extend_from_slice(b"\x1b[201~");
let result = parser.parse(&paste_data);
assert!(
result.is_ok(),
"Paste exactly at limit should succeed: {result:?}"
);
}
#[test]
fn security_combined_attack_vectors() {
let mut output = Vec::new();
{
let mut term = Terminal::new(&mut output);
term.set_title("Safe\u{009B}[2J\u{009D}Evil").unwrap();
}
let title_output = String::from_utf8_lossy(&output);
assert!(
!title_output.contains('\u{009B}'),
"C1 injection in title blocked"
);
let malicious_url = "http://x\x1b]0;Pwned\x1b\\click";
let escaped = escape_url_for_osc8(malicious_url);
assert!(!escaped.contains('\x1b'), "ESC injection in URL blocked");
let mut parser = InputParser::new();
let _ = parser.parse(b"\x1b[200~");
let overflow = vec![b'x'; MAX_PASTE_SIZE + 1];
let result = parser.parse(&overflow);
assert!(
matches!(result, Err(ParseError::PasteBufferOverflow)),
"Paste overflow blocked"
);
}