cargo : anstyle-parse @ 1.0.0
PE Patrick Elsen signed 2026-05-27 published 2026-05-27

src/lib.rs

443 lines · rust · 1 line annotation

//! Parser for implementing virtual terminal emulators//!//! [`Parser`] is implemented according to [Paul Williams' ANSI parser//! state machine]. The state machine doesn't assign meaning to the parsed data//! and is thus not itself sufficient for writing a terminal emulator. Instead,//! it is expected that an implementation of [`Perform`] is provided which does//! something useful with the parsed data. The [`Parser`] handles the book//! keeping, and the [`Perform`] gets to simply handle actions.//!//! # Examples//!//! For an example of using the [`Parser`] please see the examples folder. The example included//! there simply logs all the actions [`Perform`] does. One quick thing to see it in action is to//! pipe `vim` into it//!//! ```sh//! cargo build --release --example parselog//! vim | target/release/examples/parselog//! ```//!//! Just type `:q` to exit.//!//! # Differences from original state machine description//!//! * UTF-8 Support for Input//! * OSC Strings can be terminated by 0x07//! * Only supports 7-bit codes. Some 8-bit codes are still supported, but they no longer work in//!   all states.//!//! [Paul Williams' ANSI parser state machine]: https://vt100.net/emu/dec_ansi_parser#![cfg_attr(not(test), no_std)]#![cfg_attr(docsrs, feature(doc_cfg))]#![allow(missing_docs)]#![warn(clippy::print_stderr)]#![warn(clippy::print_stdout)]#[cfg(not(feature = "core"))]extern crate alloc;use core::mem::MaybeUninit;#[cfg(feature = "core")]use arrayvec::ArrayVec;#[cfg(feature = "utf8")]use utf8parse as utf8;mod params;pub mod state;pub use params::{Params, ParamsIter};use state::{state_change, Action, State};const MAX_INTERMEDIATES: usize = 2;const MAX_OSC_PARAMS: usize = 16;#[cfg(feature = "core")]const MAX_OSC_RAW: usize = 1024;/// Parser for raw _VTE_ protocol which delegates actions to a [`Perform`]#[allow(unused_qualifications)]#[derive(Default, Clone, Debug, PartialEq, Eq)]pub struct Parser<C = DefaultCharAccumulator> {    state: State,    intermediates: [u8; MAX_INTERMEDIATES],    intermediate_idx: usize,    params: Params,    param: u16,    #[cfg(feature = "core")]    osc_raw: ArrayVec<u8, MAX_OSC_RAW>,    #[cfg(not(feature = "core"))]    osc_raw: alloc::vec::Vec<u8>,    osc_params: [(usize, usize); MAX_OSC_PARAMS],    osc_num_params: usize,    ignoring: bool,    utf8_parser: C,}impl<C> Parser<C>where    C: CharAccumulator,{    /// Create a new Parser    pub fn new() -> Parser {        Parser::default()    }    #[inline]    fn params(&self) -> &Params {        &self.params    }    #[inline]    fn intermediates(&self) -> &[u8] {        &self.intermediates[..self.intermediate_idx]    }    /// Advance the parser state    ///    /// Requires a [`Perform`] in case `byte` triggers an action    #[inline]    pub fn advance<P: Perform>(&mut self, performer: &mut P, byte: u8) {        // Utf8 characters are handled out-of-band.        if let State::Utf8 = self.state {            self.process_utf8(performer, byte);            return;        }        let (state, action) = state_change(self.state, byte);        self.perform_state_change(performer, state, action, byte);    }    #[inline]    fn process_utf8<P>(&mut self, performer: &mut P, byte: u8)    where        P: Perform,    {        if let Some(c) = self.utf8_parser.add(byte) {            performer.print(c);            self.state = State::Ground;        }    }    #[inline]    fn perform_state_change<P>(&mut self, performer: &mut P, state: State, action: Action, byte: u8)    where        P: Perform,    {        match state {            State::Anywhere => {                // Just run the action                self.perform_action(performer, action, byte);            }            state => {                match self.state {                    State::DcsPassthrough => {                        self.perform_action(performer, Action::Unhook, byte);                    }                    State::OscString => {                        self.perform_action(performer, Action::OscEnd, byte);                    }                    _ => (),                }                match action {                    Action::Nop => (),                    action => {                        self.perform_action(performer, action, byte);                    }                }                match state {                    State::CsiEntry | State::DcsEntry | State::Escape => {                        self.perform_action(performer, Action::Clear, byte);                    }                    State::DcsPassthrough => {                        self.perform_action(performer, Action::Hook, byte);                    }                    State::OscString => {                        self.perform_action(performer, Action::OscStart, byte);                    }                    _ => (),                }                // Assume the new state                self.state = state;            }        }    }
    /// Separate method for `osc_dispatch` that borrows self as read-only    ///    /// The aliasing is needed here for multiple slices into `self.osc_raw`    #[inline]    fn osc_dispatch<P: Perform>(&self, performer: &mut P, byte: u8) {        let mut slices: [MaybeUninit<&[u8]>; MAX_OSC_PARAMS] =            unsafe { MaybeUninit::uninit().assume_init() };        for (i, slice) in slices.iter_mut().enumerate().take(self.osc_num_params) {            let indices = self.osc_params[i];            *slice = MaybeUninit::new(&self.osc_raw[indices.0..indices.1]);        }        unsafe {            let num_params = self.osc_num_params;            let params = &slices[..num_params] as *const [MaybeUninit<&[u8]>] as *const [&[u8]];            performer.osc_dispatch(&*params, byte == 0x07);        }    }
