fix escape code leak
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
use zterm::vt_parser::*;
|
||||||
|
fn main() {}
|
||||||
@@ -20,3 +20,4 @@ pub mod statusline;
|
|||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
pub mod simd_utf8;
|
pub mod simd_utf8;
|
||||||
pub mod vt_parser;
|
pub mod vt_parser;
|
||||||
|
mod vt_test_osc;
|
||||||
|
|||||||
+64
-14
@@ -482,7 +482,9 @@ impl SharedParser {
|
|||||||
|
|
||||||
// Like Kitty line 1516: consume_input(self, ...)
|
// Like Kitty line 1516: consume_input(self, ...)
|
||||||
let made_progress = self.consume_input(handler);
|
let made_progress = self.consume_input(handler);
|
||||||
|
if made_progress {
|
||||||
parsed_any = true;
|
parsed_any = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Re-acquire lock
|
// Re-acquire lock
|
||||||
state = self.state.lock().unwrap();
|
state = self.state.lock().unwrap();
|
||||||
@@ -635,14 +637,14 @@ impl SharedParser {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
State::Csi => {
|
State::Csi => {
|
||||||
// Like Kitty lines 1465-1466:
|
let state_before = *vte_state;
|
||||||
// if (consume_csi(self)) { self->read.consumed = self->read.pos; if (self->csi.is_valid) dispatch_csi(self); SET_STATE(NORMAL); }
|
|
||||||
if Self::consume_csi_impl(
|
if Self::consume_csi_impl(
|
||||||
handler,
|
handler,
|
||||||
buf,
|
buf,
|
||||||
parse_pos,
|
parse_pos,
|
||||||
parse_sz,
|
parse_sz,
|
||||||
*parse_consumed,
|
parse_consumed,
|
||||||
|
vte_state,
|
||||||
csi,
|
csi,
|
||||||
escape_len,
|
escape_len,
|
||||||
) {
|
) {
|
||||||
@@ -650,30 +652,41 @@ impl SharedParser {
|
|||||||
if csi.is_valid {
|
if csi.is_valid {
|
||||||
handler.csi(csi);
|
handler.csi(csi);
|
||||||
}
|
}
|
||||||
|
if *vte_state == state_before {
|
||||||
*vte_state = State::Normal;
|
*vte_state = State::Normal;
|
||||||
|
}
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
*vte_state != state_before
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
State::Osc => {
|
State::Osc => {
|
||||||
|
let state_before = *vte_state;
|
||||||
if Self::consume_osc_impl(
|
if Self::consume_osc_impl(
|
||||||
handler, buf, parse_pos, parse_sz, vte_state, osc_buffer,
|
handler,
|
||||||
|
buf,
|
||||||
|
parse_pos,
|
||||||
|
parse_sz,
|
||||||
|
parse_consumed,
|
||||||
|
vte_state,
|
||||||
|
osc_buffer,
|
||||||
escape_len,
|
escape_len,
|
||||||
) {
|
) {
|
||||||
*parse_consumed = *parse_pos;
|
*parse_consumed = *parse_pos;
|
||||||
*vte_state = State::Normal;
|
*vte_state = State::Normal;
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
*vte_state != state_before
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
State::Dcs | State::Apc | State::Pm | State::Sos => {
|
State::Dcs | State::Apc | State::Pm | State::Sos => {
|
||||||
|
let state_before = *vte_state;
|
||||||
if Self::consume_string_impl(
|
if Self::consume_string_impl(
|
||||||
handler,
|
handler,
|
||||||
buf,
|
buf,
|
||||||
parse_pos,
|
parse_pos,
|
||||||
parse_sz,
|
parse_sz,
|
||||||
|
parse_consumed,
|
||||||
vte_state,
|
vte_state,
|
||||||
string_buffer,
|
string_buffer,
|
||||||
escape_len,
|
escape_len,
|
||||||
@@ -682,7 +695,7 @@ impl SharedParser {
|
|||||||
*vte_state = State::Normal;
|
*vte_state = State::Normal;
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
*vte_state != state_before
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -837,6 +850,11 @@ impl SharedParser {
|
|||||||
b'\\' => {
|
b'\\' => {
|
||||||
*vte_state = State::Normal;
|
*vte_state = State::Normal;
|
||||||
}
|
}
|
||||||
|
0x1B => {
|
||||||
|
// ESC followed by ESC. Start new escape sequence.
|
||||||
|
*vte_state = State::Escape;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
log::debug!("Unknown escape sequence: ESC {:02x}", ch);
|
log::debug!("Unknown escape sequence: ESC {:02x}", ch);
|
||||||
*vte_state = State::Normal;
|
*vte_state = State::Normal;
|
||||||
@@ -913,7 +931,8 @@ impl SharedParser {
|
|||||||
buf: &[u8; BUF_SIZE],
|
buf: &[u8; BUF_SIZE],
|
||||||
parse_pos: &mut usize,
|
parse_pos: &mut usize,
|
||||||
parse_sz: usize,
|
parse_sz: usize,
|
||||||
parse_consumed: usize,
|
parse_consumed: &mut usize,
|
||||||
|
vte_state: &mut State,
|
||||||
csi: &mut CsiParams,
|
csi: &mut CsiParams,
|
||||||
escape_len: &mut usize,
|
escape_len: &mut usize,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
@@ -922,8 +941,15 @@ impl SharedParser {
|
|||||||
*parse_pos += 1;
|
*parse_pos += 1;
|
||||||
*escape_len += 1;
|
*escape_len += 1;
|
||||||
|
|
||||||
|
if ch == 0x1B {
|
||||||
|
*vte_state = State::Escape;
|
||||||
|
*parse_consumed = *parse_pos;
|
||||||
|
*escape_len = 0;
|
||||||
|
return false; // Aborted by new ESC
|
||||||
|
}
|
||||||
|
|
||||||
// Handle embedded control characters
|
// Handle embedded control characters
|
||||||
if ch <= 0x1F && ch != 0x1B {
|
if ch <= 0x1F {
|
||||||
handler.control(ch);
|
handler.control(ch);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1023,7 +1049,7 @@ impl SharedParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check max length
|
// Check max length
|
||||||
if *parse_pos - parse_consumed > MAX_ESCAPE_LEN {
|
if *parse_pos - *parse_consumed > MAX_ESCAPE_LEN {
|
||||||
log::debug!("CSI escape too long, ignoring");
|
log::debug!("CSI escape too long, ignoring");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1037,6 +1063,7 @@ impl SharedParser {
|
|||||||
buf: &[u8; BUF_SIZE],
|
buf: &[u8; BUF_SIZE],
|
||||||
parse_pos: &mut usize,
|
parse_pos: &mut usize,
|
||||||
parse_sz: usize,
|
parse_sz: usize,
|
||||||
|
parse_consumed: &mut usize,
|
||||||
vte_state: &mut State,
|
vte_state: &mut State,
|
||||||
osc_buffer: &mut Vec<u8>,
|
osc_buffer: &mut Vec<u8>,
|
||||||
escape_len: &mut usize,
|
escape_len: &mut usize,
|
||||||
@@ -1069,6 +1096,7 @@ impl SharedParser {
|
|||||||
*parse_pos += 1;
|
*parse_pos += 1;
|
||||||
handler.osc(osc_buffer);
|
handler.osc(osc_buffer);
|
||||||
*vte_state = State::Escape;
|
*vte_state = State::Escape;
|
||||||
|
*parse_consumed = *parse_pos;
|
||||||
*escape_len = 0;
|
*escape_len = 0;
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
@@ -1098,6 +1126,7 @@ impl SharedParser {
|
|||||||
buf: &[u8; BUF_SIZE],
|
buf: &[u8; BUF_SIZE],
|
||||||
parse_pos: &mut usize,
|
parse_pos: &mut usize,
|
||||||
parse_sz: usize,
|
parse_sz: usize,
|
||||||
|
parse_consumed: &mut usize,
|
||||||
vte_state: &mut State,
|
vte_state: &mut State,
|
||||||
string_buffer: &mut Vec<u8>,
|
string_buffer: &mut Vec<u8>,
|
||||||
escape_len: &mut usize,
|
escape_len: &mut usize,
|
||||||
@@ -1128,10 +1157,17 @@ impl SharedParser {
|
|||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} else if *parse_pos + 1 < parse_sz {
|
} else if *parse_pos + 1 < parse_sz {
|
||||||
// ESC not followed by \ - include in buffer
|
// ESC not followed by \ - abort string, start new escape
|
||||||
string_buffer.push(ch);
|
|
||||||
*parse_pos += 1;
|
*parse_pos += 1;
|
||||||
*escape_len += 1;
|
Self::dispatch_string_command(
|
||||||
|
handler,
|
||||||
|
vte_state,
|
||||||
|
string_buffer,
|
||||||
|
);
|
||||||
|
*vte_state = State::Escape;
|
||||||
|
*parse_consumed = *parse_pos;
|
||||||
|
*escape_len = 0;
|
||||||
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// ESC at end of buffer - need more data
|
// ESC at end of buffer - need more data
|
||||||
return false;
|
return false;
|
||||||
@@ -1368,6 +1404,11 @@ impl Parser {
|
|||||||
self.state = State::Normal;
|
self.state = State::Normal;
|
||||||
1
|
1
|
||||||
}
|
}
|
||||||
|
0x1B => {
|
||||||
|
// ESC followed by ESC. Start new escape sequence.
|
||||||
|
self.state = State::Escape;
|
||||||
|
1
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Unknown escape sequence, ignore and return to normal
|
// Unknown escape sequence, ignore and return to normal
|
||||||
log::debug!("Unknown escape sequence: ESC {:02x}", ch);
|
log::debug!("Unknown escape sequence: ESC {:02x}", ch);
|
||||||
@@ -1442,8 +1483,17 @@ impl Parser {
|
|||||||
return consumed;
|
return consumed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ch == 0x1B {
|
||||||
|
self.state = State::Escape;
|
||||||
|
self.escape_len = 0;
|
||||||
|
// Wait! If it's ESC, we consumed it. But the next loop in parse() will call consume_escape,
|
||||||
|
// which EXPECTS the char AFTER ESC.
|
||||||
|
// So consuming it is PERFECT!
|
||||||
|
return consumed;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle control characters embedded in CSI (common to all states)
|
// Handle control characters embedded in CSI (common to all states)
|
||||||
if ch <= 0x1F && ch != 0x1B {
|
if ch <= 0x1F {
|
||||||
handler.control(ch);
|
handler.control(ch);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
use crate::vt_parser::*;
|
||||||
|
|
||||||
|
pub struct DummyHandler {
|
||||||
|
pub text: String,
|
||||||
|
pub osc_calls: Vec<Vec<u8>>,
|
||||||
|
}
|
||||||
|
impl Handler for DummyHandler {
|
||||||
|
fn text(&mut self, codepoints: &[u32]) {
|
||||||
|
for &c in codepoints {
|
||||||
|
if let Some(ch) = char::from_u32(c) {
|
||||||
|
self.text.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn control(&mut self, _byte: u8) {}
|
||||||
|
fn csi(&mut self, _csi: &CsiParams) {}
|
||||||
|
fn osc(&mut self, osc: &[u8]) {
|
||||||
|
self.osc_calls.push(osc.to_vec());
|
||||||
|
}
|
||||||
|
fn save_cursor(&mut self) {}
|
||||||
|
fn restore_cursor(&mut self) {}
|
||||||
|
fn reset(&mut self) {}
|
||||||
|
fn index(&mut self) {}
|
||||||
|
fn newline(&mut self) {}
|
||||||
|
fn reverse_index(&mut self) {}
|
||||||
|
fn screen_alignment(&mut self) {}
|
||||||
|
fn set_tab_stop(&mut self) {}
|
||||||
|
fn set_keypad_mode(&mut self, _mode: bool) {}
|
||||||
|
fn add_vt_parser_ns(&mut self, _ns: u64) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_osc_leak_byte_by_byte() {
|
||||||
|
let parser = SharedParser::new();
|
||||||
|
let mut handler = DummyHandler { text: String::new(), osc_calls: Vec::new() };
|
||||||
|
|
||||||
|
let data = b"\x1b]4;1;#769E00\x1b\\\x1b]4;2;#93DE88\x1b\\";
|
||||||
|
|
||||||
|
for &byte in data {
|
||||||
|
let (ptr, _) = parser.create_write_buffer();
|
||||||
|
unsafe {
|
||||||
|
*ptr = byte;
|
||||||
|
}
|
||||||
|
parser.commit_write(1);
|
||||||
|
|
||||||
|
while parser.run_parse_pass(&mut handler) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("TEXT: {:?}", handler.text);
|
||||||
|
println!("OSC calls: {}", handler.osc_calls.len());
|
||||||
|
for call in &handler.osc_calls {
|
||||||
|
println!(" OSC: {:?}", std::str::from_utf8(call).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(handler.text, "", "Text should be empty, but leaked escape sequence bytes!");
|
||||||
|
assert_eq!(handler.osc_calls.len(), 2, "Should have parsed exactly two OSC calls");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_csi_aborted_by_osc() {
|
||||||
|
let parser = SharedParser::new();
|
||||||
|
let mut handler = DummyHandler { text: String::new(), osc_calls: Vec::new() };
|
||||||
|
|
||||||
|
// An incomplete CSI sequence aborted by an OSC sequence
|
||||||
|
let data = b"\x1b[38;2;255;0;0\x1b]4;1;#769E00\x1b\\";
|
||||||
|
|
||||||
|
let (ptr, len) = parser.create_write_buffer();
|
||||||
|
assert!(len >= data.len());
|
||||||
|
unsafe {
|
||||||
|
std::ptr::copy_nonoverlapping(data.as_ptr(), ptr, data.len());
|
||||||
|
}
|
||||||
|
parser.commit_write(data.len());
|
||||||
|
|
||||||
|
while parser.run_parse_pass(&mut handler) {}
|
||||||
|
|
||||||
|
assert_eq!(handler.text, "", "Text should be empty, but leaked escape sequence bytes!");
|
||||||
|
assert_eq!(handler.osc_calls.len(), 1, "Should have parsed the OSC sequence even after aborting CSI");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user