ree-cli 0.1.0

A fast terminal reset for macOS and Linux
Documentation
use std::borrow::Cow;
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;

use thiserror::Error as ThisError;

const MAGIC_STANDARD: u16 = 0o0432;
const MAGIC_EXTENDED: u16 = 0o01036;
pub const MAX_ENTRY_SIZE: usize = 32_768;

const STRING_IS1: usize = 48;
const STRING_IS2: usize = 49;
const STRING_IS3: usize = 50;
const STRING_RS1: usize = 122;
const STRING_RS2: usize = 123;
const STRING_RS3: usize = 124;
const STRING_CLEAR_MARGINS: usize = 270;

#[derive(Clone, Copy, Debug, Eq, PartialEq, ThisError)]
pub enum Error {
    #[error("TERM is not a valid terminal name")]
    InvalidTermName,
    #[error("no compiled terminfo entry was found")]
    NotFound,
    #[error("compiled terminfo entry is smaller than its header")]
    TooSmall,
    #[error("compiled terminfo entry has an unknown magic number")]
    BadMagic,
    #[error("compiled terminfo entry ends before a declared section")]
    Truncated,
    #[error("compiled terminfo string offset is outside its table")]
    BadStringOffset,
    #[error("compiled terminfo string is not NUL-terminated")]
    UnterminatedString,
    #[error("compiled terminfo entry is larger than 32 KiB")]
    TooLarge,
    #[error("compiled terminfo entry could not be read")]
    Unreadable,
}

#[derive(Debug)]
struct Entry<'data> {
    data: &'data [u8],
    string_offsets_start: usize,
    string_table_start: usize,
    string_table_end: usize,
    string_count: usize,
}

impl<'data> Entry<'data> {
    fn parse(data: &'data [u8]) -> Result<Self, Error> {
        if data.len() < 12 {
            return Err(Error::TooSmall);
        }

        let magic = read_u16(data, 0).ok_or(Error::TooSmall)?;
        if magic != MAGIC_STANDARD && magic != MAGIC_EXTENDED {
            return Err(Error::BadMagic);
        }

        let name_size = usize::from(read_u16(data, 2).ok_or(Error::TooSmall)?);
        let boolean_count = usize::from(read_u16(data, 4).ok_or(Error::TooSmall)?);
        let number_count = usize::from(read_u16(data, 6).ok_or(Error::TooSmall)?);
        let string_count = usize::from(read_u16(data, 8).ok_or(Error::TooSmall)?);
        let string_table_size = usize::from(read_u16(data, 10).ok_or(Error::TooSmall)?);
        let number_width = if magic == MAGIC_EXTENDED { 4 } else { 2 };

        let after_names = checked_add(12, name_size)?;
        let after_booleans = padded_even(checked_add(after_names, boolean_count)?)?;
        let string_offsets_start = checked_add(after_booleans, number_count * number_width)?;
        let string_table_start = checked_add(string_offsets_start, string_count * 2)?;
        let string_table_end = checked_add(string_table_start, string_table_size)?;

        if string_table_start > data.len() || string_table_end > data.len() {
            return Err(Error::Truncated);
        }

        Ok(Self {
            data,
            string_offsets_start,
            string_table_start,
            string_table_end,
            string_count,
        })
    }

    fn string_raw(&self, index: usize) -> Result<Option<&'data [u8]>, Error> {
        if index >= self.string_count {
            return Ok(None);
        }

        let offset_start = self.string_offsets_start + index * 2;
        let offset = read_i16(self.data, offset_start).ok_or(Error::Truncated)?;
        if offset < 0 {
            return Ok(None);
        }

        let relative = usize::try_from(offset).map_err(|_| Error::BadStringOffset)?;
        let table_length = self.string_table_end - self.string_table_start;
        if relative >= table_length {
            return Err(Error::BadStringOffset);
        }

        let start = self.string_table_start + relative;
        let rest = self
            .data
            .get(start..self.string_table_end)
            .ok_or(Error::BadStringOffset)?;
        let end = rest
            .iter()
            .position(|byte| *byte == 0)
            .ok_or(Error::UnterminatedString)?;
        Ok(rest.get(..end))
    }

    fn string(&self, index: usize) -> Result<Option<Cow<'data, [u8]>>, Error> {
        self.string_raw(index).map(|raw| raw.map(strip_padding))
    }
}

#[derive(Debug)]
pub struct ResetStrings<'data> {
    pub(crate) rs1: Option<Cow<'data, [u8]>>,
    pub(crate) rs2: Option<Cow<'data, [u8]>>,
    pub(crate) rs3: Option<Cow<'data, [u8]>>,
    pub(crate) is1: Option<Cow<'data, [u8]>>,
    pub(crate) is2: Option<Cow<'data, [u8]>>,
    pub(crate) is3: Option<Cow<'data, [u8]>>,
    pub(crate) clear_margins: Option<Cow<'data, [u8]>>,
}

pub fn load(term: &OsStr) -> Result<Vec<u8>, Error> {
    crate::terminfo_db::load(term)
}

pub fn reset_strings(data: &[u8]) -> Result<ResetStrings<'_>, Error> {
    let entry = Entry::parse(data)?;
    Ok(ResetStrings {
        rs1: entry.string(STRING_RS1)?,
        rs2: entry.string(STRING_RS2)?,
        rs3: entry.string(STRING_RS3)?,
        is1: entry.string(STRING_IS1)?,
        is2: entry.string(STRING_IS2)?,
        is3: entry.string(STRING_IS3)?,
        clear_margins: entry.string(STRING_CLEAR_MARGINS)?,
    })
}

pub fn is_vt_compatible(term: &OsStr) -> bool {
    const FAMILIES: &[&[u8]] = &[
        b"xterm",
        b"screen",
        b"tmux",
        b"rxvt",
        b"ansi",
        b"linux",
        b"cygwin",
        b"st",
        b"alacritty",
        b"kitty",
        b"wezterm",
        b"foot",
        b"ghostty",
    ];

    let term = term.as_bytes();
    is_valid_term_name(term)
        && (FAMILIES
            .iter()
            .any(|family| is_terminal_family(term, family))
            || term
                .strip_prefix(b"vt")
                .is_some_and(|suffix| suffix.first().is_some_and(u8::is_ascii_digit)))
}

pub fn validate(data: &[u8]) -> Result<(), Error> {
    Entry::parse(data).map(drop)
}

pub fn is_valid_term_name(term: &[u8]) -> bool {
    !term.is_empty()
        && term.len() <= 255
        && term != b"."
        && term != b".."
        && term
            .iter()
            .all(|byte| byte.is_ascii_alphanumeric() || matches!(*byte, b'-' | b'_' | b'.' | b'+'))
}

fn strip_padding(raw: &[u8]) -> Cow<'_, [u8]> {
    if !raw.contains(&b'$') {
        return Cow::Borrowed(raw);
    }

    let mut output = Vec::with_capacity(raw.len());
    let mut index = 0;
    while index < raw.len() {
        if raw.get(index..index + 2) == Some(b"$<") {
            let body_start = index + 2;
            if let Some(close) = raw
                .get(body_start..)
                .and_then(|tail| tail.iter().position(|byte| *byte == b'>'))
            {
                let end = body_start + close;
                if is_padding_body(&raw[body_start..end]) {
                    index = end + 1;
                    continue;
                }

                output.extend_from_slice(&raw[index..=end]);
                index = end + 1;
                continue;
            }
        }

        if let Some(byte) = raw.get(index) {
            output.push(*byte);
        }
        index += 1;
    }

    Cow::Owned(output)
}

fn is_padding_body(body: &[u8]) -> bool {
    let digits = body.iter().take_while(|byte| byte.is_ascii_digit()).count();
    if digits == 0 {
        return false;
    }

    let mut index = digits;
    if body.get(index) == Some(&b'.') {
        index += 1;
        if !body.get(index).is_some_and(u8::is_ascii_digit) {
            return false;
        }
        index += 1;
        if body.get(index).is_some_and(u8::is_ascii_digit) {
            return false;
        }
    }

    let mut star = false;
    let mut slash = false;
    for byte in &body[index..] {
        match *byte {
            b'*' if !star => star = true,
            b'/' if !slash => slash = true,
            _ => return false,
        }
    }
    true
}

fn is_terminal_family(term: &[u8], family: &[u8]) -> bool {
    term == family
        || term.strip_prefix(family).is_some_and(|suffix| {
            suffix
                .first()
                .is_some_and(|byte| matches!(byte, b'-' | b'.' | b'+'))
        })
}

fn read_u16(data: &[u8], start: usize) -> Option<u16> {
    let bytes: [u8; 2] = data.get(start..start + 2)?.try_into().ok()?;
    Some(u16::from_le_bytes(bytes))
}

fn read_i16(data: &[u8], start: usize) -> Option<i16> {
    let bytes: [u8; 2] = data.get(start..start + 2)?.try_into().ok()?;
    Some(i16::from_le_bytes(bytes))
}

fn checked_add(left: usize, right: usize) -> Result<usize, Error> {
    left.checked_add(right).ok_or(Error::Truncated)
}

fn padded_even(value: usize) -> Result<usize, Error> {
    if value.is_multiple_of(2) {
        Ok(value)
    } else {
        checked_add(value, 1)
    }
}

#[cfg(test)]
#[path = "terminfo_tests.rs"]
mod tests;