#![cfg(feature = "async")]
use std::io::Cursor;
use zesven::format::property_id;
use zesven::{
ArchivePath, AsyncArchive, AsyncExtractOptions, AsyncProgressCallback, AsyncWriter,
CancellationToken, WriteOptions,
};
fn make_empty_archive() -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&[0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]);
data.extend_from_slice(&[0x00, 0x04]);
let start_header_crc_pos = data.len();
data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
let header_data = vec![property_id::HEADER, property_id::END];
let header_size = header_data.len() as u64;
data.extend_from_slice(&header_size.to_le_bytes());
let header_crc = crc32fast::hash(&header_data);
data.extend_from_slice(&header_crc.to_le_bytes());
let start_header_crc = crc32fast::hash(&data[12..32]);
data[start_header_crc_pos..start_header_crc_pos + 4]
.copy_from_slice(&start_header_crc.to_le_bytes());
data.extend_from_slice(&header_data);
data
}
#[tokio::test]
async fn test_async_archive_open_empty() {
let data = make_empty_archive();
let cursor = Cursor::new(data);
let archive = AsyncArchive::open(cursor).await.unwrap();
assert!(archive.is_empty());
assert_eq!(archive.len(), 0);
}
#[tokio::test]
async fn test_async_archive_info() {
let data = make_empty_archive();
let cursor = Cursor::new(data);
let archive = AsyncArchive::open(cursor).await.unwrap();
let info = archive.info();
assert_eq!(info.entry_count, 0);
assert!(!info.is_solid);
assert!(!info.has_encrypted_entries);
}
#[tokio::test]
async fn test_async_archive_entries() {
let data = make_empty_archive();
let cursor = Cursor::new(data);
let archive = AsyncArchive::open(cursor).await.unwrap();
assert!(archive.entries().is_empty());
assert!(archive.entry("nonexistent").is_none());
}
#[tokio::test]
async fn test_async_archive_open_invalid_signature() {
let data: &[u8] = &[0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE];
let cursor = Cursor::new(data);
match AsyncArchive::open(cursor).await {
Err(zesven::Error::InvalidFormat(_)) => {} Err(e) => panic!(
"Expected InvalidFormat error for invalid signature, got: {:?}",
e
),
Ok(_) => panic!("Should fail for invalid signature"),
}
}
#[tokio::test]
async fn test_async_archive_extract_truncated_data() {
let buffer = Cursor::new(Vec::new());
let mut writer = AsyncWriter::create(buffer).await.unwrap();
let content = b"This content will be truncated in the test archive";
writer
.add_bytes(ArchivePath::new("test.txt").unwrap(), content)
.await
.unwrap();
let (_, cursor) = writer.finish_into_inner().await.unwrap();
let mut archive_bytes = cursor.into_inner();
if archive_bytes.len() > 30 {
archive_bytes.truncate(archive_bytes.len() - 20);
}
let read_cursor = Cursor::new(archive_bytes);
match AsyncArchive::open(read_cursor).await {
Ok(mut archive) => {
let temp_dir = tempfile::tempdir().unwrap();
let result = archive
.extract(temp_dir.path(), (), &AsyncExtractOptions::default())
.await;
assert!(
result.is_err(),
"Extraction of truncated archive should fail"
);
}
Err(_) => {
}
}
}
#[tokio::test]
async fn test_async_writer_drop_without_finish() {
let buffer = Cursor::new(Vec::new());
let mut writer = AsyncWriter::create(buffer).await.unwrap();
writer
.add_bytes(ArchivePath::new("file.txt").unwrap(), b"content")
.await
.unwrap();
drop(writer);
}
#[tokio::test]
async fn test_async_writer_create() {
let buffer = Cursor::new(Vec::new());
let _writer = AsyncWriter::create(buffer).await.unwrap();
}
#[tokio::test]
async fn test_async_writer_empty_archive() {
let buffer = Cursor::new(Vec::new());
let writer = AsyncWriter::create(buffer).await.unwrap();
let result = writer.finish().await.unwrap();
assert_eq!(result.entries_written, 0);
assert_eq!(result.directories_written, 0);
}
#[tokio::test]
async fn test_async_writer_add_bytes() {
let buffer = Cursor::new(Vec::new());
let mut writer = AsyncWriter::create(buffer).await.unwrap();
let path = ArchivePath::new("test.txt").unwrap();
writer
.add_bytes(path, b"Hello, async world!")
.await
.unwrap();
let result = writer.finish().await.unwrap();
assert_eq!(result.entries_written, 1);
assert_eq!(result.total_size, 19);
}
#[tokio::test]
async fn test_async_writer_add_multiple_entries() {
let buffer = Cursor::new(Vec::new());
let mut writer = AsyncWriter::create(buffer).await.unwrap();
writer
.add_bytes(ArchivePath::new("file1.txt").unwrap(), b"Content 1")
.await
.unwrap();
writer
.add_bytes(ArchivePath::new("file2.txt").unwrap(), b"Content 2")
.await
.unwrap();
writer
.add_bytes(ArchivePath::new("file3.txt").unwrap(), b"Content 3")
.await
.unwrap();
let result = writer.finish().await.unwrap();
assert_eq!(result.entries_written, 3);
}
#[tokio::test]
async fn test_async_writer_with_directory() {
let buffer = Cursor::new(Vec::new());
let mut writer = AsyncWriter::create(buffer).await.unwrap();
use zesven::write::EntryMeta;
let dir_path = ArchivePath::new("mydir").unwrap();
writer
.add_directory(dir_path, EntryMeta::directory())
.await
.unwrap();
let result = writer.finish().await.unwrap();
assert_eq!(result.entries_written, 0);
assert_eq!(result.directories_written, 1);
}
#[tokio::test]
async fn test_async_writer_with_options() {
use zesven::codec::CodecMethod;
let buffer = Cursor::new(Vec::new());
let writer = AsyncWriter::create(buffer).await.unwrap().options(
WriteOptions::new()
.method(CodecMethod::Copy)
.level(0)
.unwrap(),
);
let result = writer.finish().await.unwrap();
assert_eq!(result.entries_written, 0);
}
#[tokio::test]
async fn test_async_extract_options_default() {
let options = AsyncExtractOptions::default();
assert!(!options.is_cancelled());
}
#[tokio::test]
async fn test_async_extract_options_cancellation() {
let token = CancellationToken::new();
let options = AsyncExtractOptions::new().cancel_token(token.clone());
assert!(!options.is_cancelled());
token.cancel();
assert!(options.is_cancelled());
}
#[tokio::test]
async fn test_async_extract_options_builder() {
use std::num::NonZeroUsize;
use zesven::read::{OverwritePolicy, PathSafety, Threads};
let options = AsyncExtractOptions::new()
.overwrite(OverwritePolicy::Skip)
.path_safety(PathSafety::Relaxed)
.threads(Threads::Count(NonZeroUsize::new(4).unwrap()));
assert_eq!(options.overwrite, OverwritePolicy::Skip);
assert_eq!(options.path_safety, PathSafety::Relaxed);
assert_eq!(
options.threads,
Threads::Count(NonZeroUsize::new(4).unwrap())
);
}
#[tokio::test]
async fn test_async_round_trip_single_file() {
let buffer = Cursor::new(Vec::new());
let mut writer = AsyncWriter::create(buffer).await.unwrap();
let content = b"Hello, async round-trip test!";
writer
.add_bytes(ArchivePath::new("test.txt").unwrap(), content)
.await
.unwrap();
let (result, cursor) = writer.finish_into_inner().await.unwrap();
assert!(result.total_size > 0);
assert_eq!(result.entries_written, 1);
let archive_bytes = cursor.into_inner();
assert!(!archive_bytes.is_empty());
let read_cursor = Cursor::new(archive_bytes);
let mut archive = AsyncArchive::open(read_cursor).await.unwrap();
let entries = archive.entries();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].path.as_str(), "test.txt");
let temp_dir = tempfile::tempdir().unwrap();
let _ = archive
.extract(temp_dir.path(), (), &AsyncExtractOptions::default())
.await
.unwrap();
let extracted_content = tokio::fs::read(temp_dir.path().join("test.txt"))
.await
.unwrap();
assert_eq!(extracted_content, content);
}
#[tokio::test]
async fn test_async_round_trip_multiple_files() {
let buffer = Cursor::new(Vec::new());
let mut writer = AsyncWriter::create(buffer).await.unwrap();
let files = [
("file1.txt", b"First file content".as_slice()),
("file2.txt", b"Second file content".as_slice()),
("subdir/file3.txt", b"Third file in subdirectory".as_slice()),
];
for (path, content) in &files {
writer
.add_bytes(ArchivePath::new(path).unwrap(), content)
.await
.unwrap();
}
let (result, cursor) = writer.finish_into_inner().await.unwrap();
assert_eq!(result.entries_written, 3);
let archive_bytes = cursor.into_inner();
let read_cursor = Cursor::new(archive_bytes);
let mut archive = AsyncArchive::open(read_cursor).await.unwrap();
let entries = archive.entries();
assert_eq!(entries.len(), 3);
let temp_dir = tempfile::tempdir().unwrap();
let _ = archive
.extract(temp_dir.path(), (), &AsyncExtractOptions::default())
.await
.unwrap();
for (path, expected_content) in &files {
let file_path = temp_dir.path().join(path);
let actual_content = tokio::fs::read(&file_path).await.unwrap();
assert_eq!(
actual_content.as_slice(),
*expected_content,
"Content mismatch for {}",
path
);
}
}
#[tokio::test]
async fn test_cancellation_before_extract() {
let buffer = Cursor::new(Vec::new());
let mut writer = AsyncWriter::create(buffer).await.unwrap();
for i in 0..10 {
writer
.add_bytes(
ArchivePath::new(&format!("file{}.txt", i)).unwrap(),
format!("Content for file {}", i).as_bytes(),
)
.await
.unwrap();
}
let (_, cursor) = writer.finish_into_inner().await.unwrap();
let archive_bytes = cursor.into_inner();
let read_cursor = Cursor::new(archive_bytes);
let mut archive = AsyncArchive::open(read_cursor).await.unwrap();
let token = CancellationToken::new();
token.cancel();
let options = AsyncExtractOptions::new().cancel_token(token);
let temp_dir = tempfile::tempdir().unwrap();
let result = archive.extract(temp_dir.path(), (), &options).await;
assert!(
matches!(result, Err(zesven::Error::Cancelled)),
"Expected Cancelled error with pre-cancelled token, got: {:?}",
result
);
}
#[tokio::test]
async fn test_extract_with_cancellation_precancelled() {
let buffer = Cursor::new(Vec::new());
let mut writer = AsyncWriter::create(buffer).await.unwrap();
for i in 0..10 {
writer
.add_bytes(
ArchivePath::new(&format!("file{}.txt", i)).unwrap(),
format!("Content for file {}", i).as_bytes(),
)
.await
.unwrap();
}
let (_, cursor) = writer.finish_into_inner().await.unwrap();
let archive_bytes = cursor.into_inner();
let read_cursor = Cursor::new(archive_bytes);
let mut archive = AsyncArchive::open(read_cursor).await.unwrap();
let token = CancellationToken::new();
token.cancel();
let options = AsyncExtractOptions::default();
let temp_dir = tempfile::tempdir().unwrap();
let result = archive
.extract_with_cancellation(temp_dir.path(), (), &options, token)
.await;
assert!(
matches!(result, Err(zesven::Error::Cancelled)),
"Expected Cancelled error with pre-cancelled token, got: {:?}",
result
);
}
#[tokio::test]
async fn test_channel_progress_reporter() {
use std::sync::Arc;
use zesven::{ChannelProgressReporter, ProgressEvent};
let (reporter, mut rx) = ChannelProgressReporter::new(10);
let reporter = Arc::new(reporter);
reporter.on_entry_start("test.txt", 100).await;
reporter.on_progress(50, 100).await;
reporter.on_entry_complete("test.txt", true).await;
let event1 = rx.recv().await.unwrap();
assert!(matches!(
event1,
ProgressEvent::EntryStart {
name,
size: 100
} if name == "test.txt"
));
let event2 = rx.recv().await.unwrap();
assert!(matches!(
event2,
ProgressEvent::Progress {
bytes_extracted: 50,
total_bytes: 100
}
));
let event3 = rx.recv().await.unwrap();
assert!(matches!(
event3,
ProgressEvent::EntryComplete {
name,
success: true
} if name == "test.txt"
));
}
#[tokio::test]
async fn test_async_extraction_with_progress_callback() {
use std::sync::Arc;
use zesven::{ChannelProgressReporter, ProgressEvent};
let buffer = Cursor::new(Vec::new());
let mut writer = AsyncWriter::create(buffer).await.unwrap();
let files = [
("file1.txt", b"Content for file one".as_slice()),
("file2.txt", b"Content for file two".as_slice()),
("subdir/file3.txt", b"Content in subdirectory".as_slice()),
];
for (path, content) in &files {
writer
.add_bytes(ArchivePath::new(path).unwrap(), content)
.await
.unwrap();
}
let (_, cursor) = writer.finish_into_inner().await.unwrap();
let archive_bytes = cursor.into_inner();
let (reporter, mut rx) = ChannelProgressReporter::new(100);
let reporter = Arc::new(reporter);
let read_cursor = Cursor::new(archive_bytes);
let mut archive = AsyncArchive::open(read_cursor).await.unwrap();
let options = AsyncExtractOptions::new().progress(reporter);
let temp_dir = tempfile::tempdir().unwrap();
let result = archive
.extract(temp_dir.path(), (), &options)
.await
.unwrap();
assert_eq!(result.entries_extracted, 3);
let mut events = Vec::new();
while let Ok(event) = rx.try_recv() {
events.push(event);
}
let start_events: Vec<_> = events
.iter()
.filter(|e| matches!(e, ProgressEvent::EntryStart { .. }))
.collect();
let complete_events: Vec<_> = events
.iter()
.filter(|e| matches!(e, ProgressEvent::EntryComplete { .. }))
.collect();
assert_eq!(
start_events.len(),
3,
"Expected 3 EntryStart events, got {}",
start_events.len()
);
assert_eq!(
complete_events.len(),
3,
"Expected 3 EntryComplete events, got {}",
complete_events.len()
);
for event in &complete_events {
if let ProgressEvent::EntryComplete { success, .. } = event {
assert!(success, "All entries should complete successfully");
}
}
let reported_names: Vec<_> = start_events
.iter()
.filter_map(|e| {
if let ProgressEvent::EntryStart { name, .. } = e {
Some(name.as_str())
} else {
None
}
})
.collect();
for (expected_path, _) in &files {
assert!(
reported_names.contains(expected_path),
"Expected '{}' in progress events, got {:?}",
expected_path,
reported_names
);
}
}
#[cfg(feature = "aes")]
mod password_tests {
use zesven::Password;
use zesven::async_password::{
AsyncPassword, AsyncPasswordProvider, InteractivePasswordProvider,
};
#[tokio::test]
async fn test_async_password_with_value() {
let provider = AsyncPassword::new("test_password");
let password = provider.get_password().await;
assert!(password.is_some());
assert_eq!(password.unwrap().as_str(), "test_password");
}
#[tokio::test]
async fn test_async_password_none() {
let provider = AsyncPassword::none();
let password = provider.get_password().await;
assert!(password.is_none());
}
#[tokio::test]
async fn test_interactive_password_provider() {
let (tx, provider) = InteractivePasswordProvider::new();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
tx.send(Some(Password::new("interactive_password"))).ok();
});
let password = provider.get_password().await;
assert!(password.is_some());
assert_eq!(password.unwrap().as_str(), "interactive_password");
}
#[tokio::test]
async fn test_interactive_password_provider_cancelled() {
let (tx, provider) = InteractivePasswordProvider::new();
drop(tx);
let password = provider.get_password().await;
assert!(password.is_none());
}
}
#[tokio::test]
async fn test_concurrent_writes() {
let handles: Vec<_> = (0..4)
.map(|i| {
tokio::spawn(async move {
let buffer = Cursor::new(Vec::new());
let mut writer = AsyncWriter::create(buffer).await.unwrap();
writer
.add_bytes(
ArchivePath::new(&format!("file{}.txt", i)).unwrap(),
format!("Content from task {}", i).as_bytes(),
)
.await
.unwrap();
writer.finish().await.unwrap()
})
})
.collect();
for handle in handles {
let result = handle.await.unwrap();
assert_eq!(result.entries_written, 1);
}
}
#[tokio::test]
async fn test_concurrent_reads() {
let archives: Vec<_> = (0..4).map(|_| make_empty_archive()).collect();
let handles: Vec<_> = archives
.into_iter()
.map(|data| {
tokio::spawn(async move {
let cursor = Cursor::new(data);
let archive = AsyncArchive::open(cursor).await.unwrap();
archive.len()
})
})
.collect();
for handle in handles {
let count = handle.await.unwrap();
assert_eq!(count, 0); }
}
#[tokio::test]
async fn test_async_operations_dont_block() {
let result = tokio::time::timeout(std::time::Duration::from_secs(5), async {
let data = make_empty_archive();
let cursor = Cursor::new(data);
let _archive = AsyncArchive::open(cursor).await.unwrap();
let buffer = Cursor::new(Vec::new());
let mut writer = AsyncWriter::create(buffer).await.unwrap();
writer
.add_bytes(ArchivePath::new("test.txt").unwrap(), b"test content")
.await
.unwrap();
let _ = writer.finish().await.unwrap();
true
})
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_cancellation_during_larger_extraction() {
let buffer = Cursor::new(Vec::new());
let mut writer = AsyncWriter::create(buffer).await.unwrap();
for i in 0..10 {
let content = format!(
"File {} content with some padding to make it larger: {}",
i,
"x".repeat(1000)
);
writer
.add_bytes(
ArchivePath::new(&format!("file{:02}.txt", i)).unwrap(),
content.as_bytes(),
)
.await
.unwrap();
}
let (_, cursor) = writer.finish_into_inner().await.unwrap();
let archive_bytes = cursor.into_inner();
let read_cursor = Cursor::new(archive_bytes.clone());
let mut archive = AsyncArchive::open(read_cursor).await.unwrap();
let token = CancellationToken::new();
let token_clone = token.clone();
let options = AsyncExtractOptions::new().cancel_token(token_clone);
let temp_dir = tempfile::tempdir().unwrap();
let cancel_handle = tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_micros(100)).await;
token.cancel();
});
let result = archive.extract(temp_dir.path(), (), &options).await;
let _ = cancel_handle.await;
match result {
Ok(extract_result) => {
assert!(extract_result.entries_extracted > 0 || extract_result.entries_failed == 0);
}
Err(zesven::Error::Cancelled) => {
}
Err(e) => {
panic!("Expected Ok or Cancelled, got unexpected error: {:?}", e);
}
}
}