Skip to main content

tern_sdk/
input.rs

1//! Layer 2a: the input parser. It splits TSP replies and events, and DA1
2//! answers, out of the pty's input and passes every other byte through as
3//! key bytes, exactly as they came.
4//!
5//! `OSC 5522` clipboard packets are split out the same way.
6//!
7//! A TSP message is `ESC _ tsp;` (or `ESC ] 877;tsp;` through a Windows
8//! `ConPTY`), then `<verb>[;k=v]*;<body>`, ended by ST or BEL. Bytes inside a
9//! bracketed paste pass through unexamined, so pasted text can never forge
10//! an event.
11
12use serde_json::Value;
13
14use crate::wire::{Event, Reply};
15
16/// A TSP message that grows past this many bytes unterminated is dropped.
17pub const MAX_MESSAGE: usize = 32 << 20;
18
19/// The start of a bracketed paste.
20const PASTE_START: &[u8] = b"\x1b[200~";
21/// The end of a bracketed paste.
22const PASTE_END: &[u8] = b"\x1b[201~";
23/// What follows `ESC _` in a TSP message.
24const APC_PREFIX: &[u8] = b"tsp;";
25/// What follows `ESC ]` in a TSP message through `ConPTY`.
26const OSC_PREFIX: &[u8] = b"877;tsp;";
27/// What follows `ESC ]` in a clipboard packet.
28const CLIPBOARD_PREFIX: &[u8] = b"5522;";
29
30/// One thing the parser found in the input.
31#[derive(Clone, Debug, PartialEq)]
32pub enum Item {
33	/// Bytes that aren't TSP: keys, pastes, other terminal reports.
34	Keys(Vec<u8>),
35	/// A reply to a query.
36	Reply(Reply),
37	/// An event.
38	Event(Event),
39	/// A DA1 answer (`ESC [ ? … c`).
40	Da1,
41	/// An `OSC 5522` clipboard packet: the bytes after `5522;`, before the
42	/// terminator.
43	Osc5522(Vec<u8>),
44}
45
46/// Which message a held sequence is.
47#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
48enum Kind {
49	/// A TSP message.
50	#[default]
51	Tsp,
52	/// An `OSC 5522` clipboard packet.
53	Clipboard,
54}
55
56/// What a prefix of the buffer is.
57enum Scan {
58	/// Not decidable yet: hold the bytes.
59	Undecided,
60	/// Pass this many bytes through as keys.
61	Keys(usize),
62	/// A DA1 answer of this many bytes.
63	Da1(usize),
64	/// A bracketed paste starts; pass this many bytes through.
65	Paste(usize),
66	/// A message whose content starts at this offset.
67	Message(usize, Kind),
68}
69
70/// The streaming input parser.
71///
72/// # Example
73/// ```
74/// use tern_sdk::input::{Item, Parser};
75///
76/// let mut parser = Parser::new();
77/// let items = parser.feed(b"x\x1b_tsp;e;{\"ev\":\"ack\",\"sf\":\"s1\",\"s\":3}\x1b\\y");
78/// assert_eq!(items.len(), 3);
79/// assert!(matches!(&items[0], Item::Keys(k) if k == b"x"));
80/// ```
81#[derive(Clone, Debug, Default)]
82pub struct Parser {
83	/// Bytes not yet decided or a TSP message not yet terminated.
84	buf:      Vec<u8>,
85	/// Inside a bracketed paste.
86	paste:    bool,
87	/// Content offset of the TSP message at the start of `buf`.
88	tsp:      Option<usize>,
89	/// What the held message is.
90	kind:     Kind,
91	/// How far the TSP message has been searched for its terminator.
92	scanned:  usize,
93	/// Dropping an oversized TSP message until its terminator.
94	skipping: bool,
95}
96
97impl Parser {
98	/// A parser with nothing held.
99	pub fn new() -> Self {
100		Self::default()
101	}
102
103	/// Whether bytes are held that a [`flush`](Self::flush) would release
104	/// (an undecided prefix, not a recognized TSP message or active paste).
105	pub const fn pending(&self) -> bool {
106		self.tsp.is_none() && !self.skipping && !self.paste && !self.buf.is_empty()
107	}
108
109	/// Feeds bytes as they arrive and returns what they complete, in order.
110	pub fn feed(&mut self, bytes: &[u8]) -> Vec<Item> {
111		let mut out = Vec::new();
112		self.buf.extend_from_slice(bytes);
113		self.run(&mut out);
114		out
115	}
116
117	/// Releases an undecided prefix (a lone `ESC`, a partial CSI) as keys.
118	/// A recognized TSP message or a paste's partial end marker stays held.
119	pub fn flush(&mut self) -> Vec<Item> {
120		if self.pending() {
121			vec![Item::Keys(std::mem::take(&mut self.buf))]
122		} else {
123			Vec::new()
124		}
125	}
126
127	/// Consumes `buf` as far as it can be decided.
128	fn run(&mut self, out: &mut Vec<Item>) {
129		let mut keys: Vec<u8> = Vec::new();
130		let mut at = 0;
131		loop {
132			if self.skipping || self.tsp.is_some() {
133				match self.message(at, out, &mut keys) {
134					Some(next) => at = next,
135					None => break,
136				}
137				continue;
138			}
139			if self.paste {
140				let rest = &self.buf[at..];
141				if let Some(end) = find(rest, PASTE_END) {
142					keys.extend_from_slice(&rest[..end + PASTE_END.len()]);
143					at += end + PASTE_END.len();
144					self.paste = false;
145					continue;
146				}
147				let hold = partial_suffix(rest, PASTE_END);
148				keys.extend_from_slice(&rest[..rest.len() - hold]);
149				at = self.buf.len() - hold;
150				break;
151			}
152			let rest = &self.buf[at..];
153			if rest.is_empty() {
154				break;
155			}
156			let plain = rest.iter().position(|&b| b == 0x1b).unwrap_or(rest.len());
157			if plain > 0 {
158				keys.extend_from_slice(&rest[..plain]);
159				at += plain;
160				continue;
161			}
162			match scan(rest) {
163				Scan::Undecided => break,
164				Scan::Keys(n) => {
165					keys.extend_from_slice(&rest[..n]);
166					at += n;
167				},
168				Scan::Paste(n) => {
169					keys.extend_from_slice(&rest[..n]);
170					at += n;
171					self.paste = true;
172				},
173				Scan::Da1(n) => {
174					push_keys(out, &mut keys);
175					out.push(Item::Da1);
176					at += n;
177				},
178				Scan::Message(content, kind) => {
179					// Rebase the message at the buffer's start.
180					self.buf.drain(..at);
181					self.tsp = Some(content);
182					self.kind = kind;
183					self.scanned = content;
184					at = 0;
185				},
186			}
187		}
188		push_keys(out, &mut keys);
189		self.buf.drain(..at);
190	}
191
192	/// Handles the TSP message at the start of `buf[at..]` (`at` is 0 when a
193	/// message is held). Returns where to continue, or `None` when it waits
194	/// for more bytes.
195	fn message(&mut self, at: usize, out: &mut Vec<Item>, keys: &mut Vec<u8>) -> Option<usize> {
196		debug_assert_eq!(at, 0, "a TSP message is rebased to the buffer start");
197		let from = self.scanned;
198		let Some((end, term_len)) = terminator(&self.buf[from..]).map(|(i, n)| (from + i, n)) else {
199			// A trailing ESC may start the ST: search it again with what follows.
200			self.scanned = self.buf.len().saturating_sub(1).max(from);
201			if !self.skipping && self.buf.len() > MAX_MESSAGE {
202				tracing::warn!(bytes = self.buf.len(), "dropping an oversized TSP message");
203				self.skipping = true;
204				self.tsp = None;
205			}
206			if self.skipping {
207				let keep = usize::from(self.buf.last() == Some(&0x1b));
208				let len = self.buf.len();
209				self.buf.drain(..len - keep);
210				self.scanned = 0;
211			}
212			return None;
213		};
214		if !self.skipping
215			&& let Some(content) = self.tsp
216		{
217			push_keys(out, keys);
218			let body = &self.buf[content..end];
219			let item = match self.kind {
220				Kind::Tsp => decode(body),
221				Kind::Clipboard => Some(Item::Osc5522(body.to_vec())),
222			};
223			out.extend(item);
224		}
225		self.skipping = false;
226		self.tsp = None;
227		self.scanned = 0;
228		self.buf.drain(..end + term_len);
229		Some(0)
230	}
231}
232
233/// Moves accumulated key bytes into an item.
234fn push_keys(out: &mut Vec<Item>, keys: &mut Vec<u8>) {
235	if !keys.is_empty() {
236		out.push(Item::Keys(std::mem::take(keys)));
237	}
238}
239
240/// The first index of `needle` in `hay`.
241fn find(hay: &[u8], needle: &[u8]) -> Option<usize> {
242	hay.windows(needle.len()).position(|w| w == needle)
243}
244
245/// The length of the longest proper prefix of `needle` that `hay` ends with.
246fn partial_suffix(hay: &[u8], needle: &[u8]) -> usize {
247	(1..needle.len())
248		.rev()
249		.find(|&n| hay.ends_with(&needle[..n]))
250		.unwrap_or(0)
251}
252
253/// The terminator (ST or BEL) in a message's bytes: its index and length.
254fn terminator(bytes: &[u8]) -> Option<(usize, usize)> {
255	let mut i = 0;
256	while i < bytes.len() {
257		match bytes[i] {
258			0x07 => return Some((i, 1)),
259			0x1b if bytes.get(i + 1) == Some(&b'\\') => return Some((i, 2)),
260			_ => {},
261		}
262		i += 1;
263	}
264	None
265}
266
267/// Classifies the escape sequence at the start of `rest` (`rest[0]` is
268/// `ESC`).
269fn scan(rest: &[u8]) -> Scan {
270	let Some(&second) = rest.get(1) else {
271		return Scan::Undecided;
272	};
273	match second {
274		b'_' => prefixed(rest, APC_PREFIX, Kind::Tsp),
275		b']' => match (
276			prefixed(rest, OSC_PREFIX, Kind::Tsp),
277			prefixed(rest, CLIPBOARD_PREFIX, Kind::Clipboard),
278		) {
279			(scan @ Scan::Message(..), _) | (_, scan @ Scan::Message(..)) => scan,
280			(Scan::Undecided, _) | (_, Scan::Undecided) => Scan::Undecided,
281			_ => Scan::Keys(2),
282		},
283		b'[' => csi(rest),
284		// Any other ESC passes through alone; what follows is scanned anew.
285		_ => Scan::Keys(1),
286	}
287}
288
289/// `ESC <c> <prefix>` starts a `kind` message; anything else passes through.
290fn prefixed(rest: &[u8], prefix: &[u8], kind: Kind) -> Scan {
291	let after = &rest[2..];
292	let n = after.len().min(prefix.len());
293	if after[..n] != prefix[..n] {
294		return Scan::Keys(2);
295	}
296	if n < prefix.len() {
297		return Scan::Undecided;
298	}
299	Scan::Message(2 + prefix.len(), kind)
300}
301
302/// A CSI sequence: a DA1 answer, a paste start, or keys.
303fn csi(rest: &[u8]) -> Scan {
304	let mut i = 2;
305	while let Some(&b) = rest.get(i) {
306		match b {
307			0x20..=0x3f => i += 1,
308			0x40..=0x7e => {
309				let seq = &rest[..=i];
310				let params = &seq[2..i];
311				if b == b'c'
312					&& params.first() == Some(&b'?')
313					&& params[1..].iter().all(|&p| p.is_ascii_digit() || p == b';')
314				{
315					return Scan::Da1(i + 1);
316				}
317				if seq == PASTE_START {
318					return Scan::Paste(i + 1);
319				}
320				return Scan::Keys(i + 1);
321			},
322			// Not a CSI byte: the sequence ends here as keys.
323			_ => return Scan::Keys(i),
324		}
325	}
326	Scan::Undecided
327}
328
329/// Decodes a message's content (`<verb>[;k=v]*;<body>`): `r` and `e`
330/// bodies that are JSON objects; everything else is dropped.
331fn decode(content: &[u8]) -> Option<Item> {
332	let semi = content.iter().position(|&b| b == b';')?;
333	let verb = &content[..semi];
334	let mut pos = semi + 1;
335	while let Some(len) = content[pos..].iter().position(|&b| b == b';') {
336		if !parameter(&content[pos..pos + len]) {
337			break;
338		}
339		pos += len + 1;
340	}
341	let body = &content[pos..];
342	let Ok(Value::Object(map)) = serde_json::from_slice::<Value>(body) else {
343		tracing::debug!("dropping a TSP message whose body is not a JSON object");
344		return None;
345	};
346	match verb {
347		b"r" => Some(Item::Reply(Reply::decode(map))),
348		b"e" => Some(Item::Event(Event::decode(map))),
349		_ => None,
350	}
351}
352
353/// Whether a segment is `key=value` (`[A-Za-z0-9_-]+=[\x21-\x7e but ;]*`).
354fn parameter(segment: &[u8]) -> bool {
355	let Some(eq) = segment.iter().position(|&b| b == b'=') else {
356		return false;
357	};
358	let (key, value) = (&segment[..eq], &segment[eq + 1..]);
359	!key.is_empty()
360		&& key
361			.iter()
362			.all(|&b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
363		&& value
364			.iter()
365			.all(|&b| (0x21..=0x7e).contains(&b) && b != b';')
366}
367
368#[cfg(test)]
369mod tests {
370	use super::{Item, Parser};
371
372	#[test]
373	fn clipboard_packet_split_across_feeds_is_one_item() {
374		let mut parser = Parser::new();
375		let mut items = parser.feed(b"a\x1b]55");
376		items.extend(parser.feed(b"22;type=read;Zm9v\x1b\\b"));
377		assert_eq!(items, [
378			Item::Keys(b"a".to_vec()),
379			Item::Osc5522(b"type=read;Zm9v".to_vec()),
380			Item::Keys(b"b".to_vec()),
381		]);
382	}
383
384	#[test]
385	fn clipboard_packet_inside_paste_stays_keys() {
386		let mut parser = Parser::new();
387		let bytes = b"\x1b[200~\x1b]5522;x\x07\x1b[201~";
388		let items = parser.feed(bytes);
389		assert_eq!(items, [Item::Keys(bytes.to_vec())]);
390	}
391}