use std::fs::{self, File};
use std::io::{Read, Write};
use tempfile::{tempdir, NamedTempFile};
use unpackr::extraction::{CollisionPolicy, ExtractionEngine, ExtractionError, ExtractionOptions};
use zip::write::SimpleFileOptions;
use zip::ZipWriter;
#[test]
fn test_streaming_extraction_accuracy() {
let dest_dir = tempdir().unwrap();
let archive_path = std::path::Path::new("tests/test_data/valid_sample.zip");
let options = ExtractionOptions {
destination: dest_dir.path().to_path_buf(),
collision_policy: CollisionPolicy::Fail,
enable_sparse: true,
max_compression_ratio: 200.0,
reclaim_archive: false,
state_dir: None,
verbose: true,
quiet: false,
..Default::default()
};
let summary = ExtractionEngine::extract(archive_path, &options).expect("Extraction failed");
assert_eq!(summary.total_entries, 5);
assert_eq!(summary.extracted_files, 4);
assert_eq!(summary.created_directories, 1);
let plain_path = dest_dir.path().join("plain.txt");
assert!(plain_path.exists());
let mut plain_content = String::new();
File::open(&plain_path)
.unwrap()
.read_to_string(&mut plain_content)
.unwrap();
assert_eq!(plain_content, "This is uncompressed text stored as is.");
let nested_path = dest_dir.path().join("nested/data.txt");
assert!(nested_path.exists());
let mut nested_content = Vec::new();
File::open(&nested_path)
.unwrap()
.read_to_end(&mut nested_content)
.unwrap();
assert_eq!(nested_content.len(), 14000);
assert_eq!(&nested_content[..28], b"Decompression testing data! ");
let empty_path = dest_dir.path().join("empty.dat");
assert!(empty_path.exists());
assert_eq!(fs::metadata(&empty_path).unwrap().len(), 0);
let logs_path = dest_dir.path().join("logs");
assert!(logs_path.is_dir());
}
#[test]
fn test_sparse_hole_extraction() {
let dest_dir = tempdir().unwrap();
let zip_file = NamedTempFile::new().unwrap();
{
let file = File::create(zip_file.path()).unwrap();
let mut zip = ZipWriter::new(file);
zip.start_file("sparse_test.bin", SimpleFileOptions::default())
.unwrap();
let mut payload = Vec::with_capacity(1024 * 1024);
payload.extend_from_slice(&[0xAA; 256 * 1024]);
payload.extend_from_slice(&[0x00; 512 * 1024]); payload.extend_from_slice(&[0xBB; 256 * 1024]);
zip.write_all(&payload).unwrap();
zip.finish().unwrap();
}
let options = ExtractionOptions {
destination: dest_dir.path().to_path_buf(),
collision_policy: CollisionPolicy::Fail,
enable_sparse: true,
max_compression_ratio: 2000.0,
reclaim_archive: false,
state_dir: None,
verbose: false,
quiet: false,
..Default::default()
};
let summary = ExtractionEngine::extract(zip_file.path(), &options).unwrap();
assert!(summary.sparse_bytes_saved >= 512 * 1024);
let extracted_path = dest_dir.path().join("sparse_test.bin");
assert_eq!(fs::metadata(&extracted_path).unwrap().len(), 1024 * 1024);
let mut read_data = Vec::new();
File::open(&extracted_path)
.unwrap()
.read_to_end(&mut read_data)
.unwrap();
assert_eq!(&read_data[..256 * 1024], &[0xAA; 256 * 1024]);
assert_eq!(&read_data[256 * 1024..768 * 1024], &[0x00; 512 * 1024]);
assert_eq!(&read_data[768 * 1024..], &[0xBB; 256 * 1024]);
}
#[test]
fn test_zip_slip_rejection_at_extraction() {
let dest_dir = tempdir().unwrap();
let archive_path = std::path::Path::new("tests/test_data/zip_slip.zip");
let options = ExtractionOptions {
destination: dest_dir.path().to_path_buf(),
collision_policy: CollisionPolicy::Fail,
enable_sparse: true,
max_compression_ratio: 100.0,
reclaim_archive: false,
state_dir: None,
verbose: false,
quiet: false,
..Default::default()
};
let res = ExtractionEngine::extract(archive_path, &options);
assert!(res.is_err());
let err = res.unwrap_err();
assert!(matches!(err, ExtractionError::Security { .. }));
assert!(!dest_dir
.path()
.parent()
.unwrap()
.join("etc/passwd")
.exists());
}
#[test]
fn test_compression_bomb_rejection() {
let dest_dir = tempdir().unwrap();
let zip_file = NamedTempFile::new().unwrap();
{
let file = File::create(zip_file.path()).unwrap();
let mut zip = ZipWriter::new(file);
zip.start_file(
"bomb.txt",
SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated),
)
.unwrap();
zip.write_all(&vec![0x00; 15 * 1024 * 1024]).unwrap();
zip.finish().unwrap();
}
let options = ExtractionOptions {
destination: dest_dir.path().to_path_buf(),
collision_policy: CollisionPolicy::Fail,
enable_sparse: true,
max_compression_ratio: 10.0, reclaim_archive: false,
state_dir: None,
verbose: false,
quiet: false,
..Default::default()
};
let res = ExtractionEngine::extract(zip_file.path(), &options);
assert!(res.is_err());
assert!(matches!(
res.unwrap_err(),
ExtractionError::SuspiciousCompressionRatio { .. }
));
}
#[test]
fn test_collision_policies_at_engine_level() {
let dest_dir = tempdir().unwrap();
let zip_file = NamedTempFile::new().unwrap();
{
let file = File::create(zip_file.path()).unwrap();
let mut zip = ZipWriter::new(file);
zip.start_file("test.txt", SimpleFileOptions::default())
.unwrap();
zip.write_all(b"new content").unwrap();
zip.finish().unwrap();
}
let target_file = dest_dir.path().join("test.txt");
fs::write(&target_file, b"existing content").unwrap();
let mut options = ExtractionOptions {
destination: dest_dir.path().to_path_buf(),
collision_policy: CollisionPolicy::Fail,
enable_sparse: true,
max_compression_ratio: 100.0,
reclaim_archive: false,
state_dir: None,
verbose: false,
quiet: false,
..Default::default()
};
assert!(ExtractionEngine::extract(zip_file.path(), &options).is_err());
options.collision_policy = CollisionPolicy::Skip;
let summary = ExtractionEngine::extract(zip_file.path(), &options).unwrap();
assert_eq!(summary.skipped_files, 1);
assert_eq!(fs::read(&target_file).unwrap(), b"existing content");
options.collision_policy = CollisionPolicy::Overwrite;
let summary = ExtractionEngine::extract(zip_file.path(), &options).unwrap();
assert_eq!(summary.extracted_files, 1);
assert_eq!(fs::read(&target_file).unwrap(), b"new content");
options.collision_policy = CollisionPolicy::Rename;
let summary = ExtractionEngine::extract(zip_file.path(), &options).unwrap();
assert_eq!(summary.extracted_files, 1);
let renamed = dest_dir.path().join("test.1.txt");
assert!(renamed.exists());
assert_eq!(fs::read(&renamed).unwrap(), b"new content");
}
#[test]
fn test_unsupported_compression_method_error() {
let dest_dir = tempdir().unwrap();
let zip_file = NamedTempFile::new().unwrap();
{
let file = File::create(zip_file.path()).unwrap();
let mut zip = ZipWriter::new(file);
zip.start_file(
"test.txt",
SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored),
)
.unwrap();
zip.write_all(b"sample data").unwrap();
zip.finish().unwrap();
}
let mut bytes = fs::read(zip_file.path()).unwrap();
bytes[8] = 14; bytes[9] = 0;
for i in 0..bytes.len() - 4 {
if &bytes[i..i + 4] == b"PK\x01\x02" {
bytes[i + 10] = 14;
bytes[i + 11] = 0;
}
}
fs::write(zip_file.path(), &bytes).unwrap();
let options = ExtractionOptions {
destination: dest_dir.path().to_path_buf(),
collision_policy: CollisionPolicy::Fail,
enable_sparse: true,
max_compression_ratio: 100.0,
reclaim_archive: false,
state_dir: None,
verbose: false,
quiet: false,
..Default::default()
};
let res = ExtractionEngine::extract(zip_file.path(), &options);
assert!(res.is_err());
assert!(matches!(
res.unwrap_err(),
ExtractionError::UnsupportedCompression(unpackr::archive::CompressionMethod::Unsupported(
14
))
));
}
#[test]
fn test_crc32_mismatch_error() {
let dest_dir = tempdir().unwrap();
let zip_file = NamedTempFile::new().unwrap();
{
let file = File::create(zip_file.path()).unwrap();
let mut zip = ZipWriter::new(file);
zip.start_file(
"corrupt_crc.txt",
SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored),
)
.unwrap();
zip.write_all(b"authentic payload").unwrap();
zip.finish().unwrap();
}
let mut bytes = fs::read(zip_file.path()).unwrap();
for i in 0..bytes.len() - 9 {
if &bytes[i..i + 9] == b"authentic" {
bytes[i] = b'X';
break;
}
}
fs::write(zip_file.path(), &bytes).unwrap();
let options = ExtractionOptions {
destination: dest_dir.path().to_path_buf(),
collision_policy: CollisionPolicy::Fail,
enable_sparse: true,
max_compression_ratio: 100.0,
reclaim_archive: false,
state_dir: None,
verbose: false,
quiet: false,
..Default::default()
};
let res = ExtractionEngine::extract(zip_file.path(), &options);
assert!(res.is_err());
assert!(matches!(
res.unwrap_err(),
ExtractionError::CrcMismatch { .. }
));
}