use serde_json::Value;
use crate::wire::{Event, Reply};
pub const MAX_MESSAGE: usize = 32 << 20;
const PASTE_START: &[u8] = b"\x1b[200~";
const PASTE_END: &[u8] = b"\x1b[201~";
const APC_PREFIX: &[u8] = b"tsp;";
const OSC_PREFIX: &[u8] = b"877;tsp;";
const CLIPBOARD_PREFIX: &[u8] = b"5522;";
#[derive(Clone, Debug, PartialEq)]
pub enum Item {
Keys(Vec<u8>),
Reply(Reply),
Event(Event),
Da1,
Osc5522(Vec<u8>),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum Kind {
#[default]
Tsp,
Clipboard,
}
enum Scan {
Undecided,
Keys(usize),
Da1(usize),
Paste(usize),
Message(usize, Kind),
}
#[derive(Clone, Debug, Default)]
pub struct Parser {
buf: Vec<u8>,
paste: bool,
tsp: Option<usize>,
kind: Kind,
scanned: usize,
skipping: bool,
}
impl Parser {
pub fn new() -> Self {
Self::default()
}
pub const fn pending(&self) -> bool {
self.tsp.is_none() && !self.skipping && !self.paste && !self.buf.is_empty()
}
pub fn feed(&mut self, bytes: &[u8]) -> Vec<Item> {
let mut out = Vec::new();
self.buf.extend_from_slice(bytes);
self.run(&mut out);
out
}
pub fn flush(&mut self) -> Vec<Item> {
if self.pending() {
vec![Item::Keys(std::mem::take(&mut self.buf))]
} else {
Vec::new()
}
}
fn run(&mut self, out: &mut Vec<Item>) {
let mut keys: Vec<u8> = Vec::new();
let mut at = 0;
loop {
if self.skipping || self.tsp.is_some() {
match self.message(at, out, &mut keys) {
Some(next) => at = next,
None => break,
}
continue;
}
if self.paste {
let rest = &self.buf[at..];
if let Some(end) = find(rest, PASTE_END) {
keys.extend_from_slice(&rest[..end + PASTE_END.len()]);
at += end + PASTE_END.len();
self.paste = false;
continue;
}
let hold = partial_suffix(rest, PASTE_END);
keys.extend_from_slice(&rest[..rest.len() - hold]);
at = self.buf.len() - hold;
break;
}
let rest = &self.buf[at..];
if rest.is_empty() {
break;
}
let plain = rest.iter().position(|&b| b == 0x1b).unwrap_or(rest.len());
if plain > 0 {
keys.extend_from_slice(&rest[..plain]);
at += plain;
continue;
}
match scan(rest) {
Scan::Undecided => break,
Scan::Keys(n) => {
keys.extend_from_slice(&rest[..n]);
at += n;
},
Scan::Paste(n) => {
keys.extend_from_slice(&rest[..n]);
at += n;
self.paste = true;
},
Scan::Da1(n) => {
push_keys(out, &mut keys);
out.push(Item::Da1);
at += n;
},
Scan::Message(content, kind) => {
self.buf.drain(..at);
self.tsp = Some(content);
self.kind = kind;
self.scanned = content;
at = 0;
},
}
}
push_keys(out, &mut keys);
self.buf.drain(..at);
}
fn message(&mut self, at: usize, out: &mut Vec<Item>, keys: &mut Vec<u8>) -> Option<usize> {
debug_assert_eq!(at, 0, "a TSP message is rebased to the buffer start");
let from = self.scanned;
let Some((end, term_len)) = terminator(&self.buf[from..]).map(|(i, n)| (from + i, n)) else {
self.scanned = self.buf.len().saturating_sub(1).max(from);
if !self.skipping && self.buf.len() > MAX_MESSAGE {
tracing::warn!(bytes = self.buf.len(), "dropping an oversized TSP message");
self.skipping = true;
self.tsp = None;
}
if self.skipping {
let keep = usize::from(self.buf.last() == Some(&0x1b));
let len = self.buf.len();
self.buf.drain(..len - keep);
self.scanned = 0;
}
return None;
};
if !self.skipping
&& let Some(content) = self.tsp
{
push_keys(out, keys);
let body = &self.buf[content..end];
let item = match self.kind {
Kind::Tsp => decode(body),
Kind::Clipboard => Some(Item::Osc5522(body.to_vec())),
};
out.extend(item);
}
self.skipping = false;
self.tsp = None;
self.scanned = 0;
self.buf.drain(..end + term_len);
Some(0)
}
}
fn push_keys(out: &mut Vec<Item>, keys: &mut Vec<u8>) {
if !keys.is_empty() {
out.push(Item::Keys(std::mem::take(keys)));
}
}
fn find(hay: &[u8], needle: &[u8]) -> Option<usize> {
hay.windows(needle.len()).position(|w| w == needle)
}
fn partial_suffix(hay: &[u8], needle: &[u8]) -> usize {
(1..needle.len())
.rev()
.find(|&n| hay.ends_with(&needle[..n]))
.unwrap_or(0)
}
fn terminator(bytes: &[u8]) -> Option<(usize, usize)> {
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
0x07 => return Some((i, 1)),
0x1b if bytes.get(i + 1) == Some(&b'\\') => return Some((i, 2)),
_ => {},
}
i += 1;
}
None
}
fn scan(rest: &[u8]) -> Scan {
let Some(&second) = rest.get(1) else {
return Scan::Undecided;
};
match second {
b'_' => prefixed(rest, APC_PREFIX, Kind::Tsp),
b']' => match (
prefixed(rest, OSC_PREFIX, Kind::Tsp),
prefixed(rest, CLIPBOARD_PREFIX, Kind::Clipboard),
) {
(scan @ Scan::Message(..), _) | (_, scan @ Scan::Message(..)) => scan,
(Scan::Undecided, _) | (_, Scan::Undecided) => Scan::Undecided,
_ => Scan::Keys(2),
},
b'[' => csi(rest),
_ => Scan::Keys(1),
}
}
fn prefixed(rest: &[u8], prefix: &[u8], kind: Kind) -> Scan {
let after = &rest[2..];
let n = after.len().min(prefix.len());
if after[..n] != prefix[..n] {
return Scan::Keys(2);
}
if n < prefix.len() {
return Scan::Undecided;
}
Scan::Message(2 + prefix.len(), kind)
}
fn csi(rest: &[u8]) -> Scan {
let mut i = 2;
while let Some(&b) = rest.get(i) {
match b {
0x20..=0x3f => i += 1,
0x40..=0x7e => {
let seq = &rest[..=i];
let params = &seq[2..i];
if b == b'c'
&& params.first() == Some(&b'?')
&& params[1..].iter().all(|&p| p.is_ascii_digit() || p == b';')
{
return Scan::Da1(i + 1);
}
if seq == PASTE_START {
return Scan::Paste(i + 1);
}
return Scan::Keys(i + 1);
},
_ => return Scan::Keys(i),
}
}
Scan::Undecided
}
fn decode(content: &[u8]) -> Option<Item> {
let semi = content.iter().position(|&b| b == b';')?;
let verb = &content[..semi];
let mut pos = semi + 1;
while let Some(len) = content[pos..].iter().position(|&b| b == b';') {
if !parameter(&content[pos..pos + len]) {
break;
}
pos += len + 1;
}
let body = &content[pos..];
let Ok(Value::Object(map)) = serde_json::from_slice::<Value>(body) else {
tracing::debug!("dropping a TSP message whose body is not a JSON object");
return None;
};
match verb {
b"r" => Some(Item::Reply(Reply::decode(map))),
b"e" => Some(Item::Event(Event::decode(map))),
_ => None,
}
}
fn parameter(segment: &[u8]) -> bool {
let Some(eq) = segment.iter().position(|&b| b == b'=') else {
return false;
};
let (key, value) = (&segment[..eq], &segment[eq + 1..]);
!key.is_empty()
&& key
.iter()
.all(|&b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
&& value
.iter()
.all(|&b| (0x21..=0x7e).contains(&b) && b != b';')
}
#[cfg(test)]
mod tests {
use super::{Item, Parser};
#[test]
fn clipboard_packet_split_across_feeds_is_one_item() {
let mut parser = Parser::new();
let mut items = parser.feed(b"a\x1b]55");
items.extend(parser.feed(b"22;type=read;Zm9v\x1b\\b"));
assert_eq!(items, [
Item::Keys(b"a".to_vec()),
Item::Osc5522(b"type=read;Zm9v".to_vec()),
Item::Keys(b"b".to_vec()),
]);
}
#[test]
fn clipboard_packet_inside_paste_stays_keys() {
let mut parser = Parser::new();
let bytes = b"\x1b[200~\x1b]5522;x\x07\x1b[201~";
let items = parser.feed(bytes);
assert_eq!(items, [Item::Keys(bytes.to_vec())]);
}
}