use std::sync::Arc;
use vt100::Callbacks;
pub(super) const MAX_UNSUPPORTED: usize = 32;
#[derive(Debug)]
pub(super) struct Unhandled {
seen: Vec<Arc<str>>,
shared: Arc<[Arc<str>]>,
overflow: u64,
visual_bells: u64,
}
impl Default for Unhandled {
fn default() -> Self {
Self {
seen: Vec::new(),
shared: Arc::from([] as [Arc<str>; 0]),
overflow: 0,
visual_bells: 0,
}
}
}
impl Unhandled {
pub(super) fn shapes(&self) -> Arc<[Arc<str>]> {
Arc::clone(&self.shared)
}
pub(super) fn overflow(&self) -> u64 {
self.overflow
}
pub(super) fn visual_bells(&self) -> u64 {
self.visual_bells
}
fn record(&mut self, shape: String) {
if self.seen.iter().any(|s| **s == *shape) {
return;
}
if self.seen.len() >= MAX_UNSUPPORTED {
self.overflow = self.overflow.saturating_add(1);
return;
}
self.seen.push(Arc::from(shape));
self.shared = self.seen.iter().cloned().collect();
}
}
fn params_text(params: &[&[u16]]) -> String {
params
.iter()
.map(|p| p.iter().map(u16::to_string).collect::<Vec<_>>().join(":"))
.collect::<Vec<_>>()
.join(";")
}
const TRACKED_PRIVATE_MODES: &[u16] = &[9, 1000, 1002, 1003, 1005, 1006, 1015, 1004, 2026, 9001];
pub(super) const SHADOW_SGR_PARAMS: &[u16] = &[5, 6, 8, 9, 25, 28, 29];
impl Callbacks for Unhandled {
fn visual_bell(&mut self, _: &mut vt100::Screen) {
self.visual_bells = self.visual_bells.saturating_add(1);
}
fn resize(&mut self, _: &mut vt100::Screen, (rows, cols): (u16, u16)) {
self.record(format!("^[[8;{rows};{cols}t"));
}
fn unhandled_control(&mut self, _: &mut vt100::Screen, b: u8) {
if matches!(b, 0x0e | 0x0f) {
return;
}
self.record(match b {
0x7f => "^?".to_owned(),
0..=0x1f => format!("^{}", (b + 0x40) as char),
other => format!("\\x{other:02x}"),
});
}
fn unhandled_escape(&mut self, _: &mut vt100::Screen, i1: Option<u8>, i2: Option<u8>, b: u8) {
let designation = matches!(i1, Some(b'(' | b')' | b'*' | b'+')) && i2.is_none();
if designation || (i1.is_none() && matches!(b, b'H' | b'N' | b'O' | b'\\')) {
return;
}
let mut shape = String::from("^[");
shape.extend(i1.map(char::from));
shape.extend(i2.map(char::from));
shape.push(char::from(b));
self.record(shape);
}
fn unhandled_csi(
&mut self,
_: &mut vt100::Screen,
i1: Option<u8>,
i2: Option<u8>,
params: &[&[u16]],
c: char,
) {
let (prefix, intermediate) = match (i1, i2) {
(Some(p @ (b'?' | b'>' | b'=')), other) => (Some(p), other),
(other, None) => (None, other),
(a, b) => (a, b),
};
let first = params.first().and_then(|p| p.first()).copied();
let handled = match (prefix, intermediate, c) {
(None, None, 'g' | 'I' | 'Z' | 'c' | 'n' | 't') => true,
(None, None, 'h' | 'l') => first == Some(4),
(None, Some(b'!'), 'p') | (None, Some(b' '), 'q') => true,
(_, Some(b'$'), 'p' | 'y') => true,
(Some(b'>'), None, 'c') | (Some(b'?'), None, 'u') => true,
(Some(b'?'), None, 'h' | 'l') => params
.iter()
.flat_map(|p| p.iter())
.all(|m| TRACKED_PRIVATE_MODES.contains(m)),
(None, None, 'm') => {
let mut seen = false;
params.iter().flat_map(|p| p.iter()).all(|p| {
seen = true;
SHADOW_SGR_PARAMS.contains(p)
}) && seen
}
_ => false,
};
if handled {
return;
}
let mut shape = String::from("^[[");
shape.extend(prefix.map(char::from));
shape.push_str(¶ms_text(params));
shape.extend(intermediate.map(char::from));
shape.push(c);
self.record(shape);
}
fn unhandled_osc(&mut self, _: &mut vt100::Screen, params: &[&[u8]]) {
if matches!(
params.first(),
Some(&b"8" | &b"52" | &b"10" | &b"11" | &b"4")
) {
return;
}
let mut shape = String::from("^[]");
for (i, p) in params.iter().enumerate() {
if i > 0 {
shape.push(';');
}
shape.push_str(&String::from_utf8_lossy(p));
}
self.record(shape);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fed(bytes: &[u8]) -> vt100::Parser<Unhandled> {
let mut parser = vt100::Parser::new_with_callbacks(4, 20, 0, Unhandled::default());
parser.process(bytes);
parser
}
fn shapes(bytes: &[u8]) -> Vec<String> {
fed(bytes)
.callbacks()
.shapes()
.iter()
.map(|s| s.to_string())
.collect()
}
#[test]
fn a_sequence_nobody_honours_is_recorded_once_in_its_written_form() {
assert_eq!(
shapes(b"\x1b[20h\x1b[20h\x1bD\x1b]9;hi\x07\x05\x1b[?69h"),
["^[[20h", "^[D", "^[]9;hi", "^E", "^[[?69h"]
);
}
#[test]
fn what_the_tracker_handles_is_not_reported() {
let ours = b"\x1bH\x1b[3g\x1b[2I\x1b[1Z\x1b[4h\x1b[4l\x1b[6n\x1b[c\x1b[>c\x1b[18t\
\x1b(0\x1b)0\x1bN\x1bO\x1b[!p\x1b[2 q\x1b[?2026$p\x1b[?1004h\x1b[?2026h\
\x1b[?2026l\x1b[?1000h\x1b[?u\x1b]8;;http://x\x1b\\\x1b]52;c;aGk=\x07\
\x1b]11;?\x07\x0e\x0f";
assert_eq!(shapes(ours), Vec::<String>::new());
}
#[test]
fn the_record_is_bounded_and_counts_the_rest() {
let mut stream = Vec::new();
for mode in 20..(20 + MAX_UNSUPPORTED as u16 + 5) {
stream.extend_from_slice(format!("\x1b[{mode}h").as_bytes());
}
let parser = fed(&stream);
assert_eq!(parser.callbacks().shapes().len(), MAX_UNSUPPORTED);
assert_eq!(parser.callbacks().overflow(), 5);
}
#[test]
fn a_visual_bell_and_a_resize_request_are_seen() {
let parser = fed(b"\x1bg\x1b[8;10;40t");
assert_eq!(parser.callbacks().visual_bells(), 1);
assert_eq!(
parser
.callbacks()
.shapes()
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>(),
["^[[8;10;40t"]
);
assert_eq!(
parser.screen().size(),
(4, 20),
"the request is recorded, not honoured"
);
}
}