xzip 0.2.0

ZIP CLI with explicit filename encoding for pack and unpack workflows.
Documentation
use std::fs::{self, File};
use std::io::{self, Write};
use std::path::Path;
use std::time::Instant;

use crate::archive::{open_archive, sanitize_relative_path};
use crate::codec::EncodingKind;
use crate::error::XzipError;
use crate::filter::PathFilter;
use crate::verbosity::Verbosity;

pub fn unpack_archive(
    input_zip: &Path,
    output_dir: &Path,
    encoding: EncodingKind,
    include: &[String],
    exclude: &[String],
    verbosity: Verbosity,
) -> Result<(), XzipError> {
    let started = Instant::now();
    let mut archive = open_archive(input_zip)?;
    fs::create_dir_all(output_dir)?;
    let filter = PathFilter::new(include, exclude)?;

    let mut files_extracted = 0usize;
    let mut dirs_created = 0usize;

    for i in 0..archive.len() {
        let mut entry = archive.by_index(i)?;
        let raw_name = entry.name_raw();
        let decoded_name = encoding.decode(raw_name)?;
        if !filter.allows(&decoded_name) {
            continue;
        }
        let safe_rel = sanitize_relative_path(&decoded_name)?;
        let outpath = output_dir.join(&safe_rel);

        if entry.is_dir() {
            fs::create_dir_all(&outpath)?;
            dirs_created += 1;
            if verbosity.is_verbose() {
                eprintln!("extracting: {decoded_name}");
            }
            continue;
        }

        if let Some(parent) = outpath.parent() {
            fs::create_dir_all(parent)?;
        }

        if verbosity.is_verbose() {
            if verbosity.is_extra() {
                eprintln!(
                    "extracting: {decoded_name} ({} -> {} bytes)",
                    entry.size(),
                    entry.compressed_size()
                );
            } else {
                eprintln!("extracting: {decoded_name}");
            }
        }

        let mut outfile = File::create(&outpath)?;
        io::copy(&mut entry, &mut outfile)?;
        outfile.flush()?;
        files_extracted += 1;
    }

    if verbosity.is_verbose() {
        let elapsed = started.elapsed();
        let file_label = if files_extracted == 1 {
            "file"
        } else {
            "files"
        };
        eprintln!(
            "{files_extracted} {file_label} extracted ({dirs_created} directories) in {:.2}s",
            elapsed.as_secs_f64()
        );
    }

    Ok(())
}