Skip to main content

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}