diff --git a/src/commands/nsec3hash.rs b/src/commands/nsec3hash.rs index 675c583..0a75621 100644 --- a/src/commands/nsec3hash.rs +++ b/src/commands/nsec3hash.rs @@ -1,7 +1,7 @@ use crate::error::Error; use clap::builder::ValueParser; use domain::base::iana::nsec3::Nsec3HashAlg; -use domain::base::name::Name; +use domain::base::name::{self, Name}; use domain::base::ToName; use domain::rdata::nsec3::{Nsec3Salt, OwnerHash}; use lexopt::Arg; @@ -106,11 +106,11 @@ impl LdnsCommand for Nsec3Hash { } impl Nsec3Hash { - pub fn parse_name(arg: &str) -> Result>, Error> { - Name::from_str(&arg.to_lowercase()).map_err(|e| Error::from(e.to_string())) + pub fn parse_name(arg: &str) -> Result>, name::FromStrError> { + Name::from_str(&arg.to_lowercase()) } - pub fn parse_nsec_alg(arg: &str) -> Result { + pub fn parse_nsec_alg(arg: &str) -> Result { if let Ok(num) = arg.parse() { let alg = Nsec3HashAlg::from_int(num); // check for valid algorithm here, to be consistent with error messages @@ -118,11 +118,10 @@ impl Nsec3Hash { if alg.to_mnemonic().is_some() { Ok(alg) } else { - Err(Error::from("unknown algorithm number")) + Err("unknown algorithm number") } } else { - Nsec3HashAlg::from_mnemonic(arg.as_bytes()) - .ok_or(Error::from("unknown algorithm mnemonic")) + Nsec3HashAlg::from_mnemonic(arg.as_bytes()).ok_or("unknown algorithm mnemonic") } } } diff --git a/src/error.rs b/src/error.rs index 1e70425..e2c2470 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,44 +1,81 @@ -use std::borrow::Cow; -use std::num::ParseIntError; -use std::string::ParseError; use std::{error, fmt, io}; -#[derive(Clone, Debug)] -pub struct Error { - message: Cow<'static, str>, +//------------ Error --------------------------------------------------------- + +/// A program error. +/// +/// Such errors are highly likely to halt the program. +#[derive(Clone)] +pub struct Error(Box); + +/// Information about an error. +#[derive(Clone)] +struct Information { + /// The primary error message. + primary: Box, + + /// Layers of context to the error. + /// + /// Ordered from innermost to outermost. + context: Vec>, } -impl From<&'static str> for Error { - fn from(message: &'static str) -> Self { - Self { - message: Cow::Borrowed(message), +//--- Interaction + +impl Error { + /// Construct a new error from a string. + pub fn new(error: &str) -> Self { + Self(Box::new(Information { + primary: error.into(), + context: Vec::new(), + })) + } + + /// Add context to this error. + pub fn context(mut self, context: &str) -> Self { + self.0.context.push(context.into()); + self + } + + /// Pretty-print this error. + pub fn pretty_print(self) { + use std::io::IsTerminal; + + // NOTE: This is a multicall binary, so argv[0] is necessary for + // program operation. We would fail very early if it didn't exist. + let prog = std::env::args().next().unwrap(); + let term = std::io::stderr().is_terminal(); + + let error_marker = if term { + "\x1B[31mERROR:\x1B[0m" + } else { + "ERROR:" + }; + + eprint!("[{prog}] {error_marker} {}", self.0.primary); + for context in &self.0.context { + eprint!("\n... while {context}"); } } } +//--- Conversions for '?' + +impl From<&str> for Error { + fn from(error: &str) -> Self { + Self::new(error) + } +} + impl From for Error { - fn from(message: String) -> Self { - Self { - message: Cow::Owned(message), - } - } -} - -impl From for Error { - fn from(_err: ParseError) -> Self { - Self::from("message parse error") - } -} - -impl From for Error { - fn from(_err: ParseIntError) -> Self { - Self::from("message parse error") + fn from(error: String) -> Self { + Self::new(&error) } } impl From for Error { - fn from(err: io::Error) -> Self { - Self::from(err.to_string()) + fn from(error: io::Error) -> Self { + Self::new(&error.to_string()) } } @@ -48,10 +85,47 @@ impl From for Error { } } +//--- Display, Debug + impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.message, f) + f.write_str(&self.0.primary) } } +impl fmt::Debug for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Error") + .field("primary", &self.0.primary) + .field("context", &self.0.context) + .finish() + } +} + +//--- Error + impl error::Error for Error {} + +//------------ Result -------------------------------------------------------- + +/// A program result. +pub type Result = core::result::Result; + +/// An extension trait for [`Result`]s using [`Error`]. +pub trait Context: Sized { + /// Add context for an error. + fn context(self, context: &str) -> Self; + + /// Add context for an error, lazily. + fn with_context(self, context: impl FnOnce() -> String) -> Self; +} + +impl Context for Result { + fn context(self, context: &str) -> Self { + self.map_err(|err| err.context(context)) + } + + fn with_context(self, context: impl FnOnce() -> String) -> Self { + self.map_err(|err| err.context(&(context)())) + } +} diff --git a/src/main.rs b/src/main.rs index 57662a0..34d4d50 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,20 @@ use std::path::Path; +use std::process::ExitCode; use clap::Parser; use dnst::commands::{nsec3hash::Nsec3Hash, LdnsCommand}; -fn main() { +fn main() -> ExitCode { // If none of the ldns-* tools matched, then we continue with clap // argument parsing. let args = try_ldns_compatibility().unwrap_or_else(dnst::Args::parse); - if let Err(err) = args.execute() { - eprintln!("{}", err); + match args.execute() { + Ok(()) => ExitCode::SUCCESS, + Err(err) => { + err.pretty_print(); + ExitCode::FAILURE + } } } @@ -25,8 +30,8 @@ fn try_ldns_compatibility() -> Option { match res { Ok(args) => Some(args), - Err(e) => { - eprintln!("{e}"); + Err(err) => { + err.pretty_print(); std::process::exit(1) } }