//! VT-series parser for escape and control sequences //! (ZIG/src/terminal/Parser.zig). //! //! This is implemented directly as the state machine described on //! vt100.net: https://vt100.net/emu/dec_ansi_parser //! //! `MAX_PARAMS` and `SepList` were pre-created by the orchestrator because //! `sgr` depends on them; keep their API stable. //! //! Rust port note: Zig's `Action` borrows slices out of the parser //! (`csi_dispatch.params` etc., "only valid until the next call to next"). //! Here the CSI/ESC/DCS payloads are small fixed-size inline copies exposed //! through `params()` / `intermediates()` accessors, so an `Action` owns its //! data and is independent of the parser's lifetime. use crate::{osc, parse_table::TABLE}; /// Maximum number of CSI parameters. This is a const in Ghostty so we /// match it. pub const MAX_PARAMS: usize = 24; /// Bitset of length `MAX_PARAMS` marking which parameters were followed by /// a colon (`:`) separator rather than a semicolon (`;`). Bit `i` set means /// the separator *after* param `i` was a colon. /// Equivalent to Zig's `std.StaticBitSet(MAX_PARAMS)`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)] pub struct SepList(u32); impl SepList { /// An empty set (all separators are semicolons). pub const fn init_empty() -> Self { Self(0) } /// Whether bit `i` is set (separator after param `i` is a colon). pub fn is_set(&self, i: usize) -> bool { debug_assert!(i < MAX_PARAMS); self.0 & (1 << i) != 0 } /// Set bit `i`. pub fn set(&mut self, i: usize) { debug_assert!(i < MAX_PARAMS); self.0 |= 1 << i; } /// Clear bit `i`. pub fn unset(&mut self, i: usize) { debug_assert!(i < MAX_PARAMS); self.0 &= !(1 << i); } /// Number of set bits. pub fn count(&self) -> usize { self.0.count_ones() as usize } /// Raw mask. pub fn mask(&self) -> u32 { self.0 } } /// Maximum number of intermediate characters during parsing. This is /// 4 because we also use the intermediates array for UTF8 decoding which /// can be at most 4 bytes. pub const MAX_INTERMEDIATE: usize = 4; /// States for the state machine #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum State { Ground, Escape, EscapeIntermediate, CsiEntry, CsiIntermediate, CsiParam, CsiIgnore, DcsEntry, DcsParam, DcsIntermediate, DcsPassthrough, DcsIgnore, OscString, SosPmApcString, } /// Transition action is an action that can be taken during a state /// transition. This is more of an internal action, not one used by /// end users, typically. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum TransitionAction { None, Ignore, Print, Execute, Collect, Param, EscDispatch, CsiDispatch, Put, OscPut, ApcPut, } /// The separator used for CSI params. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Sep { Semicolon = 0, Colon = 1, } /// CSI dispatch payload (Zig `Action.CSI`). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Csi { intermediates_buf: [u8; MAX_INTERMEDIATE], intermediates_len: u8, params_buf: [u16; MAX_PARAMS], params_len: u8, /// The list of separators used for CSI params. The value of the /// bit can be mapped to Sep. The index of this bit set specifies /// the separator AFTER that param. For example: 0;4:3 would have /// index 1 set. pub params_sep: SepList, pub final_: u8, } impl Csi { pub fn intermediates(&self) -> &[u8] { &self.intermediates_buf[..self.intermediates_len as usize] } pub fn params(&self) -> &[u16] { &self.params_buf[..self.params_len as usize] } } impl std::fmt::Display for Csi { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "ESC [ {} {:?} {}", String::from_utf8_lossy(self.intermediates()), self.params(), self.final_ as char ) } } /// ESC dispatch payload (Zig `Action.ESC`). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Esc { intermediates_buf: [u8; MAX_INTERMEDIATE], intermediates_len: u8, pub final_: u8, } impl Esc { /// Build an ESC from slices (extra intermediates beyond the maximum are /// dropped). Used by the stream to synthesize C1 controls. pub fn new(intermediates: &[u8], final_: u8) -> Self { let mut e = Esc { intermediates_buf: [0; MAX_INTERMEDIATE], intermediates_len: 0, final_, }; let n = intermediates.len().min(MAX_INTERMEDIATE); e.intermediates_buf[..n].copy_from_slice(&intermediates[..n]); e.intermediates_len = n as u8; e } pub fn intermediates(&self) -> &[u8] { &self.intermediates_buf[..self.intermediates_len as usize] } } impl std::fmt::Display for Esc { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "ESC {} {}", String::from_utf8_lossy(self.intermediates()), self.final_ as char ) } } /// DCS hook payload (Zig `Action.DCS`). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Dcs { intermediates_buf: [u8; MAX_INTERMEDIATE], intermediates_len: u8, params_buf: [u16; MAX_PARAMS], params_len: u8, pub final_: u8, } impl Dcs { /// Build a DCS from slices (extra elements beyond the maximums are /// dropped). Useful for tests and for consumers constructing hooks. pub fn new(intermediates: &[u8], params: &[u16], final_: u8) -> Self { let mut d = Dcs { intermediates_buf: [0; MAX_INTERMEDIATE], intermediates_len: 0, params_buf: [0; MAX_PARAMS], params_len: 0, final_, }; let n = intermediates.len().min(MAX_INTERMEDIATE); d.intermediates_buf[..n].copy_from_slice(&intermediates[..n]); d.intermediates_len = n as u8; let n = params.len().min(MAX_PARAMS); d.params_buf[..n].copy_from_slice(¶ms[..n]); d.params_len = n as u8; d } pub fn intermediates(&self) -> &[u8] { &self.intermediates_buf[..self.intermediates_len as usize] } pub fn params(&self) -> &[u16] { &self.params_buf[..self.params_len as usize] } } /// Action is the action that a caller of the parser is expected to /// take as a result of some input character. #[derive(Debug, Clone, PartialEq)] pub enum Action { /// Draw character to the screen. This is a unicode codepoint. Print(u32), /// Execute the C0 or C1 function. Execute(u8), /// Execute the CSI command. CsiDispatch(Csi), /// Execute the ESC command. EscDispatch(Esc), /// Execute the OSC command. OscDispatch(osc::Command<'static>), /// DCS-related events. DcsHook(Dcs), DcsPut(u8), DcsUnhook, /// APC data ApcStart, ApcPut(u8), ApcEnd, } /// The VT parser state machine. #[derive(Debug, Clone)] pub struct Parser { /// Current state of the state machine pub state: State, /// Intermediate tracking. intermediates: [u8; MAX_INTERMEDIATE], intermediates_idx: u8, /// Param tracking, building (crate-visible for the stream's CSI fast path) pub(crate) params: [u16; MAX_PARAMS], pub(crate) params_sep: SepList, pub(crate) params_idx: u8, pub(crate) param_acc: u16, pub(crate) param_acc_idx: u8, /// Parser for OSC sequences pub osc_parser: osc::Parser, } impl Default for Parser { fn default() -> Self { Self::new() } } impl Parser { pub fn new() -> Parser { Parser { state: State::Ground, intermediates: [0; MAX_INTERMEDIATE], intermediates_idx: 0, params: [0; MAX_PARAMS], params_sep: SepList::init_empty(), params_idx: 0, param_acc: 0, param_acc_idx: 0, osc_parser: osc::Parser::init(false), } } /// Next consumes the next character c and returns the actions to execute. /// Up to 3 actions may need to be executed -- in order -- representing /// the state exit, transition, and entry actions. #[inline] pub fn next(&mut self, c: u8) -> [Option; 3] { let mut out: [Option; 3] = [None, None, None]; self.step(c, &mut |slot, action| out[slot] = Some(action)); out } /// Visitor form of [`Parser::next`]: `f` is called, in order, with each /// action (exit, transition, entry) instead of returning an array. This /// avoids moving the large `[Option; 3]` out of the parser. #[inline] pub fn next_with(&mut self, c: u8, mut f: F) { self.step(c, &mut |_, action| f(action)); } #[inline(always)] fn step(&mut self, c: u8, f: &mut F) { let effect = TABLE[c as usize][self.state as usize]; // Fast path: staying in the same state means there are no exit or // entry actions, only the transition action. if self.state == effect.state { match effect.action { TransitionAction::Print => f(1, Action::Print(u32::from(c))), TransitionAction::Execute => f(1, Action::Execute(c)), action => { if let Some(a) = self.do_action(action, c) { f(1, a); } } } return; } self.next_state_change(c, effect, f); } /// The general path of [`Parser::next`] when the state changes. #[inline(never)] fn next_state_change( &mut self, c: u8, effect: crate::parse_table::Transition, f: &mut F, ) { let next_state = effect.state; let action = effect.action; // When going from one state to another, the actions take place in this order: // // 1. exit action from old state // 2. transition action // 3. entry action to new state // Exit depends on current state (we always change state here). match self.state { State::OscString => { if let Some(cmd) = self.osc_parser.end(Some(c)) { f(0, Action::OscDispatch(cmd.into_owned())); } } State::DcsPassthrough => f(0, Action::DcsUnhook), State::SosPmApcString => f(0, Action::ApcEnd), _ => {} } if let Some(a) = self.do_action(action, c) { f(1, a); } // Entry depends on new state match next_state { State::Escape | State::DcsEntry | State::CsiEntry => self.clear(), State::OscString => self.osc_parser.reset(), State::DcsPassthrough => 'dcs_hook: { // Ignore too many parameters if self.params_idx as usize >= MAX_PARAMS { break 'dcs_hook; } // Finalize parameters if self.param_acc_idx > 0 { self.params[self.params_idx as usize] = self.param_acc; self.params_idx += 1; } f( 2, Action::DcsHook(Dcs::new( &self.intermediates[..self.intermediates_idx as usize], &self.params[..self.params_idx as usize], c, )), ); } State::SosPmApcString => f(2, Action::ApcStart), _ => {} } // After generating the actions, we set our next state. self.state = next_state; } #[inline] pub fn collect(&mut self, c: u8) { if self.intermediates_idx as usize >= MAX_INTERMEDIATE { log::warn!("invalid intermediates count"); return; } self.intermediates[self.intermediates_idx as usize] = c; self.intermediates_idx += 1; } #[inline(always)] fn do_action(&mut self, action: TransitionAction, c: u8) -> Option { match action { TransitionAction::None | TransitionAction::Ignore => None, TransitionAction::Print => Some(Action::Print(c as u32)), TransitionAction::Execute => Some(Action::Execute(c)), TransitionAction::Collect => { self.collect(c); None } TransitionAction::Param => { // Semicolon separates parameters. If we encounter a semicolon // we need to store and move on to the next parameter. if c == b';' || c == b':' { // Ignore too many parameters if self.params_idx as usize >= MAX_PARAMS { return None; } // Set param final value self.params[self.params_idx as usize] = self.param_acc; if c == b':' { self.params_sep.set(self.params_idx as usize); } self.params_idx += 1; // Reset current param value to 0 self.param_acc = 0; self.param_acc_idx = 0; return None; } // A numeric value. Add it to our accumulator. self.param_acc = self.param_acc.saturating_mul(10); self.param_acc = self.param_acc.saturating_add((c - b'0') as u16); // Increment our accumulator index. If we overflow then // we're out of bounds and we exit immediately. self.param_acc_idx = self.param_acc_idx.wrapping_add(1); // The client is expected to perform no action. None } TransitionAction::OscPut => { self.osc_parser.next(c); None } TransitionAction::CsiDispatch => { // Ignore too many parameters if self.params_idx as usize >= MAX_PARAMS { return None; } // Finalize parameters if we have one if self.param_acc_idx > 0 { self.params[self.params_idx as usize] = self.param_acc; self.params_idx += 1; } let mut csi = Csi { intermediates_buf: self.intermediates, intermediates_len: self.intermediates_idx, params_buf: [0; MAX_PARAMS], params_len: self.params_idx, params_sep: self.params_sep, final_: c, }; // A fixed-size masked copy (vectorizes) instead of a // variable-length `copy_from_slice`, which is a libc call. let n = usize::from(self.params_idx); for (i, (dst, src)) in csi.params_buf.iter_mut().zip(&self.params).enumerate() { *dst = if i < n { *src } else { 0 }; } // We only allow colon or mixed separators for the 'm' command. if c != b'm' && self.params_sep.count() > 0 { log::warn!( "CSI colon or mixed separators only allowed for 'm' command, got: {csi}" ); return None; } Some(Action::CsiDispatch(csi)) } TransitionAction::EscDispatch => Some(Action::EscDispatch(Esc { intermediates_buf: self.intermediates, intermediates_len: self.intermediates_idx, final_: c, })), TransitionAction::Put => Some(Action::DcsPut(c)), TransitionAction::ApcPut => Some(Action::ApcPut(c)), } } #[inline] pub fn clear(&mut self) { self.intermediates_idx = 0; self.params_idx = 0; self.params_sep = SepList::init_empty(); self.param_acc = 0; self.param_acc_idx = 0; } } #[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] use super::*; use crate::osc::Command; fn feed_quiet(p: &mut Parser, s: &[u8]) { for &c in s { let a = p.next(c); assert!(a[0].is_none()); assert!(a[1].is_none()); assert!(a[2].is_none()); } } fn csi(a: &[Option; 3]) -> Csi { match a[1].as_ref().expect("csi_dispatch") { Action::CsiDispatch(c) => *c, other => panic!("expected csi_dispatch, got {other:?}"), } } #[test] fn next_with_matches_next() { let input = b"\x1b[1;38:2::1:2:3m\x1b]0;title\x07a\n\x1bP1$qm\x1b\\\x1b_G\x1b\\\x1b[31"; let mut a = Parser::new(); let mut b = Parser::new(); for &c in input { let expect: Vec = a.next(c).into_iter().flatten().collect(); let mut got = Vec::new(); b.next_with(c, |act| got.push(act)); assert_eq!(expect, got); assert_eq!(a.state, b.state); } } #[test] fn parser_basic() { let mut p = Parser::new(); p.next(0x9E); assert_eq!(p.state, State::SosPmApcString); p.next(0x9C); assert_eq!(p.state, State::Ground); { let a = p.next(b'a'); assert_eq!(p.state, State::Ground); assert!(a[0].is_none()); assert!(matches!(a[1], Some(Action::Print(_)))); assert!(a[2].is_none()); } { let a = p.next(0x19); assert_eq!(p.state, State::Ground); assert!(a[0].is_none()); assert!(matches!(a[1], Some(Action::Execute(_)))); assert!(a[2].is_none()); } } #[test] fn esc_esc_paren_b() { let mut p = Parser::new(); p.next(0x1B); p.next(b'('); let a = p.next(b'B'); assert_eq!(p.state, State::Ground); assert!(a[0].is_none()); assert!(a[2].is_none()); let Some(Action::EscDispatch(d)) = &a[1] else { panic!("expected esc_dispatch"); }; assert_eq!(d.final_, b'B'); assert_eq!(d.intermediates(), b"("); } #[test] fn csi_esc_bracket_h() { let mut p = Parser::new(); p.next(0x1B); p.next(0x5B); let a = p.next(0x48); assert_eq!(p.state, State::Ground); assert!(a[0].is_none()); assert!(a[2].is_none()); let d = csi(&a); assert_eq!(d.final_, 0x48); assert_eq!(d.params().len(), 0); } #[test] fn csi_esc_bracket_1_semicolon_4_h() { let mut p = Parser::new(); p.next(0x1B); p.next(0x5B); p.next(0x31); // 1 p.next(0x3B); // ; p.next(0x34); // 4 let a = p.next(0x48); // H assert_eq!(p.state, State::Ground); assert!(a[0].is_none()); assert!(a[2].is_none()); let d = csi(&a); assert_eq!(d.final_, b'H'); assert_eq!(d.params(), &[1, 4]); } #[test] fn csi_sgr_esc_bracket_38_colon_2_m() { let mut p = Parser::new(); p.next(0x1B); p.next(b'['); p.next(b'3'); p.next(b'8'); p.next(b':'); p.next(b'2'); let a = p.next(b'm'); assert_eq!(p.state, State::Ground); assert!(a[0].is_none()); assert!(a[2].is_none()); let d = csi(&a); assert_eq!(d.final_, b'm'); assert_eq!(d.params(), &[38, 2]); assert!(d.params_sep.is_set(0)); assert!(!d.params_sep.is_set(1)); } #[test] fn csi_sgr_colon_followed_by_semicolon() { let mut p = Parser::new(); p.next(0x1B); feed_quiet(&mut p, b"[48:2"); { let a = p.next(b'm'); assert_eq!(p.state, State::Ground); assert!(a[0].is_none()); assert!(matches!(a[1], Some(Action::CsiDispatch(_)))); assert!(a[2].is_none()); } p.next(0x1B); p.next(b'['); { let a = p.next(b'H'); assert_eq!(p.state, State::Ground); assert!(a[0].is_none()); assert!(matches!(a[1], Some(Action::CsiDispatch(_)))); assert!(a[2].is_none()); } } #[test] fn csi_sgr_mixed_colon_and_semicolon() { let mut p = Parser::new(); p.next(0x1B); feed_quiet(&mut p, b"[38:5:1;48:5:0"); let a = p.next(b'm'); assert_eq!(p.state, State::Ground); assert!(a[0].is_none()); assert!(matches!(a[1], Some(Action::CsiDispatch(_)))); assert!(a[2].is_none()); } #[test] fn csi_sgr_esc_bracket_48_colon_2_m() { let mut p = Parser::new(); p.next(0x1B); feed_quiet(&mut p, b"[48:2:240:143:104"); let a = p.next(b'm'); assert_eq!(p.state, State::Ground); let d = csi(&a); assert_eq!(d.final_, b'm'); assert_eq!(d.params(), &[48, 2, 240, 143, 104]); for (i, expect) in [true, true, true, true, false].iter().enumerate() { assert_eq!(d.params_sep.is_set(i), *expect, "sep {i}"); } } #[test] fn csi_sgr_esc_bracket_4_colon_3_m_colon() { let mut p = Parser::new(); p.next(0x1B); p.next(b'['); p.next(b'4'); p.next(b':'); p.next(b'3'); let a = p.next(b'm'); assert_eq!(p.state, State::Ground); let d = csi(&a); assert_eq!(d.final_, b'm'); assert_eq!(d.params(), &[4, 3]); assert!(d.params_sep.is_set(0)); assert!(!d.params_sep.is_set(1)); } #[test] fn csi_sgr_with_many_blank_and_colon() { let mut p = Parser::new(); p.next(0x1B); feed_quiet(&mut p, b"[58:2::240:143:104"); let a = p.next(b'm'); assert_eq!(p.state, State::Ground); let d = csi(&a); assert_eq!(d.final_, b'm'); assert_eq!(d.params(), &[58, 2, 0, 240, 143, 104]); for (i, expect) in [true, true, true, true, true, false].iter().enumerate() { assert_eq!(d.params_sep.is_set(i), *expect, "sep {i}"); } } // This is from a Kakoune actual SGR sequence. #[test] fn csi_sgr_mixed_colon_and_semicolon_with_blank() { let mut p = Parser::new(); p.next(0x1B); feed_quiet(&mut p, b"[;4:3;38;2;175;175;215;58:2::190:80:70"); let a = p.next(b'm'); assert_eq!(p.state, State::Ground); assert!(a[0].is_none()); assert!(a[2].is_none()); let d = csi(&a); assert_eq!(d.final_, b'm'); assert_eq!( d.params(), &[0, 4, 3, 38, 2, 175, 175, 215, 58, 2, 0, 190, 80, 70] ); let set = [1usize, 8, 9, 10, 11, 12]; for i in 0..14 { assert_eq!(d.params_sep.is_set(i), set.contains(&i), "sep {i}"); } } // This is from a Kakoune actual SGR sequence also. #[test] fn csi_sgr_mixed_colon_and_semicolon_setting_underline_bg_fg() { let mut p = Parser::new(); p.next(0x1B); feed_quiet( &mut p, b"[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136", ); let a = p.next(b'm'); assert_eq!(p.state, State::Ground); assert!(a[0].is_none()); assert!(a[2].is_none()); let d = csi(&a); assert_eq!(d.final_, b'm'); assert_eq!( d.params(), &[ 4, 3, 38, 2, 51, 51, 51, 48, 2, 170, 170, 170, 58, 2, 255, 97, 136 ] ); for i in 0..17 { assert_eq!(d.params_sep.is_set(i), i == 0, "sep {i}"); } } #[test] fn csi_colon_for_non_m_final() { let mut p = Parser::new(); p.next(0x1B); feed_quiet(&mut p, b"[38:2h"); assert_eq!(p.state, State::Ground); } #[test] fn csi_request_mode_decrqm() { let mut p = Parser::new(); p.next(0x1B); feed_quiet(&mut p, b"[?2026$"); let a = p.next(b'p'); assert_eq!(p.state, State::Ground); assert!(a[0].is_none()); assert!(a[2].is_none()); let d = csi(&a); assert_eq!(d.final_, b'p'); assert_eq!(d.intermediates(), b"?$"); assert_eq!(d.params(), &[2026]); } #[test] fn csi_change_cursor() { let mut p = Parser::new(); p.next(0x1B); feed_quiet(&mut p, b"[3 "); let a = p.next(b'q'); assert_eq!(p.state, State::Ground); assert!(a[0].is_none()); assert!(a[2].is_none()); let d = csi(&a); assert_eq!(d.final_, b'q'); assert_eq!(d.intermediates(), b" "); assert_eq!(d.params(), &[3]); } #[test] fn osc_change_window_title() { let mut p = Parser::new(); for c in [0x1B, b']', b'0', b';', b'a', b'b', b'c'] { p.next(c); } let a = p.next(0x07); // BEL assert_eq!(p.state, State::Ground); assert!(a[1].is_none()); assert!(a[2].is_none()); assert_eq!( a[0], Some(Action::OscDispatch(Command::ChangeWindowTitle( b"abc".to_vec().into() ))) ); } #[test] fn osc_change_window_title_end_in_esc() { let mut p = Parser::new(); for c in [0x1B, b']', b'0', b';', b'a', b'b', b'c'] { p.next(c); } let a = p.next(0x1B); p.next(b'\\'); assert_eq!(p.state, State::Ground); assert!(a[1].is_none()); assert!(a[2].is_none()); assert_eq!( a[0], Some(Action::OscDispatch(Command::ChangeWindowTitle( b"abc".to_vec().into() ))) ); } // https://github.com/darrenstarr/VtNetCore/pull/14 // Saw this on HN, decided to add a test case because why not. #[test] fn osc_112_incomplete_sequence() { let mut p = Parser::new(); p.osc_parser.alloc = true; for c in [0x1B, b']', b'1', b'1', b'2'] { p.next(c); } let a = p.next(0x07); assert_eq!(p.state, State::Ground); assert!(a[1].is_none()); assert!(a[2].is_none()); let Some(Action::OscDispatch(Command::ColorOperation { op, requests, terminator, })) = a[0].clone() else { panic!("expected color_operation"); }; assert_eq!(terminator, osc::Terminator::Bel); assert_eq!(op, osc::color::Operation::Osc112); assert_eq!(requests.len(), 1); assert_eq!( requests.get(0), Some(osc::color::Request::Reset(osc::color::Target::Dynamic( crate::color::Dynamic::Cursor ))) ); } #[test] fn osc_104_empty() { let mut p = Parser::new(); p.osc_parser.alloc = true; for c in [0x1B, b']', b'1', b'0', b'4'] { p.next(c); } let a = p.next(0x07); assert_eq!(p.state, State::Ground); assert!(a[1].is_none()); assert!(a[2].is_none()); let Some(Action::OscDispatch(Command::ColorOperation { op, requests, terminator, })) = a[0].clone() else { panic!("expected color_operation"); }; assert_eq!(terminator, osc::Terminator::Bel); assert_eq!(op, osc::color::Operation::Osc104); assert_eq!(requests.len(), 1); assert_eq!(requests.get(0), Some(osc::color::Request::ResetPalette)); } #[test] fn csi_too_many_params() { let mut p = Parser::new(); p.next(0x1B); p.next(b'['); for _ in 0..100 { p.next(b'1'); p.next(b';'); } p.next(b'1'); let a = p.next(b'C'); assert_eq!(p.state, State::Ground); assert!(a[0].is_none()); assert!(a[1].is_none()); assert!(a[2].is_none()); } #[test] fn csi_sgr_with_up_to_our_max_parameters() { for max in 1..=MAX_PARAMS { let mut p = Parser::new(); p.next(0x1B); p.next(b'['); for _ in 0..max - 1 { p.next(b'1'); p.next(b';'); } p.next(b'2'); let a = p.next(b'H'); assert_eq!(p.state, State::Ground); assert!(a[0].is_none()); assert!(a[2].is_none()); let c = csi(&a); assert_eq!(c.params().len(), max); assert_eq!(c.params()[max - 1], 2); } } #[test] fn csi_sgr_beyond_our_max_drops_it() { // Has to be +2 for the loops below let max = MAX_PARAMS + 2; let mut p = Parser::new(); p.next(0x1B); p.next(b'['); for _ in 0..max - 1 { p.next(b'1'); p.next(b';'); } p.next(b'2'); let a = p.next(b'H'); assert_eq!(p.state, State::Ground); assert!(a[0].is_none()); assert!(a[1].is_none()); assert!(a[2].is_none()); } #[test] fn dcs_xtgettcap() { let mut p = Parser::new(); p.next(0x1B); feed_quiet(&mut p, b"P+"); let a = p.next(b'q'); assert_eq!(p.state, State::DcsPassthrough); assert!(a[0].is_none()); assert!(a[1].is_none()); let Some(Action::DcsHook(hook)) = &a[2] else { panic!("expected dcs_hook"); }; assert_eq!(hook.intermediates(), b"+"); assert_eq!(hook.params(), &[] as &[u16]); assert_eq!(hook.final_, b'q'); } #[test] fn dcs_params() { let mut p = Parser::new(); p.next(0x1B); feed_quiet(&mut p, b"P1000"); let a = p.next(b'p'); assert_eq!(p.state, State::DcsPassthrough); assert!(a[0].is_none()); assert!(a[1].is_none()); let Some(Action::DcsHook(hook)) = &a[2] else { panic!("expected dcs_hook"); }; assert_eq!(hook.params(), &[1000]); assert_eq!(hook.final_, b'p'); } #[test] fn dcs_too_many_params() { // Regression test for a crash found by fuzzing (afl). When a DCS // sequence has more than MAX_PARAMS parameters and param_acc_idx > 0, // entering dcs_passthrough wrote to params[params_idx] without a // bounds check, causing an out-of-bounds access. let mut p = Parser::new(); p.next(0x1B); // ESC p.next(b'P'); // DCS entry // Feed a digit then MAX_PARAMS semicolons to fill all param slots. p.next(b'6'); for _ in 0..MAX_PARAMS { p.next(b';'); } // Feed another digit so param_acc_idx > 0 while params_idx == MAX_PARAMS. p.next(b'7'); // A final byte triggers entry to dcs_passthrough. The DCS should // be dropped entirely, consistent with how CSI handles overflow. let a = p.next(b'p'); assert!(a[0].is_none()); assert!(a[1].is_none()); assert!(a[2].is_none()); } }