ouch 0.7.1

A command-line utility for easily compressing and decompressing files and directories.
//! Error types definitions.
//!
//! All usage errors will pass through the Error enum, a lot of them in the Error::Custom.

use std::{
    borrow::Cow,
    fmt::{self, Display},
    io,
};

use crate::{
    accessible::is_running_in_accessible_mode,
    extension::{PRETTY_SUPPORTED_ALIASES, PRETTY_SUPPORTED_EXTENSIONS},
};

/// 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(Cow<'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: String, reason: String },
    /// From sevenz_rust2::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 },
}

impl Error {
    /// RAR support is disabled for this build.
    pub fn rar_no_support() -> Self {
        Self::UnsupportedFormat {
            reason: "RAR support is disabled for this build, possibly due to licensing restrictions.".into(),
        }
    }

    /// BZip3 support is disabled for this build.
    pub fn bzip3_no_support() -> Self {
        Self::UnsupportedFormat {
            reason: "BZip3 support is disabled for this build, possibly due to missing bindgen-cli dependency.".into(),
        }
    }
}

/// Alias to std's Result with ouch's Error
pub type Result<T, E = self::Error> = std::result::Result<T, E>;

/// 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<CowStr>,
    /// Shown as green at the end to give hints on how to work around this error, if it's fixable
    hints: Vec<CowStr>,
}

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<CowStr>) -> 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<CowStr>) -> Self {
        self.details.push(detail.into());
        self
    }

    /// Add one hint line, can have multiple
    #[must_use]
    pub fn hint(mut self, hint: impl Into<CowStr>) -> 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, lz, sz, zst
    /// hint: Supported aliases are: tgz, tbz, tlz4, txz, tlzma, tsz, tzst, tlz
    /// ```
    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<Error> for FinalError {
    fn from(err: Error) -> Self {
        match err {
            Error::WalkdirError { reason } => Self::with_title(reason),
            Error::NotFound { error_title } => Self::with_title(error_title).detail("File not found"),
            Error::CompressingRootFolder => Self::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 } => Self::with_title(reason),
            Error::Lz4Error { reason } => Self::with_title(reason),
            Error::AlreadyExists { error_title } => Self::with_title(error_title).detail("File already exists"),
            Error::InvalidZipArchive(reason) => Self::with_title("Invalid zip archive").detail(reason),
            Error::PermissionDenied { error_title } => Self::with_title(error_title).detail("Permission denied"),
            Error::UnsupportedZipArchive(reason) => Self::with_title("Unsupported zip archive").detail(reason),
            Error::InvalidFormatFlag { reason, text } => {
                Self::with_title(format!("Failed to parse `--format {}`", &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 } => Self::with_title("7z error").detail(reason),
            Error::UnsupportedFormat { reason } => {
                Self::with_title("Recognised but unsupported format").detail(reason.clone())
            }
            Error::InvalidPassword { reason } => Self::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<io::Error> for Error {
    fn from(err: 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<bzip3::Error> 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<zip::result::ZipError> 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),
            ZipError::InvalidPassword => Self::InvalidPassword {
                reason: "The provided password is incorrect".to_string(),
            },
            _ => Self::Custom {
                reason: FinalError::with_title("Unexpected error in zip archive").detail(err.to_string()),
            },
        }
    }
}

#[cfg(feature = "unrar")]
impl From<unrar::error::UnrarError> 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<sevenz_rust2::Error> for Error {
    fn from(err: sevenz_rust2::Error) -> Self {
        Self::SevenzipError {
            reason: err.to_string(),
        }
    }
}

impl From<ignore::Error> for Error {
    fn from(err: ignore::Error) -> Self {
        Self::WalkdirError {
            reason: err.to_string(),
        }
    }
}

impl From<FinalError> for Error {
    fn from(err: FinalError) -> Self {
        Self::Custom { reason: err }
    }
}