Skip to main content

uncurses/ansi/
text.rs

1//! ANSI-aware byte-stream tokenizer for text utilities.
2//!
3//! ## Category
4//!
5//! The tokenizer classifies an input byte slice as visible grapheme clusters,
6//! complete ANSI escape/string sequences, or standalone control bytes. Width,
7//! stripping, truncation, and wrapping utilities all build on this stream.
8//!
9//! ## 7-bit and 8-bit controls
10//!
11//! Both 7-bit forms (`ESC [`, `ESC ]`, `ESC P`, `ESC X`, `ESC ^`, `ESC _`) and
12//! their 8-bit C1 bytes (`0x9B`, `0x9D`, `0x90`, `0x98`, `0x9E`, `0x9F`) open a
13//! sequence.
14//!
15//! A control string ends at ST, in either form - the byte `0x9C`, or `ESC \`.
16//! **`BEL` ends an OSC and nothing else**: `OSC Ps ; Pt BEL` is an xterm
17//! convention rather than a rule about control strings, and ECMA-48 gives them
18//! all one terminator. A `0x07` inside a DCS, SOS, PM or APC is payload, which
19//! matters because DCS carries arbitrary data. A lone `ESC` also ends a string
20//! and is re-parsed as the start of the next sequence.
21//!
22//! ## C1 bytes depend on decoder state
23//!
24//! A byte in `0x80..=0x9F` is a C1 control **between** characters and a UTF-8
25//! continuation byte **inside** one. Which it is depends on where the decoder
26//! is and on nothing else, so:
27//!
28//! - the byte `0x9C` is 8-bit ST, and the byte `0x9D` opens an OSC;
29//! - `C2 9C` is the *character* U+009C and is text, as is `C2 9D`;
30//! - the `0x9C` inside `E2 9C 85` ("✅") is neither, and is never examined.
31//!
32//! That last case is why every code point in U+2700..U+273F survives inside an
33//! OSC title. A `&str` cannot hold a raw C1 byte at all, which is what the
34//! 7-bit forms are for.
35//!
36//! ## Malformed input
37//!
38//! [`tokenize`] takes bytes, so its input need not be valid UTF-8. A byte that
39//! begins no well-formed character is emitted as [`Token::Control`] to keep
40//! forward progress, and contributes no width. Tokens always concatenate back
41//! to the input exactly.
42//!
43//! ## Mode interaction
44//!
45//! This module does not interpret terminal modes or sequence semantics. Escape
46//! bytes are passed through as zero-width tokens so callers can preserve or drop
47//! them according to their own policy.
48
49pub use crate::text::WidthMode;
50use crate::unicode::graphemes;
51
52/// A single token produced by [`tokenize`].
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum Token<'a> {
55    /// One grapheme cluster and its display width.
56    ///
57    /// Always whole, well-formed UTF-8. The width may be zero - a combining
58    /// mark, a variation selector, or a C1 character such as U+009C written
59    /// as `C2 9C` - so "visible" here means "not a control byte", not
60    /// "occupies a cell".
61    Text {
62        /// Grapheme bytes.
63        text: &'a [u8],
64        /// Display width in terminal cells.
65        width: u16,
66    },
67    /// An ANSI escape sequence (passed through verbatim, no width).
68    Escape(&'a [u8]),
69    /// A single byte that stands on its own: a C0 control, DEL, a C1 byte
70    /// that introduces nothing, or a byte that begins no well-formed UTF-8
71    /// character. All are zero width.
72    Control(u8),
73}
74
75/// Return the display width of `bytes` ignoring ANSI escapes.
76///
77/// Non-UTF-8 bytes contribute no width.
78pub fn string_width(bytes: &[u8], mode: WidthMode, eaw_wide: bool) -> usize {
79    tokenize(bytes, mode, eaw_wide)
80        .filter_map(|t| match t {
81            Token::Text { width, .. } => Some(width as usize),
82            _ => None,
83        })
84        .sum()
85}
86
87/// Tokenize an input byte slice into ANSI-aware tokens.
88///
89/// The tokens concatenate back to `bytes` exactly; the tokenizer reclassifies
90/// bytes but never invents or drops one. `bytes` need not be valid UTF-8.
91///
92/// See the [module docs](self) for which byte ends a control string, and for
93/// why a byte in `0x80..=0x9F` is a C1 control in one position and part of a
94/// character in another.
95pub fn tokenize(bytes: &[u8], mode: WidthMode, eaw_wide: bool) -> Tokenizer<'_> {
96    Tokenizer {
97        bytes,
98        pos: 0,
99        mode,
100        eaw_wide,
101        scan_end: 0,
102        run: "",
103        #[cfg(test)]
104        scanned: 0,
105        #[cfg(test)]
106        validated: 0,
107    }
108}
109
110/// Iterator returned by [`tokenize`].
111pub struct Tokenizer<'a> {
112    bytes: &'a [u8],
113    pos: usize,
114    mode: WidthMode,
115    eaw_wide: bool,
116    /// Where the current run of plain text ends - the next byte that opens an
117    /// escape or a control token, or that cannot start a UTF-8 character.
118    ///
119    /// The run is what makes this iterator linear. Finding where the plain
120    /// text ends is work proportional to the run's length, and it is done
121    /// once for the run rather than once for each grapheme inside it. `pos`
122    /// then walks the run a grapheme at a time and only crossing `scan_end`
123    /// makes this look again.
124    scan_end: usize,
125    /// The validated remainder of the run, starting at `pos`.
126    ///
127    /// Held separately from `scan_end` on purpose. Malformed UTF-8 ends this
128    /// slice early but says nothing about where the *run* ends, and conflating
129    /// the two made every malformed byte rescan the rest of the input: text
130    /// that was one byte of UTF-8 short cost O(n^2) to tokenize. Keeping the
131    /// two boundaries apart means the scan still happens once per run, and
132    /// only the validation - which stops at the first bad byte, so it is cheap
133    /// exactly when it is repeated - runs again.
134    run: &'a str,
135    /// Bytes visited by the run scan, which is the thing that went quadratic.
136    ///
137    /// It is not visible in the tokens - that is why the bug survived a full
138    /// test suite - and timing it is a benchmark, not a test. So the tests
139    /// count it.
140    #[cfg(test)]
141    scanned: usize,
142    /// Bytes handed to UTF-8 validation, counted separately from `scanned`.
143    ///
144    /// The scan and the validation are two different pieces of work over the
145    /// same run, and only one of them is refreshed at `scan_end`. A version
146    /// that finds the run once but revalidates the remainder on every token
147    /// is quadratic in real time - 4x per doubling, 8 s for a 128 KB line of
148    /// CJK - while visiting each byte exactly once *in the scan*. `scanned`
149    /// alone cannot see that; this can.
150    #[cfg(test)]
151    validated: usize,
152}
153
154impl<'a> Tokenizer<'a> {
155    /// Set `run` to the validated text from `pos` up to `to`.
156    ///
157    /// The byte count lives here rather than at the call site on purpose: the
158    /// cost that has to stay linear is "bytes handed to `from_utf8`", so
159    /// moving the validation moves its meter with it. A version that finds the
160    /// run once but revalidates the remainder per token keeps `scanned` at
161    /// exactly one visit per byte while running in quadratic time.
162    #[inline]
163    fn validate(&mut self, to: usize) {
164        let from = self.pos;
165        #[cfg(test)]
166        {
167            self.validated += to - from;
168        }
169        self.run = std::str::from_utf8(&self.bytes[from..to]).unwrap_or("");
170    }
171}
172
173impl<'a> Iterator for Tokenizer<'a> {
174    type Item = Token<'a>;
175
176    fn next(&mut self) -> Option<Token<'a>> {
177        let b = *self.bytes.get(self.pos)?;
178
179        // Printable ASCII followed by ASCII or end-of-input is a whole
180        // cluster. Consume it before scanning or validating a text run.
181        if (0x20..0x7f).contains(&b) && self.bytes.get(self.pos + 1).is_none_or(u8::is_ascii) {
182            let start = self.pos;
183            self.pos += 1;
184            if !self.run.is_empty() {
185                self.run = &self.run[1..];
186            }
187            return Some(Token::Text {
188                text: &self.bytes[start..self.pos],
189                width: 1,
190            });
191        }
192
193        // Keep escape introducers ahead of the run check for dense sequences.
194        if b == 0x1b || is_c1_introducer(b) {
195            let start = self.pos;
196            let end = scan_sequence(self.bytes, start);
197            self.pos = end;
198            return Some(Token::Escape(&self.bytes[start..end]));
199        }
200
201        // A cached run contains only validated plain text. Check remaining
202        // control bytes and find a new run only after that run ends.
203        if self.run.is_empty() {
204            // C0 controls (incl. DEL) and non-introducer C1 bytes are emitted as
205            // single control bytes.
206            if b < 0x20 || b == 0x7f || (0x80..=0x9f).contains(&b) {
207                self.pos += 1;
208                return Some(Token::Control(b));
209            }
210
211            // Plain text — walk forward one grapheme at a time, stopping at any
212            // byte that would start an escape or control token. Stepping per
213            // codepoint when scanning guarantees we never confuse a UTF-8
214            // continuation byte (which can be in 0x80..=0xBF) with a C1 control.
215            //
216            // The scan runs once per *run* of plain text, not once per grapheme
217            // in it. Doing it per grapheme is O(run) work O(run) times, which
218            // made tokenizing a single long line quadratic in its length - a
219            // 32 KB line took 852 ms, and nothing about the tokens it produced
220            // changed, so only a timing test could see it.
221            if self.pos >= self.scan_end {
222                let mut end = self.pos;
223                while end < self.bytes.len() {
224                    let bb = self.bytes[end];
225                    if bb == 0x1b || bb < 0x20 || bb == 0x7f || (0x80..=0x9f).contains(&bb) {
226                        break;
227                    }
228                    match utf8_char_at(self.bytes, end) {
229                        Some(n) => end += n,
230                        None => break,
231                    }
232                }
233                #[cfg(test)]
234                {
235                    self.scanned += end.max(self.pos + 1) - self.pos;
236                }
237                self.scan_end = end;
238                // Valid by construction: the scan stopped at the first byte that
239                // does not begin a well-formed character, so this cannot fail.
240                self.validate(end);
241            }
242            if self.run.is_empty() {
243                // Nothing plain starts here, or what does is not valid UTF-8.
244                // Either way the byte is emitted verbatim to keep forward
245                // progress, exactly as an invalid leading byte always was.
246                self.pos += 1;
247                return Some(Token::Control(b));
248            }
249        }
250        let g = graphemes(self.run).next()?;
251        let start = self.pos;
252        self.pos += g.len();
253        self.run = &self.run[g.len()..];
254        let width = self.mode.grapheme_width(g, self.eaw_wide) as u16;
255        Some(Token::Text {
256            text: &self.bytes[start..self.pos],
257            width,
258        })
259    }
260}
261
262/// The length of the well-formed UTF-8 character starting at `i`, if there is
263/// one.
264///
265/// A lead byte announces how many bytes follow it, and nothing else in this
266/// file may take that announcement on trust. Bytes that do not follow are the
267/// whole problem: a lead byte with the wrong continuations makes a naive walk
268/// step *over* whatever comes next, which is how a scan can pass a terminator
269/// it should have stopped at, and how the tokenizer's two boundaries came to
270/// disagree about where a character starts.
271fn utf8_char_at(bytes: &[u8], i: usize) -> Option<usize> {
272    let n = utf8_char_len(bytes[i]);
273    if n == 0 || i + n > bytes.len() {
274        return None;
275    }
276    // A one-byte character is ASCII, and `utf8_char_len` already established
277    // that; anything longer has to prove its continuations.
278    if n > 1 && std::str::from_utf8(&bytes[i..i + n]).is_err() {
279        return None;
280    }
281    Some(n)
282}
283
284#[inline]
285fn utf8_char_len(b: u8) -> usize {
286    match b {
287        0x00..=0x7f => 1,
288        0xc2..=0xdf => 2,
289        0xe0..=0xef => 3,
290        0xf0..=0xf4 => 4,
291        _ => 0,
292    }
293}
294
295#[inline]
296fn is_c1_introducer(b: u8) -> bool {
297    matches!(b, 0x90 | 0x98 | 0x9b | 0x9d | 0x9e | 0x9f)
298}
299
300/// Return the byte index past the end of an escape sequence starting at
301/// `start`.
302///
303/// `bytes[start]` is either `0x1B` (ESC) or an 8-bit C1 sequence introducer
304/// (`0x9B`, `0x9D`, `0x90`, `0x98`, `0x9E`, `0x9F`). The returned index points
305/// to the first byte after the sequence. If the sequence is incomplete the
306/// index is the end of `bytes`.
307fn scan_sequence(bytes: &[u8], start: usize) -> usize {
308    let len = bytes.len();
309    let head = bytes[start];
310
311    match head {
312        0x1b => {
313            let i = start + 1;
314            if i >= len {
315                return len;
316            }
317            match bytes[i] {
318                b'[' => scan_csi(bytes, i + 1),
319                // The `true` is "BEL ends this": OSC only.
320                b']' => scan_string(bytes, i + 1, true),
321                b'P' | b'X' | b'^' | b'_' => scan_string(bytes, i + 1, false),
322                _ => scan_esc_intermediate(bytes, i),
323            }
324        }
325        0x9b => scan_csi(bytes, start + 1),
326        0x9d => scan_string(bytes, start + 1, true),
327        0x90 | 0x98 | 0x9e | 0x9f => scan_string(bytes, start + 1, false),
328        _ => start + 1,
329    }
330}
331
332fn scan_csi(bytes: &[u8], from: usize) -> usize {
333    let len = bytes.len();
334    let mut i = from;
335    while i < len {
336        if (0x40..=0x7e).contains(&bytes[i]) {
337            return i + 1;
338        }
339        i += 1;
340    }
341    len
342}
343
344/// Scan a control string to its terminator, which is ST - and also `BEL` if
345/// `bel_ends`, that being an xterm convention for OSC and no other string.
346///
347/// `from` is the byte after the introducer, which for DCS is not yet the
348/// payload: a DCS carries a CSI-shaped command section first, so the shape is
349/// `ESC P` prefix params intermediates final, and only then the string. This
350/// scan does not need to know where that boundary is, because every byte the
351/// command section may hold - prefix `0x3C..=0x3F`, parameters `0x30..=0x3B`,
352/// intermediates `0x20..=0x2F`, final `0x40..=0x7E` - lies inside
353/// `0x20..=0x7E` and so is none of ST, BEL or ESC. Anything that has to treat
354/// the payload differently from the command section, such as the doubled ESC
355/// bytes in a tmux passthrough, does need to find it.
356///
357/// Terminators are only ever tested at a character boundary. `0x9C` is 8-bit
358/// ST between characters and a continuation byte inside one, and every code
359/// point in U+2700..U+273F carries one, so a scan that does not know which it
360/// is looking at ends the sequence in the middle of "✅".
361fn scan_string(bytes: &[u8], from: usize, bel_ends: bool) -> usize {
362    let len = bytes.len();
363    let mut i = from;
364    while i < len {
365        let b = bytes[i];
366        // At a boundary, so this is C1 ST and not a continuation byte.
367        if b == 0x9c || (b == 0x07 && bel_ends) {
368            return i + 1;
369        }
370        if b == 0x1b && i + 1 < len && bytes[i + 1] == b'\\' {
371            return i + 2;
372        }
373        // Lone ESC (without trailing `\`) terminates the string but is left
374        // to be re-parsed as the next sequence.
375        if b == 0x1b {
376            return i;
377        }
378        // Collect a whole character, so the bytes inside it are never tested
379        // as terminators. Only a *well-formed* one: trusting a lead byte
380        // whose continuations do not match would step over whatever followed
381        // it, terminators included, and one malformed byte in a payload would
382        // swallow the BEL that ends it and all the text after.
383        i += utf8_char_at(bytes, i).unwrap_or(1);
384    }
385    len
386}
387
388fn scan_esc_intermediate(bytes: &[u8], at: usize) -> usize {
389    let len = bytes.len();
390    let mut i = at;
391    while i < len && (0x20..=0x2f).contains(&bytes[i]) {
392        i += 1;
393    }
394    if i < len {
395        // The final byte, or the whole character it leads - splitting one
396        // strands continuation bytes that the next token reads as C1
397        // controls.
398        i + utf8_char_at(bytes, i).unwrap_or(1)
399    } else {
400        len
401    }
402}
403
404/// Helpers shared by the test modules below.
405#[cfg(test)]
406mod util {
407    use super::*;
408
409    pub(super) fn tokens(input: &[u8]) -> Vec<Token<'_>> {
410        tokenize(input, WidthMode::Grapheme, false).collect()
411    }
412
413    /// The visible text of a token stream, which is what a caller loses when
414    /// a sequence runs past its terminator or a character is cut in half.
415    ///
416    /// Not lossy: a text token that is not whole UTF-8 is a bug, and
417    /// rendering it as U+FFFD hides the failure these tests exist to catch.
418    pub(super) fn visible(toks: &[Token<'_>]) -> String {
419        toks.iter()
420            .filter_map(|t| match t {
421                Token::Text { text, .. } => {
422                    Some(std::str::from_utf8(text).expect("a text token is whole UTF-8"))
423                }
424                _ => None,
425            })
426            .collect()
427    }
428
429    /// Concatenate a token stream back into bytes. It must equal the input:
430    /// the tokenizer may reclassify a byte but must never invent or drop one.
431    pub(super) fn rebuild(toks: &[Token<'_>]) -> Vec<u8> {
432        let mut out = Vec::new();
433        for t in toks {
434            match t {
435                Token::Text { text, .. } | Token::Escape(text) => out.extend_from_slice(text),
436                Token::Control(c) => out.push(*c),
437            }
438        }
439        out
440    }
441
442    /// A control function in both of its forms: `ESC x` and the single C1
443    /// byte. Every sequence test runs over both, so every test needs this.
444    pub(super) fn forms(seven: &[u8], eight: u8) -> [(&'static str, Vec<u8>); 2] {
445        [("7-bit", seven.to_vec()), ("8-bit", vec![eight])]
446    }
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    #[test]
454    fn tokenize_plain_text() {
455        let toks: Vec<_> = tokenize(b"abc", WidthMode::Grapheme, false).collect();
456        assert_eq!(toks.len(), 3);
457        assert!(matches!(
458            toks[0],
459            Token::Text {
460                text: b"a",
461                width: 1
462            }
463        ));
464    }
465
466    #[test]
467    fn tokenize_csi() {
468        let toks: Vec<_> = tokenize(b"\x1b[31mhi\x1b[m", WidthMode::Grapheme, false).collect();
469        assert_eq!(toks.len(), 4);
470        assert!(matches!(toks[0], Token::Escape(b"\x1b[31m")));
471        assert!(matches!(toks[3], Token::Escape(b"\x1b[m")));
472    }
473
474    #[test]
475    fn tokenize_osc_bel() {
476        let toks: Vec<_> = tokenize(b"\x1b]0;title\x07rest", WidthMode::Grapheme, false).collect();
477        assert!(matches!(toks[0], Token::Escape(b"\x1b]0;title\x07")));
478    }
479
480    #[test]
481    fn tokenize_osc_st() {
482        let toks: Vec<_> =
483            tokenize(b"\x1b]0;title\x1b\\rest", WidthMode::Grapheme, false).collect();
484        assert!(matches!(toks[0], Token::Escape(b"\x1b]0;title\x1b\\")));
485    }
486
487    #[test]
488    fn tokenize_wide_char() {
489        let toks: Vec<_> = tokenize("中".as_bytes(), WidthMode::Grapheme, false).collect();
490        assert_eq!(toks.len(), 1);
491        assert!(matches!(toks[0], Token::Text { width: 2, .. }));
492    }
493
494    #[test]
495    fn tokenize_newline_control() {
496        let toks: Vec<_> = tokenize(b"a\nb", WidthMode::Grapheme, false).collect();
497        assert_eq!(toks.len(), 3);
498        assert!(matches!(toks[1], Token::Control(b'\n')));
499    }
500
501    #[test]
502    fn string_width_ignores_escapes() {
503        assert_eq!(
504            string_width(b"\x1b[31mhello\x1b[m", WidthMode::Grapheme, false),
505            5
506        );
507        assert_eq!(
508            string_width("中文".as_bytes(), WidthMode::Grapheme, false),
509            4
510        );
511    }
512
513    #[test]
514    fn tokenize_8bit_csi() {
515        let toks: Vec<_> = tokenize(b"\x9b31mhi\x9bm", WidthMode::Grapheme, false).collect();
516        assert!(matches!(toks[0], Token::Escape(b"\x9b31m")));
517        assert!(matches!(toks[3], Token::Escape(b"\x9bm")));
518    }
519
520    #[test]
521    fn tokenize_8bit_osc_with_8bit_st() {
522        // 0x9d "0;title" 0x9c "rest"
523        let toks: Vec<_> = tokenize(b"\x9d0;title\x9crest", WidthMode::Grapheme, false).collect();
524        assert!(matches!(toks[0], Token::Escape(b"\x9d0;title\x9c")));
525        // "rest" tokenizes to 4 single-grapheme Text tokens.
526        assert_eq!(toks.len(), 5);
527    }
528
529    #[test]
530    fn tokenize_8bit_dcs() {
531        let toks: Vec<_> = tokenize(b"\x90q!data\x9cafter", WidthMode::Grapheme, false).collect();
532        assert!(matches!(toks[0], Token::Escape(b"\x90q!data\x9c")));
533    }
534
535    #[test]
536    fn tokenize_8bit_sos_pm_apc() {
537        for &intro in &[0x98u8, 0x9e, 0x9f] {
538            let mut input = vec![intro];
539            input.extend_from_slice(b"payload");
540            input.push(0x9c);
541            let toks: Vec<_> = tokenize(&input, WidthMode::Grapheme, false).collect();
542            match toks[0] {
543                Token::Escape(esc) => {
544                    assert_eq!(esc[0], intro);
545                    assert_eq!(*esc.last().unwrap(), 0x9c);
546                }
547                ref other => panic!("expected Escape for 0x{intro:02x}, got {other:?}"),
548            }
549        }
550    }
551
552    #[test]
553    fn tokenize_standalone_c1_is_control() {
554        // IND (0x84) standalone — not an introducer, emitted as Control.
555        let toks: Vec<_> = tokenize(b"a\x84b", WidthMode::Grapheme, false).collect();
556        assert_eq!(toks.len(), 3);
557        assert!(matches!(toks[1], Token::Control(0x84)));
558    }
559
560    #[test]
561    fn tokenize_8bit_st_outside_string_is_control() {
562        // Bare 0x9c with no preceding string is just a control byte.
563        let toks: Vec<_> = tokenize(b"\x9c", WidthMode::Grapheme, false).collect();
564        assert_eq!(toks.len(), 1);
565        assert!(matches!(toks[0], Token::Control(0x9c)));
566    }
567
568    #[test]
569    fn tokenize_two_byte_esc() {
570        // ESC = (DECKPAM)
571        let toks: Vec<_> = tokenize(b"a\x1b=b", WidthMode::Grapheme, false).collect();
572        assert_eq!(toks.len(), 3);
573        assert!(matches!(toks[1], Token::Escape(b"\x1b=")));
574    }
575}
576
577/// The five string sequences - OSC, DCS, SOS, PM, APC - across both control
578/// forms, every terminator, and payloads that do and do not contain UTF-8.
579///
580/// These share one scanner, so they share one bug. The `0x9C` split that made
581/// this file's `from_utf8_unchecked` calls undefined behaviour was found in an
582/// OSC title, but it was never an OSC bug: DCS, SOS, PM and APC all reached
583/// the same code, and only OSC was tested. A payload is arbitrary text, so any
584/// of them can carry a character whose continuation bytes look like a
585/// terminator.
586#[cfg(test)]
587mod sequences {
588    use super::util::*;
589    use super::*;
590
591    /// `(name, 7-bit introducer, 8-bit introducer)`.
592    const STRINGS: &[(&str, &[u8], u8)] = &[
593        ("OSC", b"\x1b]", 0x9d),
594        ("DCS", b"\x1bP", 0x90),
595        ("SOS", b"\x1bX", 0x98),
596        ("PM", b"\x1b^", 0x9e),
597        ("APC", b"\x1b_", 0x9f),
598    ];
599
600    /// Every way a string sequence may end.
601    ///
602    /// `BEL` is in here for OSC only; the loop skips it elsewhere.
603    const TERMINATORS: &[(&str, &[u8])] = &[
604        ("BEL", b"\x07"),
605        ("ESC backslash", b"\x1b\\"),
606        ("8-bit ST", b"\x9c"),
607    ];
608
609    /// Payloads with and without UTF-8. The non-ASCII one is chosen for the
610    /// bytes it contains, not for how it reads: `é` is `C3 A9`, `✅` is
611    /// `E2 9C 85` and carries a `0x9C`, `一` is `E4 B8 80`, and `😀` is a
612    /// four-byte character.
613    const PAYLOADS: &[(&str, &str)] = &[
614        ("ascii", "0;plain title"),
615        ("utf8", "0;caf\u{e9} \u{2705} \u{4e00} \u{1f600}"),
616    ];
617
618    /// Whatever the type, the form, the payload or the terminator, the whole
619    /// sequence is exactly one zero-width token and the text after it
620    /// survives intact.
621    #[test]
622    fn every_string_sequence_is_one_whole_token() {
623        for (name, seven, eight) in STRINGS {
624            for (pname, payload) in PAYLOADS {
625                for (tname, term) in TERMINATORS {
626                    if *term == b"\x07" && *name != "OSC" {
627                        continue;
628                    }
629                    for (form, intro) in forms(seven, *eight) {
630                        let mut input = intro;
631                        input.extend_from_slice(payload.as_bytes());
632                        input.extend_from_slice(term);
633                        let seq_len = input.len();
634                        input.extend_from_slice("after \u{2705}".as_bytes());
635
636                        let what = format!("{name} {form} {pname} payload, {tname} terminator");
637                        let toks = tokens(&input);
638                        assert_eq!(
639                            toks.first(),
640                            Some(&Token::Escape(&input[..seq_len])),
641                            "{what}: the sequence is not one token"
642                        );
643                        assert_eq!(
644                            visible(&toks[1..]),
645                            "after \u{2705}",
646                            "{what}: text after was lost"
647                        );
648                        // The sequence itself is invisible.
649                        assert_eq!(
650                            string_width(&input, WidthMode::Grapheme, false),
651                            8,
652                            "{what}: the sequence was measured"
653                        );
654                    }
655                }
656            }
657        }
658    }
659
660    /// A payload that runs off the end is still one token, not a stream of
661    /// stray control bytes.
662    #[test]
663    fn an_unterminated_string_sequence_runs_to_the_end() {
664        for (name, seven, eight) in STRINGS {
665            for (form, intro) in forms(seven, *eight) {
666                for (pname, payload) in PAYLOADS {
667                    let mut input = intro.clone();
668                    input.extend_from_slice(payload.as_bytes());
669                    let toks = tokens(&input);
670                    assert_eq!(
671                        toks,
672                        vec![Token::Escape(&input[..])],
673                        "{name} {form} {pname}: an unterminated sequence should be one token"
674                    );
675                    assert_eq!(string_width(&input, WidthMode::Grapheme, false), 0);
676                }
677            }
678        }
679    }
680
681    /// A lone ESC ends the string and is left to open the next sequence, so a
682    /// sequence cannot swallow the one that follows it.
683    #[test]
684    fn a_lone_esc_ends_a_string_and_is_reparsed() {
685        for (name, seven, eight) in STRINGS {
686            for (form, intro) in forms(seven, *eight) {
687                let mut input = intro.clone();
688                input.extend_from_slice("pay\u{2705}".as_bytes());
689                let cut = input.len();
690                input.extend_from_slice(b"\x1b[31mZ");
691
692                let toks = tokens(&input);
693                assert_eq!(
694                    toks[0],
695                    Token::Escape(&input[..cut]),
696                    "{name} {form}: the string should stop at the ESC"
697                );
698                assert_eq!(
699                    toks[1],
700                    Token::Escape(b"\x1b[31m"),
701                    "{name} {form}: the CSI after it should survive whole"
702                );
703                assert_eq!(
704                    toks[2],
705                    Token::Text {
706                        text: b"Z",
707                        width: 1
708                    }
709                );
710            }
711        }
712    }
713
714    /// Two sequences back to back, in either control form, stay two.
715    #[test]
716    fn adjacent_sequences_do_not_merge() {
717        // (input, first sequence, second sequence); the input names the case.
718        let cases: &[(&[u8], &[u8], &[u8])] = &[
719            // 7-bit OSC then 7-bit DCS.
720            (
721                b"\x1b]0;a\x07\x1bPq\x1b\\Z",
722                b"\x1b]0;a\x07",
723                b"\x1bPq\x1b\\",
724            ),
725            // 8-bit APC then 8-bit PM.
726            (b"\x9fa\x9c\x9eb\x9cZ", b"\x9fa\x9c", b"\x9eb\x9c"),
727            // An 8-bit introducer closed by 7-bit ST, then a 7-bit introducer
728            // closed by 8-bit ST: both mixed forms, adjacent.
729            (b"\x9d0;a\x1b\\\x1b^b\x9cZ", b"\x9d0;a\x1b\\", b"\x1b^b\x9c"),
730        ];
731        for (input, first, second) in cases {
732            let toks = tokens(input);
733            assert_eq!(toks[0], Token::Escape(first), "{input:x?}");
734            assert_eq!(toks[1], Token::Escape(second), "{input:x?}");
735            assert_eq!(
736                toks[2],
737                Token::Text {
738                    text: b"Z",
739                    width: 1
740                },
741                "{input:x?}"
742            );
743        }
744    }
745
746    /// Sequences as they actually arrive from terminals and applications.
747    #[test]
748    fn real_payloads_survive() {
749        // (what, input, the opening sequence, the visible text)
750        let cases: &[(&str, &[u8], &[u8], &str)] = &[
751            (
752                "sixel",
753                b"\x1bPq#0;2;0;0;0#1;2;100;100;100\x1b\\ok",
754                b"\x1bPq#0;2;0;0;0#1;2;100;100;100\x1b\\",
755                "ok",
756            ),
757            (
758                "DECRQSS reply",
759                b"\x1bP1$r0;1m\x1b\\ok",
760                b"\x1bP1$r0;1m\x1b\\",
761                "ok",
762            ),
763            (
764                "OSC 8 hyperlink with a UTF-8 target",
765                b"\x1b]8;;https://example.com/\xe2\x9c\x85\x07link\x1b]8;;\x07",
766                b"\x1b]8;;https://example.com/\xe2\x9c\x85\x07",
767                "link",
768            ),
769            (
770                "OSC with an empty payload",
771                b"\x1b]\x07ok",
772                b"\x1b]\x07",
773                "ok",
774            ),
775            (
776                "8-bit APC with an empty payload",
777                b"\x9f\x9cok",
778                b"\x9f\x9c",
779                "ok",
780            ),
781        ];
782        for (what, input, opening, want) in cases {
783            let toks = tokens(input);
784            assert_eq!(toks.first(), Some(&Token::Escape(opening)), "{what}");
785            assert_eq!(visible(&toks), *want, "{what}");
786            // Nothing is invented or dropped anywhere in the stream.
787            assert_eq!(
788                rebuild(&toks),
789                *input,
790                "{what}: the stream did not reassemble"
791            );
792        }
793    }
794
795    /// A byte in `0x80..=0x9F` is a C1 control between characters and a
796    /// continuation byte inside one. Which it is depends only on the decoder's
797    /// state, never on what a caller meant.
798    ///
799    /// So `C2 9C` is the character U+009C - a lead byte, then a continuation
800    /// byte collected as part of it - and not 8-bit ST, which is the single
801    /// byte `9C`. Likewise `C2 9D` is the character U+009D and does not open
802    /// an OSC. A `&str` cannot hold a raw C1 byte at all, which is what 7-bit
803    /// `ESC \` and `ESC ]` are for.
804    #[test]
805    fn a_c1_byte_inside_a_character_is_not_a_control() {
806        // Between characters: 8-bit ST, and the sequence ends.
807        let toks = tokens(b"\x1b]0;title\x9cZ");
808        assert_eq!(toks[0], Token::Escape(b"\x1b]0;title\x9c"));
809        assert_eq!(
810            toks[1],
811            Token::Text {
812                text: b"Z",
813                width: 1
814            }
815        );
816
817        // Inside a character: a continuation byte, and the sequence runs on.
818        // `C2 9C` is the character U+009C - what `"\u{9c}"` would compile to -
819        // so the OSC here is simply unterminated.
820        let unterminated = b"\x1b]0;title\xc2\x9cZ";
821        assert_eq!(tokens(unterminated), vec![Token::Escape(&unterminated[..])]);
822
823        // The same for an introducer: the byte opens a sequence, the
824        // character does not.
825        let toks = tokens(b"\x9d0;title\x07Z");
826        assert_eq!(toks[0], Token::Escape(b"\x9d0;title\x07"));
827        let toks = tokens(b"\xc2\x9d0;title\x07Z");
828        assert!(
829            !matches!(toks[0], Token::Escape(_)),
830            "the character U+009D opened a sequence: {toks:?}"
831        );
832        // U+009D measures zero, `0;title` seven, BEL zero, `Z` one.
833        assert_eq!(
834            string_width(b"\xc2\x9d0;title\x07Z", WidthMode::Wc, false),
835            8
836        );
837
838        // And the case that made this matter: the `9C` inside "✅" is a
839        // continuation byte, so the title survives whole.
840        let toks = tokens(b"\x1b]0;\xe2\x9c\x85\x07Z");
841        assert_eq!(toks[0], Token::Escape(b"\x1b]0;\xe2\x9c\x85\x07"));
842    }
843
844    /// `BEL` ends an OSC and only an OSC.
845    ///
846    /// `OSC Ps ; Pt BEL` is an xterm convention, and the common way to write
847    /// one, but it is not a rule about control strings in general - ECMA-48
848    /// gives them all a single terminator, ST. A `0x07` inside a DCS, SOS, PM
849    /// or APC is payload, and ending the sequence there would spill the rest
850    /// of that payload onto the screen as visible text.
851    #[test]
852    fn bel_ends_an_osc_and_nothing_else() {
853        for (name, seven, eight) in STRINGS {
854            for (form, intro) in forms(seven, *eight) {
855                let mut input = intro;
856                input.extend_from_slice(b"pay\x07load");
857                let bel_at = input.len() - b"load".len();
858                input.extend_from_slice(b"\x9cZ");
859
860                let toks = tokens(&input);
861                let what = format!("{name} {form}");
862                if *name == "OSC" {
863                    assert_eq!(
864                        toks[0],
865                        Token::Escape(&input[..bel_at]),
866                        "{what}: BEL should have ended it"
867                    );
868                    // What followed the BEL is now visible text.
869                    assert!(
870                        toks.iter()
871                            .any(|t| matches!(t, Token::Text { text: b"l", .. })),
872                        "{what}: text after the BEL went missing"
873                    );
874                } else {
875                    assert_eq!(
876                        toks[0],
877                        Token::Escape(&input[..input.len() - 1]),
878                        "{what}: BEL is payload, so it should have run to the ST"
879                    );
880                    assert_eq!(
881                        toks[1],
882                        Token::Text {
883                            text: b"Z",
884                            width: 1
885                        },
886                        "{what}"
887                    );
888                    assert_eq!(string_width(&input, WidthMode::Grapheme, false), 1);
889                }
890            }
891        }
892    }
893
894    /// The rule, over every C1 byte rather than the two that caused trouble.
895    ///
896    /// `C2 xx` encodes U+0080..U+00BF, so for any C1 byte there is a
897    /// character that carries it as a continuation byte. On its own the byte
898    /// is a control; inside that character it is not.
899    #[test]
900    fn every_c1_byte_depends_on_the_decoder_state() {
901        for c in 0x80u8..=0x9f {
902            // Between characters: a control, either standalone or opening a
903            // sequence.
904            let single = [c];
905            let alone: Vec<Token<'_>> = tokenize(&single, WidthMode::Wc, false).collect();
906            match alone.as_slice() {
907                [Token::Control(got)] => assert_eq!(*got, c),
908                [Token::Escape(seq)] => assert!(
909                    is_c1_introducer(c) && *seq == &single[..],
910                    "0x{c:02x} opened a sequence but is not an introducer"
911                ),
912                other => panic!("0x{c:02x} alone produced {other:?}"),
913            }
914            assert_eq!(string_width(&single, WidthMode::Wc, false), 0);
915
916            // Inside a character: one zero-width text token, no control.
917            let encoded = [0xc2, c];
918            let inside: Vec<Token<'_>> = tokenize(&encoded, WidthMode::Wc, false).collect();
919            assert_eq!(
920                inside,
921                vec![Token::Text {
922                    text: &encoded[..],
923                    width: 0
924                }],
925                "U+{:04X} was not read as a character",
926                0x80 + u32::from(c) - 0x80
927            );
928
929            // The same inside a control string: the byte ends it only if it
930            // is ST, the character never does.
931            let mut raw = b"\x1b]0;".to_vec();
932            raw.push(c);
933            raw.extend_from_slice(b"Z\x07");
934            let ends_early = matches!(
935                tokenize(&raw, WidthMode::Wc, false).next(),
936                Some(Token::Escape(seq)) if seq.len() < raw.len()
937            );
938            assert_eq!(
939                ends_early,
940                c == 0x9c || c == 0x1b,
941                "0x{c:02x} in a payload terminated the sequence unexpectedly"
942            );
943
944            let mut encoded_in = b"\x1b]0;".to_vec();
945            encoded_in.extend_from_slice(&encoded);
946            encoded_in.extend_from_slice(b"Z\x07");
947            let len = encoded_in.len();
948            assert_eq!(
949                tokenize(&encoded_in, WidthMode::Wc, false).next(),
950                Some(Token::Escape(&encoded_in[..len])),
951                "U+00{c:02X} in a payload ended the sequence"
952            );
953        }
954    }
955}
956
957#[cfg(test)]
958mod fast_path {
959    use super::util::*;
960    use super::*;
961
962    #[test]
963    fn cached_runs_stop_before_controls_escapes_and_invalid_bytes() {
964        let before = "ab中e\u{301}".as_bytes();
965        let after = "xy界".as_bytes();
966        for boundary in [
967            &b"\n"[..],
968            b"\x7f",
969            b"\x9c",
970            b"\xff",
971            b"\x1b[31m",
972            b"\x9b31m",
973            b"\x1b]0;title\x07",
974            b"\x1bPpayload\x1b\\",
975        ] {
976            let input = [before, boundary, after].concat();
977            for mode in [WidthMode::Wc, WidthMode::Grapheme] {
978                for eaw_wide in [false, true] {
979                    let expected: Vec<_> = [before, boundary, after]
980                        .into_iter()
981                        .flat_map(|part| tokenize(part, mode, eaw_wide))
982                        .collect();
983                    let mut iter = tokenize(&input, mode, eaw_wide);
984                    assert_eq!(iter.by_ref().collect::<Vec<_>>(), expected);
985                    assert_eq!(iter.next(), None);
986                }
987            }
988        }
989    }
990
991    fn text_tokens_eaw(s: &str, mode: WidthMode, eaw_wide: bool) -> Vec<(String, u16)> {
992        tokenize(s.as_bytes(), mode, eaw_wide)
993            .filter_map(|t| match t {
994                // Not lossy: a token that is not valid UTF-8 is a bug, and
995                // rendering it as U+FFFD hides exactly the failure these
996                // tests exist to catch.
997                Token::Text { text, width } => Some((
998                    std::str::from_utf8(text)
999                        .expect("a text token is whole UTF-8")
1000                        .to_owned(),
1001                    width,
1002                )),
1003                _ => None,
1004            })
1005            .collect()
1006    }
1007
1008    /// The ASCII shortcut must agree with grapheme segmentation, cluster for
1009    /// cluster, in both width modes.
1010    ///
1011    /// The shortcut answers "one ASCII byte, one column" without consulting
1012    /// Unicode at all, which is only sound while nothing can join to an ASCII
1013    /// base from the byte after it. Everything that can - a combining mark, a
1014    /// zero-width joiner, a variation selector, a regional indicator - begins
1015    /// at 0x80 or above in UTF-8, so the shortcut declines whenever the next
1016    /// byte is not ASCII. These are the cases on either side of that line.
1017    #[test]
1018    fn the_ascii_shortcut_agrees_with_grapheme_segmentation() {
1019        for mode in [WidthMode::Wc, WidthMode::Grapheme] {
1020            for eaw_wide in [false, true] {
1021                let toks = |s: &str| text_tokens_eaw(s, mode, eaw_wide);
1022                assert_eq!(
1023                    toks("abc"),
1024                    vec![("a".into(), 1), ("b".into(), 1), ("c".into(), 1)],
1025                    "plain ASCII is one column per byte"
1026                );
1027                // A combining acute joins the `e` before it: one cluster, and
1028                // the shortcut must not have claimed that `e` on its own.
1029                assert_eq!(
1030                    toks("e\u{301}f"),
1031                    vec![("e\u{301}".into(), 1), ("f".into(), 1)],
1032                    "a combining mark still joins the ASCII base before it"
1033                );
1034                assert_eq!(
1035                    toks("a"),
1036                    vec![("a".into(), 1)],
1037                    "the last byte of the input takes the shortcut too"
1038                );
1039                // A variation selector and a keycap sequence both extend an
1040                // ASCII base from bytes at 0x80 and above.
1041                assert_eq!(
1042                    toks("1\u{fe0f}\u{20e3}z"),
1043                    vec![("1\u{fe0f}\u{20e3}".into(), 1), ("z".into(), 1)],
1044                    "a keycap keeps its ASCII digit"
1045                );
1046                // A zero-width joiner cannot follow ASCII in any real text,
1047                // but it must not be able to strand one either.
1048                assert_eq!(
1049                    toks("a\u{200d}b"),
1050                    vec![("a\u{200d}".into(), 1), ("b".into(), 1)],
1051                    "a ZWJ joins the ASCII base before it"
1052                );
1053                // The shortcut never applies across a non-ASCII byte, so a
1054                // wide character keeps its two columns and its neighbours
1055                // keep one each.
1056                assert_eq!(
1057                    toks("a\u{4e00}b"),
1058                    vec![("a".into(), 1), ("\u{4e00}".into(), 2), ("b".into(), 1)],
1059                    "a wide character between two ASCII ones is still wide"
1060                );
1061            }
1062        }
1063        // The ambiguous-width block is the only thing `eaw_wide` moves, and
1064        // it must not move the ASCII on either side of it.
1065        assert_eq!(
1066            text_tokens_eaw("a\u{2018}b", WidthMode::Wc, false),
1067            vec![("a".into(), 1), ("\u{2018}".into(), 1), ("b".into(), 1)]
1068        );
1069        assert_eq!(
1070            text_tokens_eaw("a\u{2018}b", WidthMode::Wc, true),
1071            vec![("a".into(), 1), ("\u{2018}".into(), 2), ("b".into(), 1)]
1072        );
1073    }
1074
1075    /// The fast path emits text only for printable ASCII.
1076    ///
1077    /// Its `0x20..0x7f` range excludes C0 controls and DEL, even though the
1078    /// shortcut runs before control handling. This test checks every
1079    /// seven-bit byte: printable ASCII has width one, controls stay controls,
1080    /// and ESC opens an escape sequence.
1081    #[test]
1082    fn controls_never_reach_the_ascii_shortcut() {
1083        for b in 0u8..=0x7f {
1084            let input = [b];
1085            let toks: Vec<_> = tokenize(&input, WidthMode::Wc, false).collect();
1086            match toks.as_slice() {
1087                [Token::Text { text, width }] => {
1088                    assert!(
1089                        (0x20..=0x7e).contains(&b),
1090                        "0x{b:02x} was emitted as text, but only printable ASCII may be"
1091                    );
1092                    assert_eq!(*text, &input[..]);
1093                    assert_eq!(*width, 1);
1094                }
1095                [Token::Control(c)] => {
1096                    assert_eq!(*c, b);
1097                    assert!(
1098                        b < 0x20 || b == 0x7f,
1099                        "0x{b:02x} is printable but was emitted as a control"
1100                    );
1101                }
1102                // A bare ESC opens a sequence with nothing in it.
1103                [Token::Escape(seq)] => assert_eq!(*seq, b"\x1b"),
1104                other => panic!("0x{b:02x} produced {other:?}"),
1105            }
1106        }
1107    }
1108
1109    /// An OSC payload may contain any UTF-8, and `0x9C` appears inside a great
1110    /// many characters as a continuation byte. Terminating the string there
1111    /// cut those characters in half, which left the trailing bytes of one
1112    /// outside the escape token - where the rest of the crate, reasonably,
1113    /// treated a token it had been told was text as UTF-8.
1114    #[test]
1115    fn a_continuation_byte_does_not_terminate_a_string_sequence() {
1116        // Every scanner that steps through a payload, not just the OSC one.
1117        // `scan_esc_intermediate` has the same bug and the same fix, and a
1118        // test that only builds OSC titles leaves half of it uncovered.
1119        const PREFIXES: &[&[u8]] = &[
1120            b"\x1b]0;", b"\x1bP", b"\x1b_", b"\x1b^", b"\x1bX", b"\x1b", b"\x1b ", b"\x1b#",
1121        ];
1122        for c in [
1123            '\u{2705}',
1124            '\u{2714}',
1125            '\u{2728}',
1126            '\u{171c}',
1127            '\u{4e00}',
1128            '\u{1f600}',
1129        ] {
1130            for prefix in PREFIXES {
1131                // Controls as bytes, the character as a character.
1132                let mut input = prefix.to_vec();
1133                input.extend_from_slice(c.to_string().as_bytes());
1134                input.extend_from_slice(b"\x07after");
1135                for t in tokenize(&input, WidthMode::Wc, false) {
1136                    let bytes = match t {
1137                        Token::Text { text, .. } | Token::Escape(text) => text,
1138                        Token::Control(_) => continue,
1139                    };
1140                    assert!(
1141                        std::str::from_utf8(bytes).is_ok(),
1142                        "{input:x?} produced a token split mid-character: {bytes:x?}"
1143                    );
1144                }
1145            }
1146        }
1147        // The OSC case in full: the sequence stays whole and the text after it
1148        // survives.
1149        for c in ['\u{2705}', '\u{2714}', '\u{2728}', '\u{171c}'] {
1150            let mut input = b"\x1b]0;Build ".to_vec();
1151            input.extend_from_slice(c.to_string().as_bytes());
1152            input.extend_from_slice(b"\x07after");
1153            let toks: Vec<_> = tokenize(&input, WidthMode::Wc, false).collect();
1154            let (escape, rest) = toks.split_first().expect("a token");
1155            let Token::Escape(seq) = escape else {
1156                panic!("expected the OSC to be one escape token, got {escape:?}")
1157            };
1158            let mut want = b"\x1b]0;Build ".to_vec();
1159            want.extend_from_slice(c.to_string().as_bytes());
1160            want.push(0x07);
1161            assert_eq!(*seq, &want[..], "the sequence is whole");
1162            assert_eq!(visible(rest), "after");
1163        }
1164        // A bare ESC takes the whole character after it, not its lead byte.
1165        let toks: Vec<_> = tokenize(b"\x1b\xe2\x9c\x85x", WidthMode::Wc, false).collect();
1166        assert_eq!(toks[0], Token::Escape(b"\x1b\xe2\x9c\x85"));
1167        // A `0x9C` on a character boundary is still an 8-bit ST.
1168        let toks: Vec<_> = tokenize(b"\x1b]0;t\x9cx", WidthMode::Wc, false).collect();
1169        assert!(matches!(toks[0], Token::Escape(b"\x1b]0;t\x9c")));
1170    }
1171
1172    /// A malformed byte in a payload must not eat the terminator.
1173    ///
1174    /// Stepping a whole character on the strength of a lead byte alone steps
1175    /// over whatever actually follows it. A stray high byte then swallowed
1176    /// the BEL or `ESC \` that ends the sequence, and with it every visible
1177    /// character after - and could consume a following legitimate CSI whole.
1178    #[test]
1179    fn a_malformed_byte_does_not_swallow_a_terminator() {
1180        let cases: &[(&[u8], &[u8])] = &[
1181            (b"\x1b]0;\xe0\x07Zz", b"\x1b]0;\xe0\x07"),
1182            (b"\x1b]0;\xf0\x1b\\Zz", b"\x1b]0;\xf0\x1b\\"),
1183            (b"\x1b]0;\xe0\x9cZz", b"\x1b]0;\xe0\x9c"),
1184            (b"\x1b_G\xf0\x9f\x1b\\Zz", b"\x1b_G\xf0\x9f\x1b\\"),
1185            (b"\x1b]0;\xc2\x07visible", b"\x1b]0;\xc2\x07"),
1186        ];
1187        for (input, escape) in cases {
1188            let toks: Vec<_> = tokenize(input, WidthMode::Wc, false).collect();
1189            assert_eq!(
1190                toks.first(),
1191                Some(&Token::Escape(escape)),
1192                "input {input:x?} did not stop at its terminator"
1193            );
1194            assert!(
1195                !visible(&toks).is_empty(),
1196                "input {input:x?} lost all its text"
1197            );
1198        }
1199        // A bare ESC must not consume the CSI that follows a malformed byte.
1200        let toks: Vec<_> = tokenize(b"\x1b\xe0\x1b[31mZ", WidthMode::Wc, false).collect();
1201        assert_eq!(toks[0], Token::Escape(b"\x1b\xe0"));
1202        assert_eq!(toks[1], Token::Escape(b"\x1b[31m"));
1203    }
1204
1205    /// Arbitrary bytes must not panic, and must not produce a token that
1206    /// splits a character.
1207    ///
1208    /// `tokenize` and `string_width` take `&[u8]`, so this is reachable from
1209    /// the public API by anyone reading a PTY. The run cache holds a byte
1210    /// offset and a validated `&str` and they have to agree about where a
1211    /// character starts; when the scan trusted a lead byte its continuations
1212    /// did not match, they disagreed, and slicing the run panicked.
1213    #[test]
1214    fn arbitrary_bytes_never_panic_or_split_a_character() {
1215        // The token stream is pinned, not just checked for well-formedness.
1216        // The desync had a quieter failure mode than the panic: a run cache
1217        // that disagrees with itself demotes perfectly good characters to
1218        // `Control` bytes, which round-trips and splits nothing while losing
1219        // every character after the first bad byte. These are the merge-base
1220        // streams, byte for byte.
1221        let pinned: &[(&[u8], &str)] = &[
1222            (
1223                b"\xe0\x20\x0d\xef\xb8\x8f",
1224                "Ce0 T[20]w1 C0d T[ef, b8, 8f]w0",
1225            ),
1226            (
1227                b"\xf0\x30\x0d\x5b\xe2\x9c\x85\x30",
1228                "Cf0 T[30]w1 C0d T[5b]w1 T[e2, 9c, 85]w2 T[30]w1",
1229            ),
1230            (
1231                b"\xe0\x41\x1b\x41\xc3\xa9",
1232                "Ce0 T[41]w1 E[1b, 41] T[c3, a9]w1",
1233            ),
1234            (b"\xcf\xc8\x9c", "Ccf T[c8, 9c]w1"),
1235            (
1236                b"\xe3\x61\x0a\xc4\x81\xc4\x81\x78",
1237                "Ce3 T[61]w1 C0a T[c4, 81]w1 T[c4, 81]w1 T[78]w1",
1238            ),
1239        ];
1240        for (seed, expected) in pinned {
1241            let rendered: Vec<String> = tokenize(seed, WidthMode::Grapheme, false)
1242                .map(|t| match t {
1243                    Token::Text { text, width } => format!("T{text:x?}w{width}"),
1244                    Token::Escape(b) => format!("E{b:x?}"),
1245                    Token::Control(c) => format!("C{c:02x}"),
1246                })
1247                .collect();
1248            assert_eq!(rendered.join(" "), *expected, "input {seed:x?}");
1249            string_width(seed, WidthMode::Grapheme, false);
1250            string_width(seed, WidthMode::Wc, false);
1251        }
1252
1253        // A cheap xorshift beats no fuzzing at all; the seeds above are the
1254        // cases it found, kept so a failure names itself.
1255        let mut state = 0x2545_f491_4f6c_dd1du64;
1256        let mut buf = [0u8; 24];
1257        for _ in 0..200_000 {
1258            let len = {
1259                state ^= state << 13;
1260                state ^= state >> 7;
1261                state ^= state << 17;
1262                (state % 24) as usize
1263            };
1264            for b in buf.iter_mut().take(len) {
1265                state ^= state << 13;
1266                state ^= state >> 7;
1267                state ^= state << 17;
1268                // Weighted towards the bytes that break things: lead bytes,
1269                // continuations, introducers, controls.
1270                *b =
1271                    match state % 4 {
1272                        0 => (state >> 8) as u8,
1273                        1 => [0x1b, 0x9b, 0x9d, 0x90, 0x98, 0x9e, 0x9f, 0x07]
1274                            [(state >> 8) as usize % 8],
1275                        2 => [0xc2, 0xe0, 0xe2, 0xf0, 0xf4, 0xff, 0x9c, 0x80]
1276                            [(state >> 8) as usize % 8],
1277                        _ => b"abc \n\r\\;0"[(state >> 8) as usize % 9],
1278                    };
1279            }
1280            let input = &buf[..len];
1281            for t in tokenize(input, WidthMode::Grapheme, false) {
1282                if let Token::Text { text, .. } = t {
1283                    assert!(
1284                        std::str::from_utf8(text).is_ok(),
1285                        "{input:x?} produced a split character {text:x?}"
1286                    );
1287                }
1288            }
1289            string_width(input, WidthMode::Grapheme, false);
1290        }
1291    }
1292}
1293
1294#[cfg(test)]
1295mod scaling {
1296    use super::util::*;
1297    use super::*;
1298
1299    /// Tokenize `bytes` fully and return the work it cost: bytes visited by
1300    /// the run scan, and bytes handed to UTF-8 validation.
1301    ///
1302    /// Both are needed. They are two passes over the same run refreshed by two
1303    /// different conditions, so either can go quadratic while the other stays
1304    /// linear - a tokenizer that scans once per run but revalidates the
1305    /// remainder once per token takes 4x as long for each doubling of the
1306    /// input and still reports exactly one scan visit per byte.
1307    ///
1308    /// The tokens are checked rather than discarded: these inputs are the
1309    /// shapes most likely to desynchronise the run cache, so measuring what
1310    /// they cost while ignoring what they produced would miss an answer that
1311    /// is wrong but cheap.
1312    fn scan_cost(bytes: &[u8]) -> (usize, usize) {
1313        let mut t = tokenize(bytes, WidthMode::Wc, false);
1314        let toks: Vec<_> = (&mut t).collect();
1315        // Measuring what a wrong answer costs is no use, so check it. The
1316        // discarded `visible` is the check: it panics on a text token that is
1317        // not whole UTF-8.
1318        let _ = visible(&toks);
1319        assert_eq!(rebuild(&toks), bytes, "tokens did not reassemble the input");
1320        (t.scanned, t.validated)
1321    }
1322
1323    /// Tokenizing one long line must cost work proportional to its length.
1324    ///
1325    /// This is not a micro-optimisation guard, it is a complexity one. The
1326    /// tokenizer used to find the end of the current run of text - and
1327    /// validate it as UTF-8 - once **per grapheme**, so a line of `n`
1328    /// characters did O(n) work `n` times. Nothing in the output changed, so
1329    /// no correctness test could see it; what it produced was a renderer
1330    /// whose frame time depended on the longest line in view. A 32 KB line
1331    /// (one JSON blob, one base64 payload, one minified file in a tool
1332    /// result) took 852 ms to wrap, which is a hundred dropped frames for
1333    /// one entry scrolling past.
1334    ///
1335    /// The work is counted rather than timed. A timing test here is a
1336    /// benchmark wearing a test's clothes: on a machine running anything else
1337    /// the linear code reads slower than the quadratic threshold it is meant
1338    /// to catch, so it fails for reasons that have nothing to do with the
1339    /// code. Byte visits are exact, identical on every machine, and are the
1340    /// thing that actually went quadratic.
1341    #[test]
1342    fn tokenizing_one_long_line_is_linear_in_its_length() {
1343        for n in [1_000usize, 4_000, 16_000, 64_000] {
1344            let line: String = std::iter::repeat_n('x', n).collect();
1345            assert_eq!(
1346                scan_cost(line.as_bytes()),
1347                (0, 0),
1348                "printable ASCII needs neither a run scan nor UTF-8 validation"
1349            );
1350        }
1351    }
1352
1353    /// The same bound for text that is not ASCII.
1354    ///
1355    /// The line above is all `x`, so every token leaves through the ASCII
1356    /// shortcut and the grapheme path is never entered. A run of non-ASCII
1357    /// characters is the one that walks `run` a cluster at a time, and it is
1358    /// the one the run cache exists for: `latin1`, wide, astral, and a base
1359    /// plus a combining mark, which is the case where a cluster spans more
1360    /// than one character.
1361    #[test]
1362    fn tokenizing_a_long_non_ascii_line_is_linear_too() {
1363        for unit in ["\u{e9}", "\u{4e00}", "\u{1f600}", "e\u{301}"] {
1364            for n in [1_000usize, 4_000, 16_000, 64_000] {
1365                let line = unit.repeat(n);
1366                let len = line.len();
1367                assert_eq!(
1368                    scan_cost(line.as_bytes()),
1369                    (len, len),
1370                    "{unit:?} x{n}: the run is found once and validated once"
1371                );
1372            }
1373        }
1374    }
1375
1376    /// The same bound with the UTF-8 broken.
1377    ///
1378    /// This is the case the first version of the run cache missed. It cached
1379    /// one boundary for two questions - where the run ends, and how much of
1380    /// it is valid UTF-8 - so a single malformed byte invalidated the run and
1381    /// sent the *scan* back over the rest of the input, once per byte. The
1382    /// tokens were right and the timing test above passed, because its input
1383    /// is valid; only text that was one byte short of valid stayed quadratic.
1384    #[test]
1385    fn malformed_utf8_is_linear_too() {
1386        for n in [1_000usize, 4_000, 16_000, 64_000] {
1387            // `C2` opens a two-byte character that `41` cannot continue: a
1388            // malformed byte every two bytes, all the way through.
1389            let bytes: Vec<u8> = std::iter::repeat_n([0xc2u8, 0x41], n / 2)
1390                .flatten()
1391                .collect();
1392            let (scanned, validated) = scan_cost(&bytes);
1393            assert!(
1394                scanned <= 2 * n,
1395                "scanning {n} malformed bytes visited {scanned} of them"
1396            );
1397            assert!(
1398                validated <= 2 * n,
1399                "validating {n} malformed bytes submitted {validated} of them"
1400            );
1401        }
1402    }
1403}