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