diff --git a/Cargo.toml b/Cargo.toml index 6b43f654..5c44ff00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "term" -version = "0.2.14" +version = "0.3.0" authors = ["The Rust Project Developers", "Steven Allen"] license = "MIT/Apache-2.0" readme = "README.md" diff --git a/src/lib.rs b/src/lib.rs index 14e67a0e..5ff9535b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,7 +48,7 @@ //! t.fg(term::color::RED).unwrap(); //! writeln!(t, "world!").unwrap(); //! -//! assert!(t.reset().unwrap()); +//! t.reset().unwrap(); //! } //! ``` //! @@ -181,6 +181,153 @@ pub enum Attr { BackgroundColor(color::Color) } +/// An error arising from interacting with the terminal. +#[derive(Debug)] +pub enum Error { + /// Indicates an error from any underlying IO + Io(io::Error), + /// Indicates an error during terminfo parsing + TerminfoParsing(terminfo::Error), + /// Indicates an error expanding a parameterized string from the terminfo database + ParameterizedExpansion(terminfo::parm::Error), + /// Indicates that the terminal does not support the requested operation. + NotSupported, + /// Indicates that the `TERM` environment variable was unset, and thus we were unable to detect + /// which terminal we should be using. + TermUnset, + /// Indicates that we were unable to find a terminfo entry for the requested terminal. + TerminfoEntryNotFound, + /// Indicates that the cursor could not be moved to the requested position. + CursorDestinationInvalid, + /// Indicates that the terminal does not support displaying the requested color. + /// + /// This is like `NotSupported`, but more specific. + ColorOutOfRange, + #[doc(hidden)] + /// Please don't match against this - if you do, we can't promise we won't break your crate + /// with a semver-compliant version bump. + __Nonexhaustive, +} + +// manually implemented because std::io::Error does not implement Eq/PartialEq +impl std::cmp::PartialEq for Error { + fn eq(&self, other: &Error) -> bool { + use Error::*; + match self { + &Io(_) => { + false + } + &TerminfoParsing(ref inner1) => { + match other { + &TerminfoParsing(ref inner2) => inner1 == inner2, + _ => false + } + } + &ParameterizedExpansion(ref inner1) => { + match other { + &ParameterizedExpansion(ref inner2) => inner1 == inner2, + _ => false + } + } + &NotSupported => { + match other { + &NotSupported => true, + _ => false + } + } + &TermUnset => { + match other { + &TermUnset => true, + _ => false + } + } + &TerminfoEntryNotFound => { + match other { + &TerminfoEntryNotFound => true, + _ => false + } + } + &CursorDestinationInvalid => { + match other { + &CursorDestinationInvalid => true, + _ => false + } + } + &ColorOutOfRange => { + match other { + &ColorOutOfRange => true, + _ => false + } + } + &__Nonexhaustive => { + match other { + &__Nonexhaustive => true, + _ => false + } + } + } + } +} + +/// The canonical `Result` type using this crate's Error type. +pub type Result = std::result::Result; + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + use std::error::Error; + if let &::Error::Io(ref e) = self { + write!(f, "{}", e) + } else { + f.write_str(self.description()) + } + } +} + +impl std::error::Error for Error { + fn description(&self) -> &str { + use Error::*; + use std::error::Error; + match self { + &Io(ref io) => io.description(), + &TerminfoParsing(ref e) => e.description(), + &ParameterizedExpansion(ref e) => e.description(), + &NotSupported => "operation not supported by the terminal", + &TermUnset => "TERM environment variable unset, unable to detect a terminal", + &TerminfoEntryNotFound => "could not find a terminfo entry for this terminal", + &CursorDestinationInvalid => "could not move cursor to requested position", + &ColorOutOfRange => "color not supported by the terminal", + &__Nonexhaustive => "placeholder variant that shouldn't be used", + } + } + + fn cause(&self) -> Option<&std::error::Error> { + match self { + &Error::Io(ref io) => Some(io), + &Error::TerminfoParsing(ref e) => Some(e), + &Error::ParameterizedExpansion(ref e) => Some(e), + _ => None, + } + } +} + +impl std::convert::From for Error { + fn from(val: io::Error) -> Self { + Error::Io(val) + } +} + +impl std::convert::From for Error { + fn from(val: terminfo::Error) -> Self { + Error::TerminfoParsing(val) + } +} + +impl std::convert::From for Error { + fn from(val: terminfo::parm::Error) -> Self { + Error::ParameterizedExpansion(val) + } +} + /// A terminal with similar capabilities to an ANSI Terminal /// (foreground/background colors etc). pub trait Terminal: Write { @@ -192,56 +339,53 @@ pub trait Terminal: Write { /// 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; + /// Returns `Ok(())` if the color change code was sent to the terminal, or `Err(e)` if there + /// was an error. + fn fg(&mut self, color: color::Color) -> 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; + /// Returns `Ok(())` if the color change code was sent to the terminal, or `Err(e)` if there + /// was an error. + fn bg(&mut self, color: color::Color) -> 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; + /// Sets the given terminal attribute, if supported. Returns `Ok(())` if the attribute is + /// supported and was sent to the terminal, or `Err(e)` if there was an error or the attribute + /// wasn't supported. + fn attr(&mut self, attr: Attr) -> Result<()>; /// Returns whether 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. + /// Returns `Ok(())` if the reset code was printed, or `Err(e)` if there was an 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; + fn reset(&mut self) -> Result<()>; /// Moves the cursor up one line. /// - /// Returns `Ok(true)` if the cursor was moved, `Ok(false)` otherwise, and `Err(e)` - /// if there was an I/O error. - fn cursor_up(&mut self) -> io::Result; + /// Returns `Ok(())` if the cursor movement code was printed, or `Err(e)` if there was an + /// error. + fn cursor_up(&mut self) -> Result<()>; /// Deletes the text from the cursor location to the end of the line. /// - /// Returns `Ok(true)` if the text was deleted, `Ok(false)` otherwise, and `Err(e)` - /// if there was an I/O error. - fn delete_line(&mut self) -> io::Result; + /// Returns `Ok(())` if the deletion code was printed, or `Err(e)` if there was an error. + fn delete_line(&mut self) -> Result<()>; /// Moves the cursor to the left edge of the current line. /// - /// Returns `Ok(true)` if the text was deleted, `Ok(false)` otherwise, and `Err(e)` - /// if there was an I/O error. - fn carriage_return(&mut self) -> io::Result; + /// Returns `Ok(true)` if the deletion code was printed, or `Err(e)` if there was an error. + fn carriage_return(&mut self) -> Result<()>; /// Gets an immutable reference to the stream inside fn get_ref<'a>(&'a self) -> &'a Self::Output; diff --git a/src/terminfo/mod.rs b/src/terminfo/mod.rs index 98aa4649..fa8dbc48 100644 --- a/src/terminfo/mod.rs +++ b/src/terminfo/mod.rs @@ -12,8 +12,6 @@ use std::collections::HashMap; use std::env; -use std::error; -use std::fmt; use std::fs::File; use std::io::prelude::*; use std::io; @@ -23,9 +21,11 @@ use std::path::Path; use Attr; use color; use Terminal; +use Result; use self::searcher::get_dbpath_for_term; use self::parser::compiled::{parse, msys_terminfo}; use self::parm::{expand, Variables, Param}; +use self::Error::*; /// A parsed terminfo database entry. @@ -41,47 +41,12 @@ pub struct TermInfo { 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 description(&self) -> &str { "failed to create TermInfo" } - - fn cause(&self) -> Option<&error::Error> { - use self::Error::*; - match self { - &IoError(ref e) => Some(e), - _ => None, - } - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::Error::*; - match self { - &TermUnset => Ok(()), - &MalformedTerminfo(ref e) => e.fmt(f), - &IoError(ref e) => e.fmt(f), - } - } -} - impl TermInfo { /// Create a TermInfo based on current environment. - pub fn from_env() -> Result { + pub fn from_env() -> Result { let term = match env::var("TERM") { Ok(name) => TermInfo::from_name(&name), - Err(..) => return Err(Error::TermUnset), + Err(..) => return Err(::Error::TermUnset), }; if term.is_err() && env::var("MSYSCON").ok().map_or(false, |s| "mintty.exe" == s) { @@ -93,26 +58,96 @@ impl TermInfo { } /// Create a TermInfo for the named terminal. - pub fn from_name(name: &str) -> Result { + 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")) + ::Error::TerminfoEntryNotFound }).and_then(|p| { TermInfo::from_path(&p) }) } /// Parse the given TermInfo. - pub fn from_path>(path: P) -> Result { + 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 = try!(File::open(path).map_err(|e| { Error::IoError(e) })); + // (That is, this uses a &Path so that this function need not be instantiated for every type + // which implements AsRef. One day, if/when rustc is a bit smarter, it might do this for + // us. Alas. ) + fn _from_path(path: &Path) -> Result { + let file = try!(File::open(path).map_err(|e| { ::Error::Io(e) })); let mut reader = BufReader::new(file); - parse(&mut reader, false).map_err(|e| { - Error::MalformedTerminfo(e) - }) + parse(&mut reader, false) + } +} + +#[derive(Debug, Eq, PartialEq)] +/// An error from parsing a terminfo entry +pub enum Error { + /// The "magic" number at the start of the file was wrong. + /// + /// It should be `0x11A` + BadMagic(u16), + /// The names in the file were not valid UTF-8. + /// + /// In theory these should only be ASCII, but to work with the Rust `str` type, we treat them + /// as UTF-8. This is valid, except when a terminfo file decides to be invalid. This hasn't + /// been encountered in the wild. + NotUtf8(::std::str::Utf8Error), + /// The names section of the file was empty + ShortNames, + /// More boolean parameters are present in the file than this crate knows how to interpret. + TooManyBools, + /// More number parameters are present in the file than this crate knows how to interpret. + TooManyNumbers, + /// More string parameters are present in the file than this crate knows how to interpret. + TooManyStrings, + /// The length of some field was not >= -1. + InvalidLength, + /// The names table was missing a trailing null terminator. + NamesMissingNull, + /// The strings table was missing a trailing null terminator. + StringsMissingNull, + +} + +impl ::std::fmt::Display for Error { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + use std::error::Error; + match self { + &NotUtf8(e) => write!(f, "{}", e), + &BadMagic(v) => write!(f, "bad magic number {:x} in terminfo header", v), + _ => f.write_str(self.description()) + } + } +} + +impl ::std::convert::From<::std::string::FromUtf8Error> for Error { + fn from(v: ::std::string::FromUtf8Error) -> Self { + NotUtf8(v.utf8_error()) + } +} + +impl ::std::error::Error for Error { + fn description(&self) -> &str { + match self { + &BadMagic(..) => "incorrect magic number at start of file", + &ShortNames => "no names exposed, need at least one", + &TooManyBools => "more boolean properties than libterm knows about", + &TooManyNumbers => "more number properties than libterm knows about", + &TooManyStrings => "more string properties than libterm knows about", + &InvalidLength => "invalid length field value, must be >= -1", + &NotUtf8(ref e) => e.description(), + &NamesMissingNull => "names table missing NUL terminator", + &StringsMissingNull => "string table missing NUL terminator", + } + } + + fn cause(&self) -> Option<&::std::error::Error> { + match self { + &NotUtf8(ref e) => Some(e), + _ => None + } } } @@ -154,23 +189,23 @@ pub struct TerminfoTerminal { impl Terminal for TerminfoTerminal { type Output = T; - fn fg(&mut self, color: color::Color) -> io::Result { + fn fg(&mut self, color: color::Color) -> 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) + Err(::Error::ColorOutOfRange) } - fn bg(&mut self, color: color::Color) -> io::Result { + fn bg(&mut self, color: color::Color) -> 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) + Err(::Error::ColorOutOfRange) } - fn attr(&mut self, attr: Attr) -> io::Result { + fn attr(&mut self, attr: Attr) -> Result<()> { match attr { Attr::ForegroundColor(c) => self.fg(c), Attr::BackgroundColor(c) => self.bg(c), @@ -190,8 +225,8 @@ impl Terminal for TerminfoTerminal { } } - fn reset(&mut self) -> io::Result { - // are there any terminals that have color/attrs and not sgr0? + fn reset(&mut self) -> Result<()> { + // are there any terminals that have color/attrs and not sg0? // Try falling back to sgr, then op let cmd = match [ "sg0", "sgr", "op" @@ -200,22 +235,23 @@ impl Terminal for TerminfoTerminal { }).next() { Some(op) => match expand(&op, &[], &mut Variables::new()) { Ok(cmd) => cmd, - Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e)) + Err(e) => return Err(e.into()), }, - None => return Ok(false), + None => return Err(::Error::NotSupported), }; - self.out.write_all(&cmd).and(Ok(true)) + try!(self.out.write_all(&cmd)); + Ok(()) } - fn cursor_up(&mut self) -> io::Result { + fn cursor_up(&mut self) -> Result<()> { self.apply_cap("cuu1", &[]) } - fn delete_line(&mut self) -> io::Result { + fn delete_line(&mut self) -> Result<()> { self.apply_cap("dl", &[]) } - fn carriage_return(&mut self) -> io::Result { + fn carriage_return(&mut self) -> Result<()> { self.apply_cap("cr", &[]) } @@ -250,17 +286,19 @@ impl TerminfoTerminal { fn dim_if_necessary(&self, color: color::Color) -> color::Color { if color >= self.num_colors && color >= 8 && color < 16 { - color-8 - } else { color } + color - 8 + } else { + color + } } - fn apply_cap(&mut self, cmd: &str, params: &[Param]) -> io::Result { + fn apply_cap(&mut self, cmd: &str, params: &[Param]) -> 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)), + Ok(s) => { try!(self.out.write_all(&s)); Ok(()) } + Err(e) => Err(e.into()), }, - None => Ok(false) + None => Err(::Error::NotSupported) } } } diff --git a/src/terminfo/parm.rs b/src/terminfo/parm.rs index b1152e52..807d6e2e 100644 --- a/src/terminfo/parm.rs +++ b/src/terminfo/parm.rs @@ -49,6 +49,61 @@ pub enum Param { Number(i32) } +/// An error from interpreting a parameterized string. +#[derive(Debug, Eq, PartialEq)] +pub enum Error { + /// Data was requested from the stack, but the stack didn't have enough elements. + StackUnderflow, + /// The type of the element(s) on top of the stack did not match the type that the operator + /// wanted. + TypeMismatch, + /// An unrecognized format option was used. + UnrecognizedFormatOption(char), + /// An invalid variable name was used. + InvalidVariableName(char), + /// An invalid parameter index was used. + InvalidParameterIndex(char), + /// A malformed character constant was used. + MalformedCharacterConstant, + /// An integer constant was too large (overflowed an i32) + IntegerConstantOverflow, + /// A malformed integer constant was used. + MalformedIntegerConstant, + /// A format width constant was too large (overflowed a usize) + FormatWidthOverflow, + /// A format precision constant was too large (overflowed a usize) + FormatPrecisionOverflow, +} + +impl ::std::fmt::Display for Error { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + use ::std::error::Error; + f.write_str(self.description()) + } +} + +impl ::std::error::Error for Error { + fn description(&self) -> &str { + use self::Error::*; + match self { + &StackUnderflow => "not enough elements on the stack", + &TypeMismatch => "type mismatch", + &UnrecognizedFormatOption(_) => "unrecognized format option", + &InvalidVariableName(_) => "invalid variable name", + &InvalidParameterIndex(_) => "invalid parameter index", + &MalformedCharacterConstant => "malformed character constant", + &IntegerConstantOverflow => "integer constant computation overflowed", + &MalformedIntegerConstant => "malformed integer constant", + &FormatWidthOverflow => "format width constant computation overflowed", + &FormatPrecisionOverflow => "format precision constant computation overflowed", + } + } + + fn cause(&self) -> Option<&::std::error::Error> { + None + } +} + /// Container for static and dynamic variable arrays pub struct Variables { /// Static variables A-Z @@ -90,8 +145,7 @@ impl Variables { /// /// 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> { +pub fn expand(cap: &[u8], params: &[Param], vars: &mut Variables) -> Result, Error> { let mut state = Nothing; // expanded cap will only rarely be larger than the cap itself @@ -127,8 +181,8 @@ pub fn expand(cap: &[u8], params: &[Param], vars: &mut Variables) 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()), + Some(_) => return Err(Error::TypeMismatch), + None => return Err(Error::StackUnderflow), }, 'p' => state = PushParam, 'P' => state = SetVar, @@ -137,8 +191,8 @@ pub fn expand(cap: &[u8], params: &[Param], vars: &mut Variables) '{' => 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()) + Some(_) => return Err(Error::TypeMismatch), + None => return Err(Error::StackUnderflow), }, '+'|'-'|'/'|'*'|'^'|'&'|'|'|'m' => match (stack.pop(), stack.pop()) { (Some(Number(y)), Some(Number(x))) => stack.push(Number(match cur { @@ -150,10 +204,10 @@ pub fn expand(cap: &[u8], params: &[Param], vars: &mut Variables) '&' => x & y, '^' => x ^ y, 'm' => x % y, - _ => unreachable!("All cases handled"), + _ => unreachable!("logic error"), })), - (Some(_), Some(_)) => return Err(format!("non-numbers on stack with {}", cur)), - _ => return Err("stack is empty".to_string()), + (Some(_), Some(_)) => return Err(Error::TypeMismatch), + _ => return Err(Error::StackUnderflow), }, '='|'>'|'<'|'A'|'O' => match (stack.pop(), stack.pop()) { (Some(Number(y)), Some(Number(x))) => stack.push(Number(if match cur { @@ -162,27 +216,27 @@ pub fn expand(cap: &[u8], params: &[Param], vars: &mut Variables) '>' => x > y, 'A' => x > 0 && y > 0, 'O' => x > 0 || y > 0, - _ => unreachable!(), + _ => unreachable!("logic error"), } { 1 } else { 0 })), - (Some(_), Some(_)) => return Err(format!("non-numbers on stack with {}", cur)), - _ => return Err("stack is empty".to_string()), + (Some(_), Some(_)) => return Err(Error::TypeMismatch), + _ => return Err(Error::StackUnderflow), }, '!'|'~' => match stack.pop() { Some(Number(x)) => stack.push(Number(match cur { '!' if x > 0 => 0, '!' => 1, '~' => !x, - _ => unreachable!(), + _ => unreachable!("logic error"), })), - Some(_) => return Err(format!("non-numbers on stack with {}", cur)), - None => return Err("stack is empty".to_string()), + Some(_) => return Err(Error::TypeMismatch), + None => return Err(Error::StackUnderflow), }, '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()) + (_, _) => return Err(Error::TypeMismatch), }, // printf-style support for %doxXs @@ -190,7 +244,7 @@ pub fn expand(cap: &[u8], params: &[Param], vars: &mut Variables) let flags = Flags::new(); let res = try!(format(arg, FormatOp::from_char(cur), flags)); output.extend(res.iter().map(|x| *x)); - } else { return Err("stack is empty".to_string()) }, + } else { return Err(Error::StackUnderflow); }, ':'|'#'|' '|'.'|'0'...'9' => { let mut flags = Flags::new(); let mut fstate = FormatStateFlags; @@ -203,7 +257,7 @@ pub fn expand(cap: &[u8], params: &[Param], vars: &mut Variables) flags.width = cur as usize - '0' as usize; fstate = FormatStateWidth; } - _ => unreachable!() + _ => unreachable!("logic error") } state = FormatPattern(flags, fstate); } @@ -213,19 +267,19 @@ pub fn expand(cap: &[u8], params: &[Param], vars: &mut Variables) '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()), + Some(_) => return Err(Error::TypeMismatch), + None => return Err(Error::StackUnderflow), }, 'e' => state = SeekIfEnd(0), ';' => (), - _ => return Err(format!("unrecognized format option {}", cur)), + c => return Err(Error::UnrecognizedFormatOption(c)), } }, 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()) + None => return Err(Error::InvalidParameterIndex(cur)), }].clone()); }, SetVar => { @@ -233,14 +287,14 @@ pub fn expand(cap: &[u8], params: &[Param], vars: &mut Variables) if let Some(arg) = stack.pop() { let idx = (cur as u8) - b'A'; vars.sta[idx as usize] = arg; - } else { return Err("stack is empty".to_string()) } + } else { return Err(Error::StackUnderflow) } } else if cur >= 'a' && cur <= 'z' { if let Some(arg) = stack.pop() { let idx = (cur as u8) - b'a'; vars.dyn[idx as usize] = arg; - } else { return Err("stack is empty".to_string()) } + } else { return Err(Error::StackUnderflow) } } else { - return Err("bad variable name in %P".to_string()); + return Err(Error::InvalidVariableName(cur)) } }, GetVar => { @@ -251,7 +305,7 @@ pub fn expand(cap: &[u8], params: &[Param], vars: &mut Variables) let idx = (cur as u8) - b'a'; stack.push(vars.dyn[idx as usize].clone()); } else { - return Err("bad variable name in %g".to_string()); + return Err(Error::InvalidVariableName(cur)) } }, CharConstant => { @@ -259,7 +313,7 @@ pub fn expand(cap: &[u8], params: &[Param], vars: &mut Variables) state = CharClose; }, CharClose => if cur != '\'' { - return Err("malformed character constant".to_string()); + return Err(Error::MalformedCharacterConstant) }, IntConstant(i) => { if cur == '}' { @@ -271,10 +325,10 @@ pub fn expand(cap: &[u8], params: &[Param], vars: &mut Variables) state = IntConstant(i); old_state = Nothing; } - None => return Err("int constant too large".to_string()) + None => return Err(Error::IntegerConstantOverflow), } } else { - return Err("bad int constant".to_string()); + return Err(Error::MalformedIntegerConstant); } } FormatPattern(ref mut flags, ref mut fstate) => { @@ -285,7 +339,7 @@ pub fn expand(cap: &[u8], params: &[Param], vars: &mut Variables) output.extend(res.iter().map(|x| *x)); // will cause state to go to Nothing old_state = FormatPattern(*flags, *fstate); - } else { return Err("stack is empty".to_string()) }, + } else { return Err(Error::StackUnderflow) }, (FormatStateFlags,'#') => { flags.alternate = true; } @@ -306,21 +360,21 @@ pub fn expand(cap: &[u8], params: &[Param], vars: &mut Variables) *fstate = FormatStatePrecision; } (FormatStateWidth,'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()) } + flags.width = match flags.width.checked_mul(10).and_then(|w| w.checked_add(cur as usize - '0' as usize)) { + Some(width) => width, + None => return Err(Error::FormatWidthOverflow), + } } (FormatStateWidth,'.') => { *fstate = FormatStatePrecision; } (FormatStatePrecision,'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()) + flags.precision = match flags.precision.checked_mul(10).and_then(|w| w.checked_add(cur as usize - '0' as usize)) { + Some(precision) => precision, + None => return Err(Error::FormatPrecisionOverflow), } } - _ => return Err("invalid format specifier".to_string()) + _ => return Err(Error::UnrecognizedFormatOption(cur)) } } SeekIfElse(level) => { @@ -408,18 +462,9 @@ impl FormatOp { _ => panic!("bad FormatOp char") } } - fn to_char(self) -> char { - match self { - FormatDigit => 'd', - FormatOctal => 'o', - FormatHex => 'x', - FormatHEX => 'X', - FormatString => 's' - } - } } -fn format(val: Param, op: FormatOp, flags: Flags) -> Result ,String> { +fn format(val: Param, op: FormatOp, flags: Flags) -> Result, Error> { let mut s = match val { Number(d) => match op { FormatDigit => { @@ -456,7 +501,7 @@ fn format(val: Param, op: FormatOp, flags: Flags) -> Result ,String> { format!("{:01$X}", d, flags.precision) } }, - FormatString => return Err("non-number on stack with %s".to_string()) + FormatString => return Err(Error::TypeMismatch), }.into_bytes(), Words(s) => match op { FormatString => { @@ -466,7 +511,7 @@ fn format(val: Param, op: FormatOp, flags: Flags) -> Result ,String> { } s }, - _ => return Err(format!("non-string on stack with %{}", op.to_char())) + _ => return Err(Error::TypeMismatch), } }; if flags.width > s.len() { @@ -517,7 +562,7 @@ mod test { let mut varstruct = Variables::new(); let vars = &mut varstruct; fn get_res(fmt: &str, cap: &str, params: &[Param], vars: &mut Variables) -> - Result, String> + Result, super::Error> { let mut u8v: Vec<_> = fmt.bytes().collect(); u8v.extend(cap.as_bytes().iter().map(|&b| b)); diff --git a/src/terminfo/parser/compiled.rs b/src/terminfo/parser/compiled.rs index 11011f49..ecc0b1d8 100644 --- a/src/terminfo/parser/compiled.rs +++ b/src/terminfo/parser/compiled.rs @@ -15,7 +15,10 @@ use std::collections::HashMap; use std::io::prelude::*; use std::io; -use super::super::TermInfo; + +use terminfo::Error::*; +use terminfo::TermInfo; +use Result; // These are the orders ncurses uses in its compiled format (as of 5.9). Not sure if portable. @@ -179,14 +182,7 @@ fn read_byte(r: &mut io::Read) -> io::Result { /// Parse a compiled terminfo entry, using long capability names if `longnames` /// is true -pub fn parse(file: &mut io::Read, longnames: bool) -> Result { - macro_rules! try( ($e:expr) => ( - match $e { - Ok(e) => e, - Err(e) => return Err(format!("{}", e)) - } - ) ); - +pub fn parse(file: &mut io::Read, longnames: bool) -> Result { let (bnames, snames, nnames) = if longnames { (boolfnames, stringfnames, numfnames) } else { @@ -196,8 +192,7 @@ pub fn parse(file: &mut io::Read, longnames: bool) -> Result { // Check magic number let magic = try!(read_le_u16(file)); if magic != 0x011A { - return Err(format!("invalid magic number: expected {:x}, found {:x}", - 0x011A, magic)); + return Err(BadMagic(magic).into()) } // According to the spec, these fields must be >= -1 where -1 means that the feature is not @@ -207,7 +202,7 @@ pub fn parse(file: &mut io::Read, longnames: bool) -> Result { match try!(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()), + _ => return Err(InvalidLength.into()), } }} } @@ -219,23 +214,19 @@ pub fn parse(file: &mut io::Read, longnames: bool) -> Result { 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()); + return Err(ShortNames.into()); } if bools_bytes > boolnames.len() { - return Err("incompatible file: more booleans than \ - expected".to_string()); + return Err(TooManyBools.into()); } if numbers_count > numnames.len() { - return Err("incompatible file: more numbers than \ - expected".to_string()); + return Err(TooManyNumbers.into()); } if string_offsets_count > stringnames.len() { - return Err("incompatible file: more string offsets than \ - expected".to_string()); + return Err(TooManyStrings.into()); } // don't read NUL @@ -243,7 +234,7 @@ pub fn parse(file: &mut io::Read, longnames: bool) -> Result { try!(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()), + Err(e) => return Err(NotUtf8(e.utf8_error()).into()) }; let term_names: Vec = names_str.split('|') @@ -251,8 +242,7 @@ pub fn parse(file: &mut io::Read, longnames: bool) -> Result { .collect(); // consume NUL if try!(read_byte(file)) != b'\0' { - return Err("incompatible file: missing null terminator \ - for names section".to_string()); + return Err(NamesMissingNull.into()); } let bools_map: HashMap = try! { @@ -305,7 +295,7 @@ pub fn parse(file: &mut io::Read, longnames: bool) -> Result { 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()), + None => return Err(::Error::TerminfoParsing(StringsMissingNull)), } }).collect()) } else { diff --git a/src/win.rs b/src/win.rs index 82ecd841..3eeebb6b 100644 --- a/src/win.rs +++ b/src/win.rs @@ -20,8 +20,10 @@ use std::io; use std::ptr; use Attr; -use color; +use Error; +use Result; use Terminal; +use color; /// A Terminal implementation which uses the Win32 Console API. pub struct WinConsole { @@ -110,7 +112,7 @@ impl WinConsole { Ok(()) } - /// Returns `None` whenever the terminal cannot be created for some + /// Returns `Err` whenever the terminal cannot be created for some /// reason. pub fn new(out: T) -> io::Result> { let fg; @@ -148,33 +150,33 @@ impl Write for WinConsole { impl Terminal for WinConsole { type Output = T; - fn fg(&mut self, color: color::Color) -> io::Result { + fn fg(&mut self, color: color::Color) -> Result<()> { self.foreground = color; try!(self.apply()); - Ok(true) + Ok(()) } - fn bg(&mut self, color: color::Color) -> io::Result { + fn bg(&mut self, color: color::Color) -> Result<()> { self.background = color; try!(self.apply()); - Ok(true) + Ok(()) } - fn attr(&mut self, attr: Attr) -> io::Result { + fn attr(&mut self, attr: Attr) -> Result<()> { match attr { Attr::ForegroundColor(f) => { self.foreground = f; try!(self.apply()); - Ok(true) + Ok(()) }, Attr::BackgroundColor(b) => { self.background = b; try!(self.apply()); - Ok(true) + Ok(()) }, - _ => Ok(false) + _ => Err(Error::NotSupported) } } @@ -187,15 +189,15 @@ impl Terminal for WinConsole { } } - fn reset(&mut self) -> io::Result { + fn reset(&mut self) -> Result<()> { self.foreground = self.def_foreground; self.background = self.def_background; try!(self.apply()); - Ok(true) + Ok(()) } - fn cursor_up(&mut self) -> io::Result { + fn cursor_up(&mut self) -> Result<()> { let _unused = self.buf.flush(); let handle = try!(conout()); unsafe { @@ -203,44 +205,50 @@ impl Terminal for WinConsole { if kernel32::GetConsoleScreenBufferInfo(handle, &mut buffer_info) != 0 { let (x, y) = (buffer_info.dwCursorPosition.X, buffer_info.dwCursorPosition.Y); if y == 0 { - Ok(false) + // Even though this might want to be a CursorPositionInvalid, on Unix there + // is no checking to see if the cursor is already on the first line. + // I'm not sure what the ideal behavior is, but I think it'd be silly to have + // cursor_up fail in this case. + Ok(()) } else { let pos = winapi::COORD { X: x, Y: y - 1 }; if kernel32::SetConsoleCursorPosition(handle, pos) != 0 { - Ok(true) + Ok(()) } else { - Err(io::Error::last_os_error()) + Err(io::Error::last_os_error().into()) } } } else { - Err(io::Error::last_os_error()) + Err(io::Error::last_os_error().into()) } } } - fn delete_line(&mut self) -> io::Result { + fn delete_line(&mut self) -> Result<()> { let _unused = self.buf.flush(); let handle = try!(conout()); unsafe { let mut buffer_info = ::std::mem::uninitialized(); if kernel32::GetConsoleScreenBufferInfo(handle, &mut buffer_info) == 0 { - return Err(io::Error::last_os_error()) + return Err(io::Error::last_os_error().into()) } let pos = buffer_info.dwCursorPosition; let size = buffer_info.dwSize; let num = (size.X - pos.X) as winapi::DWORD; let mut written = 0; if kernel32::FillConsoleOutputCharacterW(handle, 0, num, pos, &mut written) == 0 { - return Err(io::Error::last_os_error()) + return Err(io::Error::last_os_error().into()) } if kernel32::FillConsoleOutputAttribute(handle, 0, num, pos, &mut written) == 0 { - return Err(io::Error::last_os_error()) + return Err(io::Error::last_os_error().into()) } - Ok(written != 0) + // Similar reasoning for not failing as in cursor_up -- it doesn't even make sense to + // me that these APIs could have written 0, unless the terminal is width zero. + Ok(()) } } - fn carriage_return(&mut self) -> io::Result { + fn carriage_return(&mut self) -> Result<()> { let _unused = self.buf.flush(); let handle = try!(conout()); unsafe { @@ -248,17 +256,17 @@ impl Terminal for WinConsole { if kernel32::GetConsoleScreenBufferInfo(handle, &mut buffer_info) != 0 { let (x, y) = (buffer_info.dwCursorPosition.X, buffer_info.dwCursorPosition.Y); if x == 0 { - Ok(false) + Err(Error::CursorDestinationInvalid) } else { let pos = winapi::COORD { X: 0, Y: y }; if kernel32::SetConsoleCursorPosition(handle, pos) != 0 { - Ok(true) + Ok(()) } else { - Err(io::Error::last_os_error()) + Err(io::Error::last_os_error().into()) } } } else { - Err(io::Error::last_os_error()) + Err(io::Error::last_os_error().into()) } } }