xzip 0.3.0

ZIP CLI with explicit filename encoding for pack and unpack workflows.
Documentation
//! Large-scale unpack benchmark (ignored by default).
//!
//! Run manually:
//!   cargo test --release --test bench_unpack -- --ignored --nocapture
//!
//! `jobs=0` uses auto-parallel (large archives only). `jobs=1` forces sequential.

use std::fs;
use std::time::Instant;

use tempfile::tempdir;
use xzip::codec::EncodingKind;
use xzip::pack::pack_path;
use xzip::unpack::unpack_archive;
use xzip::verbosity::Verbosity;

const FILE_COUNT: usize = 6_000;

fn build_fixture_archive() -> (tempfile::TempDir, std::path::PathBuf) {
    let temp = tempdir().expect("temp dir");
    let input_dir = temp.path().join("input");
    for i in 0..FILE_COUNT {
        let sub = input_dir.join(format!("dir{i:04}"));
        fs::create_dir_all(&sub).expect("create subdir");
        let payload = format!("payload-{i:04}-{}", "x".repeat(64));
        fs::write(sub.join("file.txt"), payload).expect("write file");
    }

    let archive = temp.path().join("large.zip");
    pack_path(
        &input_dir,
        &archive,
        EncodingKind::Utf8,
        true,
        &[],
        &[],
        Verbosity::default(),
    )
    .expect("pack fixture");

    (temp, archive)
}

fn timed_unpack(
    archive: &std::path::Path,
    output: &std::path::Path,
    jobs: usize,
    label: &str,
) -> f64 {
    if output.exists() {
        fs::remove_dir_all(output).expect("clean output dir");
    }

    let started = Instant::now();
    unpack_archive(
        archive,
        output,
        EncodingKind::Utf8,
        &[],
        &[],
        Verbosity::default(),
        jobs,
    )
    .expect("unpack");
    let elapsed = started.elapsed().as_secs_f64();
    println!("{label}: {FILE_COUNT} files in {elapsed:.2}s");
    elapsed
}

#[test]
#[ignore = "manual large-scale unpack benchmark"]
fn bench_large_unpack_sequential_vs_parallel() {
    let (_temp, archive) = build_fixture_archive();
    let base = _temp.path().join("out");

    let sequential = timed_unpack(&archive, &base.join("sequential"), 1, "jobs=1");
    let parallel = timed_unpack(&archive, &base.join("parallel"), 0, "jobs=0 (auto)");

    let sample = fs::read(base.join("sequential/dir0000/file.txt")).expect("read sample");
    assert!(sample.starts_with(b"payload-0000-"));

    println!(
        "speedup (jobs=0 / jobs=1): {:.2}x",
        sequential / parallel.max(f64::EPSILON)
    );
}

#[test]
#[ignore = "manual verbose large-scale unpack benchmark"]
fn bench_large_unpack_verbose() {
    let (_temp, archive) = build_fixture_archive();
    let output = _temp.path().join("out-verbose");

    let started = Instant::now();
    unpack_archive(
        &archive,
        &output,
        EncodingKind::Utf8,
        &[],
        &[],
        Verbosity(1),
        1,
    )
    .expect("unpack");
    let elapsed = started.elapsed().as_secs_f64();
    println!("jobs=1 -v: {FILE_COUNT} files in {elapsed:.2}s");
}