zarust 0.2.1

Rust implementation of the ZArchive format
Documentation
//! The `zarust` command-line interface, a filesystem adapter over the library.

pub mod args;
pub mod extract;
pub mod help;
pub mod inspect;
pub mod pack;
pub mod path;
pub mod ui;
pub mod walk;

use std::{
    fmt::Display,
    fs::File,
    io::{self, BufReader},
};

use zarust::ArchiveReader;

use crate::cli::{
    args::{Command, Invocation, UsageError},
    ui::Reporter,
};

/// Buffer size for reading archives; a multiple of the format's 64 KiB block
/// size so block reads rarely land on a syscall boundary. Also used for the
/// read side of packing.
pub const READ_BUFFER: usize = 256 * 1024;

/// Anything that stops a command after the command line has been understood.
#[derive(Debug)]
pub struct Failure(pub String);

impl Failure {
    pub fn new(message: impl Into<String>) -> Self {
        Self(message.into())
    }

    /// The shape almost every failure here takes: an operation, the path it was
    /// attempted on, and the underlying cause.
    pub fn path(verb: &str, path: &std::path::Path, error: impl Display) -> Self {
        Self(format!(
            "cannot {verb} `{}`: {error}",
            self::path::display(path)
        ))
    }
}

impl From<io::Error> for Failure {
    fn from(error: io::Error) -> Self {
        Self(error.to_string())
    }
}

impl From<zarust::Error> for Failure {
    fn from(error: zarust::Error) -> Self {
        Self(error.to_string())
    }
}

pub type Outcome = Result<(), Failure>;

/// Process exit codes, kept small and positive so shells can act on them.
pub mod exit {
    pub const SUCCESS: u8 = 0;
    pub const FAILURE: u8 = 1;
    pub const USAGE: u8 = 2;
}

pub fn open_archive(path: &std::path::Path) -> Result<ArchiveReader<BufReader<File>>, Failure> {
    let file = File::open(path).map_err(|error| Failure::path("open", path, error))?;
    ArchiveReader::new(BufReader::with_capacity(READ_BUFFER, file))
        .map_err(|error| Failure::path("read", path, error))
}

/// Runs one invocation, returning the process exit code.
pub fn run(invocation: Invocation) -> u8 {
    let Invocation { command, options } = invocation;
    let mut reporter = Reporter::new(options.color, options.verbosity, options.progress);

    let outcome = match command {
        Command::Pack { input, output } => pack::run(&mut reporter, &input, output, &options),
        Command::Extract { archive, output } => {
            extract::run(&mut reporter, &archive, output, &options)
        }
        Command::List { archive, path } => inspect::list(&mut reporter, &archive, path, &options),
        Command::Info { archive } => inspect::info(&mut reporter, &archive),
        Command::Verify { archive } => inspect::verify(&mut reporter, &archive),
        Command::Help => help::help(&mut reporter),
        Command::Version => help::version(&mut reporter),
    };

    // Flush before reporting so the error is the last thing on screen.
    let flushed = reporter.flush();
    match outcome.and_then(|()| flushed.map_err(Failure::from)) {
        Ok(()) => exit::SUCCESS,
        Err(failure) => {
            report_error(&failure.0);
            exit::FAILURE
        }
    }
}

/// Prints a usage error and the hint to reach real help.
pub fn report_usage(error: &UsageError) -> u8 {
    report_error(&error.0);
    eprintln!("Try `zarust --help` for the list of commands.");
    exit::USAGE
}

fn report_error(message: &str) {
    let palette = ui::stderr_palette(ui::ColorChoice::Auto);
    eprintln!(
        "{}{}error{}: {message}",
        palette.red, palette.bold, palette.reset
    );
}