xzip 0.1.0

ZIP CLI with explicit filename encoding for pack and unpack workflows.
Documentation
use std::fs;
use std::io::Write;
use std::process::Command;

use tempfile::tempdir;
use xzip::codec::EncodingKind;
use xzip::pack::pack_path;
use xzip::unpack::unpack_archive;
use zip::write::SimpleFileOptions;
use zip::{CompressionMethod, ZipWriter};

#[test]
fn round_trip_pack_unpack_utf8() {
    let temp = tempdir().expect("temp dir");
    let input_dir = temp.path().join("input");
    let output_dir = temp.path().join("output");
    fs::create_dir_all(input_dir.join("sub")).expect("create input subdir");
    fs::write(input_dir.join("sub/hello.txt"), b"hello").expect("write file");
    fs::write(input_dir.join("root.txt"), b"root").expect("write file");

    let archive = temp.path().join("archive.zip");
    pack_path(&input_dir, &archive, EncodingKind::Utf8, true, &[], &[]).expect("pack");
    unpack_archive(&archive, &output_dir, EncodingKind::Utf8, &[], &[]).expect("unpack");

    let unpacked = fs::read(output_dir.join("sub/hello.txt")).expect("read unpacked file");
    assert_eq!(unpacked, b"hello");
}

#[test]
fn invalid_encoding_label_rejected() {
    assert!(EncodingKind::from_label("bad-encoding").is_err());
}

#[test]
fn blocks_zip_slip_paths() {
    let temp = tempdir().expect("temp dir");
    let archive_path = temp.path().join("unsafe.zip");
    {
        let file = fs::File::create(&archive_path).expect("create archive");
        let mut zip = ZipWriter::new(file);
        let options = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
        zip.start_file("../outside.txt", options)
            .expect("start file");
        zip.write_all(b"attack").expect("write attack payload");
        zip.finish().expect("finalize archive");
    }

    let output = temp.path().join("extract");
    let result = unpack_archive(&archive_path, &output, EncodingKind::Utf8, &[], &[]);
    assert!(result.is_err());
}

#[test]
fn non_recursive_pack_skips_nested_files() {
    let temp = tempdir().expect("temp dir");
    let input_dir = temp.path().join("input");
    fs::create_dir_all(input_dir.join("nested")).expect("create nested dir");
    fs::write(input_dir.join("root.txt"), b"root").expect("write root");
    fs::write(input_dir.join("nested/deep.txt"), b"deep").expect("write nested");

    let archive = temp.path().join("archive.zip");
    pack_path(&input_dir, &archive, EncodingKind::Utf8, false, &[], &[]).expect("pack");

    let output_dir = temp.path().join("output");
    unpack_archive(&archive, &output_dir, EncodingKind::Utf8, &[], &[]).expect("unpack");
    assert!(output_dir.join("root.txt").exists());
    assert!(!output_dir.join("nested/deep.txt").exists());
}

#[test]
fn cli_defaults_to_utf8_when_encoding_omitted() {
    let temp = tempdir().expect("temp dir");
    let input_dir = temp.path().join("input");
    fs::create_dir_all(&input_dir).expect("create input dir");
    fs::write(input_dir.join("hello.txt"), b"hello").expect("write input");

    let archive = temp.path().join("archive.zip");
    let output_dir = temp.path().join("output");
    let bin = env!("CARGO_BIN_EXE_xzip");

    let pack_status = Command::new(bin)
        .args([
            "pack",
            "-i",
            input_dir.to_string_lossy().as_ref(),
            "-o",
            archive.to_string_lossy().as_ref(),
            "-r",
        ])
        .status()
        .expect("run pack");
    assert!(pack_status.success());

    let unpack_status = Command::new(bin)
        .args([
            "unpack",
            "-i",
            archive.to_string_lossy().as_ref(),
            "-o",
            output_dir.to_string_lossy().as_ref(),
        ])
        .status()
        .expect("run unpack");
    assert!(unpack_status.success());
    assert_eq!(
        fs::read(output_dir.join("hello.txt")).expect("read output"),
        b"hello"
    );
}