//! Error types definitions. //! //! All usage errors will pass through the Error enum, a lot of them in the Error::Custom. use std::{ borrow::Cow, ffi::OsString, fmt::{self, Display}, io, }; use crate::{ accessible::is_running_in_accessible_mode, extension::{PRETTY_SUPPORTED_ALIASES, PRETTY_SUPPORTED_EXTENSIONS}, utils::os_str_to_str, }; /// All errors that can be generated by `ouch` #[derive(Debug, Clone)] pub enum Error { /// An IoError that doesn't have a dedicated error variant IoError { reason: String }, /// From lzzzz::lz4f::Error Lz4Error { reason: String }, /// Detected from io::Error if .kind() is io::ErrorKind::NotFound NotFound { error_title: String }, /// NEEDS MORE CONTEXT AlreadyExists { error_title: String }, /// From zip::result::ZipError::InvalidArchive InvalidZipArchive(&'static str), /// Detected from io::Error if .kind() is io::ErrorKind::PermissionDenied PermissionDenied { error_title: String }, /// From zip::result::ZipError::UnsupportedArchive UnsupportedZipArchive(&'static str), /// We don't support compressing the root folder. CompressingRootFolder, /// Specialized walkdir's io::Error wrapper with additional information on the error WalkdirError { reason: String }, /// Custom and unique errors are reported in this variant Custom { reason: FinalError }, /// Invalid format passed to `--format` InvalidFormatFlag { text: OsString, reason: String }, /// From sevenz_rust::Error SevenzipError { reason: String }, /// Recognised but unsupported format // currently only RAR when built without the `unrar` feature UnsupportedFormat { reason: String }, /// Invalid password provided InvalidPassword { reason: String }, } /// Alias to std's Result with ouch's Error pub type Result = std::result::Result; /// A string either heap-allocated or located in static storage pub type CowStr = Cow<'static, str>; /// Pretty final error message for end users, crashing the program after display. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct FinalError { /// Should be made of just one line, appears after the "\[ERROR\]" part title: CowStr, /// Shown as a unnumbered list in yellow details: Vec, /// Shown as green at the end to give hints on how to work around this error, if it's fixable hints: Vec, } impl Display for FinalError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use crate::utils::colors::*; // Title // // When in ACCESSIBLE mode, the square brackets are suppressed if is_running_in_accessible_mode() { write!(f, "{}ERROR{}: {}", *RED, *RESET, self.title)?; } else { write!(f, "{}[ERROR]{} {}", *RED, *RESET, self.title)?; } // Details for detail in &self.details { write!(f, "\n - {}{}{}", *YELLOW, detail, *RESET)?; } // Hints if !self.hints.is_empty() { // Separate by one blank line. writeln!(f)?; // to reduce redundant output for text-to-speech systems, braille // displays and so on, only print "hints" once in ACCESSIBLE mode if is_running_in_accessible_mode() { write!(f, "\n{}hints:{}", *GREEN, *RESET)?; for hint in &self.hints { write!(f, "\n{hint}")?; } } else { for hint in &self.hints { write!(f, "\n{}hint:{} {}", *GREEN, *RESET, hint)?; } } } Ok(()) } } impl FinalError { /// Only constructor #[must_use] pub fn with_title(title: impl Into) -> Self { Self { title: title.into(), details: vec![], hints: vec![], } } /// Add one detail line, can have multiple #[must_use] pub fn detail(mut self, detail: impl Into) -> Self { self.details.push(detail.into()); self } /// Add one hint line, can have multiple #[must_use] pub fn hint(mut self, hint: impl Into) -> Self { self.hints.push(hint.into()); self } /// Adds all supported formats as hints. /// /// This is what it looks like: /// ``` /// hint: Supported extensions are: tar, zip, bz, bz2, gz, lz4, xz, lzma, sz, zst /// hint: Supported aliases are: tgz, tbz, tlz4, txz, tzlma, tsz, tzst /// ``` pub fn hint_all_supported_formats(self) -> Self { self.hint(format!("Supported extensions are: {}", PRETTY_SUPPORTED_EXTENSIONS)) .hint(format!("Supported aliases are: {}", PRETTY_SUPPORTED_ALIASES)) } } impl From for FinalError { fn from(err: Error) -> Self { match err { Error::WalkdirError { reason } => FinalError::with_title(reason), Error::NotFound { error_title } => FinalError::with_title(error_title).detail("File not found"), Error::CompressingRootFolder => { FinalError::with_title("It seems you're trying to compress the root folder.") .detail("This is unadvisable since ouch does compressions in-memory.") .hint("Use a more appropriate tool for this, such as rsync.") } Error::IoError { reason } => FinalError::with_title(reason), Error::Lz4Error { reason } => FinalError::with_title(reason), Error::AlreadyExists { error_title } => FinalError::with_title(error_title).detail("File already exists"), Error::InvalidZipArchive(reason) => FinalError::with_title("Invalid zip archive").detail(reason), Error::PermissionDenied { error_title } => FinalError::with_title(error_title).detail("Permission denied"), Error::UnsupportedZipArchive(reason) => FinalError::with_title("Unsupported zip archive").detail(reason), Error::InvalidFormatFlag { reason, text } => { FinalError::with_title(format!("Failed to parse `--format {}`", os_str_to_str(&text))) .detail(reason) .hint_all_supported_formats() .hint("") .hint("Examples:") .hint(" --format tar") .hint(" --format gz") .hint(" --format tar.gz") } Error::Custom { reason } => reason.clone(), Error::SevenzipError { reason } => FinalError::with_title("7z error").detail(reason), Error::UnsupportedFormat { reason } => { FinalError::with_title("Recognised but unsupported format").detail(reason.clone()) } Error::InvalidPassword { reason } => FinalError::with_title("Invalid password").detail(reason.clone()), } } } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let err = FinalError::from(self.clone()); write!(f, "{err}") } } impl From for Error { fn from(err: std::io::Error) -> Self { let error_title = err.to_string(); match err.kind() { io::ErrorKind::NotFound => Self::NotFound { error_title }, io::ErrorKind::PermissionDenied => Self::PermissionDenied { error_title }, io::ErrorKind::AlreadyExists => Self::AlreadyExists { error_title }, _other => Self::IoError { reason: error_title }, } } } #[cfg(feature = "bzip3")] impl From for Error { fn from(err: bzip3::Error) -> Self { use bzip3::Error as Bz3Error; match err { Bz3Error::Io(inner) => inner.into(), Bz3Error::BlockSize | Bz3Error::ProcessBlock(_) | Bz3Error::InvalidSignature => { FinalError::with_title("bzip3 error").detail(err.to_string()).into() } } } } impl From for Error { fn from(err: zip::result::ZipError) -> Self { use zip::result::ZipError; match err { ZipError::Io(io_err) => Self::from(io_err), ZipError::InvalidArchive(filename) => Self::InvalidZipArchive(filename), ZipError::FileNotFound => Self::Custom { reason: FinalError::with_title("Unexpected error in zip archive").detail("File not found"), }, ZipError::UnsupportedArchive(filename) => Self::UnsupportedZipArchive(filename), } } } #[cfg(feature = "unrar")] impl From for Error { fn from(err: unrar::error::UnrarError) -> Self { Self::Custom { reason: FinalError::with_title("Unexpected error in rar archive").detail(format!("{:?}", err.code)), } } } impl From for Error { fn from(err: sevenz_rust2::Error) -> Self { Self::SevenzipError { reason: err.to_string(), } } } impl From for Error { fn from(err: ignore::Error) -> Self { Self::WalkdirError { reason: err.to_string(), } } } impl From for Error { fn from(err: FinalError) -> Self { Self::Custom { reason: err } } }