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 xzip::verbosity::Verbosity;
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,
&[],
&[],
Verbosity::default(),
)
.expect("pack");
unpack_archive(
&archive,
&output_dir,
EncodingKind::Utf8,
&[],
&[],
Verbosity::default(),
1,
false,
)
.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,
&[],
&[],
Verbosity::default(),
1,
false,
);
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,
&[],
&[],
Verbosity::default(),
)
.expect("pack");
let output_dir = temp.path().join("output");
unpack_archive(
&archive,
&output_dir,
EncodingKind::Utf8,
&[],
&[],
Verbosity::default(),
1,
false,
)
.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"
);
}
#[test]
fn list_shows_entries() {
let temp = tempdir().expect("temp dir");
let input_dir = temp.path().join("input");
fs::create_dir_all(input_dir.join("aa")).expect("create dir");
fs::write(input_dir.join("aa/file.txt"), b"content").expect("write file");
let archive = temp.path().join("archive.zip");
pack_path(
&input_dir,
&archive,
EncodingKind::Utf8,
true,
&[],
&[],
Verbosity::default(),
)
.expect("pack");
let bin = env!("CARGO_BIN_EXE_xzip");
let output = Command::new(bin)
.args(["list", "-i", archive.to_string_lossy().as_ref()])
.output()
.expect("run list");
assert!(output.status.success(), "list failed: {:?}", output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Length"));
assert!(stdout.contains("aa/file.txt"));
assert!(stdout.contains("files"));
}
#[test]
fn list_respects_encoding() {
let temp = tempdir().expect("temp dir");
let cjk_name = "䏿–‡.txt";
let archive = temp.path().join("archive.zip");
{
let file = fs::File::create(&archive).expect("create archive");
let mut zip = ZipWriter::new(file);
let options = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
let gbk_bytes = EncodingKind::Gbk.encode(cjk_name).expect("encode name");
let raw_name = unsafe { String::from_utf8_unchecked(gbk_bytes) };
zip.start_file(raw_name, options).expect("start file");
zip.write_all(b"data").expect("write payload");
zip.finish().expect("finalize archive");
}
let bin = env!("CARGO_BIN_EXE_xzip");
let output = Command::new(bin)
.args([
"list",
"-i",
archive.to_string_lossy().as_ref(),
"-e",
"gbk",
])
.output()
.expect("run list");
assert!(output.status.success(), "list failed: {:?}", output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains(cjk_name));
}
#[test]
fn unpack_verbose_prints_progress() {
let temp = tempdir().expect("temp dir");
let input_dir = temp.path().join("input");
fs::create_dir_all(&input_dir).expect("create dir");
fs::write(input_dir.join("one.txt"), b"one").expect("write one");
fs::write(input_dir.join("two.txt"), b"two").expect("write two");
let archive = temp.path().join("archive.zip");
let output_dir = temp.path().join("output");
let bin = env!("CARGO_BIN_EXE_xzip");
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");
let output = Command::new(bin)
.args([
"unpack",
"-v",
"-i",
archive.to_string_lossy().as_ref(),
"-o",
output_dir.to_string_lossy().as_ref(),
])
.output()
.expect("run unpack");
assert!(
output.status.success(),
"unpack failed: {:?}",
output.stderr
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("extracting:"));
assert!(stderr.contains("extracted"));
assert!(stderr.contains("[jobs="));
}
#[test]
fn list_include_exclude() {
let temp = tempdir().expect("temp dir");
let input_dir = temp.path().join("input");
fs::create_dir_all(input_dir.join("keep")).expect("create keep dir");
fs::create_dir_all(input_dir.join("skip")).expect("create skip dir");
fs::write(input_dir.join("keep/yes.txt"), b"yes").expect("write keep");
fs::write(input_dir.join("skip/no.txt"), b"no").expect("write skip");
let archive = temp.path().join("archive.zip");
pack_path(
&input_dir,
&archive,
EncodingKind::Utf8,
true,
&[],
&[],
Verbosity::default(),
)
.expect("pack");
let bin = env!("CARGO_BIN_EXE_xzip");
let output = Command::new(bin)
.args([
"list",
"-i",
archive.to_string_lossy().as_ref(),
"--include",
"keep/**",
"--exclude",
"**/skip/**",
])
.output()
.expect("run list");
assert!(output.status.success(), "list failed: {:?}", output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("keep/yes.txt"));
assert!(!stdout.contains("skip/no.txt"));
assert!(stdout.contains("2 files"));
}
#[test]
fn list_ls_alias_works() {
let temp = tempdir().expect("temp dir");
let input_dir = temp.path().join("input");
fs::create_dir_all(&input_dir).expect("create dir");
fs::write(input_dir.join("only.txt"), b"x").expect("write file");
let archive = temp.path().join("archive.zip");
pack_path(
&input_dir,
&archive,
EncodingKind::Utf8,
false,
&[],
&[],
Verbosity::default(),
)
.expect("pack");
let bin = env!("CARGO_BIN_EXE_xzip");
let output = Command::new(bin)
.args(["ls", "-i", archive.to_string_lossy().as_ref()])
.output()
.expect("run ls");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("only.txt"));
}
#[test]
fn parallel_unpack_matches_sequential() {
let temp = tempdir().expect("temp dir");
let input_dir = temp.path().join("input");
for i in 0..12 {
let sub = input_dir.join(format!("dir{i}"));
fs::create_dir_all(&sub).expect("create subdir");
fs::write(sub.join("file.txt"), format!("payload-{i}")).expect("write file");
}
let archive = temp.path().join("archive.zip");
pack_path(
&input_dir,
&archive,
EncodingKind::Utf8,
true,
&[],
&[],
Verbosity::default(),
)
.expect("pack");
let sequential_dir = temp.path().join("sequential");
let parallel_dir = temp.path().join("parallel");
unpack_archive(
&archive,
&sequential_dir,
EncodingKind::Utf8,
&[],
&[],
Verbosity::default(),
1,
false,
)
.expect("sequential unpack");
unpack_archive(
&archive,
¶llel_dir,
EncodingKind::Utf8,
&[],
&[],
Verbosity::default(),
4,
false,
)
.expect("parallel unpack");
for i in 0..12 {
let rel = format!("dir{i}/file.txt");
let sequential = fs::read(sequential_dir.join(&rel)).expect("read sequential");
let parallel = fs::read(parallel_dir.join(&rel)).expect("read parallel");
assert_eq!(sequential, parallel);
}
}