Skip to main content

ansi/
parse.rs

1//! Incremental VT/ANSI tokenizer.
2//!
3//! [`Parser::feed`] accepts a byte stream in chunks split at *any* boundary
4//! (mid-escape-sequence, mid-UTF-8 character) and yields [`Token`]s. The
5//! state machine follows the classic VT500-series design: C0 controls are
6//! *executed* (emitted as [`Token::Control`]) even when they arrive inside an
7//! escape sequence, `CAN`/`SUB` abort a sequence in progress, and a stray
8//! `ESC` restarts one. Malformed sequences are dropped, never emitted as
9//! text.
10
11/// One parsed unit of terminal output.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum Token {
14    /// A run of printable text (valid UTF-8; invalid input bytes become
15    /// U+FFFD). Runs are flushed at each control byte and at the end of each
16    /// `feed` call, so adjacent `Text` tokens are equivalent to one.
17    Text(String),
18    /// A C0 control byte executed on its own (BEL, BS, TAB, LF, CR, ...) or
19    /// DEL. `ESC` never appears here; it introduces a sequence.
20    Control(u8),
21    /// `ESC [ ...`: a CSI sequence with an optional private marker (`?`, `>`, `<`,
22    /// `=`), numeric parameters, intermediate bytes, and the final byte.
23    /// Missing parameters parse as 0; colon sub-parameters are truncated.
24    Csi {
25        private: Option<char>,
26        params: Vec<u16>,
27        intermediates: Vec<u8>,
28        final_byte: char,
29    },
30    /// A non-CSI escape: `ESC` + optional intermediates + final byte
31    /// (e.g. `ESC 7`, `ESC ( B`).
32    Esc {
33        intermediates: Vec<u8>,
34        final_byte: u8,
35    },
36    /// `ESC ] ...`: an OSC string, terminated by BEL or ST (`ESC \`).
37    /// Payload bytes are decoded as UTF-8 (lossily).
38    Osc(String),
39    /// A DCS/SOS/PM/APC string (`intro` is `P`, `X`, `^`, or `_`), swallowed
40    /// whole and surfaced raw rather than interpreted.
41    Other { intro: u8, data: Vec<u8> },
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45enum State {
46    Ground,
47    Escape,
48    EscapeIntermediate,
49    Csi,
50    Osc,
51    Str, // DCS / SOS / PM / APC
52}
53
54/// The incremental parser. Create one per stream and call
55/// [`feed`](Parser::feed) with each chunk as it arrives.
56#[derive(Debug, Default)]
57pub struct Parser {
58    state: Option<State>, // None == Ground via Default
59    text: String,
60    utf8: Vec<u8>,
61    intermediates: Vec<u8>,
62    param_bytes: Vec<u8>,
63    private: Option<char>,
64    str_intro: u8,
65    str_data: Vec<u8>,
66    str_esc: bool,
67}
68
69/// Cap accumulated sequence payloads so a hostile stream cannot grow memory
70/// without bound; sequences exceeding this are dropped.
71const MAX_SEQ: usize = 4096;
72
73impl Parser {
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    fn state(&self) -> State {
79        self.state.unwrap_or(State::Ground)
80    }
81
82    /// Feed a chunk of bytes; returns the tokens completed by this chunk.
83    /// Any accumulated text run is flushed at the end of the call; partial
84    /// escape sequences and partial UTF-8 characters are held for the next.
85    pub fn feed(&mut self, input: &[u8]) -> Vec<Token> {
86        let mut out = Vec::new();
87        for &b in input {
88            self.byte(b, &mut out);
89        }
90        self.flush_text(&mut out);
91        out
92    }
93
94    fn flush_text(&mut self, out: &mut Vec<Token>) {
95        if !self.text.is_empty() {
96            out.push(Token::Text(std::mem::take(&mut self.text)));
97        }
98    }
99
100    fn start(&mut self, state: State) {
101        self.state = Some(state);
102        self.intermediates.clear();
103        self.param_bytes.clear();
104        self.private = None;
105        self.str_data.clear();
106        self.str_esc = false;
107    }
108
109    fn byte(&mut self, b: u8, out: &mut Vec<Token>) {
110        match self.state() {
111            State::Ground => self.ground(b, out),
112            State::Escape => self.escape(b, out),
113            State::EscapeIntermediate => self.escape_intermediate(b, out),
114            State::Csi => self.csi(b, out),
115            State::Osc => self.string_byte(b, true, out),
116            State::Str => self.string_byte(b, false, out),
117        }
118    }
119
120    fn ground(&mut self, b: u8, out: &mut Vec<Token>) {
121        if !self.utf8.is_empty() {
122            if (0x80..0xC0).contains(&b) {
123                self.utf8.push(b);
124                let want = utf8_len(self.utf8[0]);
125                if self.utf8.len() == want {
126                    match std::str::from_utf8(&self.utf8) {
127                        Ok(s) => self.text.push_str(s),
128                        Err(_) => self.text.push('\u{FFFD}'),
129                    }
130                    self.utf8.clear();
131                }
132                return;
133            }
134            // Truncated sequence: the pending bytes were invalid.
135            self.text.push('\u{FFFD}');
136            self.utf8.clear();
137            // fall through to process `b` normally
138        }
139        match b {
140            0x1B => {
141                self.flush_text(out);
142                self.start(State::Escape);
143            }
144            0x00..=0x1F | 0x7F => {
145                self.flush_text(out);
146                out.push(Token::Control(b));
147            }
148            0x20..=0x7E => self.text.push(b as char),
149            0x80..=0xBF => self.text.push('\u{FFFD}'), // stray continuation
150            _ => {
151                if utf8_len(b) == 1 {
152                    self.text.push('\u{FFFD}'); // invalid lead (0xF8+)
153                } else {
154                    self.utf8.push(b);
155                }
156            }
157        }
158    }
159
160    fn escape(&mut self, b: u8, out: &mut Vec<Token>) {
161        match b {
162            b'[' => self.start(State::Csi),
163            b']' => self.start(State::Osc),
164            b'P' | b'X' | b'^' | b'_' => {
165                self.start(State::Str);
166                self.str_intro = b;
167            }
168            0x20..=0x2F => {
169                self.intermediates.push(b);
170                self.state = Some(State::EscapeIntermediate);
171            }
172            0x30..=0x7E => {
173                out.push(Token::Esc {
174                    intermediates: Vec::new(),
175                    final_byte: b,
176                });
177                self.state = None;
178            }
179            0x1B => self.start(State::Escape),
180            0x18 | 0x1A => self.state = None, // CAN / SUB abort
181            _ => out.push(Token::Control(b)), // execute C0 within sequence
182        }
183    }
184
185    fn escape_intermediate(&mut self, b: u8, out: &mut Vec<Token>) {
186        match b {
187            0x20..=0x2F => {
188                if self.intermediates.len() < 8 {
189                    self.intermediates.push(b);
190                }
191            }
192            0x30..=0x7E => {
193                out.push(Token::Esc {
194                    intermediates: std::mem::take(&mut self.intermediates),
195                    final_byte: b,
196                });
197                self.state = None;
198            }
199            0x1B => self.start(State::Escape),
200            0x18 | 0x1A => self.state = None,
201            _ => out.push(Token::Control(b)),
202        }
203    }
204
205    fn csi(&mut self, b: u8, out: &mut Vec<Token>) {
206        match b {
207            b'0'..=b'9' | b';' | b':' => {
208                if self.param_bytes.len() < MAX_SEQ {
209                    self.param_bytes.push(b);
210                } else {
211                    self.state = None; // oversized: drop the sequence
212                }
213            }
214            0x3C..=0x3F => {
215                if self.private.is_none() && self.param_bytes.is_empty() {
216                    self.private = Some(b as char);
217                } // later private markers are ignored, as most terminals do
218            }
219            0x20..=0x2F => {
220                if self.intermediates.len() < 8 {
221                    self.intermediates.push(b);
222                }
223            }
224            0x40..=0x7E => {
225                out.push(Token::Csi {
226                    private: self.private.take(),
227                    params: parse_params(&self.param_bytes),
228                    intermediates: std::mem::take(&mut self.intermediates),
229                    final_byte: b as char,
230                });
231                self.state = None;
232            }
233            0x1B => self.start(State::Escape),
234            0x18 | 0x1A => self.state = None,
235            _ => out.push(Token::Control(b)),
236        }
237    }
238
239    fn string_byte(&mut self, b: u8, is_osc: bool, out: &mut Vec<Token>) {
240        if self.str_esc {
241            self.str_esc = false;
242            if b == b'\\' {
243                self.dispatch_string(is_osc, out);
244                return;
245            }
246            if self.str_data.len() < MAX_SEQ {
247                self.str_data.push(0x1B);
248            }
249            // fall through: `b` is ordinary payload
250        }
251        match b {
252            0x07 => self.dispatch_string(is_osc, out), // BEL terminator
253            0x1B => self.str_esc = true,
254            _ => {
255                if self.str_data.len() < MAX_SEQ {
256                    self.str_data.push(b);
257                }
258            }
259        }
260    }
261
262    fn dispatch_string(&mut self, is_osc: bool, out: &mut Vec<Token>) {
263        let data = std::mem::take(&mut self.str_data);
264        if is_osc {
265            out.push(Token::Osc(String::from_utf8_lossy(&data).into_owned()));
266        } else {
267            out.push(Token::Other {
268                intro: self.str_intro,
269                data,
270            });
271        }
272        self.state = None;
273    }
274}
275
276/// Expected total length of a UTF-8 sequence from its lead byte (1 for
277/// invalid leads, so they consume exactly themselves).
278fn utf8_len(lead: u8) -> usize {
279    match lead {
280        0xC0..=0xDF => 2,
281        0xE0..=0xEF => 3,
282        0xF0..=0xF7 => 4,
283        _ => 1,
284    }
285}
286
287/// Split raw CSI parameter bytes on `;` into numbers. Empty parameters are
288/// 0; digits stop at a `:` sub-parameter; values saturate at `u16::MAX`.
289fn parse_params(bytes: &[u8]) -> Vec<u16> {
290    if bytes.is_empty() {
291        return Vec::new();
292    }
293    bytes
294        .split(|&b| b == b';')
295        .take(32)
296        .map(|seg| {
297            let mut v: u32 = 0;
298            for &b in seg {
299                if !b.is_ascii_digit() {
300                    break;
301                }
302                v = (v * 10 + (b - b'0') as u32).min(u16::MAX as u32);
303            }
304            v as u16
305        })
306        .collect()
307}