//! VT500-series escape-sequence parser. //! //! Implements the [Paul Williams state machine](https://vt100.net/emu/dec_ansi_parser) //! and adds an `apc_dispatch` callback that the `vte` crate on crates.io //! declines to expose. That callback is what makes the kitty graphics //! protocol land — its APC bodies (`\e_G…\e\`) are unreachable through //! stock vte. //! //! Deliberately narrow scope: //! - Written from the spec (not vendored from vte). //! - Params live in `Vec>` for clarity; not the tightest packing but //! trivial to iterate, and bounded by [`MAX_PARAMS`] because the writer at //! the far end of the PTY is not assumed to be well behaved. //! - State the parser holds on behalf of an incomplete sequence is bounded: //! [`MAX_PARAMS`], [`MAX_STRING_BYTES`] and [`MAX_OSC_PARAMS`]. Nothing here //! grows with what a writer sends, because the writer is whatever program //! holds the far end of the PTY. //! - No SIMD UTF-8 fast path yet — that's a `simdutf8` swap when we care. //! - No sync-update (BSU/ESU) hooks yet — added when someone starts using //! them and paint tearing shows up. //! //! State transitions are those of the reference DEC parser plus 8-bit C1 //! shortcuts (0x9B for CSI etc.) accepted only in Ground; 0x80-0xFF in //! Ground is otherwise treated as UTF-8 continuation. That matches every //! modern UTF-8 terminal. #![deny(unsafe_op_in_unsafe_fn)] /// Handler for the parser's dispatch events. pub trait Perform { /// A printable UTF-8 character was received. fn print(&mut self, c: char) { let _ = c; } /// A C0 (0x00-0x1F) or C1 (0x80-0x9F) control code was received. fn execute(&mut self, byte: u8) { let _ = byte; } /// A CSI sequence completed (`\e[…`). fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], ignore: bool, action: char) { let _ = (params, intermediates, ignore, action); } /// An ESC sequence completed (`\e`). fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, byte: u8) { let _ = (intermediates, ignore, byte); } /// An OSC (`\e]…\e\`) or (`\e]…\a`) completed. `params` are the /// semicolon-separated fields of the body. fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) { let _ = (params, bell_terminated); } /// A DCS sequence started; call [`Perform::put`] for each subsequent byte /// and [`Perform::unhook`] on termination. fn hook(&mut self, params: &Params, intermediates: &[u8], ignore: bool, action: char) { let _ = (params, intermediates, ignore, action); } fn put(&mut self, byte: u8) { let _ = byte; } fn unhook(&mut self) {} /// An APC / SOS / PM string completed. `data` is the string body without /// the leading `\e_` (etc.) or the trailing terminator. This is the hook /// the kitty graphics protocol needs. fn apc_dispatch(&mut self, data: &[u8]) { let _ = data; } } /// Parameter slots a single CSI or DCS sequence may hold, counting /// subparameters. /// /// Past this the sequence is marked ignored and the extra slots are dropped, /// which is what the reference DEC parser does with its own fixed parameter /// store and what this parser already does with a third intermediate. Without /// it every `;` pushes a fresh `Vec` for as long as a writer keeps /// sending them: `ESC [` followed by four million separators retains 27.2 bytes /// of buffer per input byte, so about 19 MB of hostile stdout retains a /// gigabyte. /// /// 32 is `vte`'s number, and this crate exists to be a drop-in for `vte` with /// an APC callback, so matching it means an application that renders under one /// renders under the other. xterm's NPARAM is 30, and the widest real sequence /// anyone sends is a 6-slot `38:2::R:G:B`. pub const MAX_PARAMS: usize = 32; /// Bytes an OSC or APC body may accumulate before the parser stops buffering /// and drops the sequence. /// /// Unterminated bodies accumulated 1:1 with no ceiling at all, so a writer that /// opens `ESC ]` and never terminates it grew the terminal by whatever it felt /// like sending. /// /// 8 MiB is far above anything either protocol asks for. The kitty graphics /// protocol requires payloads over 4096 bytes to be chunked, so an APC body is /// a few kilobytes; OSC 52 carries a base64 clipboard selection, which is the /// one field with any real size to it. A body over the limit is dropped rather /// than truncated: half a base64 clipboard write or half an image chunk is not /// a smaller version of the request, it is a different one. pub const MAX_STRING_BYTES: usize = 8 * 1024 * 1024; /// Fields (`;`-separated) an OSC body may hold before the sequence is dropped. /// /// Separated from [`MAX_STRING_BYTES`] because a separator costs 16 bytes of /// index pair and contributes no body byte, so a stream of nothing but `;` /// amplifies about 32x against the byte budget. 1024 is past any real OSC: /// the widest is a multi-colour `OSC 4`, and shop's own handler reads two /// fields. pub const MAX_OSC_PARAMS: usize = 1024; /// What the two body buffers are allocated with, and shrunk back to once a /// sequence ends. /// /// Shrinking is the half that makes the cap hold across sequences: capacity is /// a high-water mark, so without it one large body leaves the parser holding /// that much for the life of the terminal. const INITIAL_BODY_CAPACITY: usize = 2048; /// Iterable parameter list for CSI / DCS. Each iteration yields one /// parameter's subparameters (colon-separated, e.g. `38:2::R:G:B` gives one /// entry `[38, 2, 0, R, G, B]`; `38;2;R;G;B` gives five entries `[38] [2] /// [R] [G] [B]`). #[derive(Debug, Default, Clone)] pub struct Params { inner: Vec>, /// Slots used across every group, which is what [`MAX_PARAMS`] bounds. /// Held rather than summed because it is consulted per byte. slots: usize, } impl Params { pub fn iter(&self) -> impl Iterator { self.inner.iter().map(Vec::as_slice) } pub fn is_empty(&self) -> bool { self.inner.is_empty() } pub fn len(&self) -> usize { self.inner.len() } fn clear(&mut self) { self.inner.clear(); self.slots = 0; } fn is_full(&self) -> bool { self.slots >= MAX_PARAMS } /// Opens a group, or reports that the sequence has run out of slots. fn push_new(&mut self) -> bool { if self.is_full() { return false; } self.inner.push(Vec::with_capacity(1)); self.slots += 1; true } fn ensure_open(&mut self) -> bool { if self.inner.is_empty() { return self.push_new(); } true } /// Digits only ever rewrite the slot already open, so this cannot grow the /// list and does not report an overflow of its own. fn append_digit(&mut self, digit: u16) { if !self.ensure_open() { return; } let group = self.inner.last_mut().unwrap(); if group.is_empty() { group.push(digit); } else { let last = group.last_mut().unwrap(); *last = last.saturating_mul(10).saturating_add(digit); } } fn new_subparam(&mut self) -> bool { if !self.ensure_open() { return false; } if self.is_full() { return false; } self.inner.last_mut().unwrap().push(0); self.slots += 1; true } fn new_param(&mut self) -> bool { self.push_new() } /// Heap bytes this parameter list is holding. /// /// One `Vec` per parameter is the storage shape the module header admits /// to, and this is what makes the cost of that choice observable to /// [`Parser::buffered_bytes`] instead of only to the machine. fn footprint(&self) -> usize { self.inner.capacity() * std::mem::size_of::>() + self .inner .iter() .map(|g| g.capacity() * std::mem::size_of::()) .sum::() } } #[derive(Copy, Clone, Debug, PartialEq, Eq)] enum State { Ground, Escape, EscapeIntermediate, CsiEntry, CsiParam, CsiIntermediate, CsiIgnore, OscString, DcsEntry, DcsParam, DcsIntermediate, DcsIgnore, DcsPassthrough, ApcString, Utf8, } /// Byte-stream parser. State persists across `advance` calls so partial /// sequences and UTF-8 continuations survive chunked reads. #[derive(Debug)] pub struct Parser { state: State, prev_string_state: State, intermediates: [u8; 2], intermediates_idx: usize, ignoring: bool, params: Params, osc_buf: Vec, osc_params: Vec<(usize, usize)>, apc_buf: Vec, /// Set when the open OSC or APC body has passed [`MAX_STRING_BYTES`] or /// [`MAX_OSC_PARAMS`]. The parser keeps scanning for the terminator so the /// stream stays in sync, buffers nothing more, and dispatches nothing. string_overflow: bool, utf8_buf: [u8; 4], utf8_idx: usize, utf8_expected: usize, } impl Default for Parser { fn default() -> Self { Self::new() } } impl Parser { pub fn new() -> Self { Self { state: State::Ground, prev_string_state: State::Ground, intermediates: [0; 2], intermediates_idx: 0, ignoring: false, params: Params::default(), osc_buf: Vec::with_capacity(INITIAL_BODY_CAPACITY), osc_params: Vec::with_capacity(8), apc_buf: Vec::with_capacity(INITIAL_BODY_CAPACITY), string_overflow: false, utf8_buf: [0; 4], utf8_idx: 0, utf8_expected: 0, } } pub fn advance(&mut self, perform: &mut P, bytes: &[u8]) { for &b in bytes { self.step(perform, b); } } fn step(&mut self, perform: &mut P, byte: u8) { match self.state { State::Ground => self.ground(perform, byte), State::Utf8 => self.utf8(perform, byte), State::Escape => self.escape(perform, byte), State::EscapeIntermediate => self.escape_intermediate(perform, byte), State::CsiEntry => self.csi_entry(perform, byte), State::CsiParam => self.csi_param(perform, byte), State::CsiIntermediate => self.csi_intermediate(perform, byte), State::CsiIgnore => self.csi_ignore(perform, byte), State::OscString => self.osc_string(perform, byte), State::DcsEntry => self.dcs_entry(perform, byte), State::DcsParam => self.dcs_param(perform, byte), State::DcsIntermediate => self.dcs_intermediate(perform, byte), State::DcsIgnore => self.dcs_ignore(perform, byte), State::DcsPassthrough => self.dcs_passthrough(perform, byte), State::ApcString => self.apc_string(perform, byte), } } // ---- state handlers ------------------------------------------------ fn ground(&mut self, perform: &mut P, b: u8) { match b { 0x00..=0x17 | 0x19 | 0x1C..=0x1F => perform.execute(b), 0x18 | 0x1A => perform.execute(b), 0x1B => { self.clear(); self.state = State::Escape; } 0x20..=0x7F => perform.print(b as char), 0x80..=0xBF => {} // Stray continuation byte — ignore. 0xC2..=0xDF => self.begin_utf8(b, 2), 0xE0..=0xEF => self.begin_utf8(b, 3), 0xF0..=0xF4 => self.begin_utf8(b, 4), _ => {} // 0xC0/0xC1/0xF5-0xFF invalid. } } fn begin_utf8(&mut self, first: u8, expected: usize) { self.utf8_buf[0] = first; self.utf8_idx = 1; self.utf8_expected = expected; self.state = State::Utf8; } fn utf8(&mut self, perform: &mut P, b: u8) { // ESC in the middle of a UTF-8 sequence resets. if b == 0x1B { self.utf8_idx = 0; self.clear(); self.state = State::Escape; return; } // Non-continuation byte breaks the sequence; return to Ground and // re-process the byte. if !(0x80..=0xBF).contains(&b) { self.utf8_idx = 0; self.state = State::Ground; self.ground(perform, b); return; } self.utf8_buf[self.utf8_idx] = b; self.utf8_idx += 1; if self.utf8_idx == self.utf8_expected { if let Ok(s) = std::str::from_utf8(&self.utf8_buf[..self.utf8_idx]) && let Some(c) = s.chars().next() { perform.print(c); } self.utf8_idx = 0; self.state = State::Ground; } } fn escape(&mut self, perform: &mut P, b: u8) { match b { 0x00..=0x17 | 0x19 | 0x1C..=0x1F => perform.execute(b), 0x20..=0x2F => { self.collect(b); self.state = State::EscapeIntermediate; } 0x30..=0x4F | 0x51..=0x57 | 0x59 | 0x5A | 0x5C | 0x60..=0x7E => { perform.esc_dispatch( &self.intermediates[..self.intermediates_idx], self.ignoring, b, ); self.state = State::Ground; } 0x50 => { self.clear(); self.state = State::DcsEntry; } 0x58 | 0x5E => { self.apc_start(); self.state = State::ApcString; } 0x5B => { self.clear(); self.state = State::CsiEntry; } 0x5D => { self.osc_start(); self.state = State::OscString; } 0x5F => { self.apc_start(); self.state = State::ApcString; } 0x7F => {} // Ignore. 0x18 | 0x1A => { perform.execute(b); self.state = State::Ground; } 0x1B => self.clear(), _ => {} } } fn escape_intermediate(&mut self, perform: &mut P, b: u8) { match b { 0x00..=0x17 | 0x19 | 0x1C..=0x1F => perform.execute(b), 0x20..=0x2F => self.collect(b), 0x30..=0x7E => { perform.esc_dispatch( &self.intermediates[..self.intermediates_idx], self.ignoring, b, ); self.state = State::Ground; } 0x7F => {} _ => {} } } fn csi_entry(&mut self, perform: &mut P, b: u8) { match b { 0x00..=0x17 | 0x19 | 0x1C..=0x1F => perform.execute(b), 0x20..=0x2F => { self.collect(b); self.state = State::CsiIntermediate; } 0x30..=0x39 => { self.params.append_digit((b - b'0') as u16); self.state = State::CsiParam; } 0x3A => { if !self.params.new_subparam() { self.ignoring = true; } self.state = State::CsiParam; } 0x3B => { if !self.params.new_param() { self.ignoring = true; } self.state = State::CsiParam; } 0x3C..=0x3F => { self.collect(b); self.state = State::CsiParam; } 0x40..=0x7E => { perform.csi_dispatch( &self.params, &self.intermediates[..self.intermediates_idx], self.ignoring, b as char, ); self.state = State::Ground; } 0x7F => {} _ => {} } } fn csi_param(&mut self, perform: &mut P, b: u8) { match b { 0x00..=0x17 | 0x19 | 0x1C..=0x1F => perform.execute(b), 0x20..=0x2F => { self.collect(b); self.state = State::CsiIntermediate; } 0x30..=0x39 => self.params.append_digit((b - b'0') as u16), 0x3A => { if !self.params.new_subparam() { self.ignoring = true; } } 0x3B => { if !self.params.new_param() { self.ignoring = true; } } 0x3C..=0x3F => self.state = State::CsiIgnore, 0x40..=0x7E => { perform.csi_dispatch( &self.params, &self.intermediates[..self.intermediates_idx], self.ignoring, b as char, ); self.state = State::Ground; } 0x7F => {} _ => {} } } fn csi_intermediate(&mut self, perform: &mut P, b: u8) { match b { 0x00..=0x17 | 0x19 | 0x1C..=0x1F => perform.execute(b), 0x20..=0x2F => self.collect(b), 0x30..=0x3F => self.state = State::CsiIgnore, 0x40..=0x7E => { perform.csi_dispatch( &self.params, &self.intermediates[..self.intermediates_idx], self.ignoring, b as char, ); self.state = State::Ground; } 0x7F => {} _ => {} } } fn csi_ignore(&mut self, perform: &mut P, b: u8) { match b { 0x00..=0x17 | 0x19 | 0x1C..=0x1F => perform.execute(b), 0x40..=0x7E => self.state = State::Ground, _ => {} } } fn osc_string(&mut self, perform: &mut P, b: u8) { match b { 0x07 => { self.osc_close_current(); self.dispatch_osc(perform, true); self.osc_end(); self.state = State::Ground; } 0x1B => { // Possible ST — end the OSC now, transition to Escape so a // following `\` gets consumed as a no-op esc_dispatch. self.osc_close_current(); self.dispatch_osc(perform, false); self.osc_end(); self.prev_string_state = State::OscString; self.state = State::Escape; self.clear(); } 0x3B => { // Parameter separator: close current field, open next. if self.osc_params.len() >= MAX_OSC_PARAMS { self.string_overflow = true; return; } self.osc_close_current(); let end = self.osc_buf.len(); self.osc_params.push((end, end)); } _ => { if self.osc_buf.len() >= MAX_STRING_BYTES { self.string_overflow = true; return; } self.osc_buf.push(b); } } } fn osc_start(&mut self) { self.osc_buf.clear(); self.osc_params.clear(); self.string_overflow = false; // Open the first parameter with a placeholder end index that // `osc_close_current` finalizes on ; / BEL / ST. self.osc_params.push((0, 0)); } /// Returns the OSC buffers to their resting size once a body has ended. /// /// `clear` leaves capacity behind, so a single oversized body would keep /// the cap's worth of memory reserved for the life of the parser and the /// limit above would bound one sequence rather than the process. fn osc_end(&mut self) { self.osc_buf.clear(); self.osc_buf.shrink_to(INITIAL_BODY_CAPACITY); self.osc_params.clear(); self.osc_params.shrink_to(8); self.string_overflow = false; } fn osc_close_current(&mut self) { if let Some(last) = self.osc_params.last_mut() { last.1 = self.osc_buf.len(); } } fn dispatch_osc(&self, perform: &mut P, bell_terminated: bool) { // An over-long body is dropped, not truncated: half a base64 clipboard // write is a different request, not a smaller one. if self.string_overflow { return; } let slices: Vec<&[u8]> = self .osc_params .iter() .map(|&(s, e)| &self.osc_buf[s..e]) .collect(); perform.osc_dispatch(&slices, bell_terminated); } fn dcs_entry(&mut self, perform: &mut P, b: u8) { match b { 0x00..=0x17 | 0x19 | 0x1C..=0x1F => {} 0x20..=0x2F => { self.collect(b); self.state = State::DcsIntermediate; } 0x30..=0x39 => { self.params.append_digit((b - b'0') as u16); self.state = State::DcsParam; } 0x3A => self.state = State::DcsIgnore, 0x3B => { if !self.params.new_param() { self.ignoring = true; } self.state = State::DcsParam; } 0x3C..=0x3F => { self.collect(b); self.state = State::DcsParam; } 0x40..=0x7E => { perform.hook( &self.params, &self.intermediates[..self.intermediates_idx], self.ignoring, b as char, ); self.state = State::DcsPassthrough; } 0x7F => {} _ => {} } } fn dcs_param(&mut self, perform: &mut P, b: u8) { match b { 0x00..=0x17 | 0x19 | 0x1C..=0x1F => {} 0x20..=0x2F => { self.collect(b); self.state = State::DcsIntermediate; } 0x30..=0x39 => self.params.append_digit((b - b'0') as u16), 0x3A => self.state = State::DcsIgnore, 0x3B => { if !self.params.new_param() { self.ignoring = true; } } 0x3C..=0x3F => self.state = State::DcsIgnore, 0x40..=0x7E => { perform.hook( &self.params, &self.intermediates[..self.intermediates_idx], self.ignoring, b as char, ); self.state = State::DcsPassthrough; } 0x7F => {} _ => {} } } fn dcs_intermediate(&mut self, perform: &mut P, b: u8) { match b { 0x00..=0x17 | 0x19 | 0x1C..=0x1F => {} 0x20..=0x2F => self.collect(b), 0x30..=0x3F => self.state = State::DcsIgnore, 0x40..=0x7E => { perform.hook( &self.params, &self.intermediates[..self.intermediates_idx], self.ignoring, b as char, ); self.state = State::DcsPassthrough; } 0x7F => {} _ => {} } } fn dcs_ignore(&mut self, perform: &mut P, b: u8) { if b == 0x1B { self.prev_string_state = State::DcsIgnore; self.state = State::Escape; self.clear(); let _ = perform; } } fn dcs_passthrough(&mut self, perform: &mut P, b: u8) { match b { 0x1B => { perform.unhook(); self.prev_string_state = State::DcsPassthrough; self.state = State::Escape; self.clear(); } 0x07 => { perform.unhook(); self.state = State::Ground; } 0x00..=0x17 | 0x19 | 0x1C..=0x1F | 0x20..=0x7E => perform.put(b), _ => {} } } fn apc_string(&mut self, perform: &mut P, b: u8) { match b { 0x07 => { if !self.string_overflow { perform.apc_dispatch(&self.apc_buf); } self.apc_end(); self.state = State::Ground; } 0x1B => { if !self.string_overflow { perform.apc_dispatch(&self.apc_buf); } self.apc_end(); self.prev_string_state = State::ApcString; self.state = State::Escape; self.clear(); } _ => { if self.apc_buf.len() >= MAX_STRING_BYTES { self.string_overflow = true; return; } self.apc_buf.push(b); } } } /// Opens an APC body. Paired with [`Parser::apc_end`] for the same reason /// [`Parser::osc_start`] is paired with [`Parser::osc_end`]. fn apc_start(&mut self) { self.apc_buf.clear(); self.string_overflow = false; } fn apc_end(&mut self) { self.apc_buf.clear(); self.apc_buf.shrink_to(INITIAL_BODY_CAPACITY); self.string_overflow = false; } /// Is the parser between sequences, holding no partial state? /// /// Ground is the only state in which a stream can be cut without losing /// something, so this is what a caller checks to know a chunk boundary is /// safe and what the fuzz oracle checks after a terminated sequence. #[must_use] pub fn in_ground(&self) -> bool { matches!(self.state, State::Ground) } /// Heap bytes held on behalf of sequences that have not completed. /// /// The parser accumulates three unbounded things — OSC body, APC body and /// the CSI parameter list — and none of them is capped by the state /// machine. Exposing the total is what lets the soak oracle assert an /// amplification ceiling instead of waiting for libFuzzer's RSS limit to /// notice a machine-sized allocation. #[must_use] pub fn buffered_bytes(&self) -> usize { self.osc_buf.capacity() + self.apc_buf.capacity() + self.osc_params.capacity() * std::mem::size_of::<(usize, usize)>() + self.params.footprint() } fn collect(&mut self, b: u8) { if self.intermediates_idx < self.intermediates.len() { self.intermediates[self.intermediates_idx] = b; self.intermediates_idx += 1; } else { self.ignoring = true; } } fn clear(&mut self) { self.intermediates_idx = 0; self.ignoring = false; self.params.clear(); } } #[cfg(test)] mod tests;