mod append;
pub(crate) mod options;
mod codecs;
mod compression;
mod encoding_utils;
mod entry_compression;
mod entry_input;
mod header_encode;
mod header_encryption;
mod metadata_encode;
mod writer_init;
pub use append::{AppendResult, ArchiveAppender};
pub use options::{EntryMeta, Lzma2Variant, SolidOptions, WriteFilter, WriteOptions, WriteResult};
use crate::ArchivePath;
#[cfg(feature = "zstd")]
const ZSTD_LEVEL_MAP: [i32; 10] = [1, 1, 2, 3, 5, 7, 9, 12, 15, 19];
#[cfg(feature = "brotli")]
const BROTLI_QUALITY_MAP: [u32; 10] = [0, 1, 2, 3, 4, 5, 6, 8, 10, 11];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WriterState {
AcceptingEntries,
Building,
Finished,
}
#[derive(Debug)]
pub(crate) struct PendingEntry {
path: ArchivePath,
meta: options::EntryMeta,
uncompressed_size: u64,
}
#[derive(Debug)]
struct SolidBufferEntry {
path: ArchivePath,
data: Vec<u8>,
meta: options::EntryMeta,
crc: u32,
}
#[cfg(feature = "aes")]
#[derive(Debug, Clone)]
pub(crate) struct EncryptedFolderInfo {
aes_properties: Vec<u8>,
compressed_size: u64,
}
#[derive(Debug, Clone)]
pub(crate) struct FilteredFolderInfo {
filter_method: Vec<u8>,
filter_properties: Option<Vec<u8>>,
filtered_size: u64,
}
#[derive(Debug, Clone)]
struct Bcj2FolderInfo {
pack_sizes: [u64; 4],
}
#[derive(Debug, Default)]
struct StreamInfo {
pack_sizes: Vec<u64>,
unpack_sizes: Vec<u64>,
crcs: Vec<u32>,
num_unpack_streams_per_folder: Vec<u64>,
substream_sizes: Vec<u64>,
substream_crcs: Vec<u32>,
#[cfg(feature = "aes")]
encryption_info: Vec<Option<EncryptedFolderInfo>>,
filter_info: Vec<Option<FilteredFolderInfo>>,
bcj2_folder_info: Vec<Option<Bcj2FolderInfo>>,
}
pub struct Writer<W> {
sink: W,
options: options::WriteOptions,
state: WriterState,
entries: Vec<PendingEntry>,
stream_info: StreamInfo,
compressed_bytes: u64,
solid_buffer: Vec<SolidBufferEntry>,
solid_buffer_size: u64,
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_writer_create() {
let buffer = Cursor::new(Vec::new());
let writer = Writer::create(buffer).unwrap();
assert_eq!(writer.state, WriterState::AcceptingEntries);
}
#[test]
fn test_writer_options() {
let buffer = Cursor::new(Vec::new());
let writer = Writer::create(buffer)
.unwrap()
.options(WriteOptions::new().level(9).unwrap());
assert_eq!(writer.options.level, 9);
}
#[cfg(feature = "lzma")]
#[test]
fn test_writer_add_bytes_and_finish() {
let buffer = Cursor::new(Vec::new());
let mut writer = Writer::create(buffer).unwrap();
let path = ArchivePath::new("test.txt").unwrap();
writer.add_bytes(path, b"Hello, World!").unwrap();
let result = writer.finish().unwrap();
assert_eq!(result.entries_written, 1);
assert_eq!(result.total_size, 13);
}
#[test]
fn test_writer_empty_archive() {
let buffer = Cursor::new(Vec::new());
let writer = Writer::create(buffer).unwrap();
let result = writer.finish().unwrap();
assert_eq!(result.entries_written, 0);
}
#[test]
fn test_writer_with_directory() {
let buffer = Cursor::new(Vec::new());
let mut writer = Writer::create(buffer).unwrap();
let dir_path = ArchivePath::new("mydir").unwrap();
writer
.add_directory(dir_path, options::EntryMeta::directory())
.unwrap();
let result = writer.finish().unwrap();
assert_eq!(result.entries_written, 0);
assert_eq!(result.directories_written, 1);
}
#[cfg(feature = "lzma")]
#[test]
fn test_writer_with_anti_item() {
let buffer = Cursor::new(Vec::new());
let mut writer = Writer::create(buffer).unwrap();
let file_path = ArchivePath::new("keep.txt").unwrap();
writer.add_bytes(file_path, b"Keep this file").unwrap();
let anti_path = ArchivePath::new("deleted.txt").unwrap();
writer.add_anti_item(anti_path).unwrap();
let anti_dir_path = ArchivePath::new("deleted_dir").unwrap();
writer.add_anti_directory(anti_dir_path).unwrap();
let result = writer.finish().unwrap();
assert_eq!(result.entries_written, 1); assert_eq!(result.directories_written, 1); }
#[cfg(feature = "lzma")]
#[test]
fn test_anti_item_roundtrip() {
use crate::read::Archive;
let buffer = Cursor::new(Vec::new());
let mut writer = Writer::create(buffer).unwrap();
let file_path = ArchivePath::new("normal.txt").unwrap();
writer.add_bytes(file_path, b"Normal content").unwrap();
let anti_path = ArchivePath::new("delete_me.txt").unwrap();
writer.add_anti_item(anti_path).unwrap();
let (_result, cursor) = writer.finish_into_inner().unwrap();
let data = cursor.into_inner();
let archive = Archive::open(Cursor::new(data)).unwrap();
let entries = archive.entries();
assert_eq!(entries.len(), 2);
let normal = &entries[0];
assert_eq!(normal.path.as_str(), "normal.txt");
assert!(!normal.is_anti);
assert!(!normal.is_directory);
let anti = &entries[1];
assert_eq!(anti.path.as_str(), "delete_me.txt");
assert!(anti.is_anti);
assert!(!anti.is_directory);
}
#[cfg(feature = "lzma")]
#[test]
fn test_comment_roundtrip() {
use crate::read::Archive;
let buffer = Cursor::new(Vec::new());
let options = WriteOptions::new().comment("Test archive comment with Unicode: ä½ å¥½ä¸–ç•Œ");
let mut writer = Writer::create(buffer).unwrap().options(options);
let file_path = ArchivePath::new("test.txt").unwrap();
writer.add_bytes(file_path, b"Hello").unwrap();
let (_result, cursor) = writer.finish_into_inner().unwrap();
let data = cursor.into_inner();
let archive = Archive::open(Cursor::new(data)).unwrap();
let comment = archive.comment();
assert!(comment.is_some());
assert_eq!(
comment.unwrap(),
"Test archive comment with Unicode: ä½ å¥½ä¸–ç•Œ"
);
}
#[cfg(feature = "lzma")]
#[test]
fn test_no_comment() {
use crate::read::Archive;
let buffer = Cursor::new(Vec::new());
let mut writer = Writer::create(buffer).unwrap();
let file_path = ArchivePath::new("test.txt").unwrap();
writer.add_bytes(file_path, b"Hello").unwrap();
let (_result, cursor) = writer.finish_into_inner().unwrap();
let data = cursor.into_inner();
let archive = Archive::open(Cursor::new(data)).unwrap();
assert!(archive.comment().is_none());
}
#[cfg(feature = "aes")]
#[test]
fn test_header_encryption_write() {
use crate::crypto::Password;
use crate::format::property_id;
let buffer = Cursor::new(Vec::new());
let password = Password::new("secret123");
let (result, cursor) = {
let mut writer = Writer::create(buffer).unwrap().options(
WriteOptions::new()
.password(password.clone())
.encrypt_header(true),
);
let path = ArchivePath::new("secret.txt").unwrap();
writer.add_bytes(path, b"Secret content!").unwrap();
writer.finish_into_inner().unwrap()
};
assert_eq!(result.entries_written, 1);
let archive_data = cursor.into_inner();
assert!(!archive_data.is_empty());
let header_pos = {
let offset = u64::from_le_bytes(archive_data[12..20].try_into().unwrap());
32 + offset as usize
};
assert_eq!(
archive_data[header_pos],
property_id::ENCODED_HEADER,
"Archive should have encrypted header"
);
}
#[cfg(feature = "aes")]
#[test]
fn test_header_encryption_without_password() {
let buffer = Cursor::new(Vec::new());
let (result, cursor) = {
let mut writer = Writer::create(buffer)
.unwrap()
.options(WriteOptions::new().encrypt_header(true));
let path = ArchivePath::new("test.txt").unwrap();
writer.add_bytes(path, b"Hello").unwrap();
writer.finish_into_inner().unwrap()
};
assert_eq!(result.entries_written, 1);
let archive_data = cursor.into_inner();
let header_pos = {
let offset = u64::from_le_bytes(archive_data[12..20].try_into().unwrap());
32 + offset as usize
};
assert_eq!(
archive_data[header_pos],
crate::format::property_id::HEADER,
"Without password, header should not be encrypted"
);
}
#[cfg(all(feature = "aes", feature = "lzma2"))]
#[test]
fn test_content_encryption_write_and_read() {
use crate::crypto::Password;
use crate::read::Archive;
let buffer = Cursor::new(Vec::new());
let password = Password::new("secret_password_123");
let (result, cursor) = {
let mut writer = Writer::create(buffer).unwrap().options(
WriteOptions::new()
.password(password.clone())
.encrypt_data(true),
);
let path = ArchivePath::new("secret.txt").unwrap();
writer
.add_bytes(path, b"This is encrypted content!")
.unwrap();
writer.finish_into_inner().unwrap()
};
assert_eq!(result.entries_written, 1);
let archive_data = cursor.into_inner();
assert!(!archive_data.is_empty());
let mut archive =
Archive::open_with_password(Cursor::new(archive_data.clone()), password.clone())
.expect("Should open archive with correct password");
let extracted = archive
.extract_to_vec("secret.txt")
.expect("Should extract encrypted content");
assert_eq!(extracted, b"This is encrypted content!");
}
}