From 9c11113b4f71f3d23c908036e7a1a67d360f06e4 Mon Sep 17 00:00:00 2001 From: Charles Lew Date: Sun, 18 Jul 2021 14:42:14 +0800 Subject: Move `library/term/src` to `library/test/src/term/`. --- library/term/Cargo.toml | 9 - library/term/src/lib.rs | 194 ------- library/term/src/terminfo/mod.rs | 245 --------- library/term/src/terminfo/parm.rs | 559 --------------------- library/term/src/terminfo/parm/tests.rs | 138 ----- library/term/src/terminfo/parser/compiled.rs | 336 ------------- library/term/src/terminfo/parser/compiled/tests.rs | 8 - library/term/src/terminfo/searcher.rs | 69 --- library/term/src/terminfo/searcher/tests.rs | 19 - library/term/src/win.rs | 220 -------- library/test/Cargo.toml | 1 - library/test/src/term/mod.rs | 194 +++++++ library/test/src/term/terminfo/mod.rs | 245 +++++++++ library/test/src/term/terminfo/parm.rs | 559 +++++++++++++++++++++ library/test/src/term/terminfo/parm/tests.rs | 138 +++++ library/test/src/term/terminfo/parser/compiled.rs | 336 +++++++++++++ .../src/term/terminfo/parser/compiled/tests.rs | 8 + library/test/src/term/terminfo/searcher.rs | 69 +++ library/test/src/term/terminfo/searcher/tests.rs | 19 + library/test/src/term/win.rs | 220 ++++++++ 20 files changed, 1788 insertions(+), 1798 deletions(-) delete mode 100644 library/term/Cargo.toml delete mode 100644 library/term/src/lib.rs delete mode 100644 library/term/src/terminfo/mod.rs delete mode 100644 library/term/src/terminfo/parm.rs delete mode 100644 library/term/src/terminfo/parm/tests.rs delete mode 100644 library/term/src/terminfo/parser/compiled.rs delete mode 100644 library/term/src/terminfo/parser/compiled/tests.rs delete mode 100644 library/term/src/terminfo/searcher.rs delete mode 100644 library/term/src/terminfo/searcher/tests.rs delete mode 100644 library/term/src/win.rs create mode 100644 library/test/src/term/mod.rs create mode 100644 library/test/src/term/terminfo/mod.rs create mode 100644 library/test/src/term/terminfo/parm.rs create mode 100644 library/test/src/term/terminfo/parm/tests.rs create mode 100644 library/test/src/term/terminfo/parser/compiled.rs create mode 100644 library/test/src/term/terminfo/parser/compiled/tests.rs create mode 100644 library/test/src/term/terminfo/searcher.rs create mode 100644 library/test/src/term/terminfo/searcher/tests.rs create mode 100644 library/test/src/term/win.rs diff --git a/library/term/Cargo.toml b/library/term/Cargo.toml deleted file mode 100644 index ddf85b5c5bc..00000000000 --- a/library/term/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -authors = ["The Rust Project Developers"] -name = "term" -version = "0.0.0" -edition = "2018" - -[dependencies] -core = { path = "../core" } -std = { path = "../std" } diff --git a/library/term/src/lib.rs b/library/term/src/lib.rs deleted file mode 100644 index 943b276a220..00000000000 --- a/library/term/src/lib.rs +++ /dev/null @@ -1,194 +0,0 @@ -//! Terminal formatting library. -//! -//! This crate provides the `Terminal` trait, which abstracts over an [ANSI -//! Terminal][ansi] to provide color printing, among other things. There are two -//! implementations, the `TerminfoTerminal`, which uses control characters from -//! a [terminfo][ti] database, and `WinConsole`, which uses the [Win32 Console -//! API][win]. -//! -//! # Examples -//! -//! ```no_run -//! # #![feature(rustc_private)] -//! extern crate term; -//! use std::io::prelude::*; -//! -//! fn main() { -//! let mut t = term::stdout().unwrap(); -//! -//! t.fg(term::color::GREEN).unwrap(); -//! write!(t, "hello, ").unwrap(); -//! -//! t.fg(term::color::RED).unwrap(); -//! writeln!(t, "world!").unwrap(); -//! -//! assert!(t.reset().unwrap()); -//! } -//! ``` -//! -//! [ansi]: https://en.wikipedia.org/wiki/ANSI_escape_code -//! [win]: https://docs.microsoft.com/en-us/windows/console/character-mode-applications -//! [ti]: https://en.wikipedia.org/wiki/Terminfo - -#![doc(html_playground_url = "https://play.rust-lang.org/", test(attr(deny(warnings))))] -#![deny(missing_docs)] -#![cfg_attr(windows, feature(libc))] - -use std::io::prelude::*; -use std::io::{self, Stderr, Stdout}; - -pub use terminfo::TerminfoTerminal; -#[cfg(windows)] -pub use win::WinConsole; - -pub mod terminfo; - -#[cfg(windows)] -mod win; - -/// Alias for stdout terminals. -pub type StdoutTerminal = dyn Terminal + Send; -/// Alias for stderr terminals. -pub type StderrTerminal = dyn Terminal + Send; - -#[cfg(not(windows))] -/// Returns a Terminal wrapping stdout, or None if a terminal couldn't be -/// opened. -pub fn stdout() -> Option> { - TerminfoTerminal::new(io::stdout()).map(|t| Box::new(t) as Box) -} - -#[cfg(windows)] -/// Returns a Terminal wrapping stdout, or None if a terminal couldn't be -/// opened. -pub fn stdout() -> Option> { - TerminfoTerminal::new(io::stdout()) - .map(|t| Box::new(t) as Box) - .or_else(|| WinConsole::new(io::stdout()).ok().map(|t| Box::new(t) as Box)) -} - -#[cfg(not(windows))] -/// Returns a Terminal wrapping stderr, or None if a terminal couldn't be -/// opened. -pub fn stderr() -> Option> { - TerminfoTerminal::new(io::stderr()).map(|t| Box::new(t) as Box) -} - -#[cfg(windows)] -/// Returns a Terminal wrapping stderr, or None if a terminal couldn't be -/// opened. -pub fn stderr() -> Option> { - TerminfoTerminal::new(io::stderr()) - .map(|t| Box::new(t) as Box) - .or_else(|| WinConsole::new(io::stderr()).ok().map(|t| Box::new(t) as Box)) -} - -/// Terminal color definitions -#[allow(missing_docs)] -pub mod color { - /// Number for a terminal color - pub type Color = u32; - - pub const BLACK: Color = 0; - pub const RED: Color = 1; - pub const GREEN: Color = 2; - pub const YELLOW: Color = 3; - pub const BLUE: Color = 4; - pub const MAGENTA: Color = 5; - pub const CYAN: Color = 6; - pub const WHITE: Color = 7; - - pub const BRIGHT_BLACK: Color = 8; - pub const BRIGHT_RED: Color = 9; - pub const BRIGHT_GREEN: Color = 10; - pub const BRIGHT_YELLOW: Color = 11; - pub const BRIGHT_BLUE: Color = 12; - pub const BRIGHT_MAGENTA: Color = 13; - pub const BRIGHT_CYAN: Color = 14; - pub const BRIGHT_WHITE: Color = 15; -} - -/// Terminal attributes for use with term.attr(). -/// -/// Most attributes can only be turned on and must be turned off with term.reset(). -/// The ones that can be turned off explicitly take a boolean value. -/// Color is also represented as an attribute for convenience. -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub enum Attr { - /// Bold (or possibly bright) mode - Bold, - /// Dim mode, also called faint or half-bright. Often not supported - Dim, - /// Italics mode. Often not supported - Italic(bool), - /// Underline mode - Underline(bool), - /// Blink mode - Blink, - /// Standout mode. Often implemented as Reverse, sometimes coupled with Bold - Standout(bool), - /// Reverse mode, inverts the foreground and background colors - Reverse, - /// Secure mode, also called invis mode. Hides the printed text - Secure, - /// Convenience attribute to set the foreground color - ForegroundColor(color::Color), - /// Convenience attribute to set the background color - BackgroundColor(color::Color), -} - -/// A terminal with similar capabilities to an ANSI Terminal -/// (foreground/background colors etc). -pub trait Terminal: Write { - /// The terminal's output writer type. - type Output: Write; - - /// Sets the foreground color to the given color. - /// - /// If the color is a bright color, but the terminal only supports 8 colors, - /// the corresponding normal color will be used instead. - /// - /// Returns `Ok(true)` if the color was set, `Ok(false)` otherwise, and `Err(e)` - /// if there was an I/O error. - fn fg(&mut self, color: color::Color) -> io::Result; - - /// Sets the background color to the given color. - /// - /// If the color is a bright color, but the terminal only supports 8 colors, - /// the corresponding normal color will be used instead. - /// - /// Returns `Ok(true)` if the color was set, `Ok(false)` otherwise, and `Err(e)` - /// if there was an I/O error. - fn bg(&mut self, color: color::Color) -> io::Result; - - /// Sets the given terminal attribute, if supported. Returns `Ok(true)` - /// if the attribute was supported, `Ok(false)` otherwise, and `Err(e)` if - /// there was an I/O error. - fn attr(&mut self, attr: Attr) -> io::Result; - - /// Returns `true` if the given terminal attribute is supported. - fn supports_attr(&self, attr: Attr) -> bool; - - /// Resets all terminal attributes and colors to their defaults. - /// - /// Returns `Ok(true)` if the terminal was reset, `Ok(false)` otherwise, and `Err(e)` if there - /// was an I/O error. - /// - /// *Note: This does not flush.* - /// - /// That means the reset command may get buffered so, if you aren't planning on doing anything - /// else that might flush stdout's buffer (e.g., writing a line of text), you should flush after - /// calling reset. - fn reset(&mut self) -> io::Result; - - /// Gets an immutable reference to the stream inside - fn get_ref(&self) -> &Self::Output; - - /// Gets a mutable reference to the stream inside - fn get_mut(&mut self) -> &mut Self::Output; - - /// Returns the contained stream, destroying the `Terminal` - fn into_inner(self) -> Self::Output - where - Self: Sized; -} diff --git a/library/term/src/terminfo/mod.rs b/library/term/src/terminfo/mod.rs deleted file mode 100644 index fec59aaa0c2..00000000000 --- a/library/term/src/terminfo/mod.rs +++ /dev/null @@ -1,245 +0,0 @@ -//! Terminfo database interface. - -use std::collections::HashMap; -use std::env; -use std::error; -use std::fmt; -use std::fs::File; -use std::io::{self, prelude::*, BufReader}; -use std::path::Path; - -use crate::color; -use crate::Attr; -use crate::Terminal; - -use parm::{expand, Param, Variables}; -use parser::compiled::{msys_terminfo, parse}; -use searcher::get_dbpath_for_term; - -/// A parsed terminfo database entry. -#[derive(Debug)] -pub struct TermInfo { - /// Names for the terminal - pub names: Vec, - /// Map of capability name to boolean value - pub bools: HashMap, - /// Map of capability name to numeric value - pub numbers: HashMap, - /// Map of capability name to raw (unexpanded) string - pub strings: HashMap>, -} - -/// A terminfo creation error. -#[derive(Debug)] -pub enum Error { - /// TermUnset Indicates that the environment doesn't include enough information to find - /// the terminfo entry. - TermUnset, - /// MalformedTerminfo indicates that parsing the terminfo entry failed. - MalformedTerminfo(String), - /// io::Error forwards any io::Errors encountered when finding or reading the terminfo entry. - IoError(io::Error), -} - -impl error::Error for Error { - fn source(&self) -> Option<&(dyn error::Error + 'static)> { - use Error::*; - match self { - IoError(e) => Some(e), - _ => None, - } - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use Error::*; - match *self { - TermUnset => Ok(()), - MalformedTerminfo(ref e) => e.fmt(f), - IoError(ref e) => e.fmt(f), - } - } -} - -impl TermInfo { - /// Creates a TermInfo based on current environment. - pub fn from_env() -> Result { - let term = match env::var("TERM") { - Ok(name) => TermInfo::from_name(&name), - Err(..) => return Err(Error::TermUnset), - }; - - if term.is_err() && env::var("MSYSCON").map_or(false, |s| "mintty.exe" == s) { - // msys terminal - Ok(msys_terminfo()) - } else { - term - } - } - - /// Creates a TermInfo for the named terminal. - pub fn from_name(name: &str) -> Result { - get_dbpath_for_term(name) - .ok_or_else(|| { - Error::IoError(io::Error::new(io::ErrorKind::NotFound, "terminfo file not found")) - }) - .and_then(|p| TermInfo::from_path(&(*p))) - } - - /// Parse the given TermInfo. - pub fn from_path>(path: P) -> Result { - Self::_from_path(path.as_ref()) - } - // Keep the metadata small - fn _from_path(path: &Path) -> Result { - let file = File::open(path).map_err(Error::IoError)?; - let mut reader = BufReader::new(file); - parse(&mut reader, false).map_err(Error::MalformedTerminfo) - } -} - -pub mod searcher; - -/// TermInfo format parsing. -pub mod parser { - //! ncurses-compatible compiled terminfo format parsing (term(5)) - pub mod compiled; -} -pub mod parm; - -fn cap_for_attr(attr: Attr) -> &'static str { - match attr { - Attr::Bold => "bold", - Attr::Dim => "dim", - Attr::Italic(true) => "sitm", - Attr::Italic(false) => "ritm", - Attr::Underline(true) => "smul", - Attr::Underline(false) => "rmul", - Attr::Blink => "blink", - Attr::Standout(true) => "smso", - Attr::Standout(false) => "rmso", - Attr::Reverse => "rev", - Attr::Secure => "invis", - Attr::ForegroundColor(_) => "setaf", - Attr::BackgroundColor(_) => "setab", - } -} - -/// A Terminal that knows how many colors it supports, with a reference to its -/// parsed Terminfo database record. -pub struct TerminfoTerminal { - num_colors: u32, - out: T, - ti: TermInfo, -} - -impl Terminal for TerminfoTerminal { - type Output = T; - fn fg(&mut self, color: color::Color) -> io::Result { - let color = self.dim_if_necessary(color); - if self.num_colors > color { - return self.apply_cap("setaf", &[Param::Number(color as i32)]); - } - Ok(false) - } - - fn bg(&mut self, color: color::Color) -> io::Result { - let color = self.dim_if_necessary(color); - if self.num_colors > color { - return self.apply_cap("setab", &[Param::Number(color as i32)]); - } - Ok(false) - } - - fn attr(&mut self, attr: Attr) -> io::Result { - match attr { - Attr::ForegroundColor(c) => self.fg(c), - Attr::BackgroundColor(c) => self.bg(c), - _ => self.apply_cap(cap_for_attr(attr), &[]), - } - } - - fn supports_attr(&self, attr: Attr) -> bool { - match attr { - Attr::ForegroundColor(_) | Attr::BackgroundColor(_) => self.num_colors > 0, - _ => { - let cap = cap_for_attr(attr); - self.ti.strings.get(cap).is_some() - } - } - } - - fn reset(&mut self) -> io::Result { - // are there any terminals that have color/attrs and not sgr0? - // Try falling back to sgr, then op - let cmd = match ["sgr0", "sgr", "op"].iter().find_map(|cap| self.ti.strings.get(*cap)) { - Some(op) => match expand(&op, &[], &mut Variables::new()) { - Ok(cmd) => cmd, - Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e)), - }, - None => return Ok(false), - }; - self.out.write_all(&cmd).and(Ok(true)) - } - - fn get_ref(&self) -> &T { - &self.out - } - - fn get_mut(&mut self) -> &mut T { - &mut self.out - } - - fn into_inner(self) -> T - where - Self: Sized, - { - self.out - } -} - -impl TerminfoTerminal { - /// Creates a new TerminfoTerminal with the given TermInfo and Write. - pub fn new_with_terminfo(out: T, terminfo: TermInfo) -> TerminfoTerminal { - let nc = if terminfo.strings.contains_key("setaf") && terminfo.strings.contains_key("setab") - { - terminfo.numbers.get("colors").map_or(0, |&n| n) - } else { - 0 - }; - - TerminfoTerminal { out, ti: terminfo, num_colors: nc } - } - - /// Creates a new TerminfoTerminal for the current environment with the given Write. - /// - /// Returns `None` when the terminfo cannot be found or parsed. - pub fn new(out: T) -> Option> { - TermInfo::from_env().map(move |ti| TerminfoTerminal::new_with_terminfo(out, ti)).ok() - } - - fn dim_if_necessary(&self, color: color::Color) -> color::Color { - if color >= self.num_colors && color >= 8 && color < 16 { color - 8 } else { color } - } - - fn apply_cap(&mut self, cmd: &str, params: &[Param]) -> io::Result { - match self.ti.strings.get(cmd) { - Some(cmd) => match expand(&cmd, params, &mut Variables::new()) { - Ok(s) => self.out.write_all(&s).and(Ok(true)), - Err(e) => Err(io::Error::new(io::ErrorKind::InvalidData, e)), - }, - None => Ok(false), - } - } -} - -impl Write for TerminfoTerminal { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.out.write(buf) - } - - fn flush(&mut self) -> io::Result<()> { - self.out.flush() - } -} diff --git a/library/term/src/terminfo/parm.rs b/library/term/src/terminfo/parm.rs deleted file mode 100644 index 2e4e917891e..00000000000 --- a/library/term/src/terminfo/parm.rs +++ /dev/null @@ -1,559 +0,0 @@ -//! Parameterized string expansion - -use self::Param::*; -use self::States::*; - -use std::iter::repeat; - -#[cfg(test)] -mod tests; - -#[derive(Clone, Copy, PartialEq)] -enum States { - Nothing, - Percent, - SetVar, - GetVar, - PushParam, - CharConstant, - CharClose, - IntConstant(i32), - FormatPattern(Flags, FormatState), - SeekIfElse(usize), - SeekIfElsePercent(usize), - SeekIfEnd(usize), - SeekIfEndPercent(usize), -} - -#[derive(Copy, PartialEq, Clone)] -enum FormatState { - Flags, - Width, - Precision, -} - -/// Types of parameters a capability can use -#[allow(missing_docs)] -#[derive(Clone)] -pub enum Param { - Words(String), - Number(i32), -} - -/// Container for static and dynamic variable arrays -pub struct Variables { - /// Static variables A-Z - sta_va: [Param; 26], - /// Dynamic variables a-z - dyn_va: [Param; 26], -} - -impl Variables { - /// Returns a new zero-initialized Variables - pub fn new() -> Variables { - Variables { - sta_va: [ - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - ], - dyn_va: [ - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - ], - } - } -} - -/// Expand a parameterized capability -/// -/// # Arguments -/// * `cap` - string to expand -/// * `params` - vector of params for %p1 etc -/// * `vars` - Variables struct for %Pa etc -/// -/// To be compatible with ncurses, `vars` should be the same between calls to `expand` for -/// multiple capabilities for the same terminal. -pub fn expand(cap: &[u8], params: &[Param], vars: &mut Variables) -> Result, String> { - let mut state = Nothing; - - // expanded cap will only rarely be larger than the cap itself - let mut output = Vec::with_capacity(cap.len()); - - let mut stack: Vec = Vec::new(); - - // Copy parameters into a local vector for mutability - let mut mparams = [ - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - Number(0), - ]; - for (dst, src) in mparams.iter_mut().zip(params.iter()) { - *dst = (*src).clone(); - } - - for &c in cap.iter() { - let cur = c as char; - let mut old_state = state; - match state { - Nothing => { - if cur == '%' { - state = Percent; - } else { - output.push(c); - } - } - Percent => { - match cur { - '%' => { - output.push(c); - state = Nothing - } - 'c' => { - match stack.pop() { - // if c is 0, use 0200 (128) for ncurses compatibility - Some(Number(0)) => output.push(128u8), - // Don't check bounds. ncurses just casts and truncates. - Some(Number(c)) => output.push(c as u8), - Some(_) => return Err("a non-char was used with %c".to_string()), - None => return Err("stack is empty".to_string()), - } - } - 'p' => state = PushParam, - 'P' => state = SetVar, - 'g' => state = GetVar, - '\'' => state = CharConstant, - '{' => state = IntConstant(0), - 'l' => match stack.pop() { - Some(Words(s)) => stack.push(Number(s.len() as i32)), - Some(_) => return Err("a non-str was used with %l".to_string()), - None => return Err("stack is empty".to_string()), - }, - '+' | '-' | '/' | '*' | '^' | '&' | '|' | 'm' => { - match (stack.pop(), stack.pop()) { - (Some(Number(y)), Some(Number(x))) => stack.push(Number(match cur { - '+' => x + y, - '-' => x - y, - '*' => x * y, - '/' => x / y, - '|' => x | y, - '&' => x & y, - '^' => x ^ y, - 'm' => x % y, - _ => unreachable!("All cases handled"), - })), - (Some(_), Some(_)) => { - return Err(format!("non-numbers on stack with {}", cur)); - } - _ => return Err("stack is empty".to_string()), - } - } - '=' | '>' | '<' | 'A' | 'O' => match (stack.pop(), stack.pop()) { - (Some(Number(y)), Some(Number(x))) => stack.push(Number( - if match cur { - '=' => x == y, - '<' => x < y, - '>' => x > y, - 'A' => x > 0 && y > 0, - 'O' => x > 0 || y > 0, - _ => unreachable!(), - } { - 1 - } else { - 0 - }, - )), - (Some(_), Some(_)) => { - return Err(format!("non-numbers on stack with {}", cur)); - } - _ => return Err("stack is empty".to_string()), - }, - '!' | '~' => match stack.pop() { - Some(Number(x)) => stack.push(Number(match cur { - '!' if x > 0 => 0, - '!' => 1, - '~' => !x, - _ => unreachable!(), - })), - Some(_) => return Err(format!("non-numbers on stack with {}", cur)), - None => return Err("stack is empty".to_string()), - }, - 'i' => match (&mparams[0], &mparams[1]) { - (&Number(x), &Number(y)) => { - mparams[0] = Number(x + 1); - mparams[1] = Number(y + 1); - } - _ => return Err("first two params not numbers with %i".to_string()), - }, - - // printf-style support for %doxXs - 'd' | 'o' | 'x' | 'X' | 's' => { - if let Some(arg) = stack.pop() { - let flags = Flags::new(); - let res = format(arg, FormatOp::from_char(cur), flags)?; - output.extend(res.iter().cloned()); - } else { - return Err("stack is empty".to_string()); - } - } - ':' | '#' | ' ' | '.' | '0'..='9' => { - let mut flags = Flags::new(); - let mut fstate = FormatState::Flags; - match cur { - ':' => (), - '#' => flags.alternate = true, - ' ' => flags.space = true, - '.' => fstate = FormatState::Precision, - '0'..='9' => { - flags.width = cur as usize - '0' as usize; - fstate = FormatState::Width; - } - _ => unreachable!(), - } - state = FormatPattern(flags, fstate); - } - - // conditionals - '?' => (), - 't' => match stack.pop() { - Some(Number(0)) => state = SeekIfElse(0), - Some(Number(_)) => (), - Some(_) => return Err("non-number on stack with conditional".to_string()), - None => return Err("stack is empty".to_string()), - }, - 'e' => state = SeekIfEnd(0), - ';' => (), - _ => return Err(format!("unrecognized format option {}", cur)), - } - } - PushParam => { - // params are 1-indexed - stack.push( - mparams[match cur.to_digit(10) { - Some(d) => d as usize - 1, - None => return Err("bad param number".to_string()), - }] - .clone(), - ); - } - SetVar => { - if cur >= 'A' && cur <= 'Z' { - if let Some(arg) = stack.pop() { - let idx = (cur as u8) - b'A'; - vars.sta_va[idx as usize] = arg; - } else { - return Err("stack is empty".to_string()); - } - } else if cur >= 'a' && cur <= 'z' { - if let Some(arg) = stack.pop() { - let idx = (cur as u8) - b'a'; - vars.dyn_va[idx as usize] = arg; - } else { - return Err("stack is empty".to_string()); - } - } else { - return Err("bad variable name in %P".to_string()); - } - } - GetVar => { - if cur >= 'A' && cur <= 'Z' { - let idx = (cur as u8) - b'A'; - stack.push(vars.sta_va[idx as usize].clone()); - } else if cur >= 'a' && cur <= 'z' { - let idx = (cur as u8) - b'a'; - stack.push(vars.dyn_va[idx as usize].clone()); - } else { - return Err("bad variable name in %g".to_string()); - } - } - CharConstant => { - stack.push(Number(c as i32)); - state = CharClose; - } - CharClose => { - if cur != '\'' { - return Err("malformed character constant".to_string()); - } - } - IntConstant(i) => { - if cur == '}' { - stack.push(Number(i)); - state = Nothing; - } else if let Some(digit) = cur.to_digit(10) { - match i.checked_mul(10).and_then(|i_ten| i_ten.checked_add(digit as i32)) { - Some(i) => { - state = IntConstant(i); - old_state = Nothing; - } - None => return Err("int constant too large".to_string()), - } - } else { - return Err("bad int constant".to_string()); - } - } - FormatPattern(ref mut flags, ref mut fstate) => { - old_state = Nothing; - match (*fstate, cur) { - (_, 'd') | (_, 'o') | (_, 'x') | (_, 'X') | (_, 's') => { - if let Some(arg) = stack.pop() { - let res = format(arg, FormatOp::from_char(cur), *flags)?; - output.extend(res.iter().cloned()); - // will cause state to go to Nothing - old_state = FormatPattern(*flags, *fstate); - } else { - return Err("stack is empty".to_string()); - } - } - (FormatState::Flags, '#') => { - flags.alternate = true; - } - (FormatState::Flags, '-') => { - flags.left = true; - } - (FormatState::Flags, '+') => { - flags.sign = true; - } - (FormatState::Flags, ' ') => { - flags.space = true; - } - (FormatState::Flags, '0'..='9') => { - flags.width = cur as usize - '0' as usize; - *fstate = FormatState::Width; - } - (FormatState::Flags, '.') => { - *fstate = FormatState::Precision; - } - (FormatState::Width, '0'..='9') => { - let old = flags.width; - flags.width = flags.width * 10 + (cur as usize - '0' as usize); - if flags.width < old { - return Err("format width overflow".to_string()); - } - } - (FormatState::Width, '.') => { - *fstate = FormatState::Precision; - } - (FormatState::Precision, '0'..='9') => { - let old = flags.precision; - flags.precision = flags.precision * 10 + (cur as usize - '0' as usize); - if flags.precision < old { - return Err("format precision overflow".to_string()); - } - } - _ => return Err("invalid format specifier".to_string()), - } - } - SeekIfElse(level) => { - if cur == '%' { - state = SeekIfElsePercent(level); - } - old_state = Nothing; - } - SeekIfElsePercent(level) => { - if cur == ';' { - if level == 0 { - state = Nothing; - } else { - state = SeekIfElse(level - 1); - } - } else if cur == 'e' && level == 0 { - state = Nothing; - } else if cur == '?' { - state = SeekIfElse(level + 1); - } else { - state = SeekIfElse(level); - } - } - SeekIfEnd(level) => { - if cur == '%' { - state = SeekIfEndPercent(level); - } - old_state = Nothing; - } - SeekIfEndPercent(level) => { - if cur == ';' { - if level == 0 { - state = Nothing; - } else { - state = SeekIfEnd(level - 1); - } - } else if cur == '?' { - state = SeekIfEnd(level + 1); - } else { - state = SeekIfEnd(level); - } - } - } - if state == old_state { - state = Nothing; - } - } - Ok(output) -} - -#[derive(Copy, PartialEq, Clone)] -struct Flags { - width: usize, - precision: usize, - alternate: bool, - left: bool, - sign: bool, - space: bool, -} - -impl Flags { - fn new() -> Flags { - Flags { width: 0, precision: 0, alternate: false, left: false, sign: false, space: false } - } -} - -#[derive(Copy, Clone)] -enum FormatOp { - Digit, - Octal, - LowerHex, - UpperHex, - String, -} - -impl FormatOp { - fn from_char(c: char) -> FormatOp { - match c { - 'd' => FormatOp::Digit, - 'o' => FormatOp::Octal, - 'x' => FormatOp::LowerHex, - 'X' => FormatOp::UpperHex, - 's' => FormatOp::String, - _ => panic!("bad FormatOp char"), - } - } - fn to_char(self) -> char { - match self { - FormatOp::Digit => 'd', - FormatOp::Octal => 'o', - FormatOp::LowerHex => 'x', - FormatOp::UpperHex => 'X', - FormatOp::String => 's', - } - } -} - -fn format(val: Param, op: FormatOp, flags: Flags) -> Result, String> { - let mut s = match val { - Number(d) => { - match op { - FormatOp::Digit => { - if flags.sign { - format!("{:+01$}", d, flags.precision) - } else if d < 0 { - // C doesn't take sign into account in precision calculation. - format!("{:01$}", d, flags.precision + 1) - } else if flags.space { - format!(" {:01$}", d, flags.precision) - } else { - format!("{:01$}", d, flags.precision) - } - } - FormatOp::Octal => { - if flags.alternate { - // Leading octal zero counts against precision. - format!("0{:01$o}", d, flags.precision.saturating_sub(1)) - } else { - format!("{:01$o}", d, flags.precision) - } - } - FormatOp::LowerHex => { - if flags.alternate && d != 0 { - format!("0x{:01$x}", d, flags.precision) - } else { - format!("{:01$x}", d, flags.precision) - } - } - FormatOp::UpperHex => { - if flags.alternate && d != 0 { - format!("0X{:01$X}", d, flags.precision) - } else { - format!("{:01$X}", d, flags.precision) - } - } - FormatOp::String => return Err("non-number on stack with %s".to_string()), - } - .into_bytes() - } - Words(s) => match op { - FormatOp::String => { - let mut s = s.into_bytes(); - if flags.precision > 0 && flags.precision < s.len() { - s.truncate(flags.precision); - } - s - } - _ => return Err(format!("non-string on stack with %{}", op.to_char())), - }, - }; - if flags.width > s.len() { - let n = flags.width - s.len(); - if flags.left { - s.extend(repeat(b' ').take(n)); - } else { - let mut s_ = Vec::with_capacity(flags.width); - s_.extend(repeat(b' ').take(n)); - s_.extend(s.into_iter()); - s = s_; - } - } - Ok(s) -} diff --git a/library/term/src/terminfo/parm/tests.rs b/library/term/src/terminfo/parm/tests.rs deleted file mode 100644 index 1cc0967c8f4..00000000000 --- a/library/term/src/terminfo/parm/tests.rs +++ /dev/null @@ -1,138 +0,0 @@ -use super::*; - -use std::result::Result::Ok; - -#[test] -fn test_basic_setabf() { - let s = b"\\E[48;5;%p1%dm"; - assert_eq!( - expand(s, &[Number(1)], &mut Variables::new()).unwrap(), - "\\E[48;5;1m".bytes().collect::>() - ); -} - -#[test] -fn test_multiple_int_constants() { - assert_eq!( - expand(b"%{1}%{2}%d%d", &[], &mut Variables::new()).unwrap(), - "21".bytes().collect::>() - ); -} - -#[test] -fn test_op_i() { - let mut vars = Variables::new(); - assert_eq!( - expand(b"%p1%d%p2%d%p3%d%i%p1%d%p2%d%p3%d", &[Number(1), Number(2), Number(3)], &mut vars), - Ok("123233".bytes().collect::>()) - ); - assert_eq!( - expand(b"%p1%d%p2%d%i%p1%d%p2%d", &[], &mut vars), - Ok("0011".bytes().collect::>()) - ); -} - -#[test] -fn test_param_stack_failure_conditions() { - let mut varstruct = Variables::new(); - let vars = &mut varstruct; - fn get_res( - fmt: &str, - cap: &str, - params: &[Param], - vars: &mut Variables, - ) -> Result, String> { - let mut u8v: Vec<_> = fmt.bytes().collect(); - u8v.extend(cap.as_bytes().iter().map(|&b| b)); - expand(&u8v, params, vars) - } - - let caps = ["%d", "%c", "%s", "%Pa", "%l", "%!", "%~"]; - for &cap in caps.iter() { - let res = get_res("", cap, &[], vars); - assert!(res.is_err(), "Op {} succeeded incorrectly with 0 stack entries", cap); - let p = if cap == "%s" || cap == "%l" { Words("foo".to_string()) } else { Number(97) }; - let res = get_res("%p1", cap, &[p], vars); - assert!(res.is_ok(), "Op {} failed with 1 stack entry: {}", cap, res.unwrap_err()); - } - let caps = ["%+", "%-", "%*", "%/", "%m", "%&", "%|", "%A", "%O"]; - for &cap in caps.iter() { - let res = expand(cap.as_bytes(), &[], vars); - assert!(res.is_err(), "Binop {} succeeded incorrectly with 0 stack entries", cap); - let res = get_res("%{1}", cap, &[], vars); - assert!(res.is_err(), "Binop {} succeeded incorrectly with 1 stack entry", cap); - let res = get_res("%{1}%{2}", cap, &[], vars); - assert!(res.is_ok(), "Binop {} failed with 2 stack entries: {}", cap, res.unwrap_err()); - } -} - -#[test] -fn test_push_bad_param() { - assert!(expand(b"%pa", &[], &mut Variables::new()).is_err()); -} - -#[test] -fn test_comparison_ops() { - let v = [('<', [1u8, 0u8, 0u8]), ('=', [0u8, 1u8, 0u8]), ('>', [0u8, 0u8, 1u8])]; - for &(op, bs) in v.iter() { - let s = format!("%{{1}}%{{2}}%{}%d", op); - let res = expand(s.as_bytes(), &[], &mut Variables::new()); - assert!(res.is_ok(), "{}", res.unwrap_err()); - assert_eq!(res.unwrap(), vec![b'0' + bs[0]]); - let s = format!("%{{1}}%{{1}}%{}%d", op); - let res = expand(s.as_bytes(), &[], &mut Variables::new()); - assert!(res.is_ok(), "{}", res.unwrap_err()); - assert_eq!(res.unwrap(), vec![b'0' + bs[1]]); - let s = format!("%{{2}}%{{1}}%{}%d", op); - let res = expand(s.as_bytes(), &[], &mut Variables::new()); - assert!(res.is_ok(), "{}", res.unwrap_err()); - assert_eq!(res.unwrap(), vec![b'0' + bs[2]]); - } -} - -#[test] -fn test_conditionals() { - let mut vars = Variables::new(); - let s = b"\\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m"; - let res = expand(s, &[Number(1)], &mut vars); - assert!(res.is_ok(), "{}", res.unwrap_err()); - assert_eq!(res.unwrap(), "\\E[31m".bytes().collect::>()); - let res = expand(s, &[Number(8)], &mut vars); - assert!(res.is_ok(), "{}", res.unwrap_err()); - assert_eq!(res.unwrap(), "\\E[90m".bytes().collect::>()); - let res = expand(s, &[Number(42)], &mut vars); - assert!(res.is_ok(), "{}", res.unwrap_err()); - assert_eq!(res.unwrap(), "\\E[38;5;42m".bytes().collect::>()); -} - -#[test] -fn test_format() { - let mut varstruct = Variables::new(); - let vars = &mut varstruct; - assert_eq!( - expand( - b"%p1%s%p2%2s%p3%2s%p4%.2s", - &[ - Words("foo".to_string()), - Words("foo".to_string()), - Words("f".to_string()), - Words("foo".to_string()) - ], - vars - ), - Ok("foofoo ffo".bytes().collect::>()) - ); - assert_eq!( - expand(b"%p1%:-4.2s", &[Words("foo".to_string())], vars), - Ok("fo ".bytes().collect::>()) - ); - - assert_eq!( - expand(b"%p1%d%p1%.3d%p1%5d%p1%:+d", &[Number(1)], vars), - Ok("1001 1+1".bytes().collect::>()) - ); - assert_eq!( - expand(b"%p1%o%p1%#o%p2%6.4x%p2%#6.4X", &[Number(15), Number(27)], vars), - Ok("17017 001b0X001B".bytes().collect::>()) - ); -} diff --git a/library/term/src/terminfo/parser/compiled.rs b/library/term/src/terminfo/parser/compiled.rs deleted file mode 100644 index fbc5aebdb2c..00000000000 --- a/library/term/src/terminfo/parser/compiled.rs +++ /dev/null @@ -1,336 +0,0 @@ -#![allow(non_upper_case_globals, missing_docs)] - -//! ncurses-compatible compiled terminfo format parsing (term(5)) - -use super::super::TermInfo; -use std::collections::HashMap; -use std::io; -use std::io::prelude::*; - -#[cfg(test)] -mod tests; - -// These are the orders ncurses uses in its compiled format (as of 5.9). Not sure if portable. - -#[rustfmt::skip] -pub static boolfnames: &[&str] = &["auto_left_margin", "auto_right_margin", - "no_esc_ctlc", "ceol_standout_glitch", "eat_newline_glitch", "erase_overstrike", "generic_type", - "hard_copy", "has_meta_key", "has_status_line", "insert_null_glitch", "memory_above", - "memory_below", "move_insert_mode", "move_standout_mode", "over_strike", "status_line_esc_ok", - "dest_tabs_magic_smso", "tilde_glitch", "transparent_underline", "xon_xoff", "needs_xon_xoff", - "prtr_silent", "hard_cursor", "non_rev_rmcup", "no_pad_char", "non_dest_scroll_region", - "can_change", "back_color_erase", "hue_lightness_saturation", "col_addr_glitch", - "cr_cancels_micro_mode", "has_print_wheel", "row_addr_glitch", "semi_auto_right_margin", - "cpi_changes_res", "lpi_changes_res", "backspaces_with_bs", "crt_no_scrolling", - "no_correctly_working_cr", "gnu_has_meta_key", "linefeed_is_newline", "has_hardware_tabs", - "return_does_clr_eol"]; - -#[rustfmt::skip] -pub static boolnames: &[&str] = &["bw", "am", "xsb", "xhp", "xenl", "eo", - "gn", "hc", "km", "hs", "in", "db", "da", "mir", "msgr", "os", "eslok", "xt", "hz", "ul", "xon", - "nxon", "mc5i", "chts", "nrrmc", "npc", "ndscr", "ccc", "bce", "hls", "xhpa", "crxm", "daisy", - "xvpa", "sam", "cpix", "lpix", "OTbs", "OTns", "OTnc", "OTMT", "OTNL", "OTpt", "OTxr"]; - -#[rustfmt::skip] -pub static numfnames: &[&str] = &[ "columns", "init_tabs", "lines", - "lines_of_memory", "magic_cookie_glitch", "padding_baud_rate", "virtual_terminal", - "width_status_line", "num_labels", "label_height", "label_width", "max_attributes", - "maximum_windows", "max_colors", "max_pairs", "no_color_video", "buffer_capacity", - "dot_vert_spacing", "dot_horz_spacing", "max_micro_address", "max_micro_jump", "micro_col_size", - "micro_line_size", "number_of_pins", "output_res_char", "output_res_line", - "output_res_horz_inch", "output_res_vert_inch", "print_rate", "wide_char_size", "buttons", - "bit_image_entwining", "bit_image_type", "magic_cookie_glitch_ul", "carriage_return_delay", - "new_line_delay", "backspace_delay", "horizontal_tab_delay", "number_of_function_keys"]; - -#[rustfmt::skip] -pub static numnames: &[&str] = &[ "cols", "it", "lines", "lm", "xmc", "pb", - "vt", "wsl", "nlab", "lh", "lw", "ma", "wnum", "colors", "pairs", "ncv", "bufsz", "spinv", - "spinh", "maddr", "mjump", "mcs", "mls", "npins", "orc", "orl", "orhi", "orvi", "cps", "widcs", - "btns", "bitwin", "bitype", "UTug", "OTdC", "OTdN", "OTdB", "OTdT", "OTkn"]; - -#[rustfmt::skip] -pub static stringfnames: &[&str] = &[ "back_tab", "bell", "carriage_return", - "change_scroll_region", "clear_all_tabs", "clear_screen", "clr_eol", "clr_eos", - "column_address", "command_character", "cursor_address", "cursor_down", "cursor_home", - "cursor_invisible", "cursor_left", "cursor_mem_address", "cursor_normal", "cursor_right", - "cursor_to_ll", "cursor_up", "cursor_visible", "delete_character", "delete_line", - "dis_status_line", "down_half_line", "enter_alt_charset_mode", "enter_blink_mode", - "enter_bold_mode", "enter_ca_mode", "enter_delete_mode", "enter_dim_mode", "enter_insert_mode", - "enter_secure_mode", "enter_protected_mode", "enter_reverse_mode", "enter_standout_mode", - "enter_underline_mode", "erase_chars", "exit_alt_charset_mode", "exit_attribute_mode", - "exit_ca_mode", "exit_delete_mode", "exit_insert_mode", "exit_standout_mode", - "exit_underline_mode", "flash_screen", "form_feed", "from_status_line", "init_1string", - "init_2string", "init_3string", "init_file", "insert_character", "insert_line", - "insert_padding", "key_backspace", "key_catab", "key_clear", "key_ctab", "key_dc", "key_dl", - "key_down", "key_eic", "key_eol", "key_eos", "key_f0", "key_f1", "key_f10", "key_f2", "key_f3", - "key_f4", "key_f5", "key_f6", "key_f7", "key_f8", "key_f9", "key_home", "key_ic", "key_il", - "key_left", "key_ll", "key_npage", "key_ppage", "key_right", "key_sf", "key_sr", "key_stab", - "key_up", "keypad_local", "keypad_xmit", "lab_f0", "lab_f1", "lab_f10", "lab_f2", "lab_f3", - "lab_f4", "lab_f5", "lab_f6", "lab_f7", "lab_f8", "lab_f9", "meta_off", "meta_on", "newline", - "pad_char", "parm_dch", "parm_delete_line", "parm_down_cursor", "parm_ich", "parm_index", - "parm_insert_line", "parm_left_cursor", "parm_right_cursor", "parm_rindex", "parm_up_cursor", - "pkey_key", "pkey_local", "pkey_xmit", "print_screen", "prtr_off", "prtr_on", "repeat_char", - "reset_1string", "reset_2string", "reset_3string", "reset_file", "restore_cursor", - "row_address", "save_cursor", "scroll_forward", "scroll_reverse", "set_attributes", "set_tab", - "set_window", "tab", "to_status_line", "underline_char", "up_half_line", "init_prog", "key_a1", - "key_a3", "key_b2", "key_c1", "key_c3", "prtr_non", "char_padding", "acs_chars", "plab_norm", - "key_btab", "enter_xon_mode", "exit_xon_mode", "enter_am_mode", "exit_am_mode", "xon_character", - "xoff_character", "ena_acs", "label_on", "label_off", "key_beg", "key_cancel", "key_close", - "key_command", "key_copy", "key_create", "key_end", "key_enter", "key_exit", "key_find", - "key_help", "key_mark", "key_message", "key_move", "key_next", "key_open", "key_options", - "key_previous", "key_print", "key_redo", "key_reference", "key_refresh", "key_replace", - "key_restart", "key_resume", "key_save", "key_suspend", "key_undo", "key_sbeg", "key_scancel", - "key_scommand", "key_scopy", "key_screate", "key_sdc", "key_sdl", "key_select", "key_send", - "key_seol", "key_sexit", "key_sfind", "key_shelp", "key_shome", "key_sic", "key_sleft", - "key_smessage", "key_smove", "key_snext", "key_soptions", "key_sprevious", "key_sprint", - "key_sredo", "key_sreplace", "key_sright", "key_srsume", "key_ssave", "key_ssuspend", - "key_sundo", "req_for_input", "key_f11", "key_f12", "key_f13", "key_f14", "key_f15", "key_f16", - "key_f17", "key_f18", "key_f19", "key_f20", "key_f21", "key_f22", "key_f23", "key_f24", - "key_f25", "key_f26", "key_f27", "key_f28", "key_f29", "key_f30", "key_f31", "key_f32", - "key_f33", "key_f34", "key_f35", "key_f36", "key_f37", "key_f38", "key_f39", "key_f40", - "key_f41", "key_f42", "key_f43", "key_f44", "key_f45", "key_f46", "key_f47", "key_f48", - "key_f49", "key_f50", "key_f51", "key_f52", "key_f53", "key_f54", "key_f55", "key_f56", - "key_f57", "key_f58", "key_f59", "key_f60", "key_f61", "key_f62", "key_f63", "clr_bol", - "clear_margins", "set_left_margin", "set_right_margin", "label_format", "set_clock", - "display_clock", "remove_clock", "create_window", "goto_window", "hangup", "dial_phone", - "quick_dial", "tone", "pulse", "flash_hook", "fixed_pause", "wait_tone", "user0", "user1", - "user2", "user3", "user4", "user5", "user6", "user7", "user8", "user9", "orig_pair", - "orig_colors", "initialize_color", "initialize_pair", "set_color_pair", "set_foreground", - "set_background", "change_char_pitch", "change_line_pitch", "change_res_horz", - "change_res_vert", "define_char", "enter_doublewide_mode", "enter_draft_quality", - "enter_italics_mode", "enter_leftward_mode", "enter_micro_mode", "enter_near_letter_quality", - "enter_normal_quality", "enter_shadow_mode", "enter_subscript_mode", "enter_superscript_mode", - "enter_upward_mode", "exit_doublewide_mode", "exit_italics_mode", "exit_leftward_mode", - "exit_micro_mode", "exit_shadow_mode", "exit_subscript_mode", "exit_superscript_mode", - "exit_upward_mode", "micro_column_address", "micro_down", "micro_left", "micro_right", - "micro_row_address", "micro_up", "order_of_pins", "parm_down_micro", "parm_left_micro", - "parm_right_micro", "parm_up_micro", "select_char_set", "set_bottom_margin", - "set_bottom_margin_parm", "set_left_margin_parm", "set_right_margin_parm", "set_top_margin", - "set_top_margin_parm", "start_bit_image", "start_char_set_def", "stop_bit_image", - "stop_char_set_def", "subscript_characters", "superscript_characters", "these_cause_cr", - "zero_motion", "char_set_names", "key_mouse", "mouse_info", "req_mouse_pos", "get_mouse", - "set_a_foreground", "set_a_background", "pkey_plab", "device_type", "code_set_init", - "set0_des_seq", "set1_des_seq", "set2_des_seq", "set3_des_seq", "set_lr_margin", - "set_tb_margin", "bit_image_repeat", "bit_image_newline", "bit_image_carriage_return", - "color_names", "define_bit_image_region", "end_bit_image_region", "set_color_band", - "set_page_length", "display_pc_char", "enter_pc_charset_mode", "exit_pc_charset_mode", - "enter_scancode_mode", "exit_scancode_mode", "pc_term_options", "scancode_escape", - "alt_scancode_esc", "enter_horizontal_hl_mode", "enter_left_hl_mode", "enter_low_hl_mode", - "enter_right_hl_mode", "enter_top_hl_mode", "enter_vertical_hl_mode", "set_a_attributes", - "set_pglen_inch", "termcap_init2", "termcap_reset", "linefeed_if_not_lf", "backspace_if_not_bs", - "other_non_function_keys", "arrow_key_map", "acs_ulcorner", "acs_llcorner", "acs_urcorner", - "acs_lrcorner", "acs_ltee", "acs_rtee", "acs_btee", "acs_ttee", "acs_hline", "acs_vline", - "acs_plus", "memory_lock", "memory_unlock", "box_chars_1"]; - -#[rustfmt::skip] -pub static stringnames: &[&str] = &[ "cbt", "_", "cr", "csr", "tbc", "clear", - "_", "_", "hpa", "cmdch", "cup", "cud1", "home", "civis", "cub1", "mrcup", "cnorm", "cuf1", - "ll", "cuu1", "cvvis", "dch1", "dl1", "dsl", "hd", "smacs", "blink", "bold", "smcup", "smdc", - "dim", "smir", "invis", "prot", "rev", "smso", "smul", "ech", "rmacs", "sgr0", "rmcup", "rmdc", - "rmir", "rmso", "rmul", "flash", "ff", "fsl", "is1", "is2", "is3", "if", "ich1", "il1", "ip", - "kbs", "ktbc", "kclr", "kctab", "_", "_", "kcud1", "_", "_", "_", "_", "_", "_", "_", "_", "_", - "_", "_", "_", "_", "_", "khome", "_", "_", "kcub1", "_", "knp", "kpp", "kcuf1", "_", "_", - "khts", "_", "rmkx", "smkx", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "rmm", "_", - "_", "pad", "dch", "dl", "cud", "ich", "indn", "il", "cub", "cuf", "rin", "cuu", "pfkey", - "pfloc", "pfx", "mc0", "mc4", "_", "rep", "rs1", "rs2", "rs3", "rf", "rc", "vpa", "sc", "ind", - "ri", "sgr", "_", "wind", "_", "tsl", "uc", "hu", "iprog", "_", "_", "_", "_", "_", "mc5p", - "rmp", "acsc", "pln", "kcbt", "smxon", "rmxon", "smam", "rmam", "xonc", "xoffc", "_", "smln", - "rmln", "_", "kcan", "kclo", "kcmd", "kcpy", "kcrt", "_", "kent", "kext", "kfnd", "khlp", - "kmrk", "kmsg", "kmov", "knxt", "kopn", "kopt", "kprv", "kprt", "krdo", "kref", "krfr", "krpl", - "krst", "kres", "ksav", "kspd", "kund", "kBEG", "kCAN", "kCMD", "kCPY", "kCRT", "_", "_", - "kslt", "kEND", "kEOL", "kEXT", "kFND", "kHLP", "kHOM", "_", "kLFT", "kMSG", "kMOV", "kNXT", - "kOPT", "kPRV", "kPRT", "kRDO", "kRPL", "kRIT", "kRES", "kSAV", "kSPD", "kUND", "rfi", "_", "_", - "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", - "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", - "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", - "dclk", "rmclk", "cwin", "wingo", "_", "dial", "qdial", "_", "_", "hook", "pause", "wait", "_", - "_", "_", "_", "_", "_", "_", "_", "_", "_", "op", "oc", "initc", "initp", "scp", "setf", - "setb", "cpi", "lpi", "chr", "cvr", "defc", "swidm", "sdrfq", "sitm", "slm", "smicm", "snlq", - "snrmq", "sshm", "ssubm", "ssupm", "sum", "rwidm", "ritm", "rlm", "rmicm", "rshm", "rsubm", - "rsupm", "rum", "mhpa", "mcud1", "mcub1", "mcuf1", "mvpa", "mcuu1", "porder", "mcud", "mcub", - "mcuf", "mcuu", "scs", "smgb", "smgbp", "smglp", "smgrp", "smgt", "smgtp", "sbim", "scsd", - "rbim", "rcsd", "subcs", "supcs", "docr", "zerom", "csnm", "kmous", "minfo", "reqmp", "getm", - "setaf", "setab", "pfxl", "devt", "csin", "s0ds", "s1ds", "s2ds", "s3ds", "smglr", "smgtb", - "birep", "binel", "bicr", "colornm", "defbi", "endbi", "setcolor", "slines", "dispc", "smpch", - "rmpch", "smsc", "rmsc", "pctrm", "scesc", "scesa", "ehhlm", "elhlm", "elohlm", "erhlm", - "ethlm", "evhlm", "sgr1", "slength", "OTi2", "OTrs", "OTnl", "OTbs", "OTko", "OTma", "OTG2", - "OTG3", "OTG1", "OTG4", "OTGR", "OTGL", "OTGU", "OTGD", "OTGH", "OTGV", "OTGC", "meml", "memu", - "box1"]; - -fn read_le_u16(r: &mut dyn io::Read) -> io::Result { - let mut b = [0; 2]; - r.read_exact(&mut b)?; - Ok((b[0] as u16) | ((b[1] as u16) << 8)) -} - -fn read_le_u32(r: &mut dyn io::Read) -> io::Result { - let mut b = [0; 4]; - r.read_exact(&mut b)?; - Ok((b[0] as u32) | ((b[1] as u32) << 8) | ((b[2] as u32) << 16) | ((b[3] as u32) << 24)) -} - -fn read_byte(r: &mut dyn io::Read) -> io::Result { - match r.bytes().next() { - Some(s) => s, - None => Err(io::Error::new(io::ErrorKind::Other, "end of file")), - } -} - -/// Parse a compiled terminfo entry, using long capability names if `longnames` -/// is true -pub fn parse(file: &mut dyn io::Read, longnames: bool) -> Result { - macro_rules! t( ($e:expr) => ( - match $e { - Ok(e) => e, - Err(e) => return Err(e.to_string()) - } - ) ); - - let (bnames, snames, nnames) = if longnames { - (boolfnames, stringfnames, numfnames) - } else { - (boolnames, stringnames, numnames) - }; - - // Check magic number - let magic = t!(read_le_u16(file)); - - let extended = match magic { - 0o0432 => false, - 0o01036 => true, - _ => return Err(format!("invalid magic number, found {:o}", magic)), - }; - - // According to the spec, these fields must be >= -1 where -1 means that the feature is not - // supported. Using 0 instead of -1 works because we skip sections with length 0. - macro_rules! read_nonneg { - () => {{ - match t!(read_le_u16(file)) as i16 { - n if n >= 0 => n as usize, - -1 => 0, - _ => return Err("incompatible file: length fields must be >= -1".to_string()), - } - }}; - } - - let names_bytes = read_nonneg!(); - let bools_bytes = read_nonneg!(); - let numbers_count = read_nonneg!(); - let string_offsets_count = read_nonneg!(); - let string_table_bytes = read_nonneg!(); - - if names_bytes == 0 { - return Err("incompatible file: names field must be at least 1 byte wide".to_string()); - } - - if bools_bytes > boolnames.len() { - return Err("incompatible file: more booleans than expected".to_string()); - } - - if numbers_count > numnames.len() { - return Err("incompatible file: more numbers than expected".to_string()); - } - - if string_offsets_count > stringnames.len() { - return Err("incompatible file: more string offsets than expected".to_string()); - } - - // don't read NUL - let mut bytes = Vec::new(); - t!(file.take((names_bytes - 1) as u64).read_to_end(&mut bytes)); - let names_str = match String::from_utf8(bytes) { - Ok(s) => s, - Err(_) => return Err("input not utf-8".to_string()), - }; - - let term_names: Vec = names_str.split('|').map(|s| s.to_string()).collect(); - // consume NUL - if t!(read_byte(file)) != b'\0' { - return Err("incompatible file: missing null terminator for names section".to_string()); - } - - let bools_map: HashMap = t! { - (0..bools_bytes).filter_map(|i| match read_byte(file) { - Err(e) => Some(Err(e)), - Ok(1) => Some(Ok((bnames[i].to_string(), true))), - Ok(_) => None - }).collect() - }; - - if (bools_bytes + names_bytes) % 2 == 1 { - t!(read_byte(file)); // compensate for padding - } - - let numbers_map: HashMap = t! { - (0..numbers_count).filter_map(|i| { - let number = if extended { read_le_u32(file) } else { read_le_u16(file).map(Into::into) }; - - match number { - Ok(0xFFFF) => None, - Ok(n) => Some(Ok((nnames[i].to_string(), n))), - Err(e) => Some(Err(e)) - } - }).collect() - }; - - let string_map: HashMap> = if string_offsets_count > 0 { - let string_offsets: Vec = - t!((0..string_offsets_count).map(|_| read_le_u16(file)).collect()); - - let mut string_table = Vec::new(); - t!(file.take(string_table_bytes as u64).read_to_end(&mut string_table)); - - t!(string_offsets - .into_iter() - .enumerate() - .filter(|&(_, offset)| { - // non-entry - offset != 0xFFFF - }) - .map(|(i, offset)| { - let offset = offset as usize; - - let name = if snames[i] == "_" { stringfnames[i] } else { snames[i] }; - - if offset == 0xFFFE { - // undocumented: FFFE indicates cap@, which means the capability is not present - // unsure if the handling for this is correct - return Ok((name.to_string(), Vec::new())); - } - - // Find the offset of the NUL we want to go to - let nulpos = string_table[offset..string_table_bytes].iter().position(|&b| b == 0); - match nulpos { - Some(len) => { - Ok((name.to_string(), string_table[offset..offset + len].to_vec())) - } - None => Err("invalid file: missing NUL in string_table".to_string()), - } - }) - .collect()) - } else { - HashMap::new() - }; - - // And that's all there is to it - Ok(TermInfo { names: term_names, bools: bools_map, numbers: numbers_map, strings: string_map }) -} - -/// Creates a dummy TermInfo struct for msys terminals -pub fn msys_terminfo() -> TermInfo { - let mut strings = HashMap::new(); - strings.insert("sgr0".to_string(), b"\x1B[0m".to_vec()); - strings.insert("bold".to_string(), b"\x1B[1m".to_vec()); - strings.insert("setaf".to_string(), b"\x1B[3%p1%dm".to_vec()); - strings.insert("setab".to_string(), b"\x1B[4%p1%dm".to_vec()); - - let mut numbers = HashMap::new(); - numbers.insert("colors".to_string(), 8); - - TermInfo { - names: vec!["cygwin".to_string()], // msys is a fork of an older cygwin version - bools: HashMap::new(), - numbers, - strings, - } -} diff --git a/library/term/src/terminfo/parser/compiled/tests.rs b/library/term/src/terminfo/parser/compiled/tests.rs deleted file mode 100644 index 8a9187b0495..00000000000 --- a/library/term/src/terminfo/parser/compiled/tests.rs +++ /dev/null @@ -1,8 +0,0 @@ -use super::*; - -#[test] -fn test_veclens() { - assert_eq!(boolfnames.len(), boolnames.len()); - assert_eq!(numfnames.len(), numnames.len()); - assert_eq!(stringfnames.len(), stringnames.len()); -} diff --git a/library/term/src/terminfo/searcher.rs b/library/term/src/terminfo/searcher.rs deleted file mode 100644 index 5499e240e66..00000000000 --- a/library/term/src/terminfo/searcher.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! ncurses-compatible database discovery. -//! -//! Does not support hashed database, only filesystem! - -use std::env; -use std::fs; -use std::path::PathBuf; - -#[cfg(test)] -mod tests; - -/// Return path to database entry for `term` -#[allow(deprecated)] -pub fn get_dbpath_for_term(term: &str) -> Option { - let mut dirs_to_search = Vec::new(); - let first_char = term.chars().next()?; - - // Find search directory - if let Some(dir) = env::var_os("TERMINFO") { - dirs_to_search.push(PathBuf::from(dir)); - } - - if let Ok(dirs) = env::var("TERMINFO_DIRS") { - for i in dirs.split(':') { - if i == "" { - dirs_to_search.push(PathBuf::from("/usr/share/terminfo")); - } else { - dirs_to_search.push(PathBuf::from(i)); - } - } - } else { - // Found nothing in TERMINFO_DIRS, use the default paths: - // According to /etc/terminfo/README, after looking at - // ~/.terminfo, ncurses will search /etc/terminfo, then - // /lib/terminfo, and eventually /usr/share/terminfo. - // On Haiku the database can be found at /boot/system/data/terminfo - if let Some(mut homedir) = env::home_dir() { - homedir.push(".terminfo"); - dirs_to_search.push(homedir) - } - - dirs_to_search.push(PathBuf::from("/etc/terminfo")); - dirs_to_search.push(PathBuf::from("/lib/terminfo")); - dirs_to_search.push(PathBuf::from("/usr/share/terminfo")); - dirs_to_search.push(PathBuf::from("/boot/system/data/terminfo")); - } - - // Look for the terminal in all of the search directories - for mut p in dirs_to_search { - if fs::metadata(&p).is_ok() { - p.push(&first_char.to_string()); - p.push(&term); - if fs::metadata(&p).is_ok() { - return Some(p); - } - p.pop(); - p.pop(); - - // on some installations the dir is named after the hex of the char - // (e.g., macOS) - p.push(&format!("{:x}", first_char as usize)); - p.push(term); - if fs::metadata(&p).is_ok() { - return Some(p); - } - } - } - None -} diff --git a/library/term/src/terminfo/searcher/tests.rs b/library/term/src/terminfo/searcher/tests.rs deleted file mode 100644 index 4227a585e2f..00000000000 --- a/library/term/src/terminfo/searcher/tests.rs +++ /dev/null @@ -1,19 +0,0 @@ -use super::*; - -#[test] -#[ignore = "buildbots don't have ncurses installed and I can't mock everything I need"] -fn test_get_dbpath_for_term() { - // woefully inadequate test coverage - // note: current tests won't work with non-standard terminfo hierarchies (e.g., macOS's) - use std::env; - // FIXME (#9639): This needs to handle non-utf8 paths - fn x(t: &str) -> String { - let p = get_dbpath_for_term(t).expect("no terminfo entry found"); - p.to_str().unwrap().to_string() - } - assert!(x("screen") == "/usr/share/terminfo/s/screen"); - assert!(get_dbpath_for_term("") == None); - env::set_var("TERMINFO_DIRS", ":"); - assert!(x("screen") == "/usr/share/terminfo/s/screen"); - env::remove_var("TERMINFO_DIRS"); -} diff --git a/library/term/src/win.rs b/library/term/src/win.rs deleted file mode 100644 index c24cf9518aa..00000000000 --- a/library/term/src/win.rs +++ /dev/null @@ -1,220 +0,0 @@ -//! Windows console handling - -// FIXME (#13400): this is only a tiny fraction of the Windows console api - -use std::io; -use std::io::prelude::*; - -use crate::color; -use crate::Attr; -use crate::Terminal; - -/// A Terminal implementation that uses the Win32 Console API. -pub struct WinConsole { - buf: T, - def_foreground: color::Color, - def_background: color::Color, - foreground: color::Color, - background: color::Color, -} - -type SHORT = i16; -type WORD = u16; -type DWORD = u32; -type BOOL = i32; -type HANDLE = *mut u8; - -#[allow(non_snake_case)] -#[repr(C)] -struct SMALL_RECT { - Left: SHORT, - Top: SHORT, - Right: SHORT, - Bottom: SHORT, -} - -#[allow(non_snake_case)] -#[repr(C)] -struct COORD { - X: SHORT, - Y: SHORT, -} - -#[allow(non_snake_case)] -#[repr(C)] -struct CONSOLE_SCREEN_BUFFER_INFO { - dwSize: COORD, - dwCursorPosition: COORD, - wAttributes: WORD, - srWindow: SMALL_RECT, - dwMaximumWindowSize: COORD, -} - -#[allow(non_snake_case)] -#[link(name = "kernel32")] -extern "system" { - fn SetConsoleTextAttribute(handle: HANDLE, attr: WORD) -> BOOL; - fn GetStdHandle(which: DWORD) -> HANDLE; - fn GetConsoleScreenBufferInfo(handle: HANDLE, info: *mut CONSOLE_SCREEN_BUFFER_INFO) -> BOOL; -} - -fn color_to_bits(color: color::Color) -> u16 { - // magic numbers from mingw-w64's wincon.h - - let bits = match color % 8 { - color::BLACK => 0, - color::BLUE => 0x1, - color::GREEN => 0x2, - color::RED => 0x4, - color::YELLOW => 0x2 | 0x4, - color::MAGENTA => 0x1 | 0x4, - color::CYAN => 0x1 | 0x2, - color::WHITE => 0x1 | 0x2 | 0x4, - _ => unreachable!(), - }; - - if color >= 8 { bits | 0x8 } else { bits } -} - -fn bits_to_color(bits: u16) -> color::Color { - let color = match bits & 0x7 { - 0 => color::BLACK, - 0x1 => color::BLUE, - 0x2 => color::GREEN, - 0x4 => color::RED, - 0x6 => color::YELLOW, - 0x5 => color::MAGENTA, - 0x3 => color::CYAN, - 0x7 => color::WHITE, - _ => unreachable!(), - }; - - color | (u32::from(bits) & 0x8) // copy the hi-intensity bit -} - -impl WinConsole { - fn apply(&mut self) { - let _unused = self.buf.flush(); - let mut accum: WORD = 0; - accum |= color_to_bits(self.foreground); - accum |= color_to_bits(self.background) << 4; - - unsafe { - // Magic -11 means stdout, from - // https://docs.microsoft.com/en-us/windows/console/getstdhandle - // - // You may be wondering, "but what about stderr?", and the answer - // to that is that setting terminal attributes on the stdout - // handle also sets them for stderr, since they go to the same - // terminal! Admittedly, this is fragile, since stderr could be - // redirected to a different console. This is good enough for - // rustc though. See #13400. - let out = GetStdHandle(-11i32 as DWORD); - SetConsoleTextAttribute(out, accum); - } - } - - /// Returns `None` whenever the terminal cannot be created for some reason. - pub fn new(out: T) -> io::Result> { - use std::mem::MaybeUninit; - - let fg; - let bg; - unsafe { - let mut buffer_info = MaybeUninit::::uninit(); - if GetConsoleScreenBufferInfo(GetStdHandle(-11i32 as DWORD), buffer_info.as_mut_ptr()) - != 0 - { - let buffer_info = buffer_info.assume_init(); - fg = bits_to_color(buffer_info.wAttributes); - bg = bits_to_color(buffer_info.wAttributes >> 4); - } else { - fg = color::WHITE; - bg = color::BLACK; - } - } - Ok(WinConsole { - buf: out, - def_foreground: fg, - def_background: bg, - foreground: fg, - background: bg, - }) - } -} - -impl Write for WinConsole { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.buf.write(buf) - } - - fn flush(&mut self) -> io::Result<()> { - self.buf.flush() - } -} - -impl Terminal for WinConsole { - type Output = T; - - fn fg(&mut self, color: color::Color) -> io::Result { - self.foreground = color; - self.apply(); - - Ok(true) - } - - fn bg(&mut self, color: color::Color) -> io::Result { - self.background = color; - self.apply(); - - Ok(true) - } - - fn attr(&mut self, attr: Attr) -> io::Result { - match attr { - Attr::ForegroundColor(f) => { - self.foreground = f; - self.apply(); - Ok(true) - } - Attr::BackgroundColor(b) => { - self.background = b; - self.apply(); - Ok(true) - } - _ => Ok(false), - } - } - - fn supports_attr(&self, attr: Attr) -> bool { - // it claims support for underscore and reverse video, but I can't get - // it to do anything -cmr - match attr { - Attr::ForegroundColor(_) | Attr::BackgroundColor(_) => true, - _ => false, - } - } - - fn reset(&mut self) -> io::Result { - self.foreground = self.def_foreground; - self.background = self.def_background; - self.apply(); - - Ok(true) - } - - fn get_ref(&self) -> &T { - &self.buf - } - - fn get_mut(&mut self) -> &mut T { - &mut self.buf - } - - fn into_inner(self) -> T - where - Self: Sized, - { - self.buf - } -} diff --git a/library/test/Cargo.toml b/library/test/Cargo.toml index 226557430df..479d86354c0 100644 --- a/library/test/Cargo.toml +++ b/library/test/Cargo.toml @@ -10,7 +10,6 @@ crate-type = ["dylib", "rlib"] [dependencies] cfg-if = { version = "0.1.8", features = ['rustc-dep-of-std'] } getopts = { version = "0.2.21", features = ['rustc-dep-of-std'] } -term = { path = "../term" } std = { path = "../std" } core = { path = "../core" } libc = { version = "0.2", default-features = false } diff --git a/library/test/src/term/mod.rs b/library/test/src/term/mod.rs new file mode 100644 index 00000000000..943b276a220 --- /dev/null +++ b/library/test/src/term/mod.rs @@ -0,0 +1,194 @@ +//! Terminal formatting library. +//! +//! This crate provides the `Terminal` trait, which abstracts over an [ANSI +//! Terminal][ansi] to provide color printing, among other things. There are two +//! implementations, the `TerminfoTerminal`, which uses control characters from +//! a [terminfo][ti] database, and `WinConsole`, which uses the [Win32 Console +//! API][win]. +//! +//! # Examples +//! +//! ```no_run +//! # #![feature(rustc_private)] +//! extern crate term; +//! use std::io::prelude::*; +//! +//! fn main() { +//! let mut t = term::stdout().unwrap(); +//! +//! t.fg(term::color::GREEN).unwrap(); +//! write!(t, "hello, ").unwrap(); +//! +//! t.fg(term::color::RED).unwrap(); +//! writeln!(t, "world!").unwrap(); +//! +//! assert!(t.reset().unwrap()); +//! } +//! ``` +//! +//! [ansi]: https://en.wikipedia.org/wiki/ANSI_escape_code +//! [win]: https://docs.microsoft.com/en-us/windows/console/character-mode-applications +//! [ti]: https://en.wikipedia.org/wiki/Terminfo + +#![doc(html_playground_url = "https://play.rust-lang.org/", test(attr(deny(warnings))))] +#![deny(missing_docs)] +#![cfg_attr(windows, feature(libc))] + +use std::io::prelude::*; +use std::io::{self, Stderr, Stdout}; + +pub use terminfo::TerminfoTerminal; +#[cfg(windows)] +pub use win::WinConsole; + +pub mod terminfo; + +#[cfg(windows)] +mod win; + +/// Alias for stdout terminals. +pub type StdoutTerminal = dyn Terminal + Send; +/// Alias for stderr terminals. +pub type StderrTerminal = dyn Terminal + Send; + +#[cfg(not(windows))] +/// Returns a Terminal wrapping stdout, or None if a terminal couldn't be +/// opened. +pub fn stdout() -> Option> { + TerminfoTerminal::new(io::stdout()).map(|t| Box::new(t) as Box) +} + +#[cfg(windows)] +/// Returns a Terminal wrapping stdout, or None if a terminal couldn't be +/// opened. +pub fn stdout() -> Option> { + TerminfoTerminal::new(io::stdout()) + .map(|t| Box::new(t) as Box) + .or_else(|| WinConsole::new(io::stdout()).ok().map(|t| Box::new(t) as Box)) +} + +#[cfg(not(windows))] +/// Returns a Terminal wrapping stderr, or None if a terminal couldn't be +/// opened. +pub fn stderr() -> Option> { + TerminfoTerminal::new(io::stderr()).map(|t| Box::new(t) as Box) +} + +#[cfg(windows)] +/// Returns a Terminal wrapping stderr, or None if a terminal couldn't be +/// opened. +pub fn stderr() -> Option> { + TerminfoTerminal::new(io::stderr()) + .map(|t| Box::new(t) as Box) + .or_else(|| WinConsole::new(io::stderr()).ok().map(|t| Box::new(t) as Box)) +} + +/// Terminal color definitions +#[allow(missing_docs)] +pub mod color { + /// Number for a terminal color + pub type Color = u32; + + pub const BLACK: Color = 0; + pub const RED: Color = 1; + pub const GREEN: Color = 2; + pub const YELLOW: Color = 3; + pub const BLUE: Color = 4; + pub const MAGENTA: Color = 5; + pub const CYAN: Color = 6; + pub const WHITE: Color = 7; + + pub const BRIGHT_BLACK: Color = 8; + pub const BRIGHT_RED: Color = 9; + pub const BRIGHT_GREEN: Color = 10; + pub const BRIGHT_YELLOW: Color = 11; + pub const BRIGHT_BLUE: Color = 12; + pub const BRIGHT_MAGENTA: Color = 13; + pub const BRIGHT_CYAN: Color = 14; + pub const BRIGHT_WHITE: Color = 15; +} + +/// Terminal attributes for use with term.attr(). +/// +/// Most attributes can only be turned on and must be turned off with term.reset(). +/// The ones that can be turned off explicitly take a boolean value. +/// Color is also represented as an attribute for convenience. +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum Attr { + /// Bold (or possibly bright) mode + Bold, + /// Dim mode, also called faint or half-bright. Often not supported + Dim, + /// Italics mode. Often not supported + Italic(bool), + /// Underline mode + Underline(bool), + /// Blink mode + Blink, + /// Standout mode. Often implemented as Reverse, sometimes coupled with Bold + Standout(bool), + /// Reverse mode, inverts the foreground and background colors + Reverse, + /// Secure mode, also called invis mode. Hides the printed text + Secure, + /// Convenience attribute to set the foreground color + ForegroundColor(color::Color), + /// Convenience attribute to set the background color + BackgroundColor(color::Color), +} + +/// A terminal with similar capabilities to an ANSI Terminal +/// (foreground/background colors etc). +pub trait Terminal: Write { + /// The terminal's output writer type. + type Output: Write; + + /// Sets the foreground color to the given color. + /// + /// If the color is a bright color, but the terminal only supports 8 colors, + /// the corresponding normal color will be used instead. + /// + /// Returns `Ok(true)` if the color was set, `Ok(false)` otherwise, and `Err(e)` + /// if there was an I/O error. + fn fg(&mut self, color: color::Color) -> io::Result; + + /// Sets the background color to the given color. + /// + /// If the color is a bright color, but the terminal only supports 8 colors, + /// the corresponding normal color will be used instead. + /// + /// Returns `Ok(true)` if the color was set, `Ok(false)` otherwise, and `Err(e)` + /// if there was an I/O error. + fn bg(&mut self, color: color::Color) -> io::Result; + + /// Sets the given terminal attribute, if supported. Returns `Ok(true)` + /// if the attribute was supported, `Ok(false)` otherwise, and `Err(e)` if + /// there was an I/O error. + fn attr(&mut self, attr: Attr) -> io::Result; + + /// Returns `true` if the given terminal attribute is supported. + fn supports_attr(&self, attr: Attr) -> bool; + + /// Resets all terminal attributes and colors to their defaults. + /// + /// Returns `Ok(true)` if the terminal was reset, `Ok(false)` otherwise, and `Err(e)` if there + /// was an I/O error. + /// + /// *Note: This does not flush.* + /// + /// That means the reset command may get buffered so, if you aren't planning on doing anything + /// else that might flush stdout's buffer (e.g., writing a line of text), you should flush after + /// calling reset. + fn reset(&mut self) -> io::Result; + + /// Gets an immutable reference to the stream inside + fn get_ref(&self) -> &Self::Output; + + /// Gets a mutable reference to the stream inside + fn get_mut(&mut self) -> &mut Self::Output; + + /// Returns the contained stream, destroying the `Terminal` + fn into_inner(self) -> Self::Output + where + Self: Sized; +} diff --git a/library/test/src/term/terminfo/mod.rs b/library/test/src/term/terminfo/mod.rs new file mode 100644 index 00000000000..fec59aaa0c2 --- /dev/null +++ b/library/test/src/term/terminfo/mod.rs @@ -0,0 +1,245 @@ +//! Terminfo database interface. + +use std::collections::HashMap; +use std::env; +use std::error; +use std::fmt; +use std::fs::File; +use std::io::{self, prelude::*, BufReader}; +use std::path::Path; + +use crate::color; +use crate::Attr; +use crate::Terminal; + +use parm::{expand, Param, Variables}; +use parser::compiled::{msys_terminfo, parse}; +use searcher::get_dbpath_for_term; + +/// A parsed terminfo database entry. +#[derive(Debug)] +pub struct TermInfo { + /// Names for the terminal + pub names: Vec, + /// Map of capability name to boolean value + pub bools: HashMap, + /// Map of capability name to numeric value + pub numbers: HashMap, + /// Map of capability name to raw (unexpanded) string + pub strings: HashMap>, +} + +/// A terminfo creation error. +#[derive(Debug)] +pub enum Error { + /// TermUnset Indicates that the environment doesn't include enough information to find + /// the terminfo entry. + TermUnset, + /// MalformedTerminfo indicates that parsing the terminfo entry failed. + MalformedTerminfo(String), + /// io::Error forwards any io::Errors encountered when finding or reading the terminfo entry. + IoError(io::Error), +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + use Error::*; + match self { + IoError(e) => Some(e), + _ => None, + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use Error::*; + match *self { + TermUnset => Ok(()), + MalformedTerminfo(ref e) => e.fmt(f), + IoError(ref e) => e.fmt(f), + } + } +} + +impl TermInfo { + /// Creates a TermInfo based on current environment. + pub fn from_env() -> Result { + let term = match env::var("TERM") { + Ok(name) => TermInfo::from_name(&name), + Err(..) => return Err(Error::TermUnset), + }; + + if term.is_err() && env::var("MSYSCON").map_or(false, |s| "mintty.exe" == s) { + // msys terminal + Ok(msys_terminfo()) + } else { + term + } + } + + /// Creates a TermInfo for the named terminal. + pub fn from_name(name: &str) -> Result { + get_dbpath_for_term(name) + .ok_or_else(|| { + Error::IoError(io::Error::new(io::ErrorKind::NotFound, "terminfo file not found")) + }) + .and_then(|p| TermInfo::from_path(&(*p))) + } + + /// Parse the given TermInfo. + pub fn from_path>(path: P) -> Result { + Self::_from_path(path.as_ref()) + } + // Keep the metadata small + fn _from_path(path: &Path) -> Result { + let file = File::open(path).map_err(Error::IoError)?; + let mut reader = BufReader::new(file); + parse(&mut reader, false).map_err(Error::MalformedTerminfo) + } +} + +pub mod searcher; + +/// TermInfo format parsing. +pub mod parser { + //! ncurses-compatible compiled terminfo format parsing (term(5)) + pub mod compiled; +} +pub mod parm; + +fn cap_for_attr(attr: Attr) -> &'static str { + match attr { + Attr::Bold => "bold", + Attr::Dim => "dim", + Attr::Italic(true) => "sitm", + Attr::Italic(false) => "ritm", + Attr::Underline(true) => "smul", + Attr::Underline(false) => "rmul", + Attr::Blink => "blink", + Attr::Standout(true) => "smso", + Attr::Standout(false) => "rmso", + Attr::Reverse => "rev", + Attr::Secure => "invis", + Attr::ForegroundColor(_) => "setaf", + Attr::BackgroundColor(_) => "setab", + } +} + +/// A Terminal that knows how many colors it supports, with a reference to its +/// parsed Terminfo database record. +pub struct TerminfoTerminal { + num_colors: u32, + out: T, + ti: TermInfo, +} + +impl Terminal for TerminfoTerminal { + type Output = T; + fn fg(&mut self, color: color::Color) -> io::Result { + let color = self.dim_if_necessary(color); + if self.num_colors > color { + return self.apply_cap("setaf", &[Param::Number(color as i32)]); + } + Ok(false) + } + + fn bg(&mut self, color: color::Color) -> io::Result { + let color = self.dim_if_necessary(color); + if self.num_colors > color { + return self.apply_cap("setab", &[Param::Number(color as i32)]); + } + Ok(false) + } + + fn attr(&mut self, attr: Attr) -> io::Result { + match attr { + Attr::ForegroundColor(c) => self.fg(c), + Attr::BackgroundColor(c) => self.bg(c), + _ => self.apply_cap(cap_for_attr(attr), &[]), + } + } + + fn supports_attr(&self, attr: Attr) -> bool { + match attr { + Attr::ForegroundColor(_) | Attr::BackgroundColor(_) => self.num_colors > 0, + _ => { + let cap = cap_for_attr(attr); + self.ti.strings.get(cap).is_some() + } + } + } + + fn reset(&mut self) -> io::Result { + // are there any terminals that have color/attrs and not sgr0? + // Try falling back to sgr, then op + let cmd = match ["sgr0", "sgr", "op"].iter().find_map(|cap| self.ti.strings.get(*cap)) { + Some(op) => match expand(&op, &[], &mut Variables::new()) { + Ok(cmd) => cmd, + Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e)), + }, + None => return Ok(false), + }; + self.out.write_all(&cmd).and(Ok(true)) + } + + fn get_ref(&self) -> &T { + &self.out + } + + fn get_mut(&mut self) -> &mut T { + &mut self.out + } + + fn into_inner(self) -> T + where + Self: Sized, + { + self.out + } +} + +impl TerminfoTerminal { + /// Creates a new TerminfoTerminal with the given TermInfo and Write. + pub fn new_with_terminfo(out: T, terminfo: TermInfo) -> TerminfoTerminal { + let nc = if terminfo.strings.contains_key("setaf") && terminfo.strings.contains_key("setab") + { + terminfo.numbers.get("colors").map_or(0, |&n| n) + } else { + 0 + }; + + TerminfoTerminal { out, ti: terminfo, num_colors: nc } + } + + /// Creates a new TerminfoTerminal for the current environment with the given Write. + /// + /// Returns `None` when the terminfo cannot be found or parsed. + pub fn new(out: T) -> Option> { + TermInfo::from_env().map(move |ti| TerminfoTerminal::new_with_terminfo(out, ti)).ok() + } + + fn dim_if_necessary(&self, color: color::Color) -> color::Color { + if color >= self.num_colors && color >= 8 && color < 16 { color - 8 } else { color } + } + + fn apply_cap(&mut self, cmd: &str, params: &[Param]) -> io::Result { + match self.ti.strings.get(cmd) { + Some(cmd) => match expand(&cmd, params, &mut Variables::new()) { + Ok(s) => self.out.write_all(&s).and(Ok(true)), + Err(e) => Err(io::Error::new(io::ErrorKind::InvalidData, e)), + }, + None => Ok(false), + } + } +} + +impl Write for TerminfoTerminal { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.out.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.out.flush() + } +} diff --git a/library/test/src/term/terminfo/parm.rs b/library/test/src/term/terminfo/parm.rs new file mode 100644 index 00000000000..2e4e917891e --- /dev/null +++ b/library/test/src/term/terminfo/parm.rs @@ -0,0 +1,559 @@ +//! Parameterized string expansion + +use self::Param::*; +use self::States::*; + +use std::iter::repeat; + +#[cfg(test)] +mod tests; + +#[derive(Clone, Copy, PartialEq)] +enum States { + Nothing, + Percent, + SetVar, + GetVar, + PushParam, + CharConstant, + CharClose, + IntConstant(i32), + FormatPattern(Flags, FormatState), + SeekIfElse(usize), + SeekIfElsePercent(usize), + SeekIfEnd(usize), + SeekIfEndPercent(usize), +} + +#[derive(Copy, PartialEq, Clone)] +enum FormatState { + Flags, + Width, + Precision, +} + +/// Types of parameters a capability can use +#[allow(missing_docs)] +#[derive(Clone)] +pub enum Param { + Words(String), + Number(i32), +} + +/// Container for static and dynamic variable arrays +pub struct Variables { + /// Static variables A-Z + sta_va: [Param; 26], + /// Dynamic variables a-z + dyn_va: [Param; 26], +} + +impl Variables { + /// Returns a new zero-initialized Variables + pub fn new() -> Variables { + Variables { + sta_va: [ + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + ], + dyn_va: [ + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + ], + } + } +} + +/// Expand a parameterized capability +/// +/// # Arguments +/// * `cap` - string to expand +/// * `params` - vector of params for %p1 etc +/// * `vars` - Variables struct for %Pa etc +/// +/// To be compatible with ncurses, `vars` should be the same between calls to `expand` for +/// multiple capabilities for the same terminal. +pub fn expand(cap: &[u8], params: &[Param], vars: &mut Variables) -> Result, String> { + let mut state = Nothing; + + // expanded cap will only rarely be larger than the cap itself + let mut output = Vec::with_capacity(cap.len()); + + let mut stack: Vec = Vec::new(); + + // Copy parameters into a local vector for mutability + let mut mparams = [ + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + Number(0), + ]; + for (dst, src) in mparams.iter_mut().zip(params.iter()) { + *dst = (*src).clone(); + } + + for &c in cap.iter() { + let cur = c as char; + let mut old_state = state; + match state { + Nothing => { + if cur == '%' { + state = Percent; + } else { + output.push(c); + } + } + Percent => { + match cur { + '%' => { + output.push(c); + state = Nothing + } + 'c' => { + match stack.pop() { + // if c is 0, use 0200 (128) for ncurses compatibility + Some(Number(0)) => output.push(128u8), + // Don't check bounds. ncurses just casts and truncates. + Some(Number(c)) => output.push(c as u8), + Some(_) => return Err("a non-char was used with %c".to_string()), + None => return Err("stack is empty".to_string()), + } + } + 'p' => state = PushParam, + 'P' => state = SetVar, + 'g' => state = GetVar, + '\'' => state = CharConstant, + '{' => state = IntConstant(0), + 'l' => match stack.pop() { + Some(Words(s)) => stack.push(Number(s.len() as i32)), + Some(_) => return Err("a non-str was used with %l".to_string()), + None => return Err("stack is empty".to_string()), + }, + '+' | '-' | '/' | '*' | '^' | '&' | '|' | 'm' => { + match (stack.pop(), stack.pop()) { + (Some(Number(y)), Some(Number(x))) => stack.push(Number(match cur { + '+' => x + y, + '-' => x - y, + '*' => x * y, + '/' => x / y, + '|' => x | y, + '&' => x & y, + '^' => x ^ y, + 'm' => x % y, + _ => unreachable!("All cases handled"), + })), + (Some(_), Some(_)) => { + return Err(format!("non-numbers on stack with {}", cur)); + } + _ => return Err("stack is empty".to_string()), + } + } + '=' | '>' | '<' | 'A' | 'O' => match (stack.pop(), stack.pop()) { + (Some(Number(y)), Some(Number(x))) => stack.push(Number( + if match cur { + '=' => x == y, + '<' => x < y, + '>' => x > y, + 'A' => x > 0 && y > 0, + 'O' => x > 0 || y > 0, + _ => unreachable!(), + } { + 1 + } else { + 0 + }, + )), + (Some(_), Some(_)) => { + return Err(format!("non-numbers on stack with {}", cur)); + } + _ => return Err("stack is empty".to_string()), + }, + '!' | '~' => match stack.pop() { + Some(Number(x)) => stack.push(Number(match cur { + '!' if x > 0 => 0, + '!' => 1, + '~' => !x, + _ => unreachable!(), + })), + Some(_) => return Err(format!("non-numbers on stack with {}", cur)), + None => return Err("stack is empty".to_string()), + }, + 'i' => match (&mparams[0], &mparams[1]) { + (&Number(x), &Number(y)) => { + mparams[0] = Number(x + 1); + mparams[1] = Number(y + 1); + } + _ => return Err("first two params not numbers with %i".to_string()), + }, + + // printf-style support for %doxXs + 'd' | 'o' | 'x' | 'X' | 's' => { + if let Some(arg) = stack.pop() { + let flags = Flags::new(); + let res = format(arg, FormatOp::from_char(cur), flags)?; + output.extend(res.iter().cloned()); + } else { + return Err("stack is empty".to_string()); + } + } + ':' | '#' | ' ' | '.' | '0'..='9' => { + let mut flags = Flags::new(); + let mut fstate = FormatState::Flags; + match cur { + ':' => (), + '#' => flags.alternate = true, + ' ' => flags.space = true, + '.' => fstate = FormatState::Precision, + '0'..='9' => { + flags.width = cur as usize - '0' as usize; + fstate = FormatState::Width; + } + _ => unreachable!(), + } + state = FormatPattern(flags, fstate); + } + + // conditionals + '?' => (), + 't' => match stack.pop() { + Some(Number(0)) => state = SeekIfElse(0), + Some(Number(_)) => (), + Some(_) => return Err("non-number on stack with conditional".to_string()), + None => return Err("stack is empty".to_string()), + }, + 'e' => state = SeekIfEnd(0), + ';' => (), + _ => return Err(format!("unrecognized format option {}", cur)), + } + } + PushParam => { + // params are 1-indexed + stack.push( + mparams[match cur.to_digit(10) { + Some(d) => d as usize - 1, + None => return Err("bad param number".to_string()), + }] + .clone(), + ); + } + SetVar => { + if cur >= 'A' && cur <= 'Z' { + if let Some(arg) = stack.pop() { + let idx = (cur as u8) - b'A'; + vars.sta_va[idx as usize] = arg; + } else { + return Err("stack is empty".to_string()); + } + } else if cur >= 'a' && cur <= 'z' { + if let Some(arg) = stack.pop() { + let idx = (cur as u8) - b'a'; + vars.dyn_va[idx as usize] = arg; + } else { + return Err("stack is empty".to_string()); + } + } else { + return Err("bad variable name in %P".to_string()); + } + } + GetVar => { + if cur >= 'A' && cur <= 'Z' { + let idx = (cur as u8) - b'A'; + stack.push(vars.sta_va[idx as usize].clone()); + } else if cur >= 'a' && cur <= 'z' { + let idx = (cur as u8) - b'a'; + stack.push(vars.dyn_va[idx as usize].clone()); + } else { + return Err("bad variable name in %g".to_string()); + } + } + CharConstant => { + stack.push(Number(c as i32)); + state = CharClose; + } + CharClose => { + if cur != '\'' { + return Err("malformed character constant".to_string()); + } + } + IntConstant(i) => { + if cur == '}' { + stack.push(Number(i)); + state = Nothing; + } else if let Some(digit) = cur.to_digit(10) { + match i.checked_mul(10).and_then(|i_ten| i_ten.checked_add(digit as i32)) { + Some(i) => { + state = IntConstant(i); + old_state = Nothing; + } + None => return Err("int constant too large".to_string()), + } + } else { + return Err("bad int constant".to_string()); + } + } + FormatPattern(ref mut flags, ref mut fstate) => { + old_state = Nothing; + match (*fstate, cur) { + (_, 'd') | (_, 'o') | (_, 'x') | (_, 'X') | (_, 's') => { + if let Some(arg) = stack.pop() { + let res = format(arg, FormatOp::from_char(cur), *flags)?; + output.extend(res.iter().cloned()); + // will cause state to go to Nothing + old_state = FormatPattern(*flags, *fstate); + } else { + return Err("stack is empty".to_string()); + } + } + (FormatState::Flags, '#') => { + flags.alternate = true; + } + (FormatState::Flags, '-') => { + flags.left = true; + } + (FormatState::Flags, '+') => { + flags.sign = true; + } + (FormatState::Flags, ' ') => { + flags.space = true; + } + (FormatState::Flags, '0'..='9') => { + flags.width = cur as usize - '0' as usize; + *fstate = FormatState::Width; + } + (FormatState::Flags, '.') => { + *fstate = FormatState::Precision; + } + (FormatState::Width, '0'..='9') => { + let old = flags.width; + flags.width = flags.width * 10 + (cur as usize - '0' as usize); + if flags.width < old { + return Err("format width overflow".to_string()); + } + } + (FormatState::Width, '.') => { + *fstate = FormatState::Precision; + } + (FormatState::Precision, '0'..='9') => { + let old = flags.precision; + flags.precision = flags.precision * 10 + (cur as usize - '0' as usize); + if flags.precision < old { + return Err("format precision overflow".to_string()); + } + } + _ => return Err("invalid format specifier".to_string()), + } + } + SeekIfElse(level) => { + if cur == '%' { + state = SeekIfElsePercent(level); + } + old_state = Nothing; + } + SeekIfElsePercent(level) => { + if cur == ';' { + if level == 0 { + state = Nothing; + } else { + state = SeekIfElse(level - 1); + } + } else if cur == 'e' && level == 0 { + state = Nothing; + } else if cur == '?' { + state = SeekIfElse(level + 1); + } else { + state = SeekIfElse(level); + } + } + SeekIfEnd(level) => { + if cur == '%' { + state = SeekIfEndPercent(level); + } + old_state = Nothing; + } + SeekIfEndPercent(level) => { + if cur == ';' { + if level == 0 { + state = Nothing; + } else { + state = SeekIfEnd(level - 1); + } + } else if cur == '?' { + state = SeekIfEnd(level + 1); + } else { + state = SeekIfEnd(level); + } + } + } + if state == old_state { + state = Nothing; + } + } + Ok(output) +} + +#[derive(Copy, PartialEq, Clone)] +struct Flags { + width: usize, + precision: usize, + alternate: bool, + left: bool, + sign: bool, + space: bool, +} + +impl Flags { + fn new() -> Flags { + Flags { width: 0, precision: 0, alternate: false, left: false, sign: false, space: false } + } +} + +#[derive(Copy, Clone)] +enum FormatOp { + Digit, + Octal, + LowerHex, + UpperHex, + String, +} + +impl FormatOp { + fn from_char(c: char) -> FormatOp { + match c { + 'd' => FormatOp::Digit, + 'o' => FormatOp::Octal, + 'x' => FormatOp::LowerHex, + 'X' => FormatOp::UpperHex, + 's' => FormatOp::String, + _ => panic!("bad FormatOp char"), + } + } + fn to_char(self) -> char { + match self { + FormatOp::Digit => 'd', + FormatOp::Octal => 'o', + FormatOp::LowerHex => 'x', + FormatOp::UpperHex => 'X', + FormatOp::String => 's', + } + } +} + +fn format(val: Param, op: FormatOp, flags: Flags) -> Result, String> { + let mut s = match val { + Number(d) => { + match op { + FormatOp::Digit => { + if flags.sign { + format!("{:+01$}", d, flags.precision) + } else if d < 0 { + // C doesn't take sign into account in precision calculation. + format!("{:01$}", d, flags.precision + 1) + } else if flags.space { + format!(" {:01$}", d, flags.precision) + } else { + format!("{:01$}", d, flags.precision) + } + } + FormatOp::Octal => { + if flags.alternate { + // Leading octal zero counts against precision. + format!("0{:01$o}", d, flags.precision.saturating_sub(1)) + } else { + format!("{:01$o}", d, flags.precision) + } + } + FormatOp::LowerHex => { + if flags.alternate && d != 0 { + format!("0x{:01$x}", d, flags.precision) + } else { + format!("{:01$x}", d, flags.precision) + } + } + FormatOp::UpperHex => { + if flags.alternate && d != 0 { + format!("0X{:01$X}", d, flags.precision) + } else { + format!("{:01$X}", d, flags.precision) + } + } + FormatOp::String => return Err("non-number on stack with %s".to_string()), + } + .into_bytes() + } + Words(s) => match op { + FormatOp::String => { + let mut s = s.into_bytes(); + if flags.precision > 0 && flags.precision < s.len() { + s.truncate(flags.precision); + } + s + } + _ => return Err(format!("non-string on stack with %{}", op.to_char())), + }, + }; + if flags.width > s.len() { + let n = flags.width - s.len(); + if flags.left { + s.extend(repeat(b' ').take(n)); + } else { + let mut s_ = Vec::with_capacity(flags.width); + s_.extend(repeat(b' ').take(n)); + s_.extend(s.into_iter()); + s = s_; + } + } + Ok(s) +} diff --git a/library/test/src/term/terminfo/parm/tests.rs b/library/test/src/term/terminfo/parm/tests.rs new file mode 100644 index 00000000000..1cc0967c8f4 --- /dev/null +++ b/library/test/src/term/terminfo/parm/tests.rs @@ -0,0 +1,138 @@ +use super::*; + +use std::result::Result::Ok; + +#[test] +fn test_basic_setabf() { + let s = b"\\E[48;5;%p1%dm"; + assert_eq!( + expand(s, &[Number(1)], &mut Variables::new()).unwrap(), + "\\E[48;5;1m".bytes().collect::>() + ); +} + +#[test] +fn test_multiple_int_constants() { + assert_eq!( + expand(b"%{1}%{2}%d%d", &[], &mut Variables::new()).unwrap(), + "21".bytes().collect::>() + ); +} + +#[test] +fn test_op_i() { + let mut vars = Variables::new(); + assert_eq!( + expand(b"%p1%d%p2%d%p3%d%i%p1%d%p2%d%p3%d", &[Number(1), Number(2), Number(3)], &mut vars), + Ok("123233".bytes().collect::>()) + ); + assert_eq!( + expand(b"%p1%d%p2%d%i%p1%d%p2%d", &[], &mut vars), + Ok("0011".bytes().collect::>()) + ); +} + +#[test] +fn test_param_stack_failure_conditions() { + let mut varstruct = Variables::new(); + let vars = &mut varstruct; + fn get_res( + fmt: &str, + cap: &str, + params: &[Param], + vars: &mut Variables, + ) -> Result, String> { + let mut u8v: Vec<_> = fmt.bytes().collect(); + u8v.extend(cap.as_bytes().iter().map(|&b| b)); + expand(&u8v, params, vars) + } + + let caps = ["%d", "%c", "%s", "%Pa", "%l", "%!", "%~"]; + for &cap in caps.iter() { + let res = get_res("", cap, &[], vars); + assert!(res.is_err(), "Op {} succeeded incorrectly with 0 stack entries", cap); + let p = if cap == "%s" || cap == "%l" { Words("foo".to_string()) } else { Number(97) }; + let res = get_res("%p1", cap, &[p], vars); + assert!(res.is_ok(), "Op {} failed with 1 stack entry: {}", cap, res.unwrap_err()); + } + let caps = ["%+", "%-", "%*", "%/", "%m", "%&", "%|", "%A", "%O"]; + for &cap in caps.iter() { + let res = expand(cap.as_bytes(), &[], vars); + assert!(res.is_err(), "Binop {} succeeded incorrectly with 0 stack entries", cap); + let res = get_res("%{1}", cap, &[], vars); + assert!(res.is_err(), "Binop {} succeeded incorrectly with 1 stack entry", cap); + let res = get_res("%{1}%{2}", cap, &[], vars); + assert!(res.is_ok(), "Binop {} failed with 2 stack entries: {}", cap, res.unwrap_err()); + } +} + +#[test] +fn test_push_bad_param() { + assert!(expand(b"%pa", &[], &mut Variables::new()).is_err()); +} + +#[test] +fn test_comparison_ops() { + let v = [('<', [1u8, 0u8, 0u8]), ('=', [0u8, 1u8, 0u8]), ('>', [0u8, 0u8, 1u8])]; + for &(op, bs) in v.iter() { + let s = format!("%{{1}}%{{2}}%{}%d", op); + let res = expand(s.as_bytes(), &[], &mut Variables::new()); + assert!(res.is_ok(), "{}", res.unwrap_err()); + assert_eq!(res.unwrap(), vec![b'0' + bs[0]]); + let s = format!("%{{1}}%{{1}}%{}%d", op); + let res = expand(s.as_bytes(), &[], &mut Variables::new()); + assert!(res.is_ok(), "{}", res.unwrap_err()); + assert_eq!(res.unwrap(), vec![b'0' + bs[1]]); + let s = format!("%{{2}}%{{1}}%{}%d", op); + let res = expand(s.as_bytes(), &[], &mut Variables::new()); + assert!(res.is_ok(), "{}", res.unwrap_err()); + assert_eq!(res.unwrap(), vec![b'0' + bs[2]]); + } +} + +#[test] +fn test_conditionals() { + let mut vars = Variables::new(); + let s = b"\\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m"; + let res = expand(s, &[Number(1)], &mut vars); + assert!(res.is_ok(), "{}", res.unwrap_err()); + assert_eq!(res.unwrap(), "\\E[31m".bytes().collect::>()); + let res = expand(s, &[Number(8)], &mut vars); + assert!(res.is_ok(), "{}", res.unwrap_err()); + assert_eq!(res.unwrap(), "\\E[90m".bytes().collect::>()); + let res = expand(s, &[Number(42)], &mut vars); + assert!(res.is_ok(), "{}", res.unwrap_err()); + assert_eq!(res.unwrap(), "\\E[38;5;42m".bytes().collect::>()); +} + +#[test] +fn test_format() { + let mut varstruct = Variables::new(); + let vars = &mut varstruct; + assert_eq!( + expand( + b"%p1%s%p2%2s%p3%2s%p4%.2s", + &[ + Words("foo".to_string()), + Words("foo".to_string()), + Words("f".to_string()), + Words("foo".to_string()) + ], + vars + ), + Ok("foofoo ffo".bytes().collect::>()) + ); + assert_eq!( + expand(b"%p1%:-4.2s", &[Words("foo".to_string())], vars), + Ok("fo ".bytes().collect::>()) + ); + + assert_eq!( + expand(b"%p1%d%p1%.3d%p1%5d%p1%:+d", &[Number(1)], vars), + Ok("1001 1+1".bytes().collect::>()) + ); + assert_eq!( + expand(b"%p1%o%p1%#o%p2%6.4x%p2%#6.4X", &[Number(15), Number(27)], vars), + Ok("17017 001b0X001B".bytes().collect::>()) + ); +} diff --git a/library/test/src/term/terminfo/parser/compiled.rs b/library/test/src/term/terminfo/parser/compiled.rs new file mode 100644 index 00000000000..fbc5aebdb2c --- /dev/null +++ b/library/test/src/term/terminfo/parser/compiled.rs @@ -0,0 +1,336 @@ +#![allow(non_upper_case_globals, missing_docs)] + +//! ncurses-compatible compiled terminfo format parsing (term(5)) + +use super::super::TermInfo; +use std::collections::HashMap; +use std::io; +use std::io::prelude::*; + +#[cfg(test)] +mod tests; + +// These are the orders ncurses uses in its compiled format (as of 5.9). Not sure if portable. + +#[rustfmt::skip] +pub static boolfnames: &[&str] = &["auto_left_margin", "auto_right_margin", + "no_esc_ctlc", "ceol_standout_glitch", "eat_newline_glitch", "erase_overstrike", "generic_type", + "hard_copy", "has_meta_key", "has_status_line", "insert_null_glitch", "memory_above", + "memory_below", "move_insert_mode", "move_standout_mode", "over_strike", "status_line_esc_ok", + "dest_tabs_magic_smso", "tilde_glitch", "transparent_underline", "xon_xoff", "needs_xon_xoff", + "prtr_silent", "hard_cursor", "non_rev_rmcup", "no_pad_char", "non_dest_scroll_region", + "can_change", "back_color_erase", "hue_lightness_saturation", "col_addr_glitch", + "cr_cancels_micro_mode", "has_print_wheel", "row_addr_glitch", "semi_auto_right_margin", + "cpi_changes_res", "lpi_changes_res", "backspaces_with_bs", "crt_no_scrolling", + "no_correctly_working_cr", "gnu_has_meta_key", "linefeed_is_newline", "has_hardware_tabs", + "return_does_clr_eol"]; + +#[rustfmt::skip] +pub static boolnames: &[&str] = &["bw", "am", "xsb", "xhp", "xenl", "eo", + "gn", "hc", "km", "hs", "in", "db", "da", "mir", "msgr", "os", "eslok", "xt", "hz", "ul", "xon", + "nxon", "mc5i", "chts", "nrrmc", "npc", "ndscr", "ccc", "bce", "hls", "xhpa", "crxm", "daisy", + "xvpa", "sam", "cpix", "lpix", "OTbs", "OTns", "OTnc", "OTMT", "OTNL", "OTpt", "OTxr"]; + +#[rustfmt::skip] +pub static numfnames: &[&str] = &[ "columns", "init_tabs", "lines", + "lines_of_memory", "magic_cookie_glitch", "padding_baud_rate", "virtual_terminal", + "width_status_line", "num_labels", "label_height", "label_width", "max_attributes", + "maximum_windows", "max_colors", "max_pairs", "no_color_video", "buffer_capacity", + "dot_vert_spacing", "dot_horz_spacing", "max_micro_address", "max_micro_jump", "micro_col_size", + "micro_line_size", "number_of_pins", "output_res_char", "output_res_line", + "output_res_horz_inch", "output_res_vert_inch", "print_rate", "wide_char_size", "buttons", + "bit_image_entwining", "bit_image_type", "magic_cookie_glitch_ul", "carriage_return_delay", + "new_line_delay", "backspace_delay", "horizontal_tab_delay", "number_of_function_keys"]; + +#[rustfmt::skip] +pub static numnames: &[&str] = &[ "cols", "it", "lines", "lm", "xmc", "pb", + "vt", "wsl", "nlab", "lh", "lw", "ma", "wnum", "colors", "pairs", "ncv", "bufsz", "spinv", + "spinh", "maddr", "mjump", "mcs", "mls", "npins", "orc", "orl", "orhi", "orvi", "cps", "widcs", + "btns", "bitwin", "bitype", "UTug", "OTdC", "OTdN", "OTdB", "OTdT", "OTkn"]; + +#[rustfmt::skip] +pub static stringfnames: &[&str] = &[ "back_tab", "bell", "carriage_return", + "change_scroll_region", "clear_all_tabs", "clear_screen", "clr_eol", "clr_eos", + "column_address", "command_character", "cursor_address", "cursor_down", "cursor_home", + "cursor_invisible", "cursor_left", "cursor_mem_address", "cursor_normal", "cursor_right", + "cursor_to_ll", "cursor_up", "cursor_visible", "delete_character", "delete_line", + "dis_status_line", "down_half_line", "enter_alt_charset_mode", "enter_blink_mode", + "enter_bold_mode", "enter_ca_mode", "enter_delete_mode", "enter_dim_mode", "enter_insert_mode", + "enter_secure_mode", "enter_protected_mode", "enter_reverse_mode", "enter_standout_mode", + "enter_underline_mode", "erase_chars", "exit_alt_charset_mode", "exit_attribute_mode", + "exit_ca_mode", "exit_delete_mode", "exit_insert_mode", "exit_standout_mode", + "exit_underline_mode", "flash_screen", "form_feed", "from_status_line", "init_1string", + "init_2string", "init_3string", "init_file", "insert_character", "insert_line", + "insert_padding", "key_backspace", "key_catab", "key_clear", "key_ctab", "key_dc", "key_dl", + "key_down", "key_eic", "key_eol", "key_eos", "key_f0", "key_f1", "key_f10", "key_f2", "key_f3", + "key_f4", "key_f5", "key_f6", "key_f7", "key_f8", "key_f9", "key_home", "key_ic", "key_il", + "key_left", "key_ll", "key_npage", "key_ppage", "key_right", "key_sf", "key_sr", "key_stab", + "key_up", "keypad_local", "keypad_xmit", "lab_f0", "lab_f1", "lab_f10", "lab_f2", "lab_f3", + "lab_f4", "lab_f5", "lab_f6", "lab_f7", "lab_f8", "lab_f9", "meta_off", "meta_on", "newline", + "pad_char", "parm_dch", "parm_delete_line", "parm_down_cursor", "parm_ich", "parm_index", + "parm_insert_line", "parm_left_cursor", "parm_right_cursor", "parm_rindex", "parm_up_cursor", + "pkey_key", "pkey_local", "pkey_xmit", "print_screen", "prtr_off", "prtr_on", "repeat_char", + "reset_1string", "reset_2string", "reset_3string", "reset_file", "restore_cursor", + "row_address", "save_cursor", "scroll_forward", "scroll_reverse", "set_attributes", "set_tab", + "set_window", "tab", "to_status_line", "underline_char", "up_half_line", "init_prog", "key_a1", + "key_a3", "key_b2", "key_c1", "key_c3", "prtr_non", "char_padding", "acs_chars", "plab_norm", + "key_btab", "enter_xon_mode", "exit_xon_mode", "enter_am_mode", "exit_am_mode", "xon_character", + "xoff_character", "ena_acs", "label_on", "label_off", "key_beg", "key_cancel", "key_close", + "key_command", "key_copy", "key_create", "key_end", "key_enter", "key_exit", "key_find", + "key_help", "key_mark", "key_message", "key_move", "key_next", "key_open", "key_options", + "key_previous", "key_print", "key_redo", "key_reference", "key_refresh", "key_replace", + "key_restart", "key_resume", "key_save", "key_suspend", "key_undo", "key_sbeg", "key_scancel", + "key_scommand", "key_scopy", "key_screate", "key_sdc", "key_sdl", "key_select", "key_send", + "key_seol", "key_sexit", "key_sfind", "key_shelp", "key_shome", "key_sic", "key_sleft", + "key_smessage", "key_smove", "key_snext", "key_soptions", "key_sprevious", "key_sprint", + "key_sredo", "key_sreplace", "key_sright", "key_srsume", "key_ssave", "key_ssuspend", + "key_sundo", "req_for_input", "key_f11", "key_f12", "key_f13", "key_f14", "key_f15", "key_f16", + "key_f17", "key_f18", "key_f19", "key_f20", "key_f21", "key_f22", "key_f23", "key_f24", + "key_f25", "key_f26", "key_f27", "key_f28", "key_f29", "key_f30", "key_f31", "key_f32", + "key_f33", "key_f34", "key_f35", "key_f36", "key_f37", "key_f38", "key_f39", "key_f40", + "key_f41", "key_f42", "key_f43", "key_f44", "key_f45", "key_f46", "key_f47", "key_f48", + "key_f49", "key_f50", "key_f51", "key_f52", "key_f53", "key_f54", "key_f55", "key_f56", + "key_f57", "key_f58", "key_f59", "key_f60", "key_f61", "key_f62", "key_f63", "clr_bol", + "clear_margins", "set_left_margin", "set_right_margin", "label_format", "set_clock", + "display_clock", "remove_clock", "create_window", "goto_window", "hangup", "dial_phone", + "quick_dial", "tone", "pulse", "flash_hook", "fixed_pause", "wait_tone", "user0", "user1", + "user2", "user3", "user4", "user5", "user6", "user7", "user8", "user9", "orig_pair", + "orig_colors", "initialize_color", "initialize_pair", "set_color_pair", "set_foreground", + "set_background", "change_char_pitch", "change_line_pitch", "change_res_horz", + "change_res_vert", "define_char", "enter_doublewide_mode", "enter_draft_quality", + "enter_italics_mode", "enter_leftward_mode", "enter_micro_mode", "enter_near_letter_quality", + "enter_normal_quality", "enter_shadow_mode", "enter_subscript_mode", "enter_superscript_mode", + "enter_upward_mode", "exit_doublewide_mode", "exit_italics_mode", "exit_leftward_mode", + "exit_micro_mode", "exit_shadow_mode", "exit_subscript_mode", "exit_superscript_mode", + "exit_upward_mode", "micro_column_address", "micro_down", "micro_left", "micro_right", + "micro_row_address", "micro_up", "order_of_pins", "parm_down_micro", "parm_left_micro", + "parm_right_micro", "parm_up_micro", "select_char_set", "set_bottom_margin", + "set_bottom_margin_parm", "set_left_margin_parm", "set_right_margin_parm", "set_top_margin", + "set_top_margin_parm", "start_bit_image", "start_char_set_def", "stop_bit_image", + "stop_char_set_def", "subscript_characters", "superscript_characters", "these_cause_cr", + "zero_motion", "char_set_names", "key_mouse", "mouse_info", "req_mouse_pos", "get_mouse", + "set_a_foreground", "set_a_background", "pkey_plab", "device_type", "code_set_init", + "set0_des_seq", "set1_des_seq", "set2_des_seq", "set3_des_seq", "set_lr_margin", + "set_tb_margin", "bit_image_repeat", "bit_image_newline", "bit_image_carriage_return", + "color_names", "define_bit_image_region", "end_bit_image_region", "set_color_band", + "set_page_length", "display_pc_char", "enter_pc_charset_mode", "exit_pc_charset_mode", + "enter_scancode_mode", "exit_scancode_mode", "pc_term_options", "scancode_escape", + "alt_scancode_esc", "enter_horizontal_hl_mode", "enter_left_hl_mode", "enter_low_hl_mode", + "enter_right_hl_mode", "enter_top_hl_mode", "enter_vertical_hl_mode", "set_a_attributes", + "set_pglen_inch", "termcap_init2", "termcap_reset", "linefeed_if_not_lf", "backspace_if_not_bs", + "other_non_function_keys", "arrow_key_map", "acs_ulcorner", "acs_llcorner", "acs_urcorner", + "acs_lrcorner", "acs_ltee", "acs_rtee", "acs_btee", "acs_ttee", "acs_hline", "acs_vline", + "acs_plus", "memory_lock", "memory_unlock", "box_chars_1"]; + +#[rustfmt::skip] +pub static stringnames: &[&str] = &[ "cbt", "_", "cr", "csr", "tbc", "clear", + "_", "_", "hpa", "cmdch", "cup", "cud1", "home", "civis", "cub1", "mrcup", "cnorm", "cuf1", + "ll", "cuu1", "cvvis", "dch1", "dl1", "dsl", "hd", "smacs", "blink", "bold", "smcup", "smdc", + "dim", "smir", "invis", "prot", "rev", "smso", "smul", "ech", "rmacs", "sgr0", "rmcup", "rmdc", + "rmir", "rmso", "rmul", "flash", "ff", "fsl", "is1", "is2", "is3", "if", "ich1", "il1", "ip", + "kbs", "ktbc", "kclr", "kctab", "_", "_", "kcud1", "_", "_", "_", "_", "_", "_", "_", "_", "_", + "_", "_", "_", "_", "_", "khome", "_", "_", "kcub1", "_", "knp", "kpp", "kcuf1", "_", "_", + "khts", "_", "rmkx", "smkx", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "rmm", "_", + "_", "pad", "dch", "dl", "cud", "ich", "indn", "il", "cub", "cuf", "rin", "cuu", "pfkey", + "pfloc", "pfx", "mc0", "mc4", "_", "rep", "rs1", "rs2", "rs3", "rf", "rc", "vpa", "sc", "ind", + "ri", "sgr", "_", "wind", "_", "tsl", "uc", "hu", "iprog", "_", "_", "_", "_", "_", "mc5p", + "rmp", "acsc", "pln", "kcbt", "smxon", "rmxon", "smam", "rmam", "xonc", "xoffc", "_", "smln", + "rmln", "_", "kcan", "kclo", "kcmd", "kcpy", "kcrt", "_", "kent", "kext", "kfnd", "khlp", + "kmrk", "kmsg", "kmov", "knxt", "kopn", "kopt", "kprv", "kprt", "krdo", "kref", "krfr", "krpl", + "krst", "kres", "ksav", "kspd", "kund", "kBEG", "kCAN", "kCMD", "kCPY", "kCRT", "_", "_", + "kslt", "kEND", "kEOL", "kEXT", "kFND", "kHLP", "kHOM", "_", "kLFT", "kMSG", "kMOV", "kNXT", + "kOPT", "kPRV", "kPRT", "kRDO", "kRPL", "kRIT", "kRES", "kSAV", "kSPD", "kUND", "rfi", "_", "_", + "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", + "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", + "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", + "dclk", "rmclk", "cwin", "wingo", "_", "dial", "qdial", "_", "_", "hook", "pause", "wait", "_", + "_", "_", "_", "_", "_", "_", "_", "_", "_", "op", "oc", "initc", "initp", "scp", "setf", + "setb", "cpi", "lpi", "chr", "cvr", "defc", "swidm", "sdrfq", "sitm", "slm", "smicm", "snlq", + "snrmq", "sshm", "ssubm", "ssupm", "sum", "rwidm", "ritm", "rlm", "rmicm", "rshm", "rsubm", + "rsupm", "rum", "mhpa", "mcud1", "mcub1", "mcuf1", "mvpa", "mcuu1", "porder", "mcud", "mcub", + "mcuf", "mcuu", "scs", "smgb", "smgbp", "smglp", "smgrp", "smgt", "smgtp", "sbim", "scsd", + "rbim", "rcsd", "subcs", "supcs", "docr", "zerom", "csnm", "kmous", "minfo", "reqmp", "getm", + "setaf", "setab", "pfxl", "devt", "csin", "s0ds", "s1ds", "s2ds", "s3ds", "smglr", "smgtb", + "birep", "binel", "bicr", "colornm", "defbi", "endbi", "setcolor", "slines", "dispc", "smpch", + "rmpch", "smsc", "rmsc", "pctrm", "scesc", "scesa", "ehhlm", "elhlm", "elohlm", "erhlm", + "ethlm", "evhlm", "sgr1", "slength", "OTi2", "OTrs", "OTnl", "OTbs", "OTko", "OTma", "OTG2", + "OTG3", "OTG1", "OTG4", "OTGR", "OTGL", "OTGU", "OTGD", "OTGH", "OTGV", "OTGC", "meml", "memu", + "box1"]; + +fn read_le_u16(r: &mut dyn io::Read) -> io::Result { + let mut b = [0; 2]; + r.read_exact(&mut b)?; + Ok((b[0] as u16) | ((b[1] as u16) << 8)) +} + +fn read_le_u32(r: &mut dyn io::Read) -> io::Result { + let mut b = [0; 4]; + r.read_exact(&mut b)?; + Ok((b[0] as u32) | ((b[1] as u32) << 8) | ((b[2] as u32) << 16) | ((b[3] as u32) << 24)) +} + +fn read_byte(r: &mut dyn io::Read) -> io::Result { + match r.bytes().next() { + Some(s) => s, + None => Err(io::Error::new(io::ErrorKind::Other, "end of file")), + } +} + +/// Parse a compiled terminfo entry, using long capability names if `longnames` +/// is true +pub fn parse(file: &mut dyn io::Read, longnames: bool) -> Result { + macro_rules! t( ($e:expr) => ( + match $e { + Ok(e) => e, + Err(e) => return Err(e.to_string()) + } + ) ); + + let (bnames, snames, nnames) = if longnames { + (boolfnames, stringfnames, numfnames) + } else { + (boolnames, stringnames, numnames) + }; + + // Check magic number + let magic = t!(read_le_u16(file)); + + let extended = match magic { + 0o0432 => false, + 0o01036 => true, + _ => return Err(format!("invalid magic number, found {:o}", magic)), + }; + + // According to the spec, these fields must be >= -1 where -1 means that the feature is not + // supported. Using 0 instead of -1 works because we skip sections with length 0. + macro_rules! read_nonneg { + () => {{ + match t!(read_le_u16(file)) as i16 { + n if n >= 0 => n as usize, + -1 => 0, + _ => return Err("incompatible file: length fields must be >= -1".to_string()), + } + }}; + } + + let names_bytes = read_nonneg!(); + let bools_bytes = read_nonneg!(); + let numbers_count = read_nonneg!(); + let string_offsets_count = read_nonneg!(); + let string_table_bytes = read_nonneg!(); + + if names_bytes == 0 { + return Err("incompatible file: names field must be at least 1 byte wide".to_string()); + } + + if bools_bytes > boolnames.len() { + return Err("incompatible file: more booleans than expected".to_string()); + } + + if numbers_count > numnames.len() { + return Err("incompatible file: more numbers than expected".to_string()); + } + + if string_offsets_count > stringnames.len() { + return Err("incompatible file: more string offsets than expected".to_string()); + } + + // don't read NUL + let mut bytes = Vec::new(); + t!(file.take((names_bytes - 1) as u64).read_to_end(&mut bytes)); + let names_str = match String::from_utf8(bytes) { + Ok(s) => s, + Err(_) => return Err("input not utf-8".to_string()), + }; + + let term_names: Vec = names_str.split('|').map(|s| s.to_string()).collect(); + // consume NUL + if t!(read_byte(file)) != b'\0' { + return Err("incompatible file: missing null terminator for names section".to_string()); + } + + let bools_map: HashMap = t! { + (0..bools_bytes).filter_map(|i| match read_byte(file) { + Err(e) => Some(Err(e)), + Ok(1) => Some(Ok((bnames[i].to_string(), true))), + Ok(_) => None + }).collect() + }; + + if (bools_bytes + names_bytes) % 2 == 1 { + t!(read_byte(file)); // compensate for padding + } + + let numbers_map: HashMap = t! { + (0..numbers_count).filter_map(|i| { + let number = if extended { read_le_u32(file) } else { read_le_u16(file).map(Into::into) }; + + match number { + Ok(0xFFFF) => None, + Ok(n) => Some(Ok((nnames[i].to_string(), n))), + Err(e) => Some(Err(e)) + } + }).collect() + }; + + let string_map: HashMap> = if string_offsets_count > 0 { + let string_offsets: Vec = + t!((0..string_offsets_count).map(|_| read_le_u16(file)).collect()); + + let mut string_table = Vec::new(); + t!(file.take(string_table_bytes as u64).read_to_end(&mut string_table)); + + t!(string_offsets + .into_iter() + .enumerate() + .filter(|&(_, offset)| { + // non-entry + offset != 0xFFFF + }) + .map(|(i, offset)| { + let offset = offset as usize; + + let name = if snames[i] == "_" { stringfnames[i] } else { snames[i] }; + + if offset == 0xFFFE { + // undocumented: FFFE indicates cap@, which means the capability is not present + // unsure if the handling for this is correct + return Ok((name.to_string(), Vec::new())); + } + + // Find the offset of the NUL we want to go to + let nulpos = string_table[offset..string_table_bytes].iter().position(|&b| b == 0); + match nulpos { + Some(len) => { + Ok((name.to_string(), string_table[offset..offset + len].to_vec())) + } + None => Err("invalid file: missing NUL in string_table".to_string()), + } + }) + .collect()) + } else { + HashMap::new() + }; + + // And that's all there is to it + Ok(TermInfo { names: term_names, bools: bools_map, numbers: numbers_map, strings: string_map }) +} + +/// Creates a dummy TermInfo struct for msys terminals +pub fn msys_terminfo() -> TermInfo { + let mut strings = HashMap::new(); + strings.insert("sgr0".to_string(), b"\x1B[0m".to_vec()); + strings.insert("bold".to_string(), b"\x1B[1m".to_vec()); + strings.insert("setaf".to_string(), b"\x1B[3%p1%dm".to_vec()); + strings.insert("setab".to_string(), b"\x1B[4%p1%dm".to_vec()); + + let mut numbers = HashMap::new(); + numbers.insert("colors".to_string(), 8); + + TermInfo { + names: vec!["cygwin".to_string()], // msys is a fork of an older cygwin version + bools: HashMap::new(), + numbers, + strings, + } +} diff --git a/library/test/src/term/terminfo/parser/compiled/tests.rs b/library/test/src/term/terminfo/parser/compiled/tests.rs new file mode 100644 index 00000000000..8a9187b0495 --- /dev/null +++ b/library/test/src/term/terminfo/parser/compiled/tests.rs @@ -0,0 +1,8 @@ +use super::*; + +#[test] +fn test_veclens() { + assert_eq!(boolfnames.len(), boolnames.len()); + assert_eq!(numfnames.len(), numnames.len()); + assert_eq!(stringfnames.len(), stringnames.len()); +} diff --git a/library/test/src/term/terminfo/searcher.rs b/library/test/src/term/terminfo/searcher.rs new file mode 100644 index 00000000000..5499e240e66 --- /dev/null +++ b/library/test/src/term/terminfo/searcher.rs @@ -0,0 +1,69 @@ +//! ncurses-compatible database discovery. +//! +//! Does not support hashed database, only filesystem! + +use std::env; +use std::fs; +use std::path::PathBuf; + +#[cfg(test)] +mod tests; + +/// Return path to database entry for `term` +#[allow(deprecated)] +pub fn get_dbpath_for_term(term: &str) -> Option { + let mut dirs_to_search = Vec::new(); + let first_char = term.chars().next()?; + + // Find search directory + if let Some(dir) = env::var_os("TERMINFO") { + dirs_to_search.push(PathBuf::from(dir)); + } + + if let Ok(dirs) = env::var("TERMINFO_DIRS") { + for i in dirs.split(':') { + if i == "" { + dirs_to_search.push(PathBuf::from("/usr/share/terminfo")); + } else { + dirs_to_search.push(PathBuf::from(i)); + } + } + } else { + // Found nothing in TERMINFO_DIRS, use the default paths: + // According to /etc/terminfo/README, after looking at + // ~/.terminfo, ncurses will search /etc/terminfo, then + // /lib/terminfo, and eventually /usr/share/terminfo. + // On Haiku the database can be found at /boot/system/data/terminfo + if let Some(mut homedir) = env::home_dir() { + homedir.push(".terminfo"); + dirs_to_search.push(homedir) + } + + dirs_to_search.push(PathBuf::from("/etc/terminfo")); + dirs_to_search.push(PathBuf::from("/lib/terminfo")); + dirs_to_search.push(PathBuf::from("/usr/share/terminfo")); + dirs_to_search.push(PathBuf::from("/boot/system/data/terminfo")); + } + + // Look for the terminal in all of the search directories + for mut p in dirs_to_search { + if fs::metadata(&p).is_ok() { + p.push(&first_char.to_string()); + p.push(&term); + if fs::metadata(&p).is_ok() { + return Some(p); + } + p.pop(); + p.pop(); + + // on some installations the dir is named after the hex of the char + // (e.g., macOS) + p.push(&format!("{:x}", first_char as usize)); + p.push(term); + if fs::metadata(&p).is_ok() { + return Some(p); + } + } + } + None +} diff --git a/library/test/src/term/terminfo/searcher/tests.rs b/library/test/src/term/terminfo/searcher/tests.rs new file mode 100644 index 00000000000..4227a585e2f --- /dev/null +++ b/library/test/src/term/terminfo/searcher/tests.rs @@ -0,0 +1,19 @@ +use super::*; + +#[test] +#[ignore = "buildbots don't have ncurses installed and I can't mock everything I need"] +fn test_get_dbpath_for_term() { + // woefully inadequate test coverage + // note: current tests won't work with non-standard terminfo hierarchies (e.g., macOS's) + use std::env; + // FIXME (#9639): This needs to handle non-utf8 paths + fn x(t: &str) -> String { + let p = get_dbpath_for_term(t).expect("no terminfo entry found"); + p.to_str().unwrap().to_string() + } + assert!(x("screen") == "/usr/share/terminfo/s/screen"); + assert!(get_dbpath_for_term("") == None); + env::set_var("TERMINFO_DIRS", ":"); + assert!(x("screen") == "/usr/share/terminfo/s/screen"); + env::remove_var("TERMINFO_DIRS"); +} diff --git a/library/test/src/term/win.rs b/library/test/src/term/win.rs new file mode 100644 index 00000000000..c24cf9518aa --- /dev/null +++ b/library/test/src/term/win.rs @@ -0,0 +1,220 @@ +//! Windows console handling + +// FIXME (#13400): this is only a tiny fraction of the Windows console api + +use std::io; +use std::io::prelude::*; + +use crate::color; +use crate::Attr; +use crate::Terminal; + +/// A Terminal implementation that uses the Win32 Console API. +pub struct WinConsole { + buf: T, + def_foreground: color::Color, + def_background: color::Color, + foreground: color::Color, + background: color::Color, +} + +type SHORT = i16; +type WORD = u16; +type DWORD = u32; +type BOOL = i32; +type HANDLE = *mut u8; + +#[allow(non_snake_case)] +#[repr(C)] +struct SMALL_RECT { + Left: SHORT, + Top: SHORT, + Right: SHORT, + Bottom: SHORT, +} + +#[allow(non_snake_case)] +#[repr(C)] +struct COORD { + X: SHORT, + Y: SHORT, +} + +#[allow(non_snake_case)] +#[repr(C)] +struct CONSOLE_SCREEN_BUFFER_INFO { + dwSize: COORD, + dwCursorPosition: COORD, + wAttributes: WORD, + srWindow: SMALL_RECT, + dwMaximumWindowSize: COORD, +} + +#[allow(non_snake_case)] +#[link(name = "kernel32")] +extern "system" { + fn SetConsoleTextAttribute(handle: HANDLE, attr: WORD) -> BOOL; + fn GetStdHandle(which: DWORD) -> HANDLE; + fn GetConsoleScreenBufferInfo(handle: HANDLE, info: *mut CONSOLE_SCREEN_BUFFER_INFO) -> BOOL; +} + +fn color_to_bits(color: color::Color) -> u16 { + // magic numbers from mingw-w64's wincon.h + + let bits = match color % 8 { + color::BLACK => 0, + color::BLUE => 0x1, + color::GREEN => 0x2, + color::RED => 0x4, + color::YELLOW => 0x2 | 0x4, + color::MAGENTA => 0x1 | 0x4, + color::CYAN => 0x1 | 0x2, + color::WHITE => 0x1 | 0x2 | 0x4, + _ => unreachable!(), + }; + + if color >= 8 { bits | 0x8 } else { bits } +} + +fn bits_to_color(bits: u16) -> color::Color { + let color = match bits & 0x7 { + 0 => color::BLACK, + 0x1 => color::BLUE, + 0x2 => color::GREEN, + 0x4 => color::RED, + 0x6 => color::YELLOW, + 0x5 => color::MAGENTA, + 0x3 => color::CYAN, + 0x7 => color::WHITE, + _ => unreachable!(), + }; + + color | (u32::from(bits) & 0x8) // copy the hi-intensity bit +} + +impl WinConsole { + fn apply(&mut self) { + let _unused = self.buf.flush(); + let mut accum: WORD = 0; + accum |= color_to_bits(self.foreground); + accum |= color_to_bits(self.background) << 4; + + unsafe { + // Magic -11 means stdout, from + // https://docs.microsoft.com/en-us/windows/console/getstdhandle + // + // You may be wondering, "but what about stderr?", and the answer + // to that is that setting terminal attributes on the stdout + // handle also sets them for stderr, since they go to the same + // terminal! Admittedly, this is fragile, since stderr could be + // redirected to a different console. This is good enough for + // rustc though. See #13400. + let out = GetStdHandle(-11i32 as DWORD); + SetConsoleTextAttribute(out, accum); + } + } + + /// Returns `None` whenever the terminal cannot be created for some reason. + pub fn new(out: T) -> io::Result> { + use std::mem::MaybeUninit; + + let fg; + let bg; + unsafe { + let mut buffer_info = MaybeUninit::::uninit(); + if GetConsoleScreenBufferInfo(GetStdHandle(-11i32 as DWORD), buffer_info.as_mut_ptr()) + != 0 + { + let buffer_info = buffer_info.assume_init(); + fg = bits_to_color(buffer_info.wAttributes); + bg = bits_to_color(buffer_info.wAttributes >> 4); + } else { + fg = color::WHITE; + bg = color::BLACK; + } + } + Ok(WinConsole { + buf: out, + def_foreground: fg, + def_background: bg, + foreground: fg, + background: bg, + }) + } +} + +impl Write for WinConsole { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.buf.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.buf.flush() + } +} + +impl Terminal for WinConsole { + type Output = T; + + fn fg(&mut self, color: color::Color) -> io::Result { + self.foreground = color; + self.apply(); + + Ok(true) + } + + fn bg(&mut self, color: color::Color) -> io::Result { + self.background = color; + self.apply(); + + Ok(true) + } + + fn attr(&mut self, attr: Attr) -> io::Result { + match attr { + Attr::ForegroundColor(f) => { + self.foreground = f; + self.apply(); + Ok(true) + } + Attr::BackgroundColor(b) => { + self.background = b; + self.apply(); + Ok(true) + } + _ => Ok(false), + } + } + + fn supports_attr(&self, attr: Attr) -> bool { + // it claims support for underscore and reverse video, but I can't get + // it to do anything -cmr + match attr { + Attr::ForegroundColor(_) | Attr::BackgroundColor(_) => true, + _ => false, + } + } + + fn reset(&mut self) -> io::Result { + self.foreground = self.def_foreground; + self.background = self.def_background; + self.apply(); + + Ok(true) + } + + fn get_ref(&self) -> &T { + &self.buf + } + + fn get_mut(&mut self) -> &mut T { + &mut self.buf + } + + fn into_inner(self) -> T + where + Self: Sized, + { + self.buf + } +} -- cgit 1.4.1-3-g733a5