Files
ghostty-rs/crates/ghostty-vt/src/parser.rs
T
ddidderr 800fe5be5e feat(vt): port the stream layer between parser and handlers
Ports ZIG/src/terminal/stream.zig: the Stream feeds bytes through the
UTF-8 decoder and parser and dispatches every CSI/ESC/OSC/DCS/APC and
C0/C1 control to a Handler as a high-level Action (all 95 Zig
variants). Zig's comptime handler interface becomes a Handler trait with
an associated error type, a required vt() and an optional vt_raw() hook.

Performance: next_slice() processes 4 KiB chunks, finds ESC with memchr
and decodes printable runs in bulk (scalar port of ZIG/src/simd's
index_of / utf8_decode_until_control_seq), plus the CSI parameter fast
path.

Terminals must construct the stream with Stream::new_alloc so the OSC
parser accepts allocating commands (colors, OSC 133), as in Zig.

Minor differences: after a stray UTF-8 byte the rest of the chunk
returns to the fast path, and a spurious Zig warning after CSI 2/5 W is
dropped.

Test Plan:
- cargo test -p ghostty-vt (448 passed; 37/38 stream tests ported,
  the skipped one only exercises the C ABI)
2026-10-11 21:59:15 +02:00

999 lines
29 KiB
Rust

//! 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;
use crate::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(&params[..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),
/// 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.
pub fn next(&mut self, c: u8) -> [Option<Action>; 3] {
let effect = TABLE[c as usize][self.state as usize];
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
let exit = if self.state == next_state {
None
} else {
match self.state {
State::OscString => self.osc_parser.end(Some(c)).map(Action::OscDispatch),
State::DcsPassthrough => Some(Action::DcsUnhook),
State::SosPmApcString => Some(Action::ApcEnd),
_ => None,
}
};
let transition = self.do_action(action, c);
// Entry depends on new state
let entry = if self.state == next_state {
None
} else {
match next_state {
State::Escape | State::DcsEntry | State::CsiEntry => {
self.clear();
None
}
State::OscString => {
self.osc_parser.reset();
None
}
State::DcsPassthrough => 'dcs_hook: {
// Ignore too many parameters
if self.params_idx as usize >= MAX_PARAMS {
break 'dcs_hook None;
}
// Finalize parameters
if self.param_acc_idx > 0 {
self.params[self.params_idx as usize] = self.param_acc;
self.params_idx += 1;
}
Some(Action::DcsHook(Dcs::new(
&self.intermediates[..self.intermediates_idx as usize],
&self.params[..self.params_idx as usize],
c,
)))
}
State::SosPmApcString => Some(Action::ApcStart),
_ => None,
}
};
// After generating the actions, we set our next state.
self.state = next_state;
[exit, transition, entry]
}
#[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]
fn do_action(&mut self, action: TransitionAction, c: u8) -> Option<Action> {
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,
};
csi.params_buf[..self.params_idx as usize]
.copy_from_slice(&self.params[..self.params_idx as usize]);
// 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<Action>; 3]) -> Csi {
match a[1].as_ref().expect("csi_dispatch") {
Action::CsiDispatch(c) => *c,
other => panic!("expected csi_dispatch, got {other:?}"),
}
}
#[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()
)))
);
}
#[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()
)))
);
}
// 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[0],
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[0], 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());
}
}