Line 170–188

osc_dispatch builds a temporary [MaybeUninit<&[u8]>; MAX_OSC_PARAMS] (line 175-176) via the documented "array of MaybeUninit" idiom (MaybeUninit::uninit().assume_init()), initialises exactly self.osc_num_params elements with MaybeUninit::new(...) (line 178-181), and then on line 183-187 casts a slice of length num_params = self.osc_num_params to &[&[u8]] to pass to the user's Perform::osc_dispatch without allocating. The cast is sound because every element in 0..num_params was just initialised. The block lacks a per-block // SAFETY: comment (FINDING-1) but justifies uses-unsafe, unsafe-safe, unsafe-minimal.

    #[inline]    fn perform_action<P: Perform>(&mut self, performer: &mut P, action: Action, byte: u8) {        match action {            Action::Print => performer.print(byte as char),            Action::Execute => performer.execute(byte),            Action::Hook => {                if self.params.is_full() {                    self.ignoring = true;                } else {                    self.params.push(self.param);                }                performer.hook(self.params(), self.intermediates(), self.ignoring, byte);            }            Action::Put => performer.put(byte),            Action::OscStart => {                self.osc_raw.clear();                self.osc_num_params = 0;            }            Action::OscPut => {                #[cfg(feature = "core")]                {                    if self.osc_raw.is_full() {                        return;                    }                }                let idx = self.osc_raw.len();                // Param separator                if byte == b';' {                    let param_idx = self.osc_num_params;                    match param_idx {                        // Only process up to MAX_OSC_PARAMS                        MAX_OSC_PARAMS => return,                        // First param is special - 0 to current byte index                        0 => {                            self.osc_params[param_idx] = (0, idx);                        }                        // All other params depend on previous indexing                        _ => {                            let prev = self.osc_params[param_idx - 1];                            let begin = prev.1;                            self.osc_params[param_idx] = (begin, idx);                        }                    }                    self.osc_num_params += 1;                } else {                    self.osc_raw.push(byte);                }            }            Action::OscEnd => {                let param_idx = self.osc_num_params;                let idx = self.osc_raw.len();                match param_idx {                    // Finish last parameter if not already maxed                    MAX_OSC_PARAMS => (),                    // First param is special - 0 to current byte index                    0 => {                        self.osc_params[param_idx] = (0, idx);                        self.osc_num_params += 1;                    }                    // All other params depend on previous indexing                    _ => {                        let prev = self.osc_params[param_idx - 1];                        let begin = prev.1;                        self.osc_params[param_idx] = (begin, idx);                        self.osc_num_params += 1;                    }                }                self.osc_dispatch(performer, byte);            }            Action::Unhook => performer.unhook(),            Action::CsiDispatch => {                if self.params.is_full() {                    self.ignoring = true;                } else {                    self.params.push(self.param);                }                performer.csi_dispatch(self.params(), self.intermediates(), self.ignoring, byte);            }            Action::EscDispatch => {                performer.esc_dispatch(self.intermediates(), self.ignoring, byte);            }            Action::Collect => {                if self.intermediate_idx == MAX_INTERMEDIATES {                    self.ignoring = true;                } else {                    self.intermediates[self.intermediate_idx] = byte;                    self.intermediate_idx += 1;                }            }            Action::Param => {                if self.params.is_full() {                    self.ignoring = true;                    return;                }                if byte == b';' {                    self.params.push(self.param);                    self.param = 0;                } else if byte == b':' {                    self.params.extend(self.param);                    self.param = 0;                } else {                    // Continue collecting bytes into param                    self.param = self.param.saturating_mul(10);                    self.param = self.param.saturating_add((byte - b'0') as u16);                }            }            Action::Clear => {                // Reset everything on ESC/CSI/DCS entry                self.intermediate_idx = 0;                self.ignoring = false;                self.param = 0;                self.params.clear();            }            Action::BeginUtf8 => self.process_utf8(performer, byte),            Action::Ignore => (),            Action::Nop => (),        }    }}/// Build a `char` out of bytespub trait CharAccumulator: Default {    /// Build a `char` out of bytes    ///    /// Return `None` when more data is needed    fn add(&mut self, byte: u8) -> Option<char>;}/// Most flexible [`CharAccumulator`] for [`Parser`] based on active features#[cfg(feature = "utf8")]pub type DefaultCharAccumulator = Utf8Parser;#[cfg(not(feature = "utf8"))]pub type DefaultCharAccumulator = AsciiParser;/// Only allow parsing 7-bit ASCII#[allow(clippy::exhaustive_structs)]#[derive(Default, Clone, Debug, PartialEq, Eq)]pub struct AsciiParser;impl CharAccumulator for AsciiParser {    fn add(&mut self, _byte: u8) -> Option<char> {        unreachable!("multi-byte UTF8 characters are unsupported")    }}/// Allow parsing UTF-8#[cfg(feature = "utf8")]#[derive(Default, Clone, Debug, PartialEq, Eq)]pub struct Utf8Parser {    utf8_parser: utf8::Parser,}#[cfg(feature = "utf8")]impl CharAccumulator for Utf8Parser {    fn add(&mut self, byte: u8) -> Option<char> {        let mut c = None;        let mut receiver = VtUtf8Receiver(&mut c);        self.utf8_parser.advance(&mut receiver, byte);        c    }}#[cfg(feature = "utf8")]struct VtUtf8Receiver<'a>(&'a mut Option<char>);#[cfg(feature = "utf8")]impl utf8::Receiver for VtUtf8Receiver<'_> {    fn codepoint(&mut self, c: char) {        *self.0 = Some(c);    }    fn invalid_sequence(&mut self) {        *self.0 = Some('�');    }}/// Performs actions requested by the [`Parser`]////// Actions in this case mean, for example, handling a CSI escape sequence describing cursor/// movement, or simply printing characters to the screen.////// The methods on this type correspond to actions described in/// <http://vt100.net/emu/dec_ansi_parser>. I've done my best to describe them in/// a useful way in my own words for completeness, but the site should be/// referenced if something isn't clear. If the site disappears at some point in/// the future, consider checking archive.org.pub trait Perform {    /// Draw a character to the screen and update states.    fn print(&mut self, _c: char) {}    /// Execute a C0 or C1 control function.    fn execute(&mut self, _byte: u8) {}    /// Invoked when a final character arrives in first part of device control string.    ///    /// The control function should be determined from the private marker, final character, and    /// execute with a parameter list. A handler should be selected for remaining characters in the    /// string; the handler function should subsequently be called by `put` for every character in    /// the control string.    ///    /// The `ignore` flag indicates that more than two intermediates arrived and    /// subsequent characters were ignored.    fn hook(&mut self, _params: &Params, _intermediates: &[u8], _ignore: bool, _action: u8) {}    /// Pass bytes as part of a device control string to the handle chosen in `hook`. C0 controls    /// will also be passed to the handler.    fn put(&mut self, _byte: u8) {}    /// Called when a device control string is terminated.    ///    /// The previously selected handler should be notified that the DCS has    /// terminated.    fn unhook(&mut self) {}    /// Dispatch an operating system command.    fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) {}    /// A final character has arrived for a CSI sequence    ///    /// The `ignore` flag indicates that either more than two intermediates arrived    /// or the number of parameters exceeded the maximum supported length,    /// and subsequent characters were ignored.    fn csi_dispatch(        &mut self,        _params: &Params,        _intermediates: &[u8],        _ignore: bool,        _action: u8,    ) {    }    /// The final character of an escape sequence has arrived.    ///    /// The `ignore` flag indicates that more than two intermediates arrived and    /// subsequent characters were ignored.    fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) {}}#[doc = include_str!("../README.md")]#[cfg(doctest)]pub struct ReadmeDoctests;