1use alloc::string::String;
6use alloc::vec::Vec;
7use alloc::format;
8use core::fmt;
9
10#[cfg(feature = "std")]
11use std::path::Path;
12
13#[derive(Copy, Debug, Clone, Default)]
19#[repr(C)]
20pub struct ZipReadConfig {
21 pub max_file_size: u64,
23 pub allow_path_traversal: bool,
25 pub skip_encrypted: bool,
27}
28
29impl ZipReadConfig {
30 #[must_use] pub fn new() -> Self {
31 Self::default()
32 }
33
34 #[must_use] pub const fn with_max_file_size(mut self, max_size: u64) -> Self {
35 self.max_file_size = max_size;
36 self
37 }
38
39 #[must_use] pub const fn with_allow_path_traversal(mut self, allow: bool) -> Self {
40 self.allow_path_traversal = allow;
41 self
42 }
43}
44
45#[derive(Debug, Clone)]
47#[repr(C)]
48pub struct ZipWriteConfig {
49 pub compression_method: u8,
51 pub compression_level: u8,
53 pub unix_permissions: u32,
55 pub comment: String,
57}
58
59impl Default for ZipWriteConfig {
60 fn default() -> Self {
61 Self {
62 compression_method: 1, compression_level: 6, unix_permissions: 0o644,
65 comment: String::new(),
66 }
67 }
68}
69
70impl ZipWriteConfig {
71 #[must_use] pub fn new() -> Self {
72 Self::default()
73 }
74
75 #[must_use] pub fn store() -> Self {
76 Self {
77 compression_method: 0,
78 compression_level: 0,
79 ..Default::default()
80 }
81 }
82
83 #[must_use] pub fn deflate(level: u8) -> Self {
84 Self {
85 compression_method: 1,
86 compression_level: level.min(9),
87 ..Default::default()
88 }
89 }
90
91 #[must_use]
92 pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
93 self.comment = comment.into();
94 self
95 }
96}
97
98#[derive(Debug, Clone)]
104#[repr(C)]
105pub struct ZipPathEntry {
106 pub path: String,
108 pub is_directory: bool,
110 pub size: u64,
112 pub compressed_size: u64,
114 pub crc32: u32,
116}
117
118pub type ZipPathEntryVec = Vec<ZipPathEntry>;
120
121#[derive(Debug, Clone)]
123#[repr(C)]
124pub struct ZipFileEntry {
125 pub path: String,
127 pub data: Vec<u8>,
129 pub is_directory: bool,
131}
132
133impl ZipFileEntry {
134 pub fn file(path: impl Into<String>, data: Vec<u8>) -> Self {
136 Self {
137 path: path.into(),
138 data,
139 is_directory: false,
140 }
141 }
142
143 pub fn directory(path: impl Into<String>) -> Self {
145 Self {
146 path: path.into(),
147 data: Vec::new(),
148 is_directory: true,
149 }
150 }
151}
152
153pub type ZipFileEntryVec = Vec<ZipFileEntry>;
155
156#[derive(Debug, Clone, PartialEq, Eq)]
162#[repr(C, u8)]
163pub enum ZipReadError {
164 InvalidFormat(String),
166 FileNotFound(String),
168 IoError(String),
170 UnsafePath(String),
172 EncryptedFile(String),
174 FileTooLarge { path: String, size: u64, max_size: u64 },
176}
177
178impl fmt::Display for ZipReadError {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 match self {
181 Self::InvalidFormat(msg) => write!(f, "Invalid ZIP format: {msg}"),
182 Self::FileNotFound(path) => write!(f, "File not found: {path}"),
183 Self::IoError(msg) => write!(f, "I/O error: {msg}"),
184 Self::UnsafePath(path) => write!(f, "Unsafe path: {path}"),
185 Self::EncryptedFile(path) => write!(f, "Encrypted file: {path}"),
186 Self::FileTooLarge { path, size, max_size } => {
187 write!(f, "File too large: {path} ({size} > {max_size})")
188 }
189 }
190 }
191}
192
193#[cfg(feature = "std")]
194impl std::error::Error for ZipReadError {}
195
196#[derive(Debug, Clone, PartialEq, Eq)]
198#[repr(C, u8)]
199pub enum ZipWriteError {
200 IoError(String),
202 InvalidPath(String),
204 CompressionError(String),
206}
207
208impl fmt::Display for ZipWriteError {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 match self {
211 Self::IoError(msg) => write!(f, "I/O error: {msg}"),
212 Self::InvalidPath(path) => write!(f, "Invalid path: {path}"),
213 Self::CompressionError(msg) => write!(f, "Compression error: {msg}"),
214 }
215 }
216}
217
218#[cfg(feature = "std")]
219impl std::error::Error for ZipWriteError {}
220
221#[derive(Debug, Clone, Default)]
227#[repr(C)]
228pub struct ZipFile {
229 pub entries: ZipFileEntryVec,
231}
232
233impl ZipFile {
234 #[must_use] pub const fn new() -> Self {
236 Self {
237 entries: Vec::new(),
238 }
239 }
240
241 #[cfg(feature = "zip")]
250 pub fn list(data: &[u8], config: &ZipReadConfig) -> Result<ZipPathEntryVec, ZipReadError> {
254 use std::io::Cursor;
255
256 let cursor = Cursor::new(data);
257 let mut archive = zip::ZipArchive::new(cursor)
258 .map_err(|e| ZipReadError::InvalidFormat(e.to_string()))?;
259
260 let mut entries = Vec::new();
261
262 for i in 0..archive.len() {
263 let file = archive.by_index(i)
264 .map_err(|e| ZipReadError::IoError(e.to_string()))?;
265
266 let path = file.name().to_string();
267
268 if !config.allow_path_traversal && path.contains("..") {
270 return Err(ZipReadError::UnsafePath(path));
271 }
272
273 entries.push(ZipPathEntry {
274 path,
275 is_directory: file.is_dir(),
276 size: file.size(),
277 compressed_size: file.compressed_size(),
278 crc32: file.crc32(),
279 });
280 }
281
282 Ok(entries)
283 }
284
285 #[cfg(feature = "zip")]
295 pub fn get_single_file(
299 data: &[u8],
300 entry: &ZipPathEntry,
301 config: &ZipReadConfig,
302 ) -> Result<Option<Vec<u8>>, ZipReadError> {
303 use std::io::{Cursor, Read};
304
305 if config.max_file_size > 0 && entry.size > config.max_file_size {
307 return Err(ZipReadError::FileTooLarge {
308 path: entry.path.clone(),
309 size: entry.size,
310 max_size: config.max_file_size,
311 });
312 }
313
314 let cursor = Cursor::new(data);
315 let mut archive = zip::ZipArchive::new(cursor)
316 .map_err(|e| ZipReadError::InvalidFormat(e.to_string()))?;
317
318 let mut file = match archive.by_name(&entry.path) {
319 Ok(f) => f,
320 Err(zip::result::ZipError::FileNotFound) => return Ok(None),
321 Err(e) => return Err(ZipReadError::IoError(e.to_string())),
322 };
323
324 if file.is_dir() {
325 return Ok(Some(Vec::new()));
326 }
327
328 let mut contents = Vec::with_capacity(usize::try_from(entry.size).unwrap_or(0));
329 file.read_to_end(&mut contents)
330 .map_err(|e| ZipReadError::IoError(e.to_string()))?;
331
332 Ok(Some(contents))
333 }
334
335 #[cfg(feature = "zip")]
341 pub fn from_bytes(data: &[u8], config: &ZipReadConfig) -> Result<Self, ZipReadError> {
345 use std::io::{Cursor, Read};
346
347 let cursor = Cursor::new(data);
348 let mut archive = zip::ZipArchive::new(cursor)
349 .map_err(|e| ZipReadError::InvalidFormat(e.to_string()))?;
350
351 let mut entries = Vec::new();
352
353 for i in 0..archive.len() {
354 let mut file = archive.by_index(i)
355 .map_err(|e| ZipReadError::IoError(e.to_string()))?;
356
357 let path = file.name().to_string();
358
359 if !config.allow_path_traversal && path.contains("..") {
361 return Err(ZipReadError::UnsafePath(path));
362 }
363
364 if config.max_file_size > 0 && file.size() > config.max_file_size {
366 return Err(ZipReadError::FileTooLarge {
367 path,
368 size: file.size(),
369 max_size: config.max_file_size,
370 });
371 }
372
373 let is_directory = file.is_dir();
374 let mut file_data = Vec::new();
375
376 if !is_directory {
377 file.read_to_end(&mut file_data)
378 .map_err(|e| ZipReadError::IoError(e.to_string()))?;
379 }
380
381 entries.push(ZipFileEntry {
382 path,
383 data: file_data,
384 is_directory,
385 });
386 }
387
388 Ok(Self { entries })
389 }
390
391 #[cfg(all(feature = "zip", feature = "std"))]
393 pub fn from_file(path: &Path, config: &ZipReadConfig) -> Result<Self, ZipReadError> {
397 let data = std::fs::read(path)
398 .map_err(|e| ZipReadError::IoError(e.to_string()))?;
399 Self::from_bytes(&data, config)
400 }
401
402 #[cfg(feature = "zip")]
407 pub fn to_bytes(&self, config: &ZipWriteConfig) -> Result<Vec<u8>, ZipWriteError> {
411 use std::io::{Cursor, Write};
412 use zip::write::SimpleFileOptions;
413
414 let buffer = Vec::new();
415 let cursor = Cursor::new(buffer);
416 let mut writer = zip::ZipWriter::new(cursor);
417
418 if !config.comment.is_empty() {
420 writer.set_comment(config.comment.clone());
421 }
422
423 let compression = match config.compression_method {
424 0 => zip::CompressionMethod::Stored,
425 _ => zip::CompressionMethod::Deflated,
426 };
427
428 let options = SimpleFileOptions::default()
429 .compression_method(compression)
430 .compression_level(Some(i64::from(config.compression_level)))
431 .unix_permissions(config.unix_permissions);
432
433 for entry in &self.entries {
434 if entry.is_directory {
435 writer.add_directory(&entry.path, options)
436 .map_err(|e| ZipWriteError::IoError(e.to_string()))?;
437 } else {
438 writer.start_file(&entry.path, options)
439 .map_err(|e| ZipWriteError::IoError(e.to_string()))?;
440 writer.write_all(&entry.data)
441 .map_err(|e| ZipWriteError::IoError(e.to_string()))?;
442 }
443 }
444
445 let result = writer.finish()
446 .map_err(|e| ZipWriteError::IoError(e.to_string()))?;
447
448 Ok(result.into_inner())
449 }
450
451 #[cfg(all(feature = "zip", feature = "std"))]
453 pub fn to_file(&self, path: &Path, config: &ZipWriteConfig) -> Result<(), ZipWriteError> {
457 let data = self.to_bytes(config)?;
458 std::fs::write(path, data)
459 .map_err(|e| ZipWriteError::IoError(e.to_string()))?;
460 Ok(())
461 }
462
463 pub fn add_file(&mut self, path: impl Into<String>, data: Vec<u8>) {
469 let path = path.into();
470 self.entries.retain(|e| e.path != path);
472 self.entries.push(ZipFileEntry::file(path, data));
473 }
474
475 pub fn add_directory(&mut self, path: impl Into<String>) {
477 let path = path.into();
478 self.entries.retain(|e| e.path != path);
479 self.entries.push(ZipFileEntry::directory(path));
480 }
481
482 pub fn remove(&mut self, path: &str) {
484 self.entries.retain(|e| e.path != path);
485 }
486
487 #[must_use] pub fn get(&self, path: &str) -> Option<&ZipFileEntry> {
489 self.entries.iter().find(|e| e.path == path)
490 }
491
492 #[must_use] pub fn contains(&self, path: &str) -> bool {
494 self.entries.iter().any(|e| e.path == path)
495 }
496
497 #[must_use] pub fn paths(&self) -> Vec<&str> {
499 self.entries.iter().map(|e| e.path.as_str()).collect()
500 }
501
502 #[must_use] pub fn filter_by_suffix(&self, suffix: &str) -> Vec<&ZipFileEntry> {
504 self.entries.iter()
505 .filter(|e| !e.is_directory && e.path.ends_with(suffix))
506 .collect()
507 }
508}
509
510#[cfg(feature = "zip")]
516pub fn zip_create(entries: Vec<ZipFileEntry>, config: &ZipWriteConfig) -> Result<Vec<u8>, ZipWriteError> {
520 let zip = ZipFile { entries };
521 zip.to_bytes(config)
522}
523
524#[cfg(feature = "zip")]
526pub fn zip_create_from_files(
530 files: Vec<(String, Vec<u8>)>,
531 config: &ZipWriteConfig,
532) -> Result<Vec<u8>, ZipWriteError> {
533 let entries: Vec<ZipFileEntry> = files
534 .into_iter()
535 .map(|(path, data)| ZipFileEntry::file(path, data))
536 .collect();
537 zip_create(entries, config)
538}
539
540#[cfg(feature = "zip")]
542pub fn zip_extract_all(data: &[u8], config: &ZipReadConfig) -> Result<Vec<ZipFileEntry>, ZipReadError> {
546 let zip = ZipFile::from_bytes(data, config)?;
547 Ok(zip.entries)
548}
549
550#[cfg(feature = "zip")]
552pub fn zip_list_contents(data: &[u8], config: &ZipReadConfig) -> Result<Vec<ZipPathEntry>, ZipReadError> {
556 ZipFile::list(data, config)
557}
558
559#[cfg(test)]
564mod tests {
565 use super::*;
566
567 #[test]
568 fn test_zip_config_defaults() {
569 let read_config = ZipReadConfig::default();
570 assert_eq!(read_config.max_file_size, 0);
571 assert!(!read_config.allow_path_traversal);
572
573 let write_config = ZipWriteConfig::default();
574 assert_eq!(write_config.compression_method, 1);
575 assert_eq!(write_config.compression_level, 6);
576 }
577
578 #[test]
579 fn test_zip_file_entry_creation() {
580 let file = ZipFileEntry::file("test.txt", b"Hello".to_vec());
581 assert_eq!(file.path, "test.txt");
582 assert!(!file.is_directory);
583 assert_eq!(file.data, b"Hello");
584
585 let dir = ZipFileEntry::directory("subdir/");
586 assert!(dir.is_directory);
587 assert!(dir.data.is_empty());
588 }
589
590 #[cfg(feature = "zip")]
591 #[test]
592 fn test_zip_roundtrip() {
593 let files = vec![
594 ("hello.txt".to_string(), b"Hello, World!".to_vec()),
595 ("sub/nested.txt".to_string(), b"Nested file".to_vec()),
596 ];
597
598 let write_config = ZipWriteConfig::default();
599 let zip_data = zip_create_from_files(files, &write_config).expect("Failed to create ZIP");
600
601 let read_config = ZipReadConfig::default();
602 let entries = zip_extract_all(&zip_data, &read_config).expect("Failed to extract");
603
604 assert_eq!(entries.len(), 2);
605 assert!(entries.iter().any(|e| e.path == "hello.txt"));
606 assert!(entries.iter().any(|e| e.path == "sub/nested.txt"));
607 }
608
609 #[cfg(feature = "zip")]
610 #[test]
611 fn test_zip_file_manipulation() {
612 let mut zip = ZipFile::new();
613
614 zip.add_file("a.txt", b"AAA".to_vec());
615 zip.add_file("b.txt", b"BBB".to_vec());
616
617 assert_eq!(zip.entries.len(), 2);
618 assert!(zip.contains("a.txt"));
619 assert!(zip.contains("b.txt"));
620
621 zip.remove("a.txt");
622 assert_eq!(zip.entries.len(), 1);
623 assert!(!zip.contains("a.txt"));
624
625 zip.add_file("b.txt", b"NEW".to_vec());
627 assert_eq!(zip.entries.len(), 1);
628 assert_eq!(zip.get("b.txt").unwrap().data, b"NEW");
629 }
630}
631
632#[cfg(test)]
637mod autotest_generated {
638 use super::*;
639
640 #[cfg(feature = "zip")]
646 fn build(entries: Vec<ZipFileEntry>) -> Vec<u8> {
647 zip_create(entries, &ZipWriteConfig::default()).expect("default write config must work")
648 }
649
650 #[cfg(feature = "zip")]
652 fn eocd_only() -> Vec<u8> {
653 let mut v = vec![0x50, 0x4B, 0x05, 0x06];
654 v.extend_from_slice(&[0u8; 18]);
655 v
656 }
657
658 fn nasty_paths() -> Vec<String> {
660 vec![
661 String::new(),
662 " ".to_string(),
663 "\t\n".to_string(),
664 "\0".to_string(),
665 "a\0b".to_string(),
666 "..".to_string(),
667 "../../etc/passwd".to_string(),
668 "./a.txt".to_string(),
669 "a.txt ".to_string(),
670 " a.txt".to_string(),
671 "a.txt;garbage".to_string(),
672 "0".to_string(),
673 "-0".to_string(),
674 "NaN".to_string(),
675 "inf".to_string(),
676 "-inf".to_string(),
677 "9223372036854775807".to_string(),
678 "-9223372036854775808".to_string(),
679 "18446744073709551615".to_string(),
680 "1e309".to_string(),
681 "\u{1F600}".to_string(),
682 "e\u{0301}\u{0301}\u{0301}.txt".to_string(),
683 "\u{202E}txt.exe".to_string(),
684 "\u{FEFF}a.txt".to_string(),
685 "A/".repeat(2000),
686 "x".repeat(100_000),
687 ]
688 }
689
690 #[test]
695 fn autotest_read_config_builders_at_numeric_extremes() {
696 let base = ZipReadConfig::new();
697 let def = ZipReadConfig::default();
698 assert_eq!(base.max_file_size, def.max_file_size);
699 assert_eq!(base.allow_path_traversal, def.allow_path_traversal);
700 assert_eq!(base.skip_encrypted, def.skip_encrypted);
701 assert_eq!(base.max_file_size, 0);
702 assert!(!base.allow_path_traversal);
703 assert!(!base.skip_encrypted);
704
705 for size in [0u64, 1, u64::from(u32::MAX), u64::MAX / 2, u64::MAX - 1, u64::MAX] {
706 let c = ZipReadConfig::new().with_max_file_size(size);
707 assert_eq!(c.max_file_size, size);
708 assert!(!c.allow_path_traversal);
710 assert!(!c.skip_encrypted);
711 }
712
713 for allow in [false, true] {
714 let c = ZipReadConfig::new()
715 .with_max_file_size(u64::MAX)
716 .with_allow_path_traversal(allow);
717 assert_eq!(c.allow_path_traversal, allow);
718 assert_eq!(c.max_file_size, u64::MAX);
719 }
720
721 let a = ZipReadConfig::new().with_max_file_size(7).with_allow_path_traversal(true);
723 let b = ZipReadConfig::new().with_allow_path_traversal(true).with_max_file_size(7);
724 assert_eq!(a.max_file_size, b.max_file_size);
725 assert_eq!(a.allow_path_traversal, b.allow_path_traversal);
726 let c = a.with_max_file_size(7);
727 assert_eq!(c.max_file_size, 7);
728 assert!(c.allow_path_traversal);
729
730 let orig = ZipReadConfig::new();
732 let _moved = orig.with_max_file_size(99);
733 assert_eq!(orig.max_file_size, 0);
734 }
735
736 #[test]
737 fn autotest_write_config_new_store_and_defaults() {
738 let new = ZipWriteConfig::new();
739 let def = ZipWriteConfig::default();
740 assert_eq!(new.compression_method, def.compression_method);
741 assert_eq!(new.compression_level, def.compression_level);
742 assert_eq!(new.unix_permissions, def.unix_permissions);
743 assert_eq!(new.comment, def.comment);
744 assert_eq!(new.compression_method, 1);
745 assert_eq!(new.compression_level, 6);
746 assert_eq!(new.unix_permissions, 0o644);
747 assert!(new.comment.is_empty());
748
749 let store = ZipWriteConfig::store();
750 assert_eq!(store.compression_method, 0);
751 assert_eq!(store.compression_level, 0);
752 assert_eq!(store.unix_permissions, 0o644);
754 assert!(store.comment.is_empty());
755 }
756
757 #[test]
758 fn autotest_write_config_deflate_saturates_level() {
759 for level in 0u16..=255 {
761 let level = u8::try_from(level).unwrap();
762 let cfg = ZipWriteConfig::deflate(level);
763 assert_eq!(cfg.compression_method, 1, "deflate() must always select Deflate");
764 assert_eq!(
765 cfg.compression_level,
766 level.min(9),
767 "deflate({level}) did not saturate at 9"
768 );
769 assert!(cfg.compression_level <= 9);
770 }
771 assert_eq!(ZipWriteConfig::deflate(0).compression_level, 0);
773 assert_eq!(ZipWriteConfig::deflate(9).compression_level, 9);
774 assert_eq!(ZipWriteConfig::deflate(10).compression_level, 9);
775 assert_eq!(ZipWriteConfig::deflate(u8::MIN).compression_level, 0);
776 assert_eq!(ZipWriteConfig::deflate(u8::MAX).compression_level, 9);
777 }
778
779 #[test]
780 fn autotest_write_config_with_comment_extremes() {
781 let c = ZipWriteConfig::new().with_comment("");
783 assert!(c.comment.is_empty());
784
785 for s in [
787 "\u{1F600}\u{1F9F0}",
788 "e\u{0301}combining",
789 "line1\nline2\r\n",
790 "nul\0inside",
791 "\u{202E}rtl",
792 ] {
793 let c = ZipWriteConfig::new().with_comment(s);
794 assert_eq!(c.comment, s);
795 assert_eq!(c.comment.chars().count(), s.chars().count());
796 }
797
798 let huge = "z".repeat(200_000);
800 let c = ZipWriteConfig::new().with_comment(huge.clone());
801 assert_eq!(c.comment.len(), 200_000);
802 assert_eq!(c.comment, huge);
803 assert_eq!(c.compression_method, 1);
805 assert_eq!(c.compression_level, 6);
806
807 let c = ZipWriteConfig::store().with_comment("a").with_comment(String::from("b"));
809 assert_eq!(c.comment, "b");
810 assert_eq!(c.compression_method, 0);
811 }
812
813 #[test]
814 fn autotest_zip_file_entry_constructors_no_panic() {
815 let e = ZipFileEntry::file("", Vec::new());
817 assert!(e.path.is_empty());
818 assert!(e.data.is_empty());
819 assert!(!e.is_directory);
820
821 let long_path = "p".repeat(200_000);
823 let e = ZipFileEntry::file(long_path.clone(), vec![0xFFu8; 4096]);
824 assert_eq!(e.path, long_path);
825 assert_eq!(e.data.len(), 4096);
826 assert!(!e.is_directory);
827
828 let e = ZipFileEntry::file("bin", vec![0xFFu8, 0xFE, 0x00, 0x80]);
830 assert_eq!(e.data, vec![0xFFu8, 0xFE, 0x00, 0x80]);
831
832 for p in nasty_paths() {
834 let d = ZipFileEntry::directory(p.clone());
835 assert_eq!(d.path, p);
836 assert!(d.is_directory);
837 assert!(d.data.is_empty());
838 }
839
840 assert_eq!(ZipFileEntry::directory("sub").path, "sub");
842 assert_eq!(ZipFileEntry::directory("sub/").path, "sub/");
843 }
844
845 #[test]
850 fn autotest_read_error_display_all_variants_non_empty() {
851 let cases = vec![
852 (ZipReadError::InvalidFormat("bad magic".into()), "bad magic"),
853 (ZipReadError::FileNotFound("a.txt".into()), "a.txt"),
854 (ZipReadError::IoError("eof".into()), "eof"),
855 (ZipReadError::UnsafePath("../x".into()), "../x"),
856 (ZipReadError::EncryptedFile("s.bin".into()), "s.bin"),
857 (
858 ZipReadError::FileTooLarge {
859 path: "big".into(),
860 size: 10,
861 max_size: 5,
862 },
863 "big",
864 ),
865 ];
866 for (err, needle) in cases {
867 let s = err.to_string();
868 assert!(!s.is_empty(), "empty Display for {err:?}");
869 assert!(s.contains(needle), "Display {s:?} lost payload {needle:?}");
870 assert!(!format!("{err:?}").is_empty());
872 }
873 }
874
875 #[test]
876 fn autotest_read_error_display_edge_payloads() {
877 for err in [
879 ZipReadError::InvalidFormat(String::new()),
880 ZipReadError::FileNotFound(String::new()),
881 ZipReadError::IoError(String::new()),
882 ZipReadError::UnsafePath(String::new()),
883 ZipReadError::EncryptedFile(String::new()),
884 ] {
885 let s = err.to_string();
886 assert!(!s.is_empty(), "empty payload produced empty Display");
887 assert!(s.contains(':'), "expected a prefixed message, got {s:?}");
888 }
889
890 for (size, max_size) in [
892 (0u64, 0u64),
893 (0, u64::MAX),
894 (u64::MAX, 0),
895 (u64::MAX, u64::MAX),
896 (u64::MAX - 1, u64::MAX),
897 ] {
898 let err = ZipReadError::FileTooLarge {
899 path: "\u{1F600}/p".into(),
900 size,
901 max_size,
902 };
903 let s = err.to_string();
904 assert!(s.contains(&format!("{size}")));
905 assert!(s.contains(&format!("{max_size}")));
906 assert!(s.contains("\u{1F600}"));
907 }
908
909 for payload in ["\u{1F600}", "e\u{0301}", "a\0b", "line\nbreak", &"L".repeat(50_000)] {
911 let err = ZipReadError::UnsafePath(payload.to_string());
912 assert!(err.to_string().contains(payload));
913 }
914 }
915
916 #[test]
917 fn autotest_write_error_display_all_variants_non_empty() {
918 let cases = vec![
919 (ZipWriteError::IoError("disk full".into()), "disk full"),
920 (ZipWriteError::InvalidPath("\u{1F600}".into()), "\u{1F600}"),
921 (ZipWriteError::CompressionError("level".into()), "level"),
922 ];
923 for (err, needle) in cases {
924 let s = err.to_string();
925 assert!(!s.is_empty());
926 assert!(s.contains(needle));
927 assert!(s.contains(':'));
928 }
929
930 for err in [
931 ZipWriteError::IoError(String::new()),
932 ZipWriteError::InvalidPath(String::new()),
933 ZipWriteError::CompressionError(String::new()),
934 ] {
935 assert!(!err.to_string().is_empty());
936 }
937
938 let big = ZipWriteError::CompressionError("\0".to_string() + &"q".repeat(100_000));
940 assert!(big.to_string().len() >= 100_000);
941 }
942
943 #[test]
944 fn autotest_error_equality_and_std_error_impls() {
945 assert_eq!(
946 ZipReadError::UnsafePath("a".into()),
947 ZipReadError::UnsafePath("a".into())
948 );
949 assert_ne!(
950 ZipReadError::UnsafePath("a".into()),
951 ZipReadError::FileNotFound("a".into())
952 );
953 assert_ne!(
954 ZipReadError::FileTooLarge { path: "p".into(), size: 1, max_size: 2 },
955 ZipReadError::FileTooLarge { path: "p".into(), size: 1, max_size: 3 }
956 );
957 assert_eq!(
958 ZipWriteError::IoError("x".into()),
959 ZipWriteError::IoError("x".into())
960 );
961 assert_ne!(
962 ZipWriteError::IoError("x".into()),
963 ZipWriteError::InvalidPath("x".into())
964 );
965
966 let e = ZipReadError::FileTooLarge {
968 path: "p".into(),
969 size: u64::MAX,
970 max_size: 0,
971 };
972 assert_eq!(e.clone(), e);
973
974 #[cfg(feature = "std")]
975 {
976 let r: &dyn std::error::Error = &e;
977 assert!(!r.to_string().is_empty());
978 let w = ZipWriteError::IoError("x".into());
979 let r: &dyn std::error::Error = &w;
980 assert!(!r.to_string().is_empty());
981 }
982 }
983
984 #[test]
989 fn autotest_zipfile_new_and_default_are_empty() {
990 let a = ZipFile::new();
991 let b = ZipFile::default();
992 assert!(a.entries.is_empty());
993 assert!(b.entries.is_empty());
994 assert!(a.paths().is_empty());
995 assert!(a.filter_by_suffix("").is_empty());
996 assert!(a.filter_by_suffix(".txt").is_empty());
997 assert!(a.get("").is_none());
998 assert!(!a.contains(""));
999
1000 for p in nasty_paths() {
1002 assert!(a.get(&p).is_none());
1003 assert!(!a.contains(&p));
1004 }
1005
1006 let mut c = ZipFile::new();
1008 c.remove("nope");
1009 c.remove("");
1010 assert!(c.entries.is_empty());
1011 }
1012
1013 #[test]
1014 fn autotest_add_file_dedup_keeps_last_write() {
1015 let mut zip = ZipFile::new();
1016 zip.add_file("a", b"1".to_vec());
1017 zip.add_file("b", b"2".to_vec());
1018 zip.add_file("a", b"3".to_vec());
1019 assert_eq!(zip.entries.len(), 2);
1020 assert_eq!(zip.get("a").unwrap().data, b"3");
1021 assert_eq!(zip.paths(), vec!["b", "a"]);
1023
1024 for i in 0..100u32 {
1026 zip.add_file("a", format!("{i}").into_bytes());
1027 }
1028 assert_eq!(zip.entries.len(), 2);
1029 assert_eq!(zip.get("a").unwrap().data, b"99");
1030 }
1031
1032 #[test]
1033 fn autotest_add_directory_and_add_file_share_the_path_namespace() {
1034 let mut zip = ZipFile::new();
1035 zip.add_file("x", b"data".to_vec());
1036 assert!(!zip.get("x").unwrap().is_directory);
1037
1038 zip.add_directory("x");
1040 assert_eq!(zip.entries.len(), 1);
1041 assert!(zip.get("x").unwrap().is_directory);
1042 assert!(zip.get("x").unwrap().data.is_empty());
1043
1044 zip.add_file("x", b"back".to_vec());
1046 assert_eq!(zip.entries.len(), 1);
1047 assert!(!zip.get("x").unwrap().is_directory);
1048 assert_eq!(zip.get("x").unwrap().data, b"back");
1049
1050 zip.add_directory("x/");
1052 assert_eq!(zip.entries.len(), 2);
1053 assert!(zip.contains("x"));
1054 assert!(zip.contains("x/"));
1055 }
1056
1057 #[test]
1058 fn autotest_add_and_remove_adversarial_paths_no_panic() {
1059 let mut zip = ZipFile::new();
1060 let paths = nasty_paths();
1061 for (i, p) in paths.iter().enumerate() {
1062 zip.add_file(p.clone(), vec![u8::try_from(i % 256).unwrap()]);
1063 }
1064 assert_eq!(zip.entries.len(), paths.len());
1066 for p in &paths {
1067 assert!(zip.contains(p), "lost path {p:?}");
1068 assert!(zip.get(p).is_some());
1069 }
1070 for p in &paths {
1071 zip.remove(p);
1072 assert!(!zip.contains(p));
1073 }
1074 assert!(zip.entries.is_empty());
1075
1076 let mut zip = ZipFile::new();
1078 zip.add_file("dir/file.txt", b"d".to_vec());
1079 zip.remove("dir/");
1080 zip.remove("file.txt");
1081 zip.remove("dir/file.tx");
1082 zip.remove("dir/file.txt ");
1083 assert_eq!(zip.entries.len(), 1, "remove() must match the whole path only");
1084 zip.remove("dir/file.txt");
1085 assert!(zip.entries.is_empty());
1086 }
1087
1088 #[test]
1089 fn autotest_get_and_contains_agree_and_reject_junk() {
1090 let mut zip = ZipFile::new();
1091 zip.add_file("a.txt", b"A".to_vec());
1092 zip.add_file("\u{1F600}.txt", b"E".to_vec());
1093 zip.add_directory("sub/");
1094
1095 assert!(zip.contains("a.txt"));
1097 assert!(zip.contains("\u{1F600}.txt"));
1098 assert!(zip.contains("sub/"));
1099
1100 for p in [
1102 " a.txt", "a.txt ", "A.TXT", "a.txt\0", "./a.txt", "/a.txt", "a.txt;x", "sub", "sub//",
1103 "\u{1F600}", "\u{1F600}.TXT",
1104 ] {
1105 assert!(!zip.contains(p), "unexpected match for {p:?}");
1106 assert!(zip.get(p).is_none());
1107 }
1108
1109 for p in nasty_paths() {
1111 assert_eq!(zip.get(&p).is_some(), zip.contains(&p), "disagree on {p:?}");
1112 }
1113
1114 let huge = "y".repeat(1_000_000);
1116 assert!(zip.get(&huge).is_none());
1117 assert!(!zip.contains(&huge));
1118 }
1119
1120 #[test]
1121 fn autotest_paths_mirrors_entries_in_order() {
1122 let mut zip = ZipFile::new();
1123 assert!(zip.paths().is_empty());
1124
1125 for i in 0..50u32 {
1126 zip.add_file(format!("f{i}"), vec![u8::try_from(i).unwrap()]);
1127 }
1128 zip.add_directory("d/");
1129
1130 let paths = zip.paths();
1131 assert_eq!(paths.len(), zip.entries.len());
1132 for (p, e) in paths.iter().zip(zip.entries.iter()) {
1133 assert_eq!(*p, e.path.as_str());
1134 }
1135 assert!(paths.contains(&"d/"));
1137
1138 let dup = ZipFile {
1140 entries: vec![
1141 ZipFileEntry::file("same", b"1".to_vec()),
1142 ZipFileEntry::file("same", b"2".to_vec()),
1143 ],
1144 };
1145 assert_eq!(dup.paths(), vec!["same", "same"]);
1146 assert_eq!(dup.get("same").unwrap().data, b"1");
1148 assert!(dup.contains("same"));
1149 let mut dup = dup;
1151 dup.remove("same");
1152 assert!(dup.entries.is_empty());
1153 }
1154
1155 #[test]
1156 fn autotest_filter_by_suffix_edge_cases() {
1157 let zip = ZipFile {
1158 entries: vec![
1159 ZipFileEntry::file("a.txt", b"1".to_vec()),
1160 ZipFileEntry::file("b.TXT", b"2".to_vec()),
1161 ZipFileEntry::file("README", b"3".to_vec()),
1162 ZipFileEntry::file("", b"4".to_vec()),
1163 ZipFileEntry::file("\u{1F600}.json", b"5".to_vec()),
1164 ZipFileEntry::directory("dir.txt"),
1165 ZipFileEntry::directory("sub/"),
1166 ],
1167 };
1168
1169 assert_eq!(zip.filter_by_suffix("").len(), 5);
1171 assert!(zip.filter_by_suffix("").iter().all(|e| !e.is_directory));
1172
1173 let txt = zip.filter_by_suffix(".txt");
1175 assert_eq!(txt.len(), 1);
1176 assert_eq!(txt[0].path, "a.txt");
1177
1178 assert_eq!(zip.filter_by_suffix(".TXT").len(), 1);
1180 assert_eq!(zip.filter_by_suffix(".Txt").len(), 0);
1181
1182 assert_eq!(zip.filter_by_suffix("README").len(), 1);
1184
1185 assert_eq!(zip.filter_by_suffix("\u{1F600}.json").len(), 1);
1187 assert_eq!(zip.filter_by_suffix("json").len(), 1);
1188
1189 assert!(zip.filter_by_suffix(&"n".repeat(100_000)).is_empty());
1191 assert!(zip.filter_by_suffix("\0").is_empty());
1193 assert!(zip.filter_by_suffix(" ").is_empty());
1194 }
1195
1196 #[cfg(feature = "zip")]
1201 #[test]
1202 fn autotest_readers_reject_empty_and_garbage_without_panicking() {
1203 let cfg = ZipReadConfig::default();
1204
1205 let inputs: Vec<Vec<u8>> = vec![
1206 Vec::new(),
1207 b" ".to_vec(),
1208 b"\t\n\r ".to_vec(),
1209 b"not a zip file at all".to_vec(),
1210 vec![0u8; 22],
1211 vec![0xFF, 0xFE, 0x00],
1212 vec![0xC3, 0x28, 0xA0, 0xA1], b"PK".to_vec(), b"PK\x03\x04".to_vec(), b"PK\x05\x06".to_vec(), b"0 -0 NaN inf 9223372036854775807".to_vec(),
1217 "\u{1F600}\u{0301}".as_bytes().to_vec(), b"[".repeat(10_000), b"PK\x05\x06".repeat(5_000), ];
1221
1222 for data in inputs {
1223 let listed = ZipFile::list(&data, &cfg);
1224 let loaded = ZipFile::from_bytes(&data, &cfg);
1225 let extracted = zip_extract_all(&data, &cfg);
1226 let contents = zip_list_contents(&data, &cfg);
1227
1228 assert_eq!(loaded.is_err(), extracted.is_err());
1230 assert_eq!(listed.is_err(), contents.is_err());
1231
1232 match loaded {
1233 Err(e) => {
1234 assert!(
1237 matches!(
1238 e,
1239 ZipReadError::InvalidFormat(_) | ZipReadError::IoError(_)
1240 ),
1241 "unexpected error kind for {:?}: {e:?}",
1242 &data[..data.len().min(8)]
1243 );
1244 assert!(!e.to_string().is_empty());
1245 }
1246 Ok(z) => assert!(z.entries.is_empty()),
1248 }
1249 }
1250
1251 assert!(matches!(
1253 ZipFile::from_bytes(b"", &cfg),
1254 Err(ZipReadError::InvalidFormat(_))
1255 ));
1256 assert!(matches!(
1257 ZipFile::list(b"", &cfg),
1258 Err(ZipReadError::InvalidFormat(_))
1259 ));
1260 }
1261
1262 #[cfg(feature = "zip")]
1263 #[test]
1264 fn autotest_readers_handle_one_megabyte_of_junk() {
1265 let cfg = ZipReadConfig::default();
1266 let junk = vec![b'A'; 1_000_000];
1268 assert!(ZipFile::from_bytes(&junk, &cfg).is_err());
1269 assert!(ZipFile::list(&junk, &cfg).is_err());
1270
1271 let zeros = vec![0u8; 1_000_000];
1273 assert!(ZipFile::from_bytes(&zeros, &cfg).is_err());
1274
1275 let mut fake = vec![b'B'; 1_000_000];
1277 fake.extend_from_slice(&[0x50, 0x4B, 0x05, 0x06]);
1278 fake.extend_from_slice(&[0xFFu8; 18]);
1279 let res = ZipFile::from_bytes(&fake, &cfg);
1280 assert!(
1281 res.map_or(true, |z| z.entries.is_empty()),
1282 "a bogus EOCD must not yield phantom entries"
1283 );
1284 }
1285
1286 #[cfg(feature = "zip")]
1287 #[test]
1288 fn autotest_minimal_valid_archives_parse_as_empty() {
1289 let cfg = ZipReadConfig::default();
1290
1291 let own = ZipFile::new()
1293 .to_bytes(&ZipWriteConfig::default())
1294 .expect("empty archive must be writable");
1295 let round = ZipFile::from_bytes(&own, &cfg).expect("own empty archive must re-read");
1296 assert!(round.entries.is_empty());
1297 assert!(ZipFile::list(&own, &cfg).unwrap().is_empty());
1298
1299 assert!(ZipFile::new().to_bytes(&ZipWriteConfig::store()).is_ok());
1301
1302 let eocd = eocd_only();
1304 assert_eq!(eocd.len(), 22);
1305 if let Ok(z) = ZipFile::from_bytes(&eocd, &cfg) {
1306 assert!(z.entries.is_empty());
1307 }
1308 }
1309
1310 #[cfg(feature = "zip")]
1311 #[test]
1312 fn autotest_truncated_and_bitflipped_archives_never_panic() {
1313 let cfg = ZipReadConfig::default();
1314 let good = build(vec![
1315 ZipFileEntry::file("a.txt", b"hello hello hello hello".to_vec()),
1316 ZipFileEntry::file("b.bin", vec![7u8; 512]),
1317 ]);
1318 assert!(ZipFile::from_bytes(&good, &cfg).is_ok());
1319
1320 for cut in [0, 1, 3, 4, 10, good.len() / 4, good.len() / 2, good.len() - 1] {
1322 let _ = ZipFile::from_bytes(&good[..cut], &cfg);
1323 let _ = ZipFile::list(&good[..cut], &cfg);
1324 }
1325
1326 for i in (0..good.len()).step_by(7) {
1328 let mut bad = good.clone();
1329 bad[i] ^= 0xFF;
1330 let _ = ZipFile::from_bytes(&bad, &cfg);
1331 let _ = ZipFile::list(&bad, &cfg);
1332 }
1333
1334 let mut trailing = good.clone();
1336 trailing.extend_from_slice(b"garbage;garbage");
1337 let _ = ZipFile::from_bytes(&trailing, &cfg);
1338
1339 let mut leading = b"JUNK".to_vec();
1341 leading.extend_from_slice(&good);
1342 let _ = ZipFile::from_bytes(&leading, &cfg);
1343 }
1344
1345 #[cfg(feature = "zip")]
1350 #[test]
1351 fn autotest_roundtrip_all_byte_values_and_empty_files() {
1352 let all_bytes: Vec<u8> = (0..=255u8).collect();
1353 let entries = vec![
1354 ZipFileEntry::file("bytes.bin", all_bytes.clone()),
1355 ZipFileEntry::file("empty.bin", Vec::new()),
1356 ZipFileEntry::file("one.bin", vec![0u8]),
1357 ];
1358 let bytes = build(entries);
1359 let cfg = ZipReadConfig::default();
1360 let round = ZipFile::from_bytes(&bytes, &cfg).unwrap();
1361
1362 assert_eq!(round.entries.len(), 3);
1363 assert_eq!(round.paths(), vec!["bytes.bin", "empty.bin", "one.bin"]);
1364 assert_eq!(round.get("bytes.bin").unwrap().data, all_bytes);
1365 assert!(round.get("empty.bin").unwrap().data.is_empty());
1366 assert_eq!(round.get("one.bin").unwrap().data, vec![0u8]);
1367 assert!(round.entries.iter().all(|e| !e.is_directory));
1368
1369 let again = round.to_bytes(&ZipWriteConfig::default()).unwrap();
1371 let round2 = ZipFile::from_bytes(&again, &cfg).unwrap();
1372 assert_eq!(round2.paths(), round.paths());
1373 for e in &round.entries {
1374 assert_eq!(round2.get(&e.path).unwrap().data, e.data);
1375 }
1376 }
1377
1378 #[cfg(feature = "zip")]
1379 #[test]
1380 fn autotest_roundtrip_unicode_paths_and_content() {
1381 let paths = [
1382 "\u{1F600}.txt",
1383 "e\u{0301}\u{0301}combining.txt",
1384 "\u{4F60}\u{597D}/\u{4E16}\u{754C}.txt",
1385 "\u{FEFF}bom.txt",
1386 "spaces and\ttabs.txt",
1387 ];
1388 let entries: Vec<ZipFileEntry> = paths
1389 .iter()
1390 .enumerate()
1391 .map(|(i, p)| ZipFileEntry::file(*p, format!("payload \u{1F9F0} {i}").into_bytes()))
1392 .collect();
1393
1394 let bytes = build(entries);
1395 let round = ZipFile::from_bytes(&bytes, &ZipReadConfig::default()).unwrap();
1396 assert_eq!(round.entries.len(), paths.len());
1397 for (i, p) in paths.iter().enumerate() {
1398 let e = round
1399 .get(p)
1400 .unwrap_or_else(|| panic!("unicode path {p:?} was not preserved"));
1401 assert_eq!(e.data, format!("payload \u{1F9F0} {i}").into_bytes());
1402 }
1403 }
1404
1405 #[cfg(feature = "zip")]
1406 #[test]
1407 fn autotest_roundtrip_deep_paths_and_large_payload() {
1408 let deep = "a/".repeat(2000) + "leaf.txt";
1410 assert!(!deep.contains(".."));
1411 let big: Vec<u8> = (0..100_000u32).map(|i| u8::try_from(i % 251).unwrap()).collect();
1413
1414 let bytes = build(vec![
1415 ZipFileEntry::file(deep.clone(), b"leaf".to_vec()),
1416 ZipFileEntry::file("big.bin", big.clone()),
1417 ]);
1418 let round = ZipFile::from_bytes(&bytes, &ZipReadConfig::default()).unwrap();
1419 assert_eq!(round.get(&deep).unwrap().data, b"leaf");
1420 assert_eq!(round.get("big.bin").unwrap().data, big);
1421
1422 let listed = ZipFile::list(&bytes, &ZipReadConfig::default()).unwrap();
1424 let big_meta = listed.iter().find(|e| e.path == "big.bin").unwrap();
1425 assert_eq!(big_meta.size, 100_000);
1426 assert!(!big_meta.is_directory);
1427 }
1428
1429 #[cfg(feature = "zip")]
1430 #[test]
1431 fn autotest_roundtrip_directory_entries_get_a_trailing_slash() {
1432 let bytes = build(vec![
1433 ZipFileEntry::directory("with_slash/"),
1434 ZipFileEntry::directory("no_slash"),
1435 ZipFileEntry::file("f.txt", b"x".to_vec()),
1436 ]);
1437 let round = ZipFile::from_bytes(&bytes, &ZipReadConfig::default()).unwrap();
1438 assert_eq!(round.entries.len(), 3);
1439
1440 let with = round.get("with_slash/").expect("dir with slash preserved");
1441 assert!(with.is_directory);
1442 assert!(with.data.is_empty());
1443
1444 assert!(round.get("no_slash").is_none());
1447 let without = round.get("no_slash/").expect("dir without slash was rewritten");
1448 assert!(without.is_directory);
1449
1450 assert!(!round.get("f.txt").unwrap().is_directory);
1451
1452 let listed = ZipFile::list(&bytes, &ZipReadConfig::default()).unwrap();
1454 assert_eq!(listed.len(), 3);
1455 assert_eq!(listed.iter().filter(|e| e.is_directory).count(), 2);
1456 for d in listed.iter().filter(|e| e.is_directory) {
1457 assert_eq!(d.size, 0);
1458 assert!(d.path.ends_with('/'));
1459 }
1460 }
1461
1462 #[cfg(feature = "zip")]
1463 #[test]
1464 fn autotest_roundtrip_survives_config_extremes() {
1465 let entries = || vec![ZipFileEntry::file("f.bin", vec![9u8; 3000])];
1466
1467 for level in 1..=9u8 {
1469 let cfg = ZipWriteConfig::deflate(level);
1470 let bytes = zip_create(entries(), &cfg)
1471 .unwrap_or_else(|e| panic!("deflate({level}) failed: {e}"));
1472 let round = ZipFile::from_bytes(&bytes, &ZipReadConfig::default()).unwrap();
1473 assert_eq!(round.get("f.bin").unwrap().data, vec![9u8; 3000]);
1474 }
1475 for level in [10u8, 100, u8::MAX] {
1477 let bytes = zip_create(entries(), &ZipWriteConfig::deflate(level)).unwrap();
1478 let round = ZipFile::from_bytes(&bytes, &ZipReadConfig::default()).unwrap();
1479 assert_eq!(round.get("f.bin").unwrap().data.len(), 3000);
1480 }
1481
1482 let mut cfg = ZipWriteConfig {
1484 unix_permissions: u32::MAX,
1485 ..Default::default()
1486 };
1487 let bytes = zip_create(entries(), &cfg).unwrap();
1488 assert_eq!(
1489 ZipFile::from_bytes(&bytes, &ZipReadConfig::default())
1490 .unwrap()
1491 .get("f.bin")
1492 .unwrap()
1493 .data
1494 .len(),
1495 3000
1496 );
1497 cfg.unix_permissions = 0;
1498 assert!(zip_create(entries(), &cfg).is_ok());
1499
1500 let cfg = ZipWriteConfig::default().with_comment("\u{1F5DC}\u{FE0F} t\u{E9}st comment");
1502 let bytes = zip_create(entries(), &cfg).unwrap();
1503 let round = ZipFile::from_bytes(&bytes, &ZipReadConfig::default()).unwrap();
1504 assert_eq!(round.entries.len(), 1);
1505
1506 let cfg = ZipWriteConfig::default().with_comment("c".repeat(70_000));
1508 let _ = zip_create(entries(), &cfg);
1509 }
1510
1511 #[cfg(feature = "zip")]
1512 #[test]
1513 fn autotest_convenience_functions_agree_with_methods() {
1514 let files = vec![
1515 ("a.txt".to_string(), b"AAA".to_vec()),
1516 ("dir/b.bin".to_string(), vec![0u8, 255, 128]),
1517 ];
1518 let cfg = ZipWriteConfig::default();
1519 let via_files = zip_create_from_files(files.clone(), &cfg).unwrap();
1520 let via_entries = zip_create(
1521 files
1522 .iter()
1523 .map(|(p, d)| ZipFileEntry::file(p.clone(), d.clone()))
1524 .collect(),
1525 &cfg,
1526 )
1527 .unwrap();
1528 let via_method = ZipFile {
1529 entries: files
1530 .iter()
1531 .map(|(p, d)| ZipFileEntry::file(p.clone(), d.clone()))
1532 .collect(),
1533 }
1534 .to_bytes(&cfg)
1535 .unwrap();
1536
1537 let rcfg = ZipReadConfig::default();
1538 for bytes in [&via_files, &via_entries, &via_method] {
1539 let extracted = zip_extract_all(bytes, &rcfg).unwrap();
1540 let loaded = ZipFile::from_bytes(bytes, &rcfg).unwrap();
1541 assert_eq!(extracted.len(), 2);
1542 assert_eq!(loaded.entries.len(), 2);
1543 for (i, (p, d)) in files.iter().enumerate() {
1544 assert_eq!(&extracted[i].path, p);
1545 assert_eq!(&extracted[i].data, d);
1546 assert_eq!(&loaded.entries[i].path, p);
1547 }
1548
1549 let listed = zip_list_contents(bytes, &rcfg).unwrap();
1551 let listed2 = ZipFile::list(bytes, &rcfg).unwrap();
1552 assert_eq!(listed.len(), listed2.len());
1553 for (a, b) in listed.iter().zip(listed2.iter()) {
1554 assert_eq!(a.path, b.path);
1555 assert_eq!(a.size, b.size);
1556 assert_eq!(a.compressed_size, b.compressed_size);
1557 assert_eq!(a.crc32, b.crc32);
1558 assert_eq!(a.is_directory, b.is_directory);
1559 }
1560 for (meta, (p, d)) in listed.iter().zip(files.iter()) {
1561 assert_eq!(&meta.path, p);
1562 assert_eq!(meta.size, d.len() as u64);
1563 assert!(!meta.is_directory);
1564 }
1565 }
1566
1567 let empty = zip_create_from_files(Vec::new(), &cfg).unwrap();
1569 assert!(zip_extract_all(&empty, &rcfg).unwrap().is_empty());
1570 assert!(zip_list_contents(&empty, &rcfg).unwrap().is_empty());
1571 }
1572
1573 #[cfg(feature = "zip")]
1578 #[test]
1579 fn autotest_path_traversal_check_is_a_plain_substring_test() {
1580 let bytes = build(vec![
1583 ZipFileEntry::file("a..b.txt", b"harmless".to_vec()),
1584 ZipFileEntry::file("ok.txt", b"ok".to_vec()),
1585 ]);
1586
1587 let strict = ZipReadConfig::default();
1588 match ZipFile::from_bytes(&bytes, &strict) {
1589 Err(ZipReadError::UnsafePath(p)) => assert_eq!(p, "a..b.txt"),
1590 other => panic!("expected UnsafePath, got {other:?}"),
1591 }
1592 match ZipFile::list(&bytes, &strict) {
1593 Err(ZipReadError::UnsafePath(p)) => assert_eq!(p, "a..b.txt"),
1594 other => panic!("expected UnsafePath from list(), got {other:?}"),
1595 }
1596
1597 let loose = ZipReadConfig::new().with_allow_path_traversal(true);
1599 let round = ZipFile::from_bytes(&bytes, &loose).unwrap();
1600 assert_eq!(round.entries.len(), 2);
1601 assert_eq!(round.get("a..b.txt").unwrap().data, b"harmless");
1602 assert_eq!(ZipFile::list(&bytes, &loose).unwrap().len(), 2);
1603
1604 let evil = build(vec![ZipFileEntry::file("../../etc/passwd", b"x".to_vec())]);
1606 assert!(matches!(
1607 ZipFile::from_bytes(&evil, &strict),
1608 Err(ZipReadError::UnsafePath(_))
1609 ));
1610 assert!(ZipFile::from_bytes(&evil, &loose).is_ok());
1611
1612 let dotted = build(vec![ZipFileEntry::file("./a.txt", b"x".to_vec())]);
1614 assert!(ZipFile::from_bytes(&dotted, &strict).is_ok());
1615 }
1616
1617 #[cfg(feature = "zip")]
1618 #[test]
1619 fn autotest_max_file_size_is_enforced_by_from_bytes_only() {
1620 let payload = vec![b'q'; 1000];
1621 let bytes = build(vec![ZipFileEntry::file("big.bin", payload.clone())]);
1622
1623 let unlimited = ZipReadConfig::new().with_max_file_size(0);
1625 assert_eq!(
1626 ZipFile::from_bytes(&bytes, &unlimited).unwrap().entries[0].data,
1627 payload
1628 );
1629
1630 let at = ZipReadConfig::new().with_max_file_size(1000);
1632 assert!(ZipFile::from_bytes(&bytes, &at).is_ok());
1633 let under = ZipReadConfig::new().with_max_file_size(999);
1634 match ZipFile::from_bytes(&bytes, &under) {
1635 Err(ZipReadError::FileTooLarge { path, size, max_size }) => {
1636 assert_eq!(path, "big.bin");
1637 assert_eq!(size, 1000);
1638 assert_eq!(max_size, 999);
1639 }
1640 other => panic!("expected FileTooLarge, got {other:?}"),
1641 }
1642 assert!(ZipFile::from_bytes(&bytes, &ZipReadConfig::new().with_max_file_size(1)).is_err());
1643 assert!(zip_extract_all(&bytes, &under).is_err());
1644
1645 let listed = ZipFile::list(&bytes, &under).unwrap();
1648 assert_eq!(listed.len(), 1);
1649 assert_eq!(listed[0].size, 1000);
1650 assert!(listed[0].compressed_size > 0);
1651 assert_eq!(zip_list_contents(&bytes, &under).unwrap().len(), 1);
1652 }
1653
1654 #[cfg(feature = "zip")]
1655 #[test]
1656 fn autotest_get_single_file_lookup_semantics() {
1657 let bytes = build(vec![
1658 ZipFileEntry::file("a.txt", b"AAA".to_vec()),
1659 ZipFileEntry::directory("sub/"),
1660 ]);
1661 let cfg = ZipReadConfig::default();
1662 let meta = ZipFile::list(&bytes, &cfg).unwrap();
1663
1664 let loaded = ZipFile::from_bytes(&bytes, &cfg).unwrap();
1666 for m in &meta {
1667 let got = ZipFile::get_single_file(&bytes, m, &cfg).unwrap();
1668 assert_eq!(got.as_deref(), Some(loaded.get(&m.path).unwrap().data.as_slice()));
1669 }
1670
1671 let dir = meta.iter().find(|m| m.is_directory).unwrap();
1673 assert_eq!(ZipFile::get_single_file(&bytes, dir, &cfg).unwrap(), Some(Vec::new()));
1674
1675 for p in nasty_paths() {
1677 let entry = ZipPathEntry {
1678 path: p.clone(),
1679 is_directory: false,
1680 size: 0,
1681 compressed_size: 0,
1682 crc32: 0,
1683 };
1684 assert_eq!(
1685 ZipFile::get_single_file(&bytes, &entry, &cfg).unwrap(),
1686 None,
1687 "expected None for {p:?}"
1688 );
1689 }
1690
1691 let entry = ZipPathEntry {
1693 path: "a.txt".into(),
1694 is_directory: false,
1695 size: 3,
1696 compressed_size: 3,
1697 crc32: 0,
1698 };
1699 for junk in [b"".as_slice(), b" ", b"nope", &[0xFF, 0xFE, 0x00]] {
1700 assert!(matches!(
1701 ZipFile::get_single_file(junk, &entry, &cfg),
1702 Err(ZipReadError::InvalidFormat(_))
1703 ));
1704 }
1705 }
1706
1707 #[cfg(feature = "zip")]
1708 #[test]
1709 fn autotest_get_single_file_size_check_runs_before_parsing() {
1710 let cfg = ZipReadConfig::new().with_max_file_size(10);
1713 let entry = ZipPathEntry {
1714 path: "x".into(),
1715 is_directory: false,
1716 size: 11,
1717 compressed_size: 0,
1718 crc32: 0,
1719 };
1720 match ZipFile::get_single_file(b"total garbage", &entry, &cfg) {
1721 Err(ZipReadError::FileTooLarge { path, size, max_size }) => {
1722 assert_eq!(path, "x");
1723 assert_eq!(size, 11);
1724 assert_eq!(max_size, 10);
1725 }
1726 other => panic!("expected FileTooLarge before parsing, got {other:?}"),
1727 }
1728
1729 let at_limit = ZipPathEntry { size: 10, ..entry.clone() };
1731 assert!(matches!(
1732 ZipFile::get_single_file(b"total garbage", &at_limit, &cfg),
1733 Err(ZipReadError::InvalidFormat(_))
1734 ));
1735
1736 let unlimited = ZipReadConfig::default();
1738 let huge = ZipPathEntry { size: u64::MAX, ..entry };
1739 assert!(matches!(
1740 ZipFile::get_single_file(b"total garbage", &huge, &unlimited),
1741 Err(ZipReadError::InvalidFormat(_))
1742 ));
1743 }
1744
1745 #[cfg(feature = "zip")]
1746 #[test]
1747 fn autotest_get_single_file_trusts_the_callers_metadata() {
1748 let payload = vec![b'z'; 5000];
1752 let bytes = build(vec![ZipFileEntry::file("big.bin", payload.clone())]);
1753
1754 let capped = ZipReadConfig::new().with_max_file_size(10);
1755 let liar = ZipPathEntry {
1756 path: "big.bin".into(),
1757 is_directory: false,
1758 size: 0, compressed_size: 0,
1760 crc32: 0,
1761 };
1762 let got = ZipFile::get_single_file(&bytes, &liar, &capped).unwrap();
1763 assert_eq!(
1764 got,
1765 Some(payload),
1766 "the 10-byte cap was bypassed by a lying entry.size"
1767 );
1768 assert!(matches!(
1770 ZipFile::from_bytes(&bytes, &capped),
1771 Err(ZipReadError::FileTooLarge { .. })
1772 ));
1773
1774 let bytes = build(vec![ZipFileEntry::file("../evil.txt", b"pwned".to_vec())]);
1777 let strict = ZipReadConfig::default();
1778 assert!(matches!(
1779 ZipFile::from_bytes(&bytes, &strict),
1780 Err(ZipReadError::UnsafePath(_))
1781 ));
1782 let entry = ZipPathEntry {
1783 path: "../evil.txt".into(),
1784 is_directory: false,
1785 size: 5,
1786 compressed_size: 5,
1787 crc32: 0,
1788 };
1789 assert_eq!(
1790 ZipFile::get_single_file(&bytes, &entry, &strict).unwrap(),
1791 Some(b"pwned".to_vec()),
1792 "get_single_file has no UnsafePath guard"
1793 );
1794 }
1795
1796 #[cfg(all(feature = "zip", target_pointer_width = "64"))]
1802 #[test]
1803 #[should_panic]
1804 fn autotest_bug_get_single_file_capacity_overflow_on_declared_size() {
1805 let bytes = build(vec![ZipFileEntry::file("a.txt", b"AAA".to_vec())]);
1806 let entry = ZipPathEntry {
1807 path: "a.txt".into(),
1808 is_directory: false,
1809 size: u64::MAX, compressed_size: 3,
1811 crc32: 0,
1812 };
1813 let _ = ZipFile::get_single_file(&bytes, &entry, &ZipReadConfig::default());
1814 }
1815
1816 #[cfg(feature = "zip")]
1825 #[test]
1826 fn autotest_bug_store_config_cannot_write_file_entries() {
1827 let cfg = ZipWriteConfig::store();
1828 let err = zip_create(vec![ZipFileEntry::file("a.txt", b"A".to_vec())], &cfg)
1829 .expect_err("store() unexpectedly produced an archive");
1830 assert!(
1831 err.to_string().contains("compression level"),
1832 "unexpected error for store(): {err}"
1833 );
1834 assert!(matches!(err, ZipWriteError::IoError(_)));
1835
1836 assert!(ZipFile::new().to_bytes(&cfg).is_ok());
1839
1840 let mut deflate_ish = ZipWriteConfig::store();
1842 deflate_ish.compression_method = 2;
1843 deflate_ish.compression_level = 6;
1844 assert!(zip_create(vec![ZipFileEntry::file("a.txt", b"A".to_vec())], &deflate_ish).is_ok());
1845 }
1846
1847 #[cfg(feature = "zip")]
1851 #[test]
1852 fn autotest_bug_deflate_level_zero_is_unwritable() {
1853 let cfg = ZipWriteConfig::deflate(0);
1854 assert_eq!(cfg.compression_level, 0, "builder accepted level 0");
1855 let err = zip_create(vec![ZipFileEntry::file("a.txt", b"A".to_vec())], &cfg)
1856 .expect_err("deflate(0) unexpectedly produced an archive");
1857 assert!(
1858 err.to_string().contains("compression level"),
1859 "unexpected error for deflate(0): {err}"
1860 );
1861 assert!(zip_create(vec![ZipFileEntry::file("a.txt", b"A".to_vec())], &ZipWriteConfig::deflate(1)).is_ok());
1863 }
1864
1865 #[cfg(feature = "zip")]
1866 #[test]
1867 fn autotest_duplicate_paths_make_the_archive_unwritable() {
1868 let cfg = ZipWriteConfig::default();
1871 let err = zip_create(
1872 vec![
1873 ZipFileEntry::file("dup.txt", b"1".to_vec()),
1874 ZipFileEntry::file("dup.txt", b"2".to_vec()),
1875 ],
1876 &cfg,
1877 )
1878 .expect_err("duplicate paths unexpectedly accepted");
1879 assert!(matches!(err, ZipWriteError::IoError(_)));
1880 assert!(!err.to_string().is_empty());
1881
1882 assert!(zip_create_from_files(
1884 vec![
1885 ("d".to_string(), b"1".to_vec()),
1886 ("d".to_string(), b"2".to_vec()),
1887 ],
1888 &cfg
1889 )
1890 .is_err());
1891
1892 let mut zip = ZipFile::new();
1894 zip.add_file("dup.txt", b"1".to_vec());
1895 zip.add_file("dup.txt", b"2".to_vec());
1896 let bytes = zip.to_bytes(&cfg).unwrap();
1897 assert_eq!(
1898 ZipFile::from_bytes(&bytes, &ZipReadConfig::default())
1899 .unwrap()
1900 .get("dup.txt")
1901 .unwrap()
1902 .data,
1903 b"2"
1904 );
1905 }
1906
1907 #[cfg(feature = "zip")]
1908 #[test]
1909 fn autotest_to_bytes_with_hostile_paths_never_panics() {
1910 let cfg = ZipWriteConfig::default();
1911 let loose = ZipReadConfig::new().with_allow_path_traversal(true);
1912 for p in nasty_paths().into_iter().filter(|p| p.len() < 60_000) {
1921 if let Ok(bytes) = zip_create(vec![ZipFileEntry::file(p.clone(), b"x".to_vec())], &cfg)
1922 {
1923 let _ = ZipFile::from_bytes(&bytes, &loose);
1925 }
1926 if let Ok(bytes) = zip_create(vec![ZipFileEntry::directory(p)], &cfg) {
1927 let _ = ZipFile::from_bytes(&bytes, &loose);
1928 }
1929 }
1930
1931 let long = "L".repeat(60_000);
1933 let bytes = zip_create(vec![ZipFileEntry::file(long.clone(), b"x".to_vec())], &cfg)
1934 .expect("60_000-byte path must be writable");
1935 assert_eq!(
1936 ZipFile::from_bytes(&bytes, &loose).unwrap().get(&long).unwrap().data,
1937 b"x"
1938 );
1939 }
1940
1941 #[cfg(all(feature = "zip", feature = "std"))]
1946 #[test]
1947 fn autotest_from_file_missing_path_is_io_error() {
1948 let cfg = ZipReadConfig::default();
1949 for p in [
1950 "/nonexistent_dir_azul_autotest_zip/sub/archive.zip",
1951 "",
1952 "/nonexistent_dir_azul_autotest_zip/\u{1F600}.zip",
1953 ] {
1954 match ZipFile::from_file(std::path::Path::new(p), &cfg) {
1955 Err(ZipReadError::IoError(msg)) => assert!(!msg.is_empty()),
1956 other => panic!("expected IoError for {p:?}, got {other:?}"),
1957 }
1958 }
1959
1960 let tmp = std::env::temp_dir();
1962 assert!(ZipFile::from_file(&tmp, &cfg).is_err());
1963 }
1964
1965 #[cfg(all(feature = "zip", feature = "std"))]
1966 #[test]
1967 fn autotest_to_file_unwritable_path_is_io_error() {
1968 let mut zip = ZipFile::new();
1969 zip.add_file("a.txt", b"A".to_vec());
1970 let cfg = ZipWriteConfig::default();
1971 match zip.to_file(
1972 std::path::Path::new("/nonexistent_dir_azul_autotest_zip/sub/out.zip"),
1973 &cfg,
1974 ) {
1975 Err(ZipWriteError::IoError(msg)) => assert!(!msg.is_empty()),
1976 other => panic!("expected IoError, got {other:?}"),
1977 }
1978
1979 let store = ZipWriteConfig::store();
1981 assert!(zip.to_file(std::path::Path::new("/nonexistent_dir_azul_autotest_zip/x.zip"), &store).is_err());
1982 }
1983
1984 #[cfg(all(feature = "zip", feature = "std"))]
1985 #[test]
1986 fn autotest_file_roundtrip_via_temp_dir() {
1987 let mut zip = ZipFile::new();
1988 zip.add_file("a.txt", b"AAA".to_vec());
1989 zip.add_file("\u{1F600}/b.bin", vec![0u8, 255, 128]);
1990 zip.add_directory("d/");
1991
1992 let path = std::env::temp_dir().join(format!(
1993 "azul_autotest_zip_roundtrip_{}.zip",
1994 std::process::id()
1995 ));
1996 let _ = std::fs::remove_file(&path);
1997
1998 match zip.to_file(&path, &ZipWriteConfig::default()) {
1999 Ok(()) => {
2000 let round = ZipFile::from_file(&path, &ZipReadConfig::default())
2001 .expect("archive written by to_file must be readable");
2002 assert_eq!(round.entries.len(), 3);
2003 assert_eq!(round.get("a.txt").unwrap().data, b"AAA");
2004 assert_eq!(round.get("\u{1F600}/b.bin").unwrap().data, vec![0u8, 255, 128]);
2005 assert!(round.get("d/").unwrap().is_directory);
2006 let in_memory = zip.to_bytes(&ZipWriteConfig::default()).unwrap();
2008 let on_disk = std::fs::read(&path).unwrap();
2009 assert_eq!(in_memory.len(), on_disk.len());
2010 let _ = std::fs::remove_file(&path);
2011 }
2012 Err(ZipWriteError::IoError(_)) => {
2013 }
2015 Err(other) => panic!("unexpected write error: {other:?}"),
2016 }
2017 }
2018}