Escaped config strings (ghostty_os::osbytes) kept reaching OS boundaries undecoded, and decoding was occasionally applied to plain text. The root cause was that both were plain `String`s. Now: - `EncodedOs` is a newtype (no Display/AsRef<Path>) that decodes only via to_os/to_path/to_bytes/to_wide; plain text enters via from_plain_text. OS helpers (env, home, xdg, passwd, resources dir, path expansion), config::Path, WorkingDirectory, theme/file_load/edit paths, terminal pwd, OpenUrl/Pwd actions and termio cwd use it, so the compiler forces decoding at every boundary. os::open takes &OsStr. Config output is unchanged. Fixes all rows of docs/rewrite/notes/review-rv5-codex.md (final-Z7.md): screen-file copy; regex/OSC 8 URLs no longer decoded (regression); lossless Windows WTF-8/WTF-16 for args, env and CreateProcessW; Windows resources dir/TERMINFO; color OSC request lists and X11 color lookup no longer allocate globally (C zero-allocation test extended); macOS Mach handler resumes all threads before KERN_FAILURE (hang regression) and SIGABRT minidumps go through the exception thread; plus open-config, background images, crash dir, ssh cache, exe dir/PATH, fontconfig filenames, file links, kitty TMPDIR, .hushlogin and benchmark paths. The typing also surfaced and fixed gtk-custom-css, bell audio, bash HISTFILE, OSC 52 writes and the C config getters. Test Plan: - cargo test --workspace (3376 passed) - just check-macos (both), just check-windows, just check-wasm - diff <(ghostty-rs +show-config --default --docs) \ <(ghostty +show-config --default --docs)
1053 lines
31 KiB
Rust
1053 lines
31 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, 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<Action>; 3] {
|
|
let mut out: [Option<Action>; 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<Action>; 3]` out of the parser.
|
|
#[inline]
|
|
pub fn next_with<F: FnMut(Action)>(&mut self, c: u8, mut f: F) {
|
|
self.step(c, &mut |_, action| f(action));
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn step<F: FnMut(usize, Action)>(&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<F: FnMut(usize, Action)>(
|
|
&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<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,
|
|
};
|
|
// 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<Action>; 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<Action> = 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());
|
|
}
|
|
}
|