use std::borrow::Cow;
use alacritty_terminal::vte::{Params, Parser, Perform};
const MAX_DEPTH: u32 = 1024;
const CAN: u8 = 0x18;
const SUB: u8 = 0x1a;
const ESC: u8 = 0x1b;
#[derive(Default)]
pub(super) struct KeyboardStackGuard {
parser: Parser,
seen: Seen,
depth: [u32; 2],
alt: bool,
margin: bool,
margin_seen: bool,
unfinished: bool,
}
#[derive(Debug, PartialEq)]
enum Sequence {
Skip(usize),
Read(usize),
Other,
}
fn sequence(s: &[u8]) -> Sequence {
match s.get(1) {
Some(b'[') => {
let body = s[2..].iter().position(|b| !(0x20..=0x3f).contains(b));
match body.map(|n| (n + 2, s[n + 2])) {
Some((at, b'u' | b'h' | b'l' | b'r')) => Sequence::Read(at + 1),
Some((at, 0x40..=0x7e)) => Sequence::Skip(at + 1),
_ => Sequence::Other,
}
}
Some(0x20..=0x2f) => match s[1..].iter().position(|b| !(0x20..=0x2f).contains(b)) {
Some(n) if (0x30..=0x7e).contains(&s[n + 1]) => Sequence::Skip(n + 2),
_ => Sequence::Other,
},
Some(b']') => {
let body = &s[2..];
let end = memchr::memchr3(0x07, CAN, ESC, body);
match memchr::memchr(SUB, &body[..end.unwrap_or(body.len())]).or(end) {
Some(n) if body[n] == ESC => Sequence::Skip(n + 2),
Some(n) => Sequence::Skip(n + 3),
None => Sequence::Other,
}
}
Some(b'X' | b'^' | b'_') => match memchr::memchr3(CAN, SUB, ESC, &s[2..]) {
Some(n) if s[n + 2] == ESC => Sequence::Skip(n + 2),
Some(n) => Sequence::Skip(n + 3),
None => Sequence::Other,
},
Some(b'c') => Sequence::Read(2),
Some(b'P') => Sequence::Other,
Some(0x30..=0x7e) => Sequence::Skip(2),
_ => Sequence::Other,
}
}
#[derive(Default)]
struct Seen(Vec<Change>);
#[derive(Debug, Clone, Copy, PartialEq)]
enum Change {
Push,
Pop(u32),
AltScreen(bool),
Reset,
Margin(bool),
}
impl Perform for Seen {
fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], ignore: bool, action: char) {
if ignore || intermediates.len() > 2 {
return;
}
let first = || params.iter().next().map(|p| p[0]).filter(|p| *p != 0);
match (action, intermediates) {
('u', [b'>']) => self.0.push(Change::Push),
('u', [b'<']) => self.0.push(Change::Pop(u32::from(first().unwrap_or(1)))),
('h' | 'l', [b'?']) => {
for _ in params.iter().filter(|p| p[0] == 1049) {
self.0.push(Change::AltScreen(action == 'h'));
}
}
('r', []) => {
let mut it = params.iter().map(|p| p[0]);
let top = it.next().filter(|t| *t != 0).unwrap_or(1);
let bottom = it.next().filter(|b| *b != 0);
if bottom.is_none_or(|b| top < b) {
self.0.push(Change::Margin(top > 1));
}
}
_ => {}
}
}
fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {
if byte == b'c' && intermediates.is_empty() {
self.0.push(Change::Reset);
}
}
}
impl KeyboardStackGuard {
pub(super) fn filter<'a>(&mut self, bytes: &'a [u8]) -> Cow<'a, [u8]> {
self.note_margin();
let mut refused = Vec::new();
let mut at = 0;
if std::mem::take(&mut self.unfinished) {
at = memchr::memchr(ESC, bytes).unwrap_or(bytes.len());
self.read(&bytes[..at], 0, &mut refused);
self.unfinished = at == bytes.len();
}
while let Some(n) = memchr::memchr(ESC, &bytes[at..]) {
let esc = at + n;
match sequence(&bytes[esc..]) {
Sequence::Skip(len) => at = esc + len,
Sequence::Read(len) => {
self.read(&bytes[esc..esc + len], esc, &mut refused);
at = esc + len;
}
Sequence::Other => {
let end = memchr::memchr(ESC, &bytes[esc + 1..]).map_or(bytes.len(), |n| esc + 1 + n);
self.read(&bytes[esc..end], esc, &mut refused);
self.unfinished = end == bytes.len();
at = end;
}
}
}
if refused.is_empty() {
return Cow::Borrowed(bytes);
}
let mut out = bytes.to_vec();
for i in refused {
out[i] = CAN;
}
Cow::Owned(out)
}
fn read(&mut self, bytes: &[u8], offset: usize, refused: &mut Vec<usize>) {
let mut start = 0;
for at in memchr::memchr_iter(b'u', bytes) {
self.read_run(&bytes[start..at]);
self.parser.advance(&mut self.seen, b"u");
if self.apply() {
refused.push(offset + at);
}
start = at + 1;
}
self.read_run(&bytes[start..]);
}
fn read_run(&mut self, bytes: &[u8]) {
if !bytes.is_empty() {
self.parser.advance(&mut self.seen, bytes);
self.apply();
}
}
fn apply(&mut self) -> bool {
let mut refused = false;
for change in std::mem::take(&mut self.seen.0) {
let screen = usize::from(self.alt);
match change {
Change::Push if self.depth[screen] >= MAX_DEPTH => refused = true,
Change::Push => self.depth[screen] += 1,
Change::Pop(n) => self.depth[screen] = self.depth[screen].saturating_sub(n),
Change::AltScreen(on) => self.alt = on,
Change::Reset => {
self.depth = [0, 0];
self.alt = false;
self.margin = false;
}
Change::Margin(on) => self.margin = on,
}
self.note_margin();
}
refused
}
fn note_margin(&mut self) {
if self.margin && !self.alt {
self.margin_seen = true;
}
}
pub(super) fn take_top_margin(&mut self) -> bool {
std::mem::take(&mut self.margin_seen) || (self.margin && !self.alt)
}
pub(super) fn screen_resized(&mut self) {
self.margin = false;
}
}
#[cfg(test)]
mod tests {
use alacritty_terminal::term::{Config, Term};
use alacritty_terminal::vte::ansi::Processor;
use super::*;
#[test]
fn pushes_past_the_limit_are_cancelled_and_the_rest_passes() {
let mut g = KeyboardStackGuard::default();
let push = b"\x1b[>1u".repeat(MAX_DEPTH as usize + 5);
let out = g.filter(&push);
assert_eq!(out.windows(5).filter(|w| *w == b"\x1b[>1u").count(), MAX_DEPTH as usize);
assert_eq!(out.len(), push.len(), "nothing is removed, the last byte of a push changes");
let mut out = g.filter(b"\x1b[<2u\x1b[>1u\x1b[>1u\x1b[>1").into_owned();
out.extend_from_slice(&g.filter(b"u hello \x1b[1;31mred\x1b[0m"));
assert_eq!(out, b"\x1b[<2u\x1b[>1u\x1b[>1u\x1b[>1\x18 hello \x1b[1;31mred\x1b[0m");
let out = g.filter(b"\x1b[?1049h\x1b[>1u\x1b[?1049l\x1b[>1u");
assert_eq!(&*out, b"\x1b[?1049h\x1b[>1u\x1b[?1049l\x1b[>1\x18");
let out = g.filter(b"\x1bc\x1b[>1u");
assert_eq!(&*out, b"\x1bc\x1b[>1u");
assert!(matches!(g.filter(b"plain \x1b[1mtext\x1b]0;title\x07\x1b[>1u"), Cow::Borrowed(_)));
}
#[test]
fn sequences_are_recognised_as_the_parser_reads_them() {
use Sequence::*;
for (bytes, want) in [
(&b"\x1b[1;31mtext"[..], Skip(7)),
(b"\x1b[?25l", Read(6)),
(b"\x1b[>1u", Read(5)),
(b"\x1b[2;20r", Read(7)),
(b"\x1b[1;3", Other),
(b"\x1b[1\n;3m", Other),
(b"\x1b(B", Skip(3)),
(b"\x1b7", Skip(2)),
(b"\x1bc", Read(2)),
(b"\x1b]0;title\x07text", Skip(10)),
(b"\x1b]0;title\x1b\\", Skip(9)),
(b"\x1b]0;ti\x1atle\x07", Skip(7)),
(b"\x1b]0;title", Other),
(b"\x1b_Gkitty\x18", Skip(9)),
(b"\x1bP1$q\x1b\\", Other),
(b"\x1b", Other),
(b"\x1b\x1b[>1u", Other),
] {
assert_eq!(sequence(bytes), want, "{bytes:?}");
}
}
fn filter_every_byte(g: &mut KeyboardStackGuard, bytes: &[u8]) -> Vec<u8> {
g.note_margin();
let mut refused = Vec::new();
g.read(bytes, 0, &mut refused);
let mut out = bytes.to_vec();
for i in refused {
out[i] = CAN;
}
out
}
#[test]
fn skipping_what_cannot_matter_changes_nothing() {
const PIECES: &[&[u8]] = &[
b"\x1b",
b"[",
b"]",
b">",
b"<",
b"?",
b"1",
b"0",
b"1049",
b";",
b"u",
b"h",
b"l",
b"r",
b"c",
b"P",
b"_",
b"X",
b"^",
b"\\",
b"m",
b"$",
b"(",
b" ",
b"text",
b"\x07",
b"\x18",
b"\x1a",
b"\0",
b"\x7f",
b"\x9c",
b"\xe6\x97\xa5",
b"\xe6",
b"\n",
b"\x1b[>1u",
b"\x1b[<u",
b"\x1b[?1049h",
b"\x1b[?1049l",
b"\x1b[3r",
b"\x1b[r",
b"\x1bc",
b"\x1b]0;t\x07",
b"\x1b[1;31m",
];
let mut seed = 0x9e37_79b9_7f4a_7c15u64;
let mut next = move |n: usize| {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
(seed % n as u64) as usize
};
for round in 0..300 {
let mut fast = KeyboardStackGuard::default();
let mut slow = KeyboardStackGuard::default();
let near = b"\x1b[>1u".repeat(MAX_DEPTH as usize - 3);
assert_eq!(&*fast.filter(&near), filter_every_byte(&mut slow, &near));
let mut output = Vec::new();
for _ in 0..400 {
output.extend_from_slice(PIECES[next(PIECES.len())]);
}
let mut at = 0;
while at < output.len() {
let end = (at + 1 + next(40)).min(output.len());
let read = &output[at..end];
assert_eq!(&*fast.filter(read), filter_every_byte(&mut slow, read), "round {round}");
assert_eq!((fast.depth, fast.alt, fast.margin), (slow.depth, slow.alt, slow.margin));
assert_eq!(fast.take_top_margin(), slow.take_top_margin());
at = end;
}
}
}
#[test]
fn an_emulator_fed_endless_pushes_survives() {
let size = super::super::pty::Size { cols: 20, rows: 5 };
let config = Config { kitty_keyboard: true, ..Config::default() };
for push in [
&b"\x1b[>1u"[..],
b"\x1b[\0>1u",
b"\x1b\0[>1u",
b"\x1b[>\x071u",
b"\x1b[\x7f>1u",
b"\x1b\x7f[>1u",
b"\x1b\x1b[>1u",
b"\x1b]0;title\x1b[>1u",
b"\x1b]0;title\x07\x1b[>1u",
b"\x1b]0;ti\x18tle\x1b[>1u",
b"\x1bP1$q\x1b[>1u",
b"\x1b_kitty\x1b[>1u",
b"\x1b_kit\x1aty\x1b[>1u",
b"\xe6\x97\xa5\x1b[>1u",
b"\x1b[1;31mred\x1b(B\x1b[>1u",
] {
let mut g = KeyboardStackGuard::default();
let mut term = Term::new(config.clone(), &size, super::super::pty::Listener::default());
let mut parser: Processor = Processor::new();
for chunk in push.repeat(4100).chunks(7) {
parser.advance(&mut term, &g.filter(chunk));
}
assert_eq!(g.depth[0], MAX_DEPTH, "{push:?}");
}
}
}