ansi/parse.rs
1//! Incremental VT/ANSI tokenizer.
2//!
3//! Two APIs over one state machine, so they can never disagree:
4//!
5//! * [`Parser::feed_with`] is the engine. It calls a closure with an
6//! [`Event`] that **borrows** — text runs point into the caller's buffer,
7//! sequence payloads into the parser's own reused scratch — so tokenizing a
8//! stream allocates nothing after the first few sequences.
9//! * [`Parser::feed`] is the owned convenience API: the same events collected
10//! into a `Vec<Token>`, with text decoded as UTF-8 (invalid bytes become
11//! U+FFFD). It costs a `String`/`Vec` per token, which is why the engine
12//! exists.
13//!
14//! Both accept a byte stream in chunks split at *any* boundary (mid-escape
15//! sequence, mid-UTF-8 character). The state machine follows the classic
16//! VT500-series design: C0 controls are *executed* (emitted as
17//! [`Event::Control`]) even when they arrive inside an escape sequence,
18//! `CAN`/`SUB` abort a sequence in progress, and a stray `ESC` restarts one.
19//! Malformed sequences are dropped, never emitted as text.
20//!
21//! # Choosing between them
22//!
23//! ```
24//! # use ansi::{Event, Parser};
25//! let mut p = Parser::new();
26//! let mut printable = 0usize;
27//! p.feed_with(b"hi\x1b[1;31mred", |event| {
28//! if let Event::Text(bytes) = event {
29//! printable += bytes.len();
30//! }
31//! });
32//! assert_eq!(printable, 5); // "hi" + "red", no allocation
33//! ```
34
35/// One parsed unit of terminal output, owned.
36///
37/// [`Event`] is the same information borrowed; this is what [`Parser::feed`]
38/// builds from it.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum Token {
41 /// A run of printable text (valid UTF-8; invalid input bytes become
42 /// U+FFFD). Runs are flushed at each control byte and at the end of each
43 /// `feed` call, so adjacent `Text` tokens are equivalent to one.
44 Text(String),
45 /// A C0 control byte executed on its own (BEL, BS, TAB, LF, CR, ...) or
46 /// DEL. `ESC` never appears here; it introduces a sequence.
47 Control(u8),
48 /// `ESC [ ...`: a CSI sequence with an optional private marker (`?`, `>`, `<`,
49 /// `=`), numeric parameters, intermediate bytes, and the final byte.
50 /// Missing parameters parse as 0; colon sub-parameters are truncated.
51 Csi {
52 private: Option<char>,
53 params: Vec<u16>,
54 intermediates: Vec<u8>,
55 final_byte: char,
56 },
57 /// A non-CSI escape: `ESC` + optional intermediates + final byte
58 /// (e.g. `ESC 7`, `ESC ( B`).
59 Esc {
60 intermediates: Vec<u8>,
61 final_byte: u8,
62 },
63 /// `ESC ] ...`: an OSC string, terminated by BEL or ST (`ESC \`).
64 /// Payload bytes are decoded as UTF-8 (lossily).
65 Osc(String),
66 /// A DCS/SOS/PM/APC string (`intro` is `P`, `X`, `^`, or `_`), swallowed
67 /// whole and surfaced raw rather than interpreted.
68 Other { intro: u8, data: Vec<u8> },
69}
70
71/// One parsed unit, borrowed: nothing here is owned or copied.
72///
73/// The lifetime is per call, not per `feed_with`: [`Event::Text`] borrows the
74/// bytes passed in, while sequence payloads borrow the parser's scratch
75/// buffers, which the next sequence reuses. Keep what you need before
76/// returning.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum Event<'a> {
79 /// A run of printable bytes, exactly as they arrived: **not** validated as
80 /// UTF-8, and a multi-byte character split across two `feed_with` calls
81 /// arrives as the tail of one run and the head of the next. Callers that
82 /// need `str` decode it themselves; [`Parser::feed`] does that and
83 /// replaces invalid bytes with U+FFFD.
84 Text(&'a [u8]),
85 /// A C0 control byte executed on its own, or DEL.
86 Control(u8),
87 /// A CSI sequence. `params` and `intermediates` borrow parser scratch.
88 Csi {
89 private: Option<u8>,
90 params: &'a [u16],
91 intermediates: &'a [u8],
92 final_byte: u8,
93 },
94 /// A non-CSI escape sequence.
95 Esc {
96 intermediates: &'a [u8],
97 final_byte: u8,
98 },
99 /// An OSC string's payload, without the introducer or terminator.
100 Osc(&'a [u8]),
101 /// A DCS/SOS/PM/APC string's payload.
102 Other { intro: u8, data: &'a [u8] },
103}
104
105#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
106enum State {
107 #[default]
108 Ground,
109 Escape,
110 EscapeIntermediate,
111 Csi,
112 Osc,
113 Str, // DCS / SOS / PM / APC
114}
115
116/// Cap accumulated sequence payloads so a hostile stream cannot grow memory
117/// without bound; sequences exceeding this are dropped.
118const MAX_SEQ: usize = 4096;
119/// Parameters kept per CSI sequence; later ones are ignored, as terminals do.
120const MAX_PARAMS: usize = 32;
121/// Intermediate bytes kept per sequence.
122const MAX_INTERMEDIATES: usize = 8;
123
124/// The incremental parser. Create one per stream and feed it each chunk as it
125/// arrives, with [`feed_with`](Parser::feed_with) or [`feed`](Parser::feed).
126#[derive(Debug, Default)]
127pub struct Parser {
128 state: State,
129 // CSI scratch, all reused between sequences.
130 private: Option<u8>,
131 params: [u16; MAX_PARAMS],
132 params_len: usize,
133 param_value: u32,
134 param_open: bool,
135 param_sub: bool,
136 param_bytes: usize,
137 intermediates: [u8; MAX_INTERMEDIATES],
138 intermediates_len: usize,
139 // String (OSC/DCS/SOS/PM/APC) scratch.
140 str_intro: u8,
141 str_data: Vec<u8>,
142 str_esc: bool,
143 // Used only by `feed`: a UTF-8 character split across calls.
144 decoder: Utf8Decoder,
145}
146
147impl Parser {
148 /// A parser in the ground state.
149 pub fn new() -> Self {
150 Self::default()
151 }
152
153 /// True when no escape sequence or string is in progress: the next byte
154 /// starts fresh. A consumer that snapshots its own state (a terminal
155 /// checkpoint) can do so here without saving the parser's internals,
156 /// because a fresh `Parser` is then indistinguishable from this one.
157 ///
158 /// UTF-8 is not the parser's concern in [`feed_with`](Parser::feed_with)
159 /// (text arrives as raw bytes); a caller decoding text checks its
160 /// [`Utf8Decoder`] as well.
161 pub fn is_ground(&self) -> bool {
162 self.state == State::Ground
163 }
164
165 /// Tokenize `input`, calling `f` once per [`Event`], allocating nothing.
166 ///
167 /// Partial sequences are held for the next call. A text run in progress is
168 /// always delivered before returning, so no input byte is buffered except
169 /// inside an unfinished escape sequence.
170 pub fn feed_with<F: FnMut(Event<'_>)>(&mut self, input: &[u8], mut f: F) {
171 let mut run: Option<usize> = None;
172 let mut i = 0;
173 while i < input.len() {
174 let b = input[i];
175 match self.state {
176 State::Ground => match b {
177 0x1B => {
178 flush_run(&mut run, input, i, &mut f);
179 self.begin(State::Escape);
180 }
181 0x00..=0x1F | 0x7F => {
182 flush_run(&mut run, input, i, &mut f);
183 f(Event::Control(b));
184 }
185 _ => {
186 if run.is_none() {
187 run = Some(i);
188 }
189 // Text runs to the next control byte: skip to it in
190 // one go instead of a state-machine step per byte.
191 i = skip_text(input, i + 1);
192 continue;
193 }
194 },
195 State::Escape => match b {
196 b'[' => self.begin(State::Csi),
197 b']' => self.begin(State::Osc),
198 b'P' | b'X' | b'^' | b'_' => {
199 self.begin(State::Str);
200 self.str_intro = b;
201 }
202 0x20..=0x2F => {
203 self.push_intermediate(b);
204 self.state = State::EscapeIntermediate;
205 }
206 0x30..=0x7E => {
207 f(Event::Esc {
208 intermediates: &[],
209 final_byte: b,
210 });
211 self.state = State::Ground;
212 }
213 0x1B => self.begin(State::Escape),
214 0x18 | 0x1A => self.state = State::Ground, // CAN / SUB abort
215 _ => f(Event::Control(b)), // execute C0 within sequence
216 },
217 State::EscapeIntermediate => match b {
218 0x20..=0x2F => self.push_intermediate(b),
219 0x30..=0x7E => {
220 f(Event::Esc {
221 intermediates: &self.intermediates[..self.intermediates_len],
222 final_byte: b,
223 });
224 self.state = State::Ground;
225 }
226 0x1B => self.begin(State::Escape),
227 0x18 | 0x1A => self.state = State::Ground,
228 _ => f(Event::Control(b)),
229 },
230 State::Csi => match b {
231 b'0'..=b'9' | b';' | b':' => self.param_byte(b),
232 0x3C..=0x3F => {
233 if self.private.is_none() && self.param_bytes == 0 {
234 self.private = Some(b);
235 } // later private markers are ignored, as most terminals do
236 }
237 0x20..=0x2F => self.push_intermediate(b),
238 0x40..=0x7E => {
239 self.close_param();
240 f(Event::Csi {
241 private: self.private,
242 params: &self.params[..self.params_len],
243 intermediates: &self.intermediates[..self.intermediates_len],
244 final_byte: b,
245 });
246 self.private = None;
247 self.state = State::Ground;
248 }
249 0x1B => self.begin(State::Escape),
250 0x18 | 0x1A => self.state = State::Ground,
251 _ => f(Event::Control(b)),
252 },
253 State::Osc | State::Str => {
254 let is_osc = self.state == State::Osc;
255 if self.str_esc {
256 self.str_esc = false;
257 if b == b'\\' {
258 self.dispatch_string(is_osc, &mut f);
259 i += 1;
260 continue;
261 }
262 self.push_str_byte(0x1B);
263 // fall through: `b` is ordinary payload
264 }
265 match b {
266 0x07 => self.dispatch_string(is_osc, &mut f), // BEL terminator
267 0x1B => self.str_esc = true,
268 _ => self.push_str_byte(b),
269 }
270 }
271 }
272 i += 1;
273 }
274 flush_run(&mut run, input, input.len(), &mut f);
275 }
276
277 /// Feed a chunk of bytes; returns the tokens completed by this chunk.
278 ///
279 /// Any accumulated text run is flushed at the end of the call; partial
280 /// escape sequences and partial UTF-8 characters are held for the next.
281 /// This is [`feed_with`](Parser::feed_with) with the events copied into
282 /// owned [`Token`]s; prefer the engine where the allocation matters.
283 pub fn feed(&mut self, input: &[u8]) -> Vec<Token> {
284 let mut out = Vec::new();
285 let mut text = String::new();
286 let mut decoder = std::mem::take(&mut self.decoder);
287
288 self.feed_with(input, |event| match event {
289 Event::Text(bytes) => decoder.decode(bytes, |s| text.push_str(s)),
290 other => {
291 decoder.flush_incomplete(|s| text.push_str(s));
292 if !text.is_empty() {
293 out.push(Token::Text(std::mem::take(&mut text)));
294 }
295 out.push(own(other));
296 }
297 });
298
299 if !text.is_empty() {
300 out.push(Token::Text(text));
301 }
302 self.decoder = decoder;
303 out
304 }
305
306 fn begin(&mut self, state: State) {
307 self.state = state;
308 self.intermediates_len = 0;
309 self.params_len = 0;
310 self.param_value = 0;
311 self.param_open = false;
312 self.param_sub = false;
313 self.param_bytes = 0;
314 self.private = None;
315 self.str_data.clear();
316 self.str_esc = false;
317 }
318
319 fn push_intermediate(&mut self, b: u8) {
320 if self.intermediates_len < MAX_INTERMEDIATES {
321 self.intermediates[self.intermediates_len] = b;
322 self.intermediates_len += 1;
323 }
324 }
325
326 fn push_str_byte(&mut self, b: u8) {
327 if self.str_data.len() < MAX_SEQ {
328 self.str_data.push(b);
329 }
330 }
331
332 /// One byte of a CSI parameter list: a digit, `;` or `:`.
333 fn param_byte(&mut self, b: u8) {
334 if self.param_bytes >= MAX_SEQ {
335 self.state = State::Ground; // oversized: drop the sequence
336 return;
337 }
338 self.param_bytes += 1;
339 self.param_open = true;
340 match b {
341 b';' => {
342 self.store_param();
343 self.param_value = 0;
344 self.param_sub = false;
345 }
346 b':' => self.param_sub = true, // sub-parameters are truncated
347 _ => {
348 if !self.param_sub {
349 let digit = u32::from(b - b'0');
350 self.param_value = (self.param_value * 10 + digit).min(u16::MAX as u32);
351 }
352 }
353 }
354 }
355
356 fn store_param(&mut self) {
357 if self.params_len < MAX_PARAMS {
358 self.params[self.params_len] = self.param_value as u16;
359 self.params_len += 1;
360 }
361 }
362
363 /// Close the parameter list at the final byte.
364 fn close_param(&mut self) {
365 if self.param_open {
366 self.store_param();
367 self.param_open = false;
368 }
369 }
370
371 fn dispatch_string<F: FnMut(Event<'_>)>(&mut self, is_osc: bool, f: &mut F) {
372 if is_osc {
373 f(Event::Osc(&self.str_data));
374 } else {
375 f(Event::Other {
376 intro: self.str_intro,
377 data: &self.str_data,
378 });
379 }
380 self.str_data.clear();
381 self.state = State::Ground;
382 }
383}
384
385/// Deliver the text run that ends at `end`, if any.
386fn flush_run<F: FnMut(Event<'_>)>(run: &mut Option<usize>, input: &[u8], end: usize, f: &mut F) {
387 if let Some(start) = run.take() {
388 if start < end {
389 f(Event::Text(&input[start..end]));
390 }
391 }
392}
393
394/// Copy a borrowed event into an owned token. Text is handled by the caller,
395/// which decodes UTF-8 across chunk boundaries.
396fn own(event: Event<'_>) -> Token {
397 match event {
398 Event::Text(bytes) => Token::Text(String::from_utf8_lossy(bytes).into_owned()),
399 Event::Control(b) => Token::Control(b),
400 Event::Csi {
401 private,
402 params,
403 intermediates,
404 final_byte,
405 } => Token::Csi {
406 private: private.map(char::from),
407 params: params.to_vec(),
408 intermediates: intermediates.to_vec(),
409 final_byte: char::from(final_byte),
410 },
411 Event::Esc {
412 intermediates,
413 final_byte,
414 } => Token::Esc {
415 intermediates: intermediates.to_vec(),
416 final_byte,
417 },
418 Event::Osc(data) => Token::Osc(String::from_utf8_lossy(data).into_owned()),
419 Event::Other { intro, data } => Token::Other {
420 intro,
421 data: data.to_vec(),
422 },
423 }
424}
425
426/// Decodes [`Event::Text`] bytes into `str` pieces, across chunk boundaries.
427///
428/// [`Event::Text`] is raw bytes, because the parser never copies them. A
429/// consumer that wants characters needs two things this handles: a multi-byte
430/// character split across two `feed_with` calls, and invalid bytes, which
431/// become U+FFFD exactly as [`Parser::feed`] renders them.
432///
433/// Pieces are borrowed: an ASCII run is handed over as one slice of the input
434/// with nothing copied, and only a character straddling a boundary is
435/// assembled in the decoder's own four bytes. It allocates nothing, ever.
436///
437/// ```
438/// # use ansi::{Event, Parser, Utf8Decoder};
439/// let mut parser = Parser::new();
440/// let mut decoder = Utf8Decoder::new();
441/// let mut seen = String::new();
442/// // The 'é' is split across the two chunks.
443/// for chunk in [&b"caf\xC3"[..], &b"\xA9!"[..]] {
444/// parser.feed_with(chunk, |event| match event {
445/// Event::Text(bytes) => decoder.decode(bytes, |s| seen.push_str(s)),
446/// _ => decoder.flush_incomplete(|s| seen.push_str(s)),
447/// });
448/// }
449/// assert_eq!(seen, "café!");
450/// ```
451#[derive(Debug, Default, Clone)]
452pub struct Utf8Decoder {
453 partial: [u8; 4],
454 len: usize,
455}
456
457impl Utf8Decoder {
458 /// A decoder with nothing held over.
459 pub fn new() -> Self {
460 Self::default()
461 }
462
463 /// True when no partial character is held over from a previous chunk.
464 /// Then a run of ASCII bytes decodes to itself, and a caller may use it
465 /// directly without going through [`Utf8Decoder::decode`].
466 pub fn is_idle(&self) -> bool {
467 self.len == 0
468 }
469
470 /// Decode `bytes`, calling `f` with each piece of text in order.
471 ///
472 /// A character left incomplete at the end is held for the next call.
473 pub fn decode<F: FnMut(&str)>(&mut self, bytes: &[u8], mut f: F) {
474 let mut i = 0;
475 while i < bytes.len() {
476 let b = bytes[i];
477 if self.len > 0 {
478 if (0x80..0xC0).contains(&b) {
479 self.partial[self.len] = b;
480 self.len += 1;
481 i += 1;
482 if self.len == utf8_len(self.partial[0]) {
483 match std::str::from_utf8(&self.partial[..self.len]) {
484 Ok(s) => f(s),
485 Err(_) => f(REPLACEMENT), // overlong, surrogate, out of range
486 }
487 self.len = 0;
488 }
489 continue;
490 }
491 // Truncated sequence: the held bytes were invalid.
492 self.len = 0;
493 f(REPLACEMENT);
494 // fall through and process `b` normally
495 }
496 // The longest valid prefix goes over borrowed, in one call: std's
497 // validator runs a vectorized ASCII fast path, where scanning byte
498 // by byte and then validating again ran at ~1.4 GB/s. Only what
499 // follows the prefix (a split or invalid sequence) takes the
500 // careful byte-by-byte path below, exactly as before.
501 let valid = match std::str::from_utf8(&bytes[i..]) {
502 Ok(s) => s,
503 Err(e) => std::str::from_utf8(&bytes[i..i + e.valid_up_to()])
504 .expect("valid_up_to is a valid prefix"),
505 };
506 if !valid.is_empty() {
507 f(valid);
508 i += valid.len();
509 continue;
510 }
511 i += 1;
512 if b < 0xC0 || utf8_len(b) == 1 {
513 f(REPLACEMENT); // stray continuation, or an invalid lead (0xF8+)
514 } else {
515 self.partial[0] = b;
516 self.len = 1;
517 }
518 }
519 }
520
521 /// Give up on a character left incomplete, emitting U+FFFD for it.
522 ///
523 /// Call this when something other than text arrives — a control byte or an
524 /// escape sequence ends a text run, and the held bytes can never be
525 /// completed. Does nothing when no character is pending.
526 pub fn flush_incomplete<F: FnMut(&str)>(&mut self, mut f: F) {
527 if self.len > 0 {
528 self.len = 0;
529 f(REPLACEMENT);
530 }
531 }
532
533 /// True when a partial character is held for the next [`decode`](Self::decode).
534 pub fn is_pending(&self) -> bool {
535 self.len > 0
536 }
537}
538
539const REPLACEMENT: &str = "\u{FFFD}";
540
541/// Expected total length of a UTF-8 sequence from its lead byte (1 for
542/// invalid leads, so they consume exactly themselves).
543/// The index of the first byte at or after `from` that ends a text run in
544/// the ground state — a C0 control (below 0x20, ESC included) or DEL — or
545/// `input.len()` if there is none.
546///
547/// Eight bytes at a time: with `x` a little-endian word, `(x - 0x20..) & !x &
548/// 0x80..` flags bytes below 0x20 and the same test on `x ^ 0x7F..` flags
549/// DEL. Either test can also flag bytes above a true match (a borrow carries
550/// upward), never below one, so the lowest flagged byte is always real.
551#[inline]
552fn skip_text(input: &[u8], from: usize) -> usize {
553 const ONES: u64 = 0x0101_0101_0101_0101;
554 const HIGHS: u64 = 0x8080_8080_8080_8080;
555 let mut j = from;
556 while j + 8 <= input.len() {
557 let x = u64::from_le_bytes(input[j..j + 8].try_into().expect("eight bytes"));
558 let below = x.wrapping_sub(ONES * 0x20) & !x & HIGHS;
559 let d = x ^ (ONES * 0x7F);
560 let del = d.wrapping_sub(ONES) & !d & HIGHS;
561 let hit = below | del;
562 if hit != 0 {
563 return j + (hit.trailing_zeros() / 8) as usize;
564 }
565 j += 8;
566 }
567 while j < input.len() {
568 let b = input[j];
569 if b < 0x20 || b == 0x7F {
570 return j;
571 }
572 j += 1;
573 }
574 input.len()
575}
576
577fn utf8_len(lead: u8) -> usize {
578 match lead {
579 0xC0..=0xDF => 2,
580 0xE0..=0xEF => 3,
581 0xF0..=0xF7 => 4,
582 _ => 1,
583 }
584}
585
586#[cfg(test)]
587mod tests {
588 use super::*;
589
590 /// The word-at-a-time skip finds exactly what a byte loop would, for
591 /// every byte value at every position of a word and across its end.
592 #[test]
593 fn skip_text_matches_the_byte_rule_everywhere() {
594 let naive = |input: &[u8], from: usize| {
595 (from..input.len())
596 .find(|&j| input[j] < 0x20 || input[j] == 0x7F)
597 .unwrap_or(input.len())
598 };
599 for special in 0u8..=255 {
600 for len in 0..20 {
601 for at in 0..len {
602 let mut v = vec![b'a'; len];
603 v[at] = special;
604 // Noise that must not trigger: high bytes, 0x7E, 0x80.
605 if at + 1 < len {
606 v[at + 1] = 0xFF;
607 }
608 for from in 0..=len {
609 assert_eq!(
610 skip_text(&v, from),
611 naive(&v, from),
612 "{special:#x} at {at} len {len} from {from}"
613 );
614 }
615 }
616 }
617 }
618 let mixed: Vec<u8> = (0..=255u8).cycle().take(4096).collect();
619 for from in 0..mixed.len() {
620 assert_eq!(skip_text(&mixed, from), naive(&mixed, from));
621 }
622 }
623
624 /// Decoding does not depend on how the bytes are chunked, for mixes of
625 /// ASCII, valid multi-byte characters and every kind of invalid byte.
626 #[test]
627 fn utf8_decoding_is_independent_of_chunking() {
628 let pieces: [&[u8]; 9] = [
629 b"plain ascii ",
630 "caf\u{e9} \u{754c} \u{1F600} ".as_bytes(),
631 b"\x80", // stray continuation
632 b"\xC3", // truncated lead, then ASCII
633 b"\xE2\x82", // truncated three-byte
634 b"\xC0\x80", // overlong
635 b"\xED\xA0\x80", // surrogate
636 b"\xF8", // invalid lead
637 b"\n",
638 ];
639 let mut seed = 0x9E37_79B9_7F4A_7C15u64;
640 let mut next = |n: u64| {
641 seed ^= seed << 13;
642 seed ^= seed >> 7;
643 seed ^= seed << 17;
644 seed % n
645 };
646 for _ in 0..300 {
647 let mut data = Vec::new();
648 for _ in 0..40 {
649 data.extend_from_slice(pieces[next(9) as usize]);
650 }
651 let decode = |chunks: &[&[u8]]| {
652 let mut d = Utf8Decoder::new();
653 let mut out = String::new();
654 for c in chunks {
655 d.decode(c, |s| out.push_str(s));
656 }
657 d.flush_incomplete(|s| out.push_str(s));
658 out
659 };
660 let whole = decode(&[&data]);
661 let bytes: Vec<&[u8]> = data.chunks(1).collect();
662 assert_eq!(decode(&bytes), whole, "byte by byte");
663 let cut = next(data.len() as u64) as usize;
664 assert_eq!(
665 decode(&[&data[..cut], &data[cut..]]),
666 whole,
667 "split at {cut}"
668 );
669 }
670 }
671
672 fn events(input: &[u8]) -> Vec<Token> {
673 let mut p = Parser::new();
674 let mut out = Vec::new();
675 p.feed_with(input, |e| out.push(own(e)));
676 out
677 }
678
679 #[test]
680 fn engine_and_feed_agree_on_every_chunk_split() {
681 let corpus: &[&[u8]] = &[
682 b"hello world\r\n",
683 b"\x1b[1;31mred\x1b[0m",
684 b"\x1b[?25l\x1b[>4;2m\x1b[38;2;10;20;30mx",
685 b"\x1b]0;title\x07after\x1b]133;A\x1b\\done",
686 b"\x1bPq#0;2\x1b\\tail",
687 b"\x1b7\x1b(B\x1b[12\x18aborted",
688 "caf\u{e9} \u{1F600} \u{4e2d}\u{6587}".as_bytes(),
689 &[0xE2, 0x82, b'x', 0xFF, 0x80, b'y'],
690 ];
691 for input in corpus {
692 let whole = {
693 let mut p = Parser::new();
694 p.feed(input)
695 };
696 for split in 1..input.len() {
697 let mut p = Parser::new();
698 let mut got = p.feed(&input[..split]);
699 got.extend(p.feed(&input[split..]));
700 assert_eq!(merge_text(&got), merge_text(&whole), "split at {split}");
701 }
702 }
703 }
704
705 /// Adjacent text tokens are equivalent to one, so compare merged runs.
706 fn merge_text(tokens: &[Token]) -> Vec<Token> {
707 let mut out: Vec<Token> = Vec::new();
708 for t in tokens {
709 match (out.last_mut(), t) {
710 (Some(Token::Text(prev)), Token::Text(next)) => prev.push_str(next),
711 _ => out.push(t.clone()),
712 }
713 }
714 out
715 }
716
717 #[test]
718 fn text_events_borrow_the_input_verbatim() {
719 let mut p = Parser::new();
720 let mut runs: Vec<Vec<u8>> = Vec::new();
721 p.feed_with(b"ab\x1b[0mcd\xFF", |e| {
722 if let Event::Text(b) = e {
723 runs.push(b.to_vec());
724 }
725 });
726 // Raw bytes, including the invalid 0xFF, and no U+FFFD substitution.
727 assert_eq!(runs, vec![b"ab".to_vec(), b"cd\xFF".to_vec()]);
728 }
729
730 #[test]
731 fn parameters_match_the_owned_api() {
732 assert_eq!(
733 events(b"\x1b[m"),
734 vec![Token::Csi {
735 private: None,
736 params: vec![],
737 intermediates: vec![],
738 final_byte: 'm'
739 }]
740 );
741 assert_eq!(
742 events(b"\x1b[;1;;2m"),
743 vec![Token::Csi {
744 private: None,
745 params: vec![0, 1, 0, 2],
746 intermediates: vec![],
747 final_byte: 'm'
748 }]
749 );
750 // Colon sub-parameters are truncated, not split.
751 assert_eq!(
752 events(b"\x1b[38:2:10:20:30;1m"),
753 vec![Token::Csi {
754 private: None,
755 params: vec![38, 1],
756 intermediates: vec![],
757 final_byte: 'm'
758 }]
759 );
760 // Values saturate at u16::MAX.
761 assert_eq!(
762 events(b"\x1b[999999m"),
763 vec![Token::Csi {
764 private: None,
765 params: vec![u16::MAX],
766 intermediates: vec![],
767 final_byte: 'm'
768 }]
769 );
770 // Only the first private marker counts, and only before parameters.
771 assert_eq!(
772 events(b"\x1b[?>25h"),
773 vec![Token::Csi {
774 private: Some('?'),
775 params: vec![25],
776 intermediates: vec![],
777 final_byte: 'h'
778 }]
779 );
780 }
781
782 #[test]
783 fn more_than_max_params_are_ignored_not_dropped() {
784 let mut input = b"\x1b[".to_vec();
785 for i in 0..40 {
786 if i > 0 {
787 input.push(b';');
788 }
789 input.extend_from_slice(b"7");
790 }
791 input.push(b'm');
792 let Token::Csi { params, .. } = &events(&input)[0] else {
793 panic!("expected a CSI");
794 };
795 assert_eq!(params.len(), MAX_PARAMS);
796 assert!(params.iter().all(|&p| p == 7));
797 }
798
799 #[test]
800 fn oversized_parameter_lists_drop_the_sequence() {
801 let mut input = b"\x1b[".to_vec();
802 input.extend(std::iter::repeat(b'1').take(MAX_SEQ + 8));
803 input.push(b'm');
804 // Dropped: nothing is emitted as a CSI, and the tail returns to ground.
805 let tokens = events(&input);
806 assert!(!tokens.iter().any(|t| matches!(t, Token::Csi { .. })));
807 }
808
809 #[test]
810 fn strings_cap_their_payload() {
811 let mut input = b"\x1b]0;".to_vec();
812 input.extend(std::iter::repeat(b'x').take(MAX_SEQ + 100));
813 input.push(0x07);
814 let Token::Osc(payload) = &events(&input)[0] else {
815 panic!("expected an OSC");
816 };
817 assert_eq!(payload.len(), MAX_SEQ);
818 }
819
820 #[test]
821 fn scratch_is_reused_across_sequences() {
822 let mut p = Parser::new();
823 let mut seen = Vec::new();
824 p.feed_with(b"\x1b[1;2m\x1b[?7h\x1b]0;t\x07", |e| seen.push(own(e)));
825 assert_eq!(seen.len(), 3);
826 // A second sequence must not inherit the first one's parameters,
827 // private marker or payload.
828 assert_eq!(
829 seen[1],
830 Token::Csi {
831 private: Some('?'),
832 params: vec![7],
833 intermediates: vec![],
834 final_byte: 'h'
835 }
836 );
837 assert_eq!(seen[2], Token::Osc("0;t".into()));
838 }
839
840 #[test]
841 fn is_ground_is_false_exactly_while_a_sequence_is_open() {
842 let mut p = Parser::new();
843 assert!(p.is_ground());
844 let steps: [(&[u8], bool); 7] = [
845 (b"text", true),
846 (b"", false),
847 (b"[1;3", false),
848 (b"1m", true),
849 (b"]0;title", false),
850 (b"", true),
851 (b"P", false),
852 ];
853 for (chunk, ground) in steps {
854 p.feed_with(chunk, |_| {});
855 assert_eq!(p.is_ground(), ground, "after {chunk:?}");
856 }
857 }
858
859 #[test]
860 fn a_partial_sequence_survives_the_chunk_boundary() {
861 let mut p = Parser::new();
862 let mut seen = Vec::new();
863 for chunk in [&b"\x1b"[..], &b"[1"[..], &b";2"[..], &b"H"[..]] {
864 p.feed_with(chunk, |e| seen.push(own(e)));
865 }
866 assert_eq!(
867 seen,
868 vec![Token::Csi {
869 private: None,
870 params: vec![1, 2],
871 intermediates: vec![],
872 final_byte: 'H'
873 }]
874 );
875 }
876}