Skip to main content

rmux_core/input/
mod.rs

1//! tmux-compatible VT parser state machine.
2//!
3//! It implements DEC-style terminal parsing for tmux-compatible streams.
4//! This module provides the parser, state tables, command enums, parameter
5//! splitting, and SGR logic as pure safe Rust. Screen-write effects are
6//! delegated through the [`crate::input::ScreenWriter`] trait.
7
8mod cell;
9mod colour;
10mod commands;
11mod csi_helpers;
12mod dispatch;
13pub mod mode;
14mod params;
15mod passthrough;
16mod recovery;
17mod sgr;
18mod states;
19mod tables;
20#[cfg(test)]
21mod tests;
22mod writer;
23
24pub use cell::{CellState, GridAttr, SavedState};
25pub use colour::{
26    colour_join_rgb, Colour, COLOUR_DEFAULT, COLOUR_FLAG_256, COLOUR_FLAG_RGB, COLOUR_NONE,
27    COLOUR_TERMINAL,
28};
29pub use dispatch::{CsiCommand, DcsPayload, EscCommand, InputAction, OscCommand, ScreenWriter};
30pub use params::{InputParam, ParamType};
31pub use states::InputState;
32
33use params::ParamList;
34use states::Transition;
35
36use crate::terminal_passthrough::MAX_TERMINAL_PASSTHROUGH_PAYLOAD_BYTES;
37
38/// Maximum number of parameters in a CSI/DCS sequence.
39const PARAM_LIST_MAX: usize = 24;
40
41/// Intermediate buffer capacity.
42const INTERM_BUF_MAX: usize = 4;
43
44/// Initial input buffer size.
45const INPUT_BUF_START: usize = 32;
46
47/// Maximum input buffer size (1 MiB, matching `INPUT_BUF_DEFAULT_SIZE`).
48const INPUT_BUF_MAX: usize = 1_048_576;
49
50/// Parameter buffer capacity for raw parameter bytes.
51const PARAM_BUF_MAX: usize = 64;
52
53/// Parser flags.
54const INPUT_DISCARD: u32 = 0x1;
55/// Last printable character was emitted (for REP).
56const INPUT_LAST: u32 = 0x2;
57
58/// Type of string terminator seen for OSC/DCS.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum InputEndType {
61    /// ESC \\ (ST)
62    St,
63    /// BEL (0x07)
64    Bel,
65}
66
67/// Colour slots reported to OSC 10/11/12 queries.
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum OscColourSlot {
70    /// OSC 10 — default foreground.
71    Foreground,
72    /// OSC 11 — default background.
73    Background,
74    /// OSC 12 — cursor colour.
75    Cursor,
76}
77
78impl OscColourSlot {
79    pub(crate) const fn osc_number(self) -> u32 {
80        match self {
81            Self::Foreground => 10,
82            Self::Background => 11,
83            Self::Cursor => 12,
84        }
85    }
86
87    const fn index(self) -> usize {
88        match self {
89            Self::Foreground => 0,
90            Self::Background => 1,
91            Self::Cursor => 2,
92        }
93    }
94}
95
96/// Per-pane VT input parser, matching tmux `input_ctx`.
97pub struct InputParser {
98    /// Current parser state.
99    state: InputState,
100    /// Parser flags (INPUT_DISCARD, INPUT_LAST).
101    flags: u32,
102
103    /// Current character being processed.
104    ch: u8,
105
106    /// Intermediate character buffer.
107    interm_buf: [u8; INTERM_BUF_MAX],
108    interm_len: usize,
109
110    /// Raw parameter buffer.
111    param_buf: [u8; PARAM_BUF_MAX],
112    param_len: usize,
113
114    /// Dynamic input/string buffer.
115    input_buf: Vec<u8>,
116    input_buf_max: usize,
117    /// Which terminator ended the string.
118    input_end: InputEndType,
119
120    /// Parsed parameter list.
121    param_list: ParamList,
122
123    /// Cell state (current attributes, character set, etc.).
124    cell: CellState,
125    /// Saved cell state for DECSC/DECRC.
126    saved: SavedState,
127
128    /// UTF-8 accumulator.
129    utf8_buf: [u8; 4],
130    utf8_len: u8,
131    utf8_expected: u8,
132    utf8_started: bool,
133
134    /// Last printed character data for REP.
135    last_char: Option<char>,
136    /// Whether this parse call emitted a printable character before REP.
137    printed_in_current_parse: bool,
138    /// A typed REP transition consumed parser state that ANSI cannot restore.
139    recovery_rebase_required: bool,
140
141    /// Bytes accumulated since last ground state, for control-mode catch-up.
142    since_ground: Vec<u8>,
143
144    /// Whether ground timer would be active (modeled as flag; actual timer
145    /// is a server-side concern).
146    ground_timer_active: bool,
147
148    /// Reply buffer: replies to be sent back to the PTY.
149    reply_buf: Vec<u8>,
150    /// Dropped terminal passthrough events caused by parser string limits.
151    terminal_passthrough_dropped_count: u64,
152
153    /// Application-set OSC 10/11/12 colours (fg, bg, cursor). The daemon does
154    /// not invent colours for an unknown attached terminal palette.
155    osc_colours: [Option<String>; 3],
156}
157
158impl InputParser {
159    /// Creates a new parser in the ground state with default cell attributes.
160    #[must_use]
161    pub fn new() -> Self {
162        Self {
163            state: InputState::Ground,
164            flags: 0,
165            ch: 0,
166            interm_buf: [0; INTERM_BUF_MAX],
167            interm_len: 0,
168            param_buf: [0; PARAM_BUF_MAX],
169            param_len: 0,
170            input_buf: Vec::with_capacity(INPUT_BUF_START),
171            input_buf_max: INPUT_BUF_MAX,
172            input_end: InputEndType::St,
173            param_list: ParamList::new(),
174            cell: CellState::default(),
175            saved: SavedState::default(),
176            utf8_buf: [0; 4],
177            utf8_len: 0,
178            utf8_expected: 0,
179            utf8_started: false,
180            last_char: None,
181            printed_in_current_parse: false,
182            recovery_rebase_required: false,
183            since_ground: Vec::new(),
184            ground_timer_active: false,
185            reply_buf: Vec::new(),
186            terminal_passthrough_dropped_count: 0,
187            osc_colours: [None, None, None],
188        }
189    }
190
191    /// Returns an application-defined colour for an OSC 10/11/12 query.
192    pub(crate) fn osc_colour(&self, slot: OscColourSlot) -> Option<&str> {
193        self.osc_colours[slot.index()].as_deref()
194    }
195
196    /// Records an application-set OSC 10/11/12 colour so later queries
197    /// round-trip the value.
198    pub(crate) fn set_osc_colour(&mut self, slot: OscColourSlot, value: &str) {
199        // A stored colour is reflected verbatim to every later query, so bound
200        // it to a sane colour-spec length: otherwise a pane could set a huge
201        // (up to input-buffer-sized) value and then flood queries in one read
202        // batch, amplifying it into unbounded reply memory and OOM-ing the
203        // daemon. A real X11 / `rgb:` / `#rrggbb` spec is far shorter; longer
204        // values are not colours and are left unstored, so queries keep
205        // answering the prior value or remain silent when the palette is
206        // unknown.
207        const OSC_COLOUR_MAX_LEN: usize = 64;
208        if value.len() > OSC_COLOUR_MAX_LEN {
209            return;
210        }
211        self.osc_colours[slot.index()] = Some(value.to_owned());
212    }
213
214    /// Resets an OSC colour back to unknown (OSC 110/111/112).
215    pub(crate) fn reset_osc_colour(&mut self, slot: OscColourSlot) {
216        self.osc_colours[slot.index()] = None;
217    }
218
219    /// Updates the maximum regular string buffer size used for OSC/DCS input.
220    pub fn set_input_buffer_limit(&mut self, limit: usize) {
221        self.input_buf_max = limit.max(INPUT_BUF_START);
222    }
223
224    pub(crate) const fn configured_input_buffer_limit(&self) -> usize {
225        self.input_buf_max
226    }
227
228    /// Returns the current parser state.
229    #[must_use]
230    pub fn state(&self) -> InputState {
231        self.state
232    }
233
234    /// Returns and drains accumulated reply bytes.
235    pub fn take_replies(&mut self) -> Vec<u8> {
236        std::mem::take(&mut self.reply_buf)
237    }
238
239    /// Returns and drains terminal passthrough drops caused by parser limits.
240    pub(crate) fn take_terminal_passthrough_dropped_count(&mut self) -> u64 {
241        let dropped = self.terminal_passthrough_dropped_count;
242        self.terminal_passthrough_dropped_count = 0;
243        dropped
244    }
245
246    /// Returns and drains accumulated since-ground bytes.
247    pub fn take_since_ground(&mut self) -> Vec<u8> {
248        std::mem::take(&mut self.since_ground)
249    }
250
251    /// Returns any bytes still buffered in an incomplete parser state.
252    #[must_use]
253    pub fn pending_bytes(&self) -> Vec<u8> {
254        self.pending_bytes_ref().to_vec()
255    }
256
257    /// Borrows bytes still buffered in an incomplete parser state.
258    #[must_use]
259    pub(crate) fn pending_bytes_ref(&self) -> &[u8] {
260        if self.state != InputState::Ground {
261            return &self.since_ground;
262        }
263        if self.utf8_started {
264            return &self.utf8_buf[..usize::from(self.utf8_len)];
265        }
266        &[]
267    }
268
269    /// Returns true if the ground timer should be running.
270    #[must_use]
271    pub fn ground_timer_active(&self) -> bool {
272        self.ground_timer_active
273    }
274
275    /// Called by the server when the ground timer expires (5s timeout).
276    pub fn ground_timer_expired(&mut self) {
277        self.reset_to_ground();
278    }
279
280    /// Resets the parser to ground state.
281    pub fn reset_to_ground(&mut self) {
282        self.clear();
283        self.state = InputState::Ground;
284        self.flags = 0;
285        self.enter_ground();
286    }
287
288    /// Returns a reference to the current cell state.
289    #[must_use]
290    pub fn cell_state(&self) -> &CellState {
291        &self.cell
292    }
293
294    /// Returns the state restored by DECRC/SCP.
295    #[must_use]
296    pub const fn saved_state(&self) -> &SavedState {
297        &self.saved
298    }
299
300    pub(crate) fn plain_output_forwarding_safe(&self) -> bool {
301        self.state == InputState::Ground && !self.utf8_started && self.cell == CellState::default()
302    }
303
304    /// Parse a buffer of bytes, dispatching actions to the screen writer.
305    pub fn parse<W: ScreenWriter + ?Sized>(&mut self, buf: &[u8], writer: &mut W) {
306        self.printed_in_current_parse = false;
307        let mut index = 0;
308        while index < buf.len() {
309            if self.state == InputState::Ground && !self.utf8_started {
310                let printable_end = buf[index..]
311                    .iter()
312                    .position(|byte| !byte.is_ascii_graphic() && *byte != b' ')
313                    .map_or(buf.len(), |offset| index + offset);
314                if printable_end > index {
315                    self.handle_printable_ascii_run(&buf[index..printable_end], writer);
316                    index = printable_end;
317                    continue;
318                }
319                if self.handle_ground_c0_fast_path(buf[index], writer) {
320                    index += 1;
321                    continue;
322                }
323            }
324
325            self.ch = buf[index];
326            let transition = self.find_transition();
327            self.execute_transition(transition, writer);
328            index += 1;
329        }
330    }
331
332    fn handle_printable_ascii_run<W: ScreenWriter + ?Sized>(
333        &mut self,
334        bytes: &[u8],
335        writer: &mut W,
336    ) {
337        debug_assert_eq!(self.state, InputState::Ground);
338        let set = if self.cell.set == 0 {
339            self.cell.g0set
340        } else {
341            self.cell.g1set
342        };
343        let acs = set != 0;
344        writer.collect_add_ascii_run(bytes, &self.cell, acs);
345        if let Some(&last) = bytes.last() {
346            self.last_char = Some(char::from(last));
347            self.printed_in_current_parse = true;
348        }
349        self.flags |= INPUT_LAST;
350    }
351
352    fn handle_ground_c0_fast_path<W: ScreenWriter + ?Sized>(
353        &mut self,
354        byte: u8,
355        writer: &mut W,
356    ) -> bool {
357        match byte {
358            0x0a..=0x0c => {
359                writer.collect_end();
360                writer.linefeed(false, self.cell.bg());
361                if writer.current_mode() & mode::MODE_CRLF != 0 {
362                    writer.carriage_return();
363                }
364            }
365            0x0d => {
366                writer.collect_end();
367                writer.carriage_return();
368            }
369            _ => return false,
370        }
371        self.flags &= !INPUT_LAST;
372        true
373    }
374
375    fn find_transition(&self) -> Transition {
376        self.state.transition_for_byte(self.ch)
377    }
378
379    fn execute_transition<W: ScreenWriter + ?Sized>(&mut self, trans: Transition, writer: &mut W) {
380        // Any state except print stops collect_end equivalent.
381        if !matches!(
382            trans.handler,
383            states::Handler::Print | states::Handler::TopBitSet
384        ) {
385            writer.collect_end();
386        }
387
388        // Execute handler; if it returns true, skip state transition.
389        let skip_state = match trans.handler {
390            states::Handler::None => false,
391            states::Handler::Print => self.handle_print(writer),
392            states::Handler::C0Dispatch => self.handle_c0_dispatch(writer),
393            states::Handler::EscDispatch => self.handle_esc_dispatch(writer),
394            states::Handler::CsiDispatch => self.handle_csi_dispatch(writer),
395            states::Handler::DcsDispatch => self.handle_dcs_dispatch(writer),
396            states::Handler::Intermediate => self.handle_intermediate(),
397            states::Handler::Parameter => self.handle_parameter(),
398            states::Handler::Input => self.handle_input(),
399            states::Handler::TopBitSet => self.handle_top_bit_set(writer),
400            states::Handler::EndBel => self.handle_end_bel(),
401        };
402
403        if skip_state {
404            return;
405        }
406
407        if self.should_recover_tmux_passthrough_osc_bel() {
408            self.input_end = InputEndType::Bel;
409            self.handle_dcs_dispatch(writer);
410            self.set_state(InputState::Ground, writer);
411            return;
412        }
413
414        if let Some(next) = trans.next_state {
415            self.set_state(next, writer);
416        }
417
418        // If not in ground state, save byte to since_ground.
419        if self.state != InputState::Ground && self.since_ground.len() < self.input_buf_max {
420            self.since_ground.push(self.ch);
421        }
422    }
423
424    fn set_state<W: ScreenWriter + ?Sized>(&mut self, next: InputState, writer: &mut W) {
425        // Call exit handler for current state.
426        self.exit_state(writer);
427        self.state = next;
428        // Call enter handler for new state.
429        self.enter_state(writer);
430    }
431
432    fn enter_state<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) {
433        match self.state {
434            InputState::Ground => self.enter_ground(),
435            InputState::EscEnter => self.clear(),
436            InputState::CsiEnter => self.clear(),
437            InputState::DcsEnter => self.enter_dcs(),
438            InputState::OscString => self.enter_osc(),
439            InputState::ApcString => self.enter_apc(),
440            InputState::RenameString => self.enter_rename(),
441            InputState::ConsumeSt => self.enter_rename(), // same as rename in tmux
442            _ => {}
443        }
444        let _ = writer; // writer not needed for enter handlers currently
445    }
446
447    fn exit_state<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) {
448        match self.state {
449            InputState::OscString => self.exit_osc(writer),
450            InputState::ApcString => self.exit_apc(writer),
451            InputState::RenameString => self.exit_rename(writer),
452            _ => {}
453        }
454    }
455
456    fn clear(&mut self) {
457        self.ground_timer_active = false;
458        self.interm_buf = [0; INTERM_BUF_MAX];
459        self.interm_len = 0;
460        self.param_buf = [0; PARAM_BUF_MAX];
461        self.param_len = 0;
462        self.input_buf.clear();
463        self.input_end = InputEndType::St;
464        self.flags &= !INPUT_DISCARD;
465    }
466
467    fn enter_ground(&mut self) {
468        self.ground_timer_active = false;
469        self.since_ground.clear();
470        // Shrink input buffer back to start size.
471        if self.input_buf.capacity() > INPUT_BUF_START {
472            self.input_buf = Vec::with_capacity(INPUT_BUF_START);
473        }
474    }
475
476    fn enter_dcs(&mut self) {
477        self.clear();
478        self.ground_timer_active = true;
479        self.flags &= !INPUT_LAST;
480    }
481
482    fn enter_osc(&mut self) {
483        self.clear();
484        self.ground_timer_active = true;
485        self.flags &= !INPUT_LAST;
486    }
487
488    fn enter_apc(&mut self) {
489        self.clear();
490        self.ground_timer_active = true;
491        self.flags &= !INPUT_LAST;
492    }
493
494    fn enter_rename(&mut self) {
495        self.clear();
496        self.ground_timer_active = true;
497        self.flags &= !INPUT_LAST;
498    }
499
500    fn exit_osc<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) {
501        if self.flags & INPUT_DISCARD != 0 {
502            return;
503        }
504        dispatch::dispatch_osc(self, writer);
505    }
506
507    fn exit_apc<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) {
508        if self.flags & INPUT_DISCARD != 0 {
509            return;
510        }
511        if passthrough::is_kitty_graphics_apc(&self.input_buf) {
512            writer.apc_passthrough(&self.input_buf);
513            return;
514        }
515        let buf = String::from_utf8_lossy(&self.input_buf).into_owned();
516        writer.set_title(&buf);
517    }
518
519    fn exit_rename<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) {
520        if self.flags & INPUT_DISCARD != 0 {
521            return;
522        }
523        let buf = String::from_utf8_lossy(&self.input_buf).into_owned();
524        writer.set_window_name(&buf);
525    }
526
527    /// Stop any in-progress UTF-8 sequence and emit U+FFFD.
528    fn stop_utf8<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) {
529        if self.utf8_started {
530            writer.collect_add('\u{FFFD}', &self.cell);
531            self.utf8_started = false;
532            self.utf8_len = 0;
533            self.utf8_expected = 0;
534        }
535    }
536
537    fn handle_print<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) -> bool {
538        self.stop_utf8(writer);
539
540        let ch = self.ch as char;
541        let set = if self.cell.set == 0 {
542            self.cell.g0set
543        } else {
544            self.cell.g1set
545        };
546
547        writer.collect_add_with_charset(ch, &self.cell, set != 0);
548
549        self.last_char = Some(ch);
550        self.printed_in_current_parse = true;
551        self.flags |= INPUT_LAST;
552
553        false
554    }
555
556    fn handle_intermediate(&mut self) -> bool {
557        if self.interm_len >= INTERM_BUF_MAX - 1 {
558            self.flags |= INPUT_DISCARD;
559        } else {
560            self.interm_buf[self.interm_len] = self.ch;
561            self.interm_len += 1;
562        }
563        false
564    }
565
566    fn handle_parameter(&mut self) -> bool {
567        if self.param_len >= PARAM_BUF_MAX - 1 {
568            self.flags |= INPUT_DISCARD;
569        } else {
570            self.param_buf[self.param_len] = self.ch;
571            self.param_len += 1;
572        }
573        false
574    }
575
576    fn handle_input(&mut self) -> bool {
577        let escaped_dcs_byte = self.state == InputState::DcsEscape;
578        let bytes_to_push = if escaped_dcs_byte && self.ch != 0x1b {
579            2
580        } else {
581            1
582        };
583        let input_limit = self.input_buffer_limit();
584        if self.input_buf.len() + bytes_to_push >= input_limit {
585            if self.flags & INPUT_DISCARD == 0 && self.is_terminal_passthrough_string() {
586                self.terminal_passthrough_dropped_count =
587                    self.terminal_passthrough_dropped_count.saturating_add(1);
588            }
589            self.flags |= INPUT_DISCARD;
590        } else if escaped_dcs_byte && self.ch == 0x1b {
591            self.input_buf.push(0x1b);
592        } else if escaped_dcs_byte {
593            self.input_buf.push(0x1b);
594            self.input_buf.push(self.ch);
595        } else {
596            self.input_buf.push(self.ch);
597        }
598        false
599    }
600
601    fn input_buffer_limit(&self) -> usize {
602        if self.is_terminal_passthrough_string() {
603            return MAX_TERMINAL_PASSTHROUGH_PAYLOAD_BYTES;
604        }
605        self.input_buf_max
606    }
607
608    fn is_terminal_passthrough_string(&self) -> bool {
609        (self.state == InputState::ApcString && passthrough::is_kitty_graphics_apc(&self.input_buf))
610            || (matches!(self.state, InputState::DcsHandler | InputState::DcsEscape)
611                && self.interm_len == 0
612                && (self.input_buf.first() == Some(&b'q') || self.input_buf.starts_with(b"tmux;")))
613    }
614
615    fn should_recover_tmux_passthrough_osc_bel(&self) -> bool {
616        if self.state != InputState::DcsHandler || self.ch != 0x07 || self.interm_len != 0 {
617            return false;
618        }
619
620        let Some(payload) = self.input_buf.strip_prefix(b"tmux;") else {
621            return false;
622        };
623
624        payload.starts_with(b"\x1b]") || payload.first() == Some(&0x9d)
625    }
626
627    fn handle_end_bel(&mut self) -> bool {
628        self.input_end = InputEndType::Bel;
629        false
630    }
631
632    fn handle_c0_dispatch<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) -> bool {
633        self.stop_utf8(writer);
634        dispatch::dispatch_c0(self, writer);
635        self.flags &= !INPUT_LAST;
636        false
637    }
638
639    fn handle_esc_dispatch<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) -> bool {
640        if self.flags & INPUT_DISCARD != 0 {
641            return false;
642        }
643        dispatch::dispatch_esc(self, writer);
644        self.flags &= !INPUT_LAST;
645        false
646    }
647
648    fn handle_csi_dispatch<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) -> bool {
649        if self.flags & INPUT_DISCARD != 0 {
650            return false;
651        }
652        dispatch::dispatch_csi(self, writer);
653        self.flags &= !INPUT_LAST;
654        false
655    }
656
657    fn handle_dcs_dispatch<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) -> bool {
658        if self.flags & INPUT_DISCARD != 0 {
659            return false;
660        }
661        dispatch::dispatch_dcs(self, writer);
662        false
663    }
664
665    fn handle_top_bit_set<W: ScreenWriter + ?Sized>(&mut self, writer: &mut W) -> bool {
666        self.flags &= !INPUT_LAST;
667
668        if !self.utf8_started {
669            self.utf8_started = true;
670            self.utf8_len = 0;
671            // Determine expected byte count from first byte.
672            let expected = if self.ch & 0xE0 == 0xC0 {
673                2
674            } else if self.ch & 0xF0 == 0xE0 {
675                3
676            } else if self.ch & 0xF8 == 0xF0 {
677                4
678            } else {
679                // Invalid start byte.
680                self.stop_utf8(writer);
681                return false;
682            };
683            self.utf8_expected = expected;
684            self.utf8_buf[0] = self.ch;
685            self.utf8_len = 1;
686            return false;
687        }
688
689        // Continuation byte.
690        if self.ch & 0xC0 != 0x80 {
691            // Not a valid continuation: emit replacement and re-process.
692            self.stop_utf8(writer);
693            // Re-start UTF-8 with current byte if it's a start byte.
694            if self.ch >= 0x80 {
695                return self.handle_top_bit_set(writer);
696            }
697            return false;
698        }
699
700        self.utf8_buf[self.utf8_len as usize] = self.ch;
701        self.utf8_len += 1;
702
703        if self.utf8_len < self.utf8_expected {
704            return false; // More bytes expected.
705        }
706
707        // Complete: decode.
708        self.utf8_started = false;
709        let bytes = &self.utf8_buf[..self.utf8_len as usize];
710        let s = match std::str::from_utf8(bytes) {
711            Ok(s) => s,
712            Err(_) => {
713                writer.collect_add('\u{FFFD}', &self.cell);
714                return false;
715            }
716        };
717        let c = match s.chars().next() {
718            Some(c) => c,
719            None => {
720                writer.collect_add('\u{FFFD}', &self.cell);
721                return false;
722            }
723        };
724
725        writer.collect_add(c, &self.cell);
726
727        self.last_char = Some(c);
728        self.printed_in_current_parse = true;
729        self.flags |= INPUT_LAST;
730
731        false
732    }
733
734    /// Append a reply string to the reply buffer.
735    fn reply(&mut self, s: &str) {
736        self.reply_buf.extend_from_slice(s.as_bytes());
737    }
738
739    /// Interm buf as a string slice for table lookups.
740    fn interm_str(&self) -> &[u8] {
741        &self.interm_buf[..self.interm_len]
742    }
743
744    fn note_effective_rep(&mut self) {
745        if !self.printed_in_current_parse {
746            self.recovery_rebase_required = true;
747        }
748    }
749
750    /// Returns and clears the signal that raw recovery needs an authoritative
751    /// rebase because REP consumed parser-only state from an earlier feed.
752    #[must_use]
753    pub(crate) fn take_recovery_rebase_required(&mut self) -> bool {
754        std::mem::take(&mut self.recovery_rebase_required)
755    }
756}
757
758impl Default for InputParser {
759    fn default() -> Self {
760        Self::new()
761    }
762}