Add context tracking for errors (#18)

* [error] Introduce context tracking

* [error] Add an extension trait to 'Result'

* [error] Move pretty-printing to explicit function

* Pretty-print errors on exit

* [erro] Fix primary field documentation

---------

Co-authored-by: arya dradjica <arya@nlnetlabs.nl>
This commit is contained in:
arya dradjica
2024-11-11 19:55:46 +01:00
committed by GitHub
co-authored by arya dradjica
parent 2e6d8a9199
commit 8dc82c7ff6
3 changed files with 119 additions and 41 deletions
+6 -7
View File
@@ -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<Name<Vec<u8>>, Error> {
Name::from_str(&arg.to_lowercase()).map_err(|e| Error::from(e.to_string()))
pub fn parse_name(arg: &str) -> Result<Name<Vec<u8>>, name::FromStrError> {
Name::from_str(&arg.to_lowercase())
}
pub fn parse_nsec_alg(arg: &str) -> Result<Nsec3HashAlg, Error> {
pub fn parse_nsec_alg(arg: &str) -> Result<Nsec3HashAlg, &'static str> {
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")
}
}
}
+103 -29
View File
@@ -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>);
/// Information about an error.
#[derive(Clone)]
struct Information {
/// The primary error message.
primary: Box<str>,
/// Layers of context to the error.
///
/// Ordered from innermost to outermost.
context: Vec<Box<str>>,
}
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<String> for Error {
fn from(message: String) -> Self {
Self {
message: Cow::Owned(message),
}
}
}
impl From<ParseError> for Error {
fn from(_err: ParseError) -> Self {
Self::from("message parse error")
}
}
impl From<ParseIntError> for Error {
fn from(_err: ParseIntError) -> Self {
Self::from("message parse error")
fn from(error: String) -> Self {
Self::new(&error)
}
}
impl From<io::Error> 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<lexopt::Error> 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<T> = core::result::Result<T, Error>;
/// 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<T> Context for Result<T> {
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)()))
}
}
+10 -5
View File
@@ -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<dnst::Args> {
match res {
Ok(args) => Some(args),
Err(e) => {
eprintln!("{e}");
Err(err) => {
err.pretty_print();
std::process::exit(1)
}
}