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    /// Tokenize `input`, calling `f` once per [`Event`], allocating nothing.
154    ///
155    /// Partial sequences are held for the next call. A text run in progress is
156    /// always delivered before returning, so no input byte is buffered except
157    /// inside an unfinished escape sequence.
158    pub fn feed_with<F: FnMut(Event<'_>)>(&mut self, input: &[u8], mut f: F) {
159        let mut run: Option<usize> = None;
160        let mut i = 0;
161        while i < input.len() {
162            let b = input[i];
163            match self.state {
164                State::Ground => match b {
165                    0x1B => {
166                        flush_run(&mut run, input, i, &mut f);
167                        self.begin(State::Escape);
168                    }
169                    0x00..=0x1F | 0x7F => {
170                        flush_run(&mut run, input, i, &mut f);
171                        f(Event::Control(b));
172                    }
173                    _ => {
174                        if run.is_none() {
175                            run = Some(i);
176                        }
177                    }
178                },
179                State::Escape => match b {
180                    b'[' => self.begin(State::Csi),
181                    b']' => self.begin(State::Osc),
182                    b'P' | b'X' | b'^' | b'_' => {
183                        self.begin(State::Str);
184                        self.str_intro = b;
185                    }
186                    0x20..=0x2F => {
187                        self.push_intermediate(b);
188                        self.state = State::EscapeIntermediate;
189                    }
190                    0x30..=0x7E => {
191                        f(Event::Esc {
192                            intermediates: &[],
193                            final_byte: b,
194                        });
195                        self.state = State::Ground;
196                    }
197                    0x1B => self.begin(State::Escape),
198                    0x18 | 0x1A => self.state = State::Ground, // CAN / SUB abort
199                    _ => f(Event::Control(b)),                 // execute C0 within sequence
200                },
201                State::EscapeIntermediate => match b {
202                    0x20..=0x2F => self.push_intermediate(b),
203                    0x30..=0x7E => {
204                        f(Event::Esc {
205                            intermediates: &self.intermediates[..self.intermediates_len],
206                            final_byte: b,
207                        });
208                        self.state = State::Ground;
209                    }
210                    0x1B => self.begin(State::Escape),
211                    0x18 | 0x1A => self.state = State::Ground,
212                    _ => f(Event::Control(b)),
213                },
214                State::Csi => match b {
215                    b'0'..=b'9' | b';' | b':' => self.param_byte(b),
216                    0x3C..=0x3F => {
217                        if self.private.is_none() && self.param_bytes == 0 {
218                            self.private = Some(b);
219                        } // later private markers are ignored, as most terminals do
220                    }
221                    0x20..=0x2F => self.push_intermediate(b),
222                    0x40..=0x7E => {
223                        self.close_param();
224                        f(Event::Csi {
225                            private: self.private,
226                            params: &self.params[..self.params_len],
227                            intermediates: &self.intermediates[..self.intermediates_len],
228                            final_byte: b,
229                        });
230                        self.private = None;
231                        self.state = State::Ground;
232                    }
233                    0x1B => self.begin(State::Escape),
234                    0x18 | 0x1A => self.state = State::Ground,
235                    _ => f(Event::Control(b)),
236                },
237                State::Osc | State::Str => {
238                    let is_osc = self.state == State::Osc;
239                    if self.str_esc {
240                        self.str_esc = false;
241                        if b == b'\\' {
242                            self.dispatch_string(is_osc, &mut f);
243                            i += 1;
244                            continue;
245                        }
246                        self.push_str_byte(0x1B);
247                        // fall through: `b` is ordinary payload
248                    }
249                    match b {
250                        0x07 => self.dispatch_string(is_osc, &mut f), // BEL terminator
251                        0x1B => self.str_esc = true,
252                        _ => self.push_str_byte(b),
253                    }
254                }
255            }
256            i += 1;
257        }
258        flush_run(&mut run, input, input.len(), &mut f);
259    }
260
261    /// Feed a chunk of bytes; returns the tokens completed by this chunk.
262    ///
263    /// Any accumulated text run is flushed at the end of the call; partial
264    /// escape sequences and partial UTF-8 characters are held for the next.
265    /// This is [`feed_with`](Parser::feed_with) with the events copied into
266    /// owned [`Token`]s; prefer the engine where the allocation matters.
267    pub fn feed(&mut self, input: &[u8]) -> Vec<Token> {
268        let mut out = Vec::new();
269        let mut text = String::new();
270        let mut decoder = std::mem::take(&mut self.decoder);
271
272        self.feed_with(input, |event| match event {
273            Event::Text(bytes) => decoder.decode(bytes, |s| text.push_str(s)),
274            other => {
275                decoder.flush_incomplete(|s| text.push_str(s));
276                if !text.is_empty() {
277                    out.push(Token::Text(std::mem::take(&mut text)));
278                }
279                out.push(own(other));
280            }
281        });
282
283        if !text.is_empty() {
284            out.push(Token::Text(text));
285        }
286        self.decoder = decoder;
287        out
288    }
289
290    fn begin(&mut self, state: State) {
291        self.state = state;
292        self.intermediates_len = 0;
293        self.params_len = 0;
294        self.param_value = 0;
295        self.param_open = false;
296        self.param_sub = false;
297        self.param_bytes = 0;
298        self.private = None;
299        self.str_data.clear();
300        self.str_esc = false;
301    }
302
303    fn push_intermediate(&mut self, b: u8) {
304        if self.intermediates_len < MAX_INTERMEDIATES {
305            self.intermediates[self.intermediates_len] = b;
306            self.intermediates_len += 1;
307        }
308    }
309
310    fn push_str_byte(&mut self, b: u8) {
311        if self.str_data.len() < MAX_SEQ {
312            self.str_data.push(b);
313        }
314    }
315
316    /// One byte of a CSI parameter list: a digit, `;` or `:`.
317    fn param_byte(&mut self, b: u8) {
318        if self.param_bytes >= MAX_SEQ {
319            self.state = State::Ground; // oversized: drop the sequence
320            return;
321        }
322        self.param_bytes += 1;
323        self.param_open = true;
324        match b {
325            b';' => {
326                self.store_param();
327                self.param_value = 0;
328                self.param_sub = false;
329            }
330            b':' => self.param_sub = true, // sub-parameters are truncated
331            _ => {
332                if !self.param_sub {
333                    let digit = u32::from(b - b'0');
334                    self.param_value = (self.param_value * 10 + digit).min(u16::MAX as u32);
335                }
336            }
337        }
338    }
339
340    fn store_param(&mut self) {
341        if self.params_len < MAX_PARAMS {
342            self.params[self.params_len] = self.param_value as u16;
343            self.params_len += 1;
344        }
345    }
346
347    /// Close the parameter list at the final byte.
348    fn close_param(&mut self) {
349        if self.param_open {
350            self.store_param();
351            self.param_open = false;
352        }
353    }
354
355    fn dispatch_string<F: FnMut(Event<'_>)>(&mut self, is_osc: bool, f: &mut F) {
356        if is_osc {
357            f(Event::Osc(&self.str_data));
358        } else {
359            f(Event::Other {
360                intro: self.str_intro,
361                data: &self.str_data,
362            });
363        }
364        self.str_data.clear();
365        self.state = State::Ground;
366    }
367}
368
369/// Deliver the text run that ends at `end`, if any.
370fn flush_run<F: FnMut(Event<'_>)>(run: &mut Option<usize>, input: &[u8], end: usize, f: &mut F) {
371    if let Some(start) = run.take() {
372        if start < end {
373            f(Event::Text(&input[start..end]));
374        }
375    }
376}
377
378/// Copy a borrowed event into an owned token. Text is handled by the caller,
379/// which decodes UTF-8 across chunk boundaries.
380fn own(event: Event<'_>) -> Token {
381    match event {
382        Event::Text(bytes) => Token::Text(String::from_utf8_lossy(bytes).into_owned()),
383        Event::Control(b) => Token::Control(b),
384        Event::Csi {
385            private,
386            params,
387            intermediates,
388            final_byte,
389        } => Token::Csi {
390            private: private.map(char::from),
391            params: params.to_vec(),
392            intermediates: intermediates.to_vec(),
393            final_byte: char::from(final_byte),
394        },
395        Event::Esc {
396            intermediates,
397            final_byte,
398        } => Token::Esc {
399            intermediates: intermediates.to_vec(),
400            final_byte,
401        },
402        Event::Osc(data) => Token::Osc(String::from_utf8_lossy(data).into_owned()),
403        Event::Other { intro, data } => Token::Other {
404            intro,
405            data: data.to_vec(),
406        },
407    }
408}
409
410/// Decodes [`Event::Text`] bytes into `str` pieces, across chunk boundaries.
411///
412/// [`Event::Text`] is raw bytes, because the parser never copies them. A
413/// consumer that wants characters needs two things this handles: a multi-byte
414/// character split across two `feed_with` calls, and invalid bytes, which
415/// become U+FFFD exactly as [`Parser::feed`] renders them.
416///
417/// Pieces are borrowed: an ASCII run is handed over as one slice of the input
418/// with nothing copied, and only a character straddling a boundary is
419/// assembled in the decoder's own four bytes. It allocates nothing, ever.
420///
421/// ```
422/// # use ansi::{Event, Parser, Utf8Decoder};
423/// let mut parser = Parser::new();
424/// let mut decoder = Utf8Decoder::new();
425/// let mut seen = String::new();
426/// // The 'é' is split across the two chunks.
427/// for chunk in [&b"caf\xC3"[..], &b"\xA9!"[..]] {
428///     parser.feed_with(chunk, |event| match event {
429///         Event::Text(bytes) => decoder.decode(bytes, |s| seen.push_str(s)),
430///         _ => decoder.flush_incomplete(|s| seen.push_str(s)),
431///     });
432/// }
433/// assert_eq!(seen, "café!");
434/// ```
435#[derive(Debug, Default, Clone)]
436pub struct Utf8Decoder {
437    partial: [u8; 4],
438    len: usize,
439}
440
441impl Utf8Decoder {
442    /// A decoder with nothing held over.
443    pub fn new() -> Self {
444        Self::default()
445    }
446
447    /// Decode `bytes`, calling `f` with each piece of text in order.
448    ///
449    /// A character left incomplete at the end is held for the next call.
450    pub fn decode<F: FnMut(&str)>(&mut self, bytes: &[u8], mut f: F) {
451        let mut i = 0;
452        while i < bytes.len() {
453            let b = bytes[i];
454            if self.len > 0 {
455                if (0x80..0xC0).contains(&b) {
456                    self.partial[self.len] = b;
457                    self.len += 1;
458                    i += 1;
459                    if self.len == utf8_len(self.partial[0]) {
460                        match std::str::from_utf8(&self.partial[..self.len]) {
461                            Ok(s) => f(s),
462                            Err(_) => f(REPLACEMENT), // overlong, surrogate, out of range
463                        }
464                        self.len = 0;
465                    }
466                    continue;
467                }
468                // Truncated sequence: the held bytes were invalid.
469                self.len = 0;
470                f(REPLACEMENT);
471                // fall through and process `b` normally
472            }
473            if b < 0x80 {
474                // An ASCII run is already valid UTF-8: hand it over borrowed.
475                let start = i;
476                while i < bytes.len() && bytes[i] < 0x80 {
477                    i += 1;
478                }
479                match std::str::from_utf8(&bytes[start..i]) {
480                    Ok(s) => f(s),
481                    Err(_) => unreachable!("bytes below 0x80 are valid UTF-8"),
482                }
483                continue;
484            }
485            i += 1;
486            if b < 0xC0 || utf8_len(b) == 1 {
487                f(REPLACEMENT); // stray continuation, or an invalid lead (0xF8+)
488            } else {
489                self.partial[0] = b;
490                self.len = 1;
491            }
492        }
493    }
494
495    /// Give up on a character left incomplete, emitting U+FFFD for it.
496    ///
497    /// Call this when something other than text arrives — a control byte or an
498    /// escape sequence ends a text run, and the held bytes can never be
499    /// completed. Does nothing when no character is pending.
500    pub fn flush_incomplete<F: FnMut(&str)>(&mut self, mut f: F) {
501        if self.len > 0 {
502            self.len = 0;
503            f(REPLACEMENT);
504        }
505    }
506
507    /// True when a partial character is held for the next [`decode`](Self::decode).
508    pub fn is_pending(&self) -> bool {
509        self.len > 0
510    }
511}
512
513const REPLACEMENT: &str = "\u{FFFD}";
514
515/// Expected total length of a UTF-8 sequence from its lead byte (1 for
516/// invalid leads, so they consume exactly themselves).
517fn utf8_len(lead: u8) -> usize {
518    match lead {
519        0xC0..=0xDF => 2,
520        0xE0..=0xEF => 3,
521        0xF0..=0xF7 => 4,
522        _ => 1,
523    }
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529
530    fn events(input: &[u8]) -> Vec<Token> {
531        let mut p = Parser::new();
532        let mut out = Vec::new();
533        p.feed_with(input, |e| out.push(own(e)));
534        out
535    }
536
537    #[test]
538    fn engine_and_feed_agree_on_every_chunk_split() {
539        let corpus: &[&[u8]] = &[
540            b"hello world\r\n",
541            b"\x1b[1;31mred\x1b[0m",
542            b"\x1b[?25l\x1b[>4;2m\x1b[38;2;10;20;30mx",
543            b"\x1b]0;title\x07after\x1b]133;A\x1b\\done",
544            b"\x1bPq#0;2\x1b\\tail",
545            b"\x1b7\x1b(B\x1b[12\x18aborted",
546            "caf\u{e9} \u{1F600} \u{4e2d}\u{6587}".as_bytes(),
547            &[0xE2, 0x82, b'x', 0xFF, 0x80, b'y'],
548        ];
549        for input in corpus {
550            let whole = {
551                let mut p = Parser::new();
552                p.feed(input)
553            };
554            for split in 1..input.len() {
555                let mut p = Parser::new();
556                let mut got = p.feed(&input[..split]);
557                got.extend(p.feed(&input[split..]));
558                assert_eq!(merge_text(&got), merge_text(&whole), "split at {split}");
559            }
560        }
561    }
562
563    /// Adjacent text tokens are equivalent to one, so compare merged runs.
564    fn merge_text(tokens: &[Token]) -> Vec<Token> {
565        let mut out: Vec<Token> = Vec::new();
566        for t in tokens {
567            match (out.last_mut(), t) {
568                (Some(Token::Text(prev)), Token::Text(next)) => prev.push_str(next),
569                _ => out.push(t.clone()),
570            }
571        }
572        out
573    }
574
575    #[test]
576    fn text_events_borrow_the_input_verbatim() {
577        let mut p = Parser::new();
578        let mut runs: Vec<Vec<u8>> = Vec::new();
579        p.feed_with(b"ab\x1b[0mcd\xFF", |e| {
580            if let Event::Text(b) = e {
581                runs.push(b.to_vec());
582            }
583        });
584        // Raw bytes, including the invalid 0xFF, and no U+FFFD substitution.
585        assert_eq!(runs, vec![b"ab".to_vec(), b"cd\xFF".to_vec()]);
586    }
587
588    #[test]
589    fn parameters_match_the_owned_api() {
590        assert_eq!(
591            events(b"\x1b[m"),
592            vec![Token::Csi {
593                private: None,
594                params: vec![],
595                intermediates: vec![],
596                final_byte: 'm'
597            }]
598        );
599        assert_eq!(
600            events(b"\x1b[;1;;2m"),
601            vec![Token::Csi {
602                private: None,
603                params: vec![0, 1, 0, 2],
604                intermediates: vec![],
605                final_byte: 'm'
606            }]
607        );
608        // Colon sub-parameters are truncated, not split.
609        assert_eq!(
610            events(b"\x1b[38:2:10:20:30;1m"),
611            vec![Token::Csi {
612                private: None,
613                params: vec![38, 1],
614                intermediates: vec![],
615                final_byte: 'm'
616            }]
617        );
618        // Values saturate at u16::MAX.
619        assert_eq!(
620            events(b"\x1b[999999m"),
621            vec![Token::Csi {
622                private: None,
623                params: vec![u16::MAX],
624                intermediates: vec![],
625                final_byte: 'm'
626            }]
627        );
628        // Only the first private marker counts, and only before parameters.
629        assert_eq!(
630            events(b"\x1b[?>25h"),
631            vec![Token::Csi {
632                private: Some('?'),
633                params: vec![25],
634                intermediates: vec![],
635                final_byte: 'h'
636            }]
637        );
638    }
639
640    #[test]
641    fn more_than_max_params_are_ignored_not_dropped() {
642        let mut input = b"\x1b[".to_vec();
643        for i in 0..40 {
644            if i > 0 {
645                input.push(b';');
646            }
647            input.extend_from_slice(b"7");
648        }
649        input.push(b'm');
650        let Token::Csi { params, .. } = &events(&input)[0] else {
651            panic!("expected a CSI");
652        };
653        assert_eq!(params.len(), MAX_PARAMS);
654        assert!(params.iter().all(|&p| p == 7));
655    }
656
657    #[test]
658    fn oversized_parameter_lists_drop_the_sequence() {
659        let mut input = b"\x1b[".to_vec();
660        input.extend(std::iter::repeat(b'1').take(MAX_SEQ + 8));
661        input.push(b'm');
662        // Dropped: nothing is emitted as a CSI, and the tail returns to ground.
663        let tokens = events(&input);
664        assert!(!tokens.iter().any(|t| matches!(t, Token::Csi { .. })));
665    }
666
667    #[test]
668    fn strings_cap_their_payload() {
669        let mut input = b"\x1b]0;".to_vec();
670        input.extend(std::iter::repeat(b'x').take(MAX_SEQ + 100));
671        input.push(0x07);
672        let Token::Osc(payload) = &events(&input)[0] else {
673            panic!("expected an OSC");
674        };
675        assert_eq!(payload.len(), MAX_SEQ);
676    }
677
678    #[test]
679    fn scratch_is_reused_across_sequences() {
680        let mut p = Parser::new();
681        let mut seen = Vec::new();
682        p.feed_with(b"\x1b[1;2m\x1b[?7h\x1b]0;t\x07", |e| seen.push(own(e)));
683        assert_eq!(seen.len(), 3);
684        // A second sequence must not inherit the first one's parameters,
685        // private marker or payload.
686        assert_eq!(
687            seen[1],
688            Token::Csi {
689                private: Some('?'),
690                params: vec![7],
691                intermediates: vec![],
692                final_byte: 'h'
693            }
694        );
695        assert_eq!(seen[2], Token::Osc("0;t".into()));
696    }
697
698    #[test]
699    fn a_partial_sequence_survives_the_chunk_boundary() {
700        let mut p = Parser::new();
701        let mut seen = Vec::new();
702        for chunk in [&b"\x1b"[..], &b"[1"[..], &b";2"[..], &b"H"[..]] {
703            p.feed_with(chunk, |e| seen.push(own(e)));
704        }
705        assert_eq!(
706            seen,
707            vec![Token::Csi {
708                private: None,
709                params: vec![1, 2],
710                intermediates: vec![],
711                final_byte: 'H'
712            }]
713        );
714    }
715}