#![cfg(feature = "lzma2")]
mod common;
use std::io::Cursor;
use zesven::read::Archive;
use zesven::streaming::{StreamingArchive, StreamingConfig};
use zesven::{ArchivePath, WriteOptions, Writer};
use common::create_archive;
fn create_solid_archive(entries: &[(&str, &[u8])]) -> zesven::Result<Vec<u8>> {
let mut archive_bytes = Vec::new();
{
let cursor = Cursor::new(&mut archive_bytes);
let options = WriteOptions::new().solid();
let mut writer = Writer::create(cursor)?.options(options);
for (name, data) in entries {
let path = ArchivePath::new(name)?;
writer.add_bytes(path, data)?;
}
let _ = writer.finish()?;
}
Ok(archive_bytes)
}
#[test]
fn test_streaming_archive_open_and_list_entries() {
let entries = [
("file1.txt", b"Hello, World!" as &[u8]),
("file2.txt", b"Goodbye, World!"),
("dir/file3.txt", b"Nested file"),
];
let archive_bytes = create_archive(&entries).unwrap();
let cursor = Cursor::new(archive_bytes);
let archive = StreamingArchive::open(cursor, "").unwrap();
assert_eq!(archive.len(), 3);
assert!(!archive.is_empty());
assert!(archive.entry("file1.txt").is_some());
assert!(archive.entry("file2.txt").is_some());
assert!(archive.entry("dir/file3.txt").is_some());
assert!(archive.entry("nonexistent.txt").is_none());
}
#[test]
fn test_streaming_archive_total_size() {
let entries = [
("a.txt", b"12345" as &[u8]), ("b.txt", b"1234567890"), ("c.txt", b"123456789012345"), ];
let archive_bytes = create_archive(&entries).unwrap();
let cursor = Cursor::new(archive_bytes);
let archive = StreamingArchive::open(cursor, "").unwrap();
assert_eq!(archive.total_size(), 30);
}
#[test]
fn test_streaming_archive_is_solid_detection() {
let entries = [("file.txt", b"content" as &[u8])];
let non_solid_bytes = create_archive(&entries).unwrap();
let non_solid = StreamingArchive::open(Cursor::new(non_solid_bytes), "").unwrap();
assert!(!non_solid.is_solid());
let entries = [
("a.txt", b"content a" as &[u8]),
("b.txt", b"content b"),
("c.txt", b"content c"),
];
let solid_bytes = create_solid_archive(&entries).unwrap();
let solid = StreamingArchive::open(Cursor::new(solid_bytes), "").unwrap();
assert!(solid.is_solid());
}
#[test]
fn test_streaming_config_presets() {
let low = StreamingConfig::low_memory();
let high = StreamingConfig::high_performance();
let default = StreamingConfig::default();
assert!(low.max_memory_buffer < high.max_memory_buffer);
assert!(low.validate().is_ok());
assert!(high.validate().is_ok());
assert!(default.validate().is_ok());
}
#[test]
fn test_streaming_config_custom() {
let config = StreamingConfig::new()
.max_memory_buffer(16 * 1024 * 1024) .read_buffer_size(64 * 1024) .verify_crc(true);
assert!(config.validate().is_ok());
assert_eq!(config.max_memory_buffer, 16 * 1024 * 1024);
}
#[test]
fn test_streaming_config_invalid() {
let config = StreamingConfig::new().max_memory_buffer(0);
let result = config.validate();
assert!(
result.is_err(),
"max_memory_buffer=0 should fail validation"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("max_memory_buffer"),
"Error should mention max_memory_buffer: {}",
err_msg
);
let config = StreamingConfig::new().read_buffer_size(0);
let result = config.validate();
assert!(result.is_err(), "read_buffer_size=0 should fail validation");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("read_buffer_size"),
"Error should mention read_buffer_size: {}",
err_msg
);
let config = StreamingConfig::new()
.max_memory_buffer(1024)
.read_buffer_size(2048);
let result = config.validate();
assert!(
result.is_err(),
"read_buffer_size > max_memory_buffer should fail validation"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("read_buffer_size") && err_msg.contains("max_memory_buffer"),
"Error should mention both fields: {}",
err_msg
);
}
#[test]
fn test_streaming_entry_iterator_early_drop() {
let entries = [
("file1.txt", b"Content 1" as &[u8]),
("file2.txt", b"Content 2"),
("file3.txt", b"Content 3"),
];
let archive_bytes = create_archive(&entries).unwrap();
let cursor = Cursor::new(archive_bytes);
let mut archive = StreamingArchive::open(cursor, "").unwrap();
{
let mut iter = archive.entries().unwrap();
let first_entry = iter.next().unwrap().unwrap();
assert_eq!(first_entry.name(), "file1.txt");
}
let mut iter2 = archive.entries().unwrap();
let mut count = 0;
for entry_result in iter2.by_ref() {
let entry = entry_result.unwrap();
assert!(!entry.is_directory());
count += 1;
}
assert_eq!(
count, 3,
"Should be able to iterate all entries after early drop"
);
}
#[test]
fn test_streaming_entries_iteration() {
let entries = [
("file1.txt", b"Content 1" as &[u8]),
("file2.txt", b"Content 2"),
("file3.txt", b"Content 3"),
];
let archive_bytes = create_archive(&entries).unwrap();
let cursor = Cursor::new(archive_bytes);
let mut archive = StreamingArchive::open(cursor, "").unwrap();
let iter = archive.entries().unwrap();
let mut count = 0;
for entry_result in iter {
let entry = entry_result.unwrap();
assert!(!entry.is_directory());
count += 1;
}
assert_eq!(count, 3);
}
#[test]
fn test_streaming_entry_extraction() {
let content = b"This is test content for streaming extraction";
let entries = [("test.txt", content.as_slice())];
let archive_bytes = create_archive(&entries).unwrap();
let cursor = Cursor::new(archive_bytes);
let mut archive = StreamingArchive::open(cursor, "").unwrap();
let mut iter = archive.entries().unwrap();
if let Some(entry_result) = iter.next() {
let entry = entry_result.unwrap();
assert_eq!(entry.name(), "test.txt");
assert_eq!(entry.size(), content.len() as u64);
let mut extracted = Vec::new();
iter.extract_current_to(&mut extracted).unwrap();
assert_eq!(extracted, content);
} else {
panic!("Expected at least one entry");
}
}
#[test]
fn test_streaming_extract_all() {
let entries = [
("file1.txt", b"Content 1" as &[u8]),
("subdir/file2.txt", b"Content 2"),
];
let archive_bytes = create_archive(&entries).unwrap();
let cursor = Cursor::new(archive_bytes);
let mut archive = StreamingArchive::open(cursor, "").unwrap();
let temp_dir = tempfile::tempdir().unwrap();
let result = archive
.extract_all(temp_dir.path(), &Default::default())
.unwrap();
assert!(result.is_success());
assert_eq!(result.entries_extracted, 2);
assert_eq!(result.entries_failed, 0);
assert!(temp_dir.path().join("file1.txt").exists());
assert!(temp_dir.path().join("subdir/file2.txt").exists());
let content1 = std::fs::read(temp_dir.path().join("file1.txt")).unwrap();
assert_eq!(content1, b"Content 1");
}
#[test]
fn test_streaming_extract_to_sinks() {
let entries = [
("include.txt", b"Include this" as &[u8]),
("exclude.txt", b"Exclude this"),
("also_include.txt", b"Also include"),
];
let archive_bytes = create_archive(&entries).unwrap();
let cursor = Cursor::new(archive_bytes);
let mut archive = StreamingArchive::open(cursor, "").unwrap();
let result = archive
.extract_all_to_sinks(|entry| {
if entry.path.as_str().contains("include") {
Some(Vec::new())
} else {
None }
})
.unwrap();
assert_eq!(result.entries_extracted, 2);
assert_eq!(result.entries_skipped, 1);
assert!(result.is_success());
}
#[test]
fn test_streaming_early_drop_is_safe() {
let entries = [
("file1.txt", b"Content 1" as &[u8]),
("file2.txt", b"Content 2"),
("file3.txt", b"Content 3"),
("file4.txt", b"Content 4"),
];
let archive_bytes = create_archive(&entries).unwrap();
{
let cursor = Cursor::new(&archive_bytes);
let _archive = StreamingArchive::open(cursor, "").unwrap();
}
{
let cursor = Cursor::new(&archive_bytes);
let mut archive = StreamingArchive::open(cursor, "").unwrap();
let temp_dir = tempfile::tempdir().unwrap();
let mut extracted_count = 0;
let result = archive.extract_all_to_sinks(|_entry| {
extracted_count += 1;
if extracted_count <= 1 {
Some(Vec::new()) } else {
None }
});
assert!(result.is_ok(), "Partial extraction should succeed");
let extract_result = result.unwrap();
assert_eq!(extract_result.entries_extracted, 1);
assert_eq!(extract_result.entries_skipped, 3);
drop(temp_dir);
}
for _ in 0..5 {
let cursor = Cursor::new(&archive_bytes);
let _archive = StreamingArchive::open(cursor, "").unwrap();
}
}
#[test]
fn test_streaming_selective_extraction_no_resource_leak() {
let entries: Vec<(String, Vec<u8>)> = (0..20)
.map(|i| {
(
format!("file{:02}.txt", i),
format!("Content {}", i).into_bytes(),
)
})
.collect();
let entry_refs: Vec<(&str, &[u8])> = entries
.iter()
.map(|(s, d)| (s.as_str(), d.as_slice()))
.collect();
let archive_bytes = create_archive(&entry_refs).unwrap();
let cursor = Cursor::new(archive_bytes);
let mut archive = StreamingArchive::open(cursor, "").unwrap();
let result = archive
.extract_all_to_sinks(|entry| {
let index: usize = entry
.path
.as_str()
.strip_prefix("file")
.and_then(|s| s.strip_suffix(".txt"))
.and_then(|s| s.parse().ok())
.unwrap_or(0);
if index % 5 == 0 {
Some(Vec::new())
} else {
None
}
})
.unwrap();
assert_eq!(result.entries_extracted, 4);
assert_eq!(result.entries_skipped, 16);
assert!(result.is_success());
}
#[test]
fn test_streaming_solid_archive_sequential_access() {
let entries = [
("a.txt", b"Content A" as &[u8]),
("b.txt", b"Content B"),
("c.txt", b"Content C"),
];
let archive_bytes = create_solid_archive(&entries).unwrap();
let cursor = Cursor::new(archive_bytes);
let mut archive = StreamingArchive::open(cursor, "").unwrap();
assert!(archive.is_solid());
let temp_dir = tempfile::tempdir().unwrap();
let result = archive
.extract_all(temp_dir.path(), &Default::default())
.unwrap();
assert!(result.is_success());
assert_eq!(result.entries_extracted, 3);
assert_eq!(
std::fs::read(temp_dir.path().join("a.txt")).unwrap(),
b"Content A"
);
assert_eq!(
std::fs::read(temp_dir.path().join("b.txt")).unwrap(),
b"Content B"
);
assert_eq!(
std::fs::read(temp_dir.path().join("c.txt")).unwrap(),
b"Content C"
);
}
#[test]
fn test_streaming_non_solid_archive_supports_parallel() {
let entries = [("a.txt", b"Content A" as &[u8]), ("b.txt", b"Content B")];
let archive_bytes = create_archive(&entries).unwrap();
let cursor = Cursor::new(archive_bytes);
let archive = StreamingArchive::open(cursor, "").unwrap();
assert!(
!archive.is_solid(),
"Test setup: should be non-solid archive"
);
assert!(
archive.supports_parallel_extraction(),
"Non-solid archives should support parallel extraction"
);
}
#[test]
fn test_streaming_memory_tracker() {
let entries = [("file.txt", b"Small content" as &[u8])];
let archive_bytes = create_archive(&entries).unwrap();
let cursor = Cursor::new(archive_bytes);
let config = StreamingConfig::new().max_memory_buffer(1024 * 1024);
let archive = StreamingArchive::open_with_config(cursor, "", config).unwrap();
let tracker = archive.memory_tracker();
assert!(tracker.available() > 0);
}
#[test]
fn test_solid_archive_parallel_extraction_returns_error() {
use zesven::Error;
use zesven::streaming::ParallelExtractionOptions;
let entries = [
("a.txt", b"Content A" as &[u8]),
("b.txt", b"Content B"),
("c.txt", b"Content C"),
];
let archive_bytes = create_solid_archive(&entries).unwrap();
let cursor = Cursor::new(archive_bytes);
let mut archive = StreamingArchive::open(cursor, "").unwrap();
assert!(archive.is_solid(), "Test setup: should be a solid archive");
assert!(
!archive.supports_parallel_extraction(),
"Solid archives should not support parallel extraction"
);
let temp_dir = tempfile::tempdir().unwrap();
let options = ParallelExtractionOptions::default();
let result = archive.extract_all_parallel(temp_dir.path(), &options);
match result {
Err(Error::UnsupportedFeature { feature }) => {
assert!(
feature.to_lowercase().contains("solid")
|| feature.to_lowercase().contains("parallel"),
"Error message should mention solid or parallel: {}",
feature
);
}
Err(other) => {
panic!(
"Expected UnsupportedFeature error for solid archive parallel extraction, got: {:?}",
other
);
}
Ok(result) => {
panic!(
"Solid archive parallel extraction should fail, but succeeded with {} entries",
result.entries_extracted
);
}
}
}
#[test]
fn test_streaming_skipped_entries_tracking() {
let entries = [("valid.txt", b"content" as &[u8])];
let archive_bytes = create_archive(&entries).unwrap();
let cursor = Cursor::new(archive_bytes);
let archive = StreamingArchive::open(cursor, "").unwrap();
assert!(!archive.has_skipped_entries());
assert!(archive.skipped_entries().is_empty());
}
#[test]
fn test_streaming_produces_same_result_as_standard_api() {
let entries = [
("file1.txt", b"Hello streaming!" as &[u8]),
("dir/file2.txt", b"Nested content"),
];
let archive_bytes = create_archive(&entries).unwrap();
let standard_temp = tempfile::tempdir().unwrap();
{
let mut archive = Archive::open(Cursor::new(&archive_bytes)).unwrap();
let _ = archive
.extract(
standard_temp.path(),
(),
&zesven::read::ExtractOptions::default(),
)
.unwrap();
}
let streaming_temp = tempfile::tempdir().unwrap();
{
let mut archive = StreamingArchive::open(Cursor::new(&archive_bytes), "").unwrap();
archive
.extract_all(streaming_temp.path(), &Default::default())
.unwrap();
}
let standard_file1 = std::fs::read(standard_temp.path().join("file1.txt")).unwrap();
let streaming_file1 = std::fs::read(streaming_temp.path().join("file1.txt")).unwrap();
assert_eq!(standard_file1, streaming_file1);
let standard_file2 = std::fs::read(standard_temp.path().join("dir/file2.txt")).unwrap();
let streaming_file2 = std::fs::read(streaming_temp.path().join("dir/file2.txt")).unwrap();
assert_eq!(standard_file2, streaming_file2);
}
#[test]
fn test_streaming_open_truncated_archive() {
let entries = [("file.txt", b"content" as &[u8])];
let archive_bytes = create_archive(&entries).unwrap();
for truncate_at in [6, 10, 20, archive_bytes.len() / 2] {
if truncate_at >= archive_bytes.len() {
continue;
}
let truncated = &archive_bytes[..truncate_at];
let cursor = Cursor::new(truncated);
let result = StreamingArchive::open(cursor, "");
assert!(
result.is_err(),
"Opening archive truncated at {} bytes should fail",
truncate_at
);
}
}
#[test]
fn test_streaming_open_invalid_signature() {
use zesven::Error;
let random_data = vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x00, 0x11];
let cursor = Cursor::new(random_data);
let result = StreamingArchive::open(cursor, "");
assert!(result.is_err(), "Opening random bytes should fail");
let err = common::expect_err(result);
match err {
Error::InvalidFormat(msg) => {
assert!(!msg.is_empty(), "Error message should not be empty");
}
Error::Io(_) => {
}
Error::CorruptHeader { .. } => {
}
other => {
panic!("Unexpected error type for invalid signature: {:?}", other);
}
}
}
#[test]
fn test_streaming_open_empty_input() {
let empty: Vec<u8> = vec![];
let cursor = Cursor::new(empty);
let result = StreamingArchive::open(cursor, "");
assert!(result.is_err(), "Opening empty input should fail");
}
#[test]
fn test_streaming_memory_tracker_at_limit() {
let entries = [("file.txt", b"Small content" as &[u8])];
let archive_bytes = create_archive(&entries).unwrap();
let cursor = Cursor::new(archive_bytes);
let memory_limit = 512 * 1024; let config = StreamingConfig::new().max_memory_buffer(memory_limit);
let archive = StreamingArchive::open_with_config(cursor, "", config).unwrap();
let tracker = archive.memory_tracker();
assert_eq!(
tracker.limit(),
memory_limit,
"Memory tracker limit should match configured limit"
);
assert!(
tracker.available() <= tracker.limit(),
"Available should not exceed limit"
);
let current = tracker.limit() - tracker.available();
assert!(
current <= tracker.limit(),
"Current usage should not exceed limit"
);
}
#[test]
fn test_solid_archive_truncated_extraction_fails() {
let entries = [
("a.txt", b"Content A - first file in solid block" as &[u8]),
("b.txt", b"Content B - second file in solid block"),
("c.txt", b"Content C - third file in solid block"),
];
let archive_bytes = create_solid_archive(&entries).unwrap();
let truncate_at = archive_bytes.len().saturating_sub(20);
if truncate_at < 50 {
return; }
let truncated = &archive_bytes[..truncate_at];
let cursor = Cursor::new(truncated);
match StreamingArchive::open(cursor, "") {
Ok(mut archive) => {
let temp_dir = tempfile::tempdir().unwrap();
let result = archive.extract_all(temp_dir.path(), &Default::default());
match result {
Ok(extract_result) => {
let total = extract_result.entries_extracted + extract_result.entries_failed;
assert!(
extract_result.entries_failed > 0 || total < 3,
"Truncated solid archive should not extract all entries cleanly"
);
}
Err(_) => {
}
}
}
Err(_) => {
}
}
}
#[test]
fn test_solid_archive_bit_flip_detected() {
let entries = [
("file1.txt", b"Some content for file 1" as &[u8]),
("file2.txt", b"Some content for file 2"),
];
let mut archive_bytes = create_solid_archive(&entries).unwrap();
let corrupt_pos = archive_bytes.len() / 2;
archive_bytes[corrupt_pos] ^= 0x01;
let cursor = Cursor::new(&archive_bytes);
match StreamingArchive::open(cursor, "") {
Ok(mut archive) => {
let temp_dir = tempfile::tempdir().unwrap();
let result = archive.extract_all(temp_dir.path(), &Default::default());
match result {
Ok(_extract_result) => {
}
Err(_) => {
}
}
}
Err(_) => {
}
}
}
#[test]
fn test_solid_archive_crc_verification() {
use zesven::read::{Archive, SelectAll, TestOptions};
let entries = [
("test1.txt", b"Content for test file 1" as &[u8]),
("test2.txt", b"Content for test file 2"),
];
let archive_bytes = create_solid_archive(&entries).unwrap();
let cursor = Cursor::new(&archive_bytes);
let mut archive = Archive::open(cursor).expect("Should open valid archive");
let test_result = archive
.test(SelectAll, &TestOptions::default())
.expect("Test should complete");
assert_eq!(
test_result.entries_failed, 0,
"Valid solid archive should pass CRC verification"
);
assert_eq!(
test_result.entries_tested, 2,
"Both entries should be tested"
);
}