ree-cli 0.1.0

A fast terminal reset for macOS and Linux
Documentation
use std::env;
use std::ffi::{OsStr, OsString};
use std::io::Read;
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::{Path, PathBuf};

use fs_err::File;

use crate::terminfo::{self, Error, MAX_ENTRY_SIZE};

const SYSTEM_DIRECTORIES: &[&str] = &[
    "/usr/share/terminfo",
    "/usr/share/misc/terminfo",
    "/lib/terminfo",
    "/usr/local/share/terminfo",
];

pub fn load(term: &OsStr) -> Result<Vec<u8>, Error> {
    let term = term.as_bytes();
    if !terminfo::is_valid_term_name(term) {
        return Err(Error::InvalidTermName);
    }

    load_from_directories(term, search_directories())
}

fn search_directories() -> Vec<PathBuf> {
    let terminfo = env::var_os("TERMINFO");
    let home = env::var_os("HOME");
    let terminfo_dirs = env::var_os("TERMINFO_DIRS");
    search_directories_from(
        terminfo.as_deref(),
        home.as_deref(),
        terminfo_dirs.as_deref(),
    )
}

fn search_directories_from(
    terminfo: Option<&OsStr>,
    home: Option<&OsStr>,
    terminfo_dirs: Option<&OsStr>,
) -> Vec<PathBuf> {
    let mut directories = Vec::new();

    if let Some(directory) = terminfo.filter(|value| !value.is_empty()) {
        push_unique(&mut directories, PathBuf::from(directory));
    }

    if let Some(home) = home.filter(|value| !value.is_empty()) {
        push_unique(&mut directories, PathBuf::from(home).join(".terminfo"));
    }

    if let Some(paths) = terminfo_dirs {
        for path in paths.as_bytes().split(|byte| *byte == b':') {
            let directory = if path.is_empty() {
                PathBuf::from("/usr/share/terminfo")
            } else {
                PathBuf::from(OsString::from_vec(path.to_vec()))
            };
            push_unique(&mut directories, directory);
        }
    }

    for directory in SYSTEM_DIRECTORIES {
        push_unique(&mut directories, PathBuf::from(directory));
    }
    directories
}

fn push_unique(directories: &mut Vec<PathBuf>, directory: PathBuf) {
    if !directories.contains(&directory) {
        directories.push(directory);
    }
}

fn load_from_directories(
    term: &[u8],
    directories: impl IntoIterator<Item = PathBuf>,
) -> Result<Vec<u8>, Error> {
    let mut first_error = None;

    for directory in directories {
        match try_directory(&directory, term) {
            Ok(Some(data)) => return Ok(data),
            Ok(None) => {}
            Err(error) => {
                first_error.get_or_insert(error);
            }
        }
    }

    Err(first_error.unwrap_or(Error::NotFound))
}

fn try_directory(directory: &Path, term: &[u8]) -> Result<Option<Vec<u8>>, Error> {
    let Some(first) = term.first() else {
        return Ok(None);
    };
    let hex_subdirectory = format!("{first:02x}");
    let ascii_subdirectory = OsStr::from_bytes(&term[..1]);
    let name = OsStr::from_bytes(term);
    let mut first_error = None;

    for path in [
        directory.join(&hex_subdirectory).join(name),
        directory.join(ascii_subdirectory).join(name),
    ] {
        match read_entry(&path) {
            Ok(data) => match terminfo::validate(&data) {
                Ok(()) => return Ok(Some(data)),
                Err(error) => {
                    first_error.get_or_insert(error);
                }
            },
            Err(Error::NotFound) => {}
            Err(error) => {
                first_error.get_or_insert(error);
            }
        }
    }

    first_error.map_or(Ok(None), Err)
}

fn read_entry(path: &Path) -> Result<Vec<u8>, Error> {
    let file = File::open(path).map_err(|error| {
        if error.kind() == std::io::ErrorKind::NotFound {
            Error::NotFound
        } else {
            Error::Unreadable
        }
    })?;
    let mut data = Vec::new();
    file.take((MAX_ENTRY_SIZE + 1) as u64)
        .read_to_end(&mut data)
        .map_err(|_| Error::Unreadable)?;

    if data.len() > MAX_ENTRY_SIZE {
        return Err(Error::TooLarge);
    }

    Ok(data)
}

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