1mod append;
24pub(crate) mod options;
25
26mod codecs;
28mod compression;
29mod encoding_utils;
30mod entry_compression;
31mod entry_input;
32mod header_encode;
33mod header_encryption;
34mod metadata_encode;
35mod writer_init;
36
37pub use append::{AppendResult, ArchiveAppender};
39pub use options::{EntryMeta, Lzma2Variant, SolidOptions, WriteFilter, WriteOptions, WriteResult};
40
41use crate::ArchivePath;
42
43#[cfg(feature = "zstd")]
57const ZSTD_LEVEL_MAP: [i32; 10] = [1, 1, 2, 3, 5, 7, 9, 12, 15, 19];
58
59#[cfg(feature = "brotli")]
68const BROTLI_QUALITY_MAP: [u32; 10] = [0, 1, 2, 3, 4, 5, 6, 8, 10, 11];
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72enum WriterState {
73 AcceptingEntries,
75 Building,
77 Finished,
79}
80
81#[derive(Debug)]
83pub(crate) struct PendingEntry {
84 path: ArchivePath,
86 meta: options::EntryMeta,
88 uncompressed_size: u64,
90}
91
92#[derive(Debug)]
94struct SolidBufferEntry {
95 path: ArchivePath,
97 data: Vec<u8>,
99 meta: options::EntryMeta,
101 crc: u32,
103}
104
105#[cfg(feature = "aes")]
107#[derive(Debug, Clone)]
108pub(crate) struct EncryptedFolderInfo {
109 aes_properties: Vec<u8>,
111 compressed_size: u64,
113}
114
115#[derive(Debug, Clone)]
117pub(crate) struct FilteredFolderInfo {
118 filter_method: Vec<u8>,
120 filter_properties: Option<Vec<u8>>,
122 filtered_size: u64,
124}
125
126#[derive(Debug, Clone)]
134struct Bcj2FolderInfo {
135 pack_sizes: [u64; 4],
137}
138
139#[derive(Debug, Default)]
141struct StreamInfo {
142 pack_sizes: Vec<u64>,
145 unpack_sizes: Vec<u64>,
147 crcs: Vec<u32>,
149 num_unpack_streams_per_folder: Vec<u64>,
151 substream_sizes: Vec<u64>,
153 substream_crcs: Vec<u32>,
155 #[cfg(feature = "aes")]
157 encryption_info: Vec<Option<EncryptedFolderInfo>>,
158 filter_info: Vec<Option<FilteredFolderInfo>>,
160 bcj2_folder_info: Vec<Option<Bcj2FolderInfo>>,
162}
163
164pub struct Writer<W> {
166 sink: W,
167 options: options::WriteOptions,
168 state: WriterState,
169 entries: Vec<PendingEntry>,
170 stream_info: StreamInfo,
171 compressed_bytes: u64,
173 solid_buffer: Vec<SolidBufferEntry>,
175 solid_buffer_size: u64,
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use std::io::Cursor;
183
184 #[test]
185 fn test_writer_create() {
186 let buffer = Cursor::new(Vec::new());
187 let writer = Writer::create(buffer).unwrap();
188 assert_eq!(writer.state, WriterState::AcceptingEntries);
189 }
190
191 #[test]
192 fn test_writer_options() {
193 let buffer = Cursor::new(Vec::new());
194 let writer = Writer::create(buffer)
195 .unwrap()
196 .options(WriteOptions::new().level(9).unwrap());
197 assert_eq!(writer.options.level, 9);
198 }
199
200 #[cfg(feature = "lzma")]
201 #[test]
202 fn test_writer_add_bytes_and_finish() {
203 let buffer = Cursor::new(Vec::new());
204 let mut writer = Writer::create(buffer).unwrap();
205
206 let path = ArchivePath::new("test.txt").unwrap();
207 writer.add_bytes(path, b"Hello, World!").unwrap();
208
209 let result = writer.finish().unwrap();
210 assert_eq!(result.entries_written, 1);
211 assert_eq!(result.total_size, 13);
212 }
213
214 #[test]
215 fn test_writer_empty_archive() {
216 let buffer = Cursor::new(Vec::new());
217 let writer = Writer::create(buffer).unwrap();
218 let result = writer.finish().unwrap();
219 assert_eq!(result.entries_written, 0);
220 }
221
222 #[test]
223 fn test_writer_with_directory() {
224 let buffer = Cursor::new(Vec::new());
225 let mut writer = Writer::create(buffer).unwrap();
226
227 let dir_path = ArchivePath::new("mydir").unwrap();
228 writer
229 .add_directory(dir_path, options::EntryMeta::directory())
230 .unwrap();
231
232 let result = writer.finish().unwrap();
233 assert_eq!(result.entries_written, 0);
234 assert_eq!(result.directories_written, 1);
235 }
236
237 #[cfg(feature = "lzma")]
238 #[test]
239 fn test_writer_with_anti_item() {
240 let buffer = Cursor::new(Vec::new());
241 let mut writer = Writer::create(buffer).unwrap();
242
243 let file_path = ArchivePath::new("keep.txt").unwrap();
245 writer.add_bytes(file_path, b"Keep this file").unwrap();
246
247 let anti_path = ArchivePath::new("deleted.txt").unwrap();
249 writer.add_anti_item(anti_path).unwrap();
250
251 let anti_dir_path = ArchivePath::new("deleted_dir").unwrap();
253 writer.add_anti_directory(anti_dir_path).unwrap();
254
255 let result = writer.finish().unwrap();
256 assert_eq!(result.entries_written, 1); assert_eq!(result.directories_written, 1); }
259
260 #[cfg(feature = "lzma")]
261 #[test]
262 fn test_anti_item_roundtrip() {
263 use crate::read::Archive;
264
265 let buffer = Cursor::new(Vec::new());
266 let mut writer = Writer::create(buffer).unwrap();
267
268 let file_path = ArchivePath::new("normal.txt").unwrap();
270 writer.add_bytes(file_path, b"Normal content").unwrap();
271
272 let anti_path = ArchivePath::new("delete_me.txt").unwrap();
273 writer.add_anti_item(anti_path).unwrap();
274
275 let (_result, cursor) = writer.finish_into_inner().unwrap();
276 let data = cursor.into_inner();
277
278 let archive = Archive::open(Cursor::new(data)).unwrap();
280
281 let entries = archive.entries();
283 assert_eq!(entries.len(), 2);
284
285 let normal = &entries[0];
287 assert_eq!(normal.path.as_str(), "normal.txt");
288 assert!(!normal.is_anti);
289 assert!(!normal.is_directory);
290
291 let anti = &entries[1];
293 assert_eq!(anti.path.as_str(), "delete_me.txt");
294 assert!(anti.is_anti);
295 assert!(!anti.is_directory);
296 }
297
298 #[cfg(feature = "lzma")]
299 #[test]
300 fn test_comment_roundtrip() {
301 use crate::read::Archive;
302
303 let buffer = Cursor::new(Vec::new());
304 let options = WriteOptions::new().comment("Test archive comment with Unicode: 你好世界");
305 let mut writer = Writer::create(buffer).unwrap().options(options);
306
307 let file_path = ArchivePath::new("test.txt").unwrap();
308 writer.add_bytes(file_path, b"Hello").unwrap();
309
310 let (_result, cursor) = writer.finish_into_inner().unwrap();
311 let data = cursor.into_inner();
312
313 let archive = Archive::open(Cursor::new(data)).unwrap();
315
316 let comment = archive.comment();
318 assert!(comment.is_some());
319 assert_eq!(
320 comment.unwrap(),
321 "Test archive comment with Unicode: 你好世界"
322 );
323 }
324
325 #[cfg(feature = "lzma")]
326 #[test]
327 fn test_no_comment() {
328 use crate::read::Archive;
329
330 let buffer = Cursor::new(Vec::new());
331 let mut writer = Writer::create(buffer).unwrap();
332
333 let file_path = ArchivePath::new("test.txt").unwrap();
334 writer.add_bytes(file_path, b"Hello").unwrap();
335
336 let (_result, cursor) = writer.finish_into_inner().unwrap();
337 let data = cursor.into_inner();
338
339 let archive = Archive::open(Cursor::new(data)).unwrap();
341
342 assert!(archive.comment().is_none());
344 }
345
346 #[cfg(feature = "aes")]
347 #[test]
348 fn test_header_encryption_write() {
349 use crate::crypto::Password;
350 use crate::format::property_id;
351
352 let buffer = Cursor::new(Vec::new());
353 let password = Password::new("secret123");
354
355 let (result, cursor) = {
357 let mut writer = Writer::create(buffer).unwrap().options(
358 WriteOptions::new()
359 .password(password.clone())
360 .encrypt_header(true),
361 );
362
363 let path = ArchivePath::new("secret.txt").unwrap();
364 writer.add_bytes(path, b"Secret content!").unwrap();
365
366 writer.finish_into_inner().unwrap()
367 };
368
369 assert_eq!(result.entries_written, 1);
370 let archive_data = cursor.into_inner();
371 assert!(!archive_data.is_empty());
372
373 let header_pos = {
378 let offset = u64::from_le_bytes(archive_data[12..20].try_into().unwrap());
380 32 + offset as usize
381 };
382
383 assert_eq!(
385 archive_data[header_pos],
386 property_id::ENCODED_HEADER,
387 "Archive should have encrypted header"
388 );
389 }
390
391 #[cfg(feature = "aes")]
392 #[test]
393 fn test_header_encryption_without_password() {
394 let buffer = Cursor::new(Vec::new());
396
397 let (result, cursor) = {
398 let mut writer = Writer::create(buffer)
399 .unwrap()
400 .options(WriteOptions::new().encrypt_header(true)); let path = ArchivePath::new("test.txt").unwrap();
403 writer.add_bytes(path, b"Hello").unwrap();
404
405 writer.finish_into_inner().unwrap()
406 };
407
408 assert_eq!(result.entries_written, 1);
409 let archive_data = cursor.into_inner();
410
411 let header_pos = {
413 let offset = u64::from_le_bytes(archive_data[12..20].try_into().unwrap());
414 32 + offset as usize
415 };
416
417 assert_eq!(
419 archive_data[header_pos],
420 crate::format::property_id::HEADER,
421 "Without password, header should not be encrypted"
422 );
423 }
424
425 #[cfg(all(feature = "aes", feature = "lzma2"))]
426 #[test]
427 fn test_content_encryption_write_and_read() {
428 use crate::crypto::Password;
429 use crate::read::Archive;
430
431 let buffer = Cursor::new(Vec::new());
432 let password = Password::new("secret_password_123");
433
434 let (result, cursor) = {
436 let mut writer = Writer::create(buffer).unwrap().options(
437 WriteOptions::new()
438 .password(password.clone())
439 .encrypt_data(true),
440 );
441
442 let path = ArchivePath::new("secret.txt").unwrap();
443 writer
444 .add_bytes(path, b"This is encrypted content!")
445 .unwrap();
446
447 writer.finish_into_inner().unwrap()
448 };
449
450 assert_eq!(result.entries_written, 1);
451 let archive_data = cursor.into_inner();
452 assert!(!archive_data.is_empty());
453
454 let mut archive =
456 Archive::open_with_password(Cursor::new(archive_data.clone()), password.clone())
457 .expect("Should open archive with correct password");
458
459 let extracted = archive
461 .extract_to_vec("secret.txt")
462 .expect("Should extract encrypted content");
463
464 assert_eq!(extracted, b"This is encrypted content!");
465 }
466}