use alacritty_terminal::vte::{Params, Parser, Perform};
const MAX_DEPTH: u32 = 1024;
const CAN: u8 = 0x18;
#[derive(Default)]
pub(super) struct KeyboardStackGuard {
parser: Parser,
seen: Seen,
depth: [u32; 2],
alt: bool,
margin: bool,
margin_seen: bool,
}
#[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(&mut self, bytes: &[u8]) -> Vec<u8> {
self.note_margin();
let mut out = Vec::with_capacity(bytes.len());
let mut start = 0;
for at in bytes.iter().enumerate().filter(|(_, b)| **b == b'u').map(|(i, _)| i) {
self.read(&bytes[start..at]);
out.extend_from_slice(&bytes[start..at]);
self.parser.advance(&mut self.seen, b"u");
let refused = self.apply();
out.push(if refused { CAN } else { b'u' });
start = at + 1;
}
self.read(&bytes[start..]);
out.extend_from_slice(&bytes[start..]);
out
}
fn read(&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");
out.extend(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");
}
#[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"\x1bP1$q\x1b[>1u",
b"\x1b_kitty\x1b[>1u",
b"\xe6\x97\xa5\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:?}");
}
}
}