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