use async_deflate_zip::{Compression, ZipWriter};
use std::path::{Path, PathBuf};
use std::process::Command;
use tokio::io::AsyncWriteExt;
async fn test_basic_single_file() {
println!("--- Basic single file (default compression) ---");
let path = zip_out_path("test_basic_single_file");
let file = tokio::fs::File::create(&path).await.unwrap();
let mut zip = ZipWriter::new(file);
let mut entry = zip.append_file("hello.txt").await.unwrap();
entry.write_all(b"Hello, async-deflate-zip!").await.unwrap();
entry.close().await.unwrap();
zip.finalize().await.unwrap();
verify_zip_structure(&path, 1).await;
}
async fn test_compression_levels() {
println!("--- All compression levels ---");
let levels: Vec<(&str, Compression)> = vec![
("NONE", Compression::none()),
("FAST", Compression::fast()),
("DEFAULT", Compression::default()),
("BEST", Compression::best()),
];
let data = vec![b'A'; 10_000];
for (label, level) in &levels {
let path = zip_out_path(&format!("test_compression_levels_{label}"));
let file = tokio::fs::File::create(&path).await.unwrap();
let mut zip = ZipWriter::new(file).with_level(*level);
let mut entry = zip.append_file(&format!("data_{label}.bin")).await.unwrap();
entry.write_all(&data).await.unwrap();
entry.close().await.unwrap();
zip.finalize().await.unwrap();
let size = tokio::fs::metadata(&path).await.unwrap().len();
let ratio = size as f64 / data.len() as f64;
println!(
" {label:8} level={} ZIP size={:>8} ratio={:.3}",
level.level(),
size,
ratio
);
verify_zip_structure(&path, 1).await;
}
}
async fn test_multiple_files_nested() {
println!("--- Multiple files with nested paths ---");
let path = zip_out_path("test_multiple_files_nested");
let file = tokio::fs::File::create(&path).await.unwrap();
let mut zip = ZipWriter::new(file);
let files: [(&str, &[u8]); 4] = [
("top.txt", b"top level file" as &[u8]),
("sub/a.txt", b"file in subdir a" as &[u8]),
("sub/b.txt", b"file in subdir b" as &[u8]),
("sub/nested/deep.txt", b"deeply nested" as &[u8]),
];
for (name, content) in &files {
let mut entry = zip.append_file(name).await.unwrap();
entry.write_all(content).await.unwrap();
entry.close().await.unwrap();
}
zip.finalize().await.unwrap();
verify_zip_structure(&path, files.len()).await;
}
async fn test_directory_entries() {
println!("--- Directory entries ---");
let path = zip_out_path("test_directory_entries");
let file = tokio::fs::File::create(&path).await.unwrap();
let mut zip = ZipWriter::new(file);
let dir = zip.append_directory("emptydir/").await.unwrap();
dir.close().await.unwrap();
let mut dir = zip.append_directory("dated_dir/").await.unwrap();
dir.set_mtime(std::time::SystemTime::UNIX_EPOCH);
dir.close().await.unwrap();
let mut dir = zip.append_directory("protected_dir/").await.unwrap();
dir.set_permissions(0o755);
dir.close().await.unwrap();
let mut dir = zip.append_directory("full_meta_dir/").await.unwrap();
dir.set_mtime(std::time::SystemTime::now());
dir.set_permissions(0o700);
dir.close().await.unwrap();
let mut entry = zip.append_file("emptydir/hello.txt").await.unwrap();
entry.write_all(b"nested").await.unwrap();
entry.close().await.unwrap();
zip.finalize().await.unwrap();
verify_zip_structure(&path, 5).await;
}
async fn test_symlink_entries() {
println!("--- Symlink entries ---");
let path = zip_out_path("test_symlink_entries");
let file = tokio::fs::File::create(&path).await.unwrap();
let mut zip = ZipWriter::new(file);
zip.append_symlink("link.txt", "hello.txt").await.unwrap();
zip.append_symlink("sub/alink", "../link.txt")
.await
.unwrap();
zip.finalize().await.unwrap();
verify_zip_structure(&path, 2).await;
}
async fn test_entry_metadata() {
println!("--- Entry metadata (mtime + permissions) ---");
let path = zip_out_path("test_entry_metadata");
let file = tokio::fs::File::create(&path).await.unwrap();
let mut zip = ZipWriter::new(file);
{
let mut entry = zip.append_file("mtime_only.txt").await.unwrap();
entry.set_mtime(std::time::SystemTime::UNIX_EPOCH);
entry.write_all(b"epoch").await.unwrap();
entry.close().await.unwrap();
}
{
let mut entry = zip.append_file("perm_only.txt").await.unwrap();
entry.set_permissions(0o644);
entry.write_all(b"readable").await.unwrap();
entry.close().await.unwrap();
}
{
let mut entry = zip.append_file("both_meta.txt").await.unwrap();
entry.set_mtime(std::time::SystemTime::now());
entry.set_permissions(0o755);
entry.write_all(b"executable").await.unwrap();
entry.close().await.unwrap();
}
{
let mut entry = zip.append_file("setuid.txt").await.unwrap();
entry.set_permissions(0o4755);
entry.write_all(b"setuid").await.unwrap();
entry.close().await.unwrap();
}
zip.finalize().await.unwrap();
verify_zip_structure(&path, 4).await;
}
async fn test_from_filesystem() {
println!("--- Add files from filesystem ---");
let tmp_dir = zip_out_dir().join("test_from_filesystem");
ensure_dir_clean(&tmp_dir).await;
tokio::fs::write(tmp_dir.join("readme.txt"), b"Hello from disk!\n")
.await
.unwrap();
tokio::fs::write(tmp_dir.join("data.bin"), &[0xABu8; 1024])
.await
.unwrap();
tokio::fs::write(
tmp_dir.join("code.rs"),
b"fn main() { println!(\"hi\"); }\n",
)
.await
.unwrap();
let path = zip_out_path("test_from_filesystem");
let file = tokio::fs::File::create(&path).await.unwrap();
let mut zip = ZipWriter::new(file);
let mut dir = tokio::fs::read_dir(&tmp_dir).await.unwrap();
while let Some(entry) = dir.next_entry().await.unwrap() {
let fpath = entry.path();
if fpath.is_file() {
let name = fpath.file_name().unwrap().to_str().unwrap().to_string();
let content = tokio::fs::read(&fpath).await.unwrap();
let mut zw = zip.append_file(&name).await.unwrap();
zw.write_all(&content).await.unwrap();
zw.close().await.unwrap();
println!(" added: {} ({} bytes)", name, content.len());
}
}
zip.finalize().await.unwrap();
verify_zip_structure(&path, 3).await;
}
async fn test_stored_entry() {
println!("--- Stored entry (level=NONE / method=STORED) ---");
let path = zip_out_path("test_stored_entry");
let file = tokio::fs::File::create(&path).await.unwrap();
let mut zip = ZipWriter::new(file).with_level(Compression::none());
let mut entry = zip.append_file("stored.bin").await.unwrap();
entry.write_all(&[0xFF; 500]).await.unwrap();
entry.close().await.unwrap();
let mut entry = zip.append_file("stored2.bin").await.unwrap();
entry.write_all(b"small stored data").await.unwrap();
entry.close().await.unwrap();
zip.finalize().await.unwrap();
verify_zip_structure(&path, 2).await;
}
async fn test_unicode_entries() {
println!("--- UTF-8 filenames and content ---");
let path = zip_out_path("test_unicode_entries");
let file = tokio::fs::File::create(&path).await.unwrap();
let mut zip = ZipWriter::new(file);
{
let mut entry = zip.append_file("Grüß Gott.txt").await.unwrap();
entry
.write_all("Schöne Grüße aus der UTF-8-Welt!".as_bytes())
.await
.unwrap();
entry.close().await.unwrap();
println!(" added: Grüß Gott.txt");
}
{
let mut entry = zip.append_file("世界.txt").await.unwrap();
entry
.write_all("你好,世界!来自异步压缩库的问候。".as_bytes())
.await
.unwrap();
entry.close().await.unwrap();
println!(" added: 世界.txt");
}
{
let dir = zip.append_directory("목차/").await.unwrap();
dir.close().await.unwrap();
println!(" added: 목차/");
}
zip.finalize().await.unwrap();
verify_zip_structure(&path, 3).await;
}
async fn test_zip64_many_entries() {
println!("--- Many entries (>=65535) to trigger ZIP64 via count ---");
let path = zip_out_path("test_zip64_many_entries");
let file = tokio::fs::File::create(&path).await.unwrap();
let mut zip = ZipWriter::new(file).with_level(Compression::none());
let count = 0xFFFF + 1; for i in 0..count {
let name = format!("files/f{i}");
let mut entry = zip.append_file(&name).await.unwrap();
entry.write_all(b"x").await.unwrap();
entry.close().await.unwrap();
}
zip.finalize().await.unwrap();
verify_zip_structure(&path, count).await;
}
async fn test_large_file_zip64() {
println!("--- Large file >4GiB (ZIP64) ---");
let large_file = Path::new("/tmp")
.join("comprehensive_test")
.join("bigfile.bin");
let output_zip = zip_out_path("test_large_file_zip64");
println!(" generating 4.5 GiB test file with dd...");
let status = Command::new("dd")
.args([
"if=/dev/zero",
&format!("of={}", large_file.display()),
"bs=1M",
"count=4608",
"status=progress",
])
.status();
match status {
Ok(s) if s.success() => {}
_ => {
println!(" SKIP: dd unavailable, skipping ZIP64 large file test\n");
return;
}
}
let file_size = tokio::fs::metadata(&large_file).await.unwrap().len();
println!(
" file size: {} bytes ({:.2} GiB)",
file_size,
file_size as f64 / (1 << 30) as f64
);
assert!(
file_size > 4_294_967_296,
"file must be >4GiB for ZIP64 test, got {file_size}"
);
let out_file = tokio::fs::File::create(&output_zip).await.unwrap();
let mut zip = ZipWriter::new(out_file).with_level(Compression::default());
{
let mut entry = zip
.append_file("large_data_4GB.bin")
.await
.expect("append_file for large entry");
let mut in_file = tokio::fs::File::open(&large_file).await.unwrap();
let mut buffer = vec![0u8; 64 * 1024]; loop {
let n = tokio::io::AsyncReadExt::read(&mut in_file, &mut buffer)
.await
.unwrap();
if n == 0 {
break;
}
entry.write_all(&buffer[..n]).await.unwrap();
}
entry.close().await.expect("close large entry");
}
{
let mut entry = zip.append_file("readme.txt").await.unwrap();
entry
.write_all(b"This archive contains a >4GB file.")
.await
.unwrap();
entry.close().await.unwrap();
}
zip.finalize().await.expect("finalize");
let zip_meta = tokio::fs::metadata(&output_zip).await.unwrap();
println!(
" output ZIP: {} bytes ({:.2} GiB)",
zip_meta.len(),
zip_meta.len() as f64 / (1 << 30) as f64
);
verify_zip_structure(&output_zip, 2).await;
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
println!("================================================");
println!(" async-deflate-zip — Comprehensive Test Suite");
println!("================================================\n");
let out_dir = zip_out_dir();
ensure_dir_clean(&out_dir).await;
test_basic_single_file().await;
test_compression_levels().await;
test_multiple_files_nested().await;
test_directory_entries().await;
test_symlink_entries().await;
test_entry_metadata().await;
test_from_filesystem().await;
test_stored_entry().await;
test_unicode_entries().await;
test_zip64_many_entries().await;
test_large_file_zip64().await;
println!("================================================");
println!(" ALL TESTS PASSED");
println!("================================================");
}
async fn verify_zip_structure(path: &Path, expected_entries: usize) {
let has_unzip = Command::new("unzip")
.arg("-v")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if has_unzip {
let zip_str = path.to_string_lossy().into_owned();
let out = Command::new("unzip")
.args(["-t", &zip_str])
.output()
.expect("unzip -t failed");
assert!(
out.status.success(),
"unzip -t failed:\n{}",
String::from_utf8_lossy(&out.stderr)
);
let output = unsafe { std::str::from_utf8_unchecked(&out.stdout) };
let count = output
.lines()
.filter(|l| l.starts_with(" testing:"))
.count();
let lines: Vec<&str> = output.lines().collect();
if lines.len() > 10 {
for line in &lines[..5] {
println!("{line}");
}
println!(" ...");
for line in &lines[lines.len() - 5..] {
println!("{line}");
}
println!();
} else {
println!("{output}");
}
assert_eq!(
count, expected_entries,
"unzip reports {count} entries, expected {expected_entries}"
);
} else {
let buf = tokio::fs::read(path)
.await
.expect("failed to read zip file");
let lfh_count = buf.windows(4).filter(|w| w == b"PK\x03\x04").count();
let has_eocdr = buf.windows(4).rposition(|w| w == b"PK\x05\x06").is_some();
assert!(
has_eocdr,
"no EOCDR signature found — not a valid ZIP archive"
);
assert_eq!(
lfh_count, expected_entries,
"found {lfh_count} local file headers, expected {expected_entries}"
);
}
}
fn zip_out_dir() -> PathBuf {
Path::new("/tmp").join("comprehensive_test")
}
fn zip_out_path(label: &str) -> PathBuf {
zip_out_dir().join(format!("{label}.zip"))
}
async fn ensure_dir_clean(out_dir: &PathBuf) {
if out_dir.exists() {
let mut dir = tokio::fs::read_dir(out_dir)
.await
.expect("failed to read output directory");
while let Some(entry) = dir
.next_entry()
.await
.expect("failed to read directory entry")
{
let path = entry.path();
if path.is_dir() {
tokio::fs::remove_dir_all(&path)
.await
.expect("failed to remove subdirectory");
} else {
tokio::fs::remove_file(&path)
.await
.expect("failed to remove file");
}
}
} else {
tokio::fs::create_dir_all(out_dir)
.await
.expect("failed to create output directory");
}
}