1use std::io::Read;
114use std::io::Seek;
115use std::path::Path;
116use std::path::PathBuf;
117use std::time::Instant;
118
119use zip::ZipArchive as ZipReader;
120
121use crate::ExtractionError;
122use crate::ExtractionReport;
123use crate::Result;
124use crate::SecurityConfig;
125use crate::copy::CopyBuffer;
126use crate::security::EntryValidator;
127use crate::security::validator::ValidatedEntryType;
128use crate::types::DestDir;
129use crate::types::EntryType;
130
131use super::common;
132use super::traits::ArchiveFormat;
133
134pub struct ZipArchive<R: Read + Seek> {
170 inner: ZipReader<R>,
171}
172
173impl<R: Read + Seek> ZipArchive<R> {
174 pub fn new(reader: R) -> Result<Self> {
197 let mut inner = ZipReader::new(reader).map_err(|e| {
198 ExtractionError::InvalidArchive(format!("failed to open ZIP archive: {e}"))
199 })?;
200
201 if Self::is_password_protected(&mut inner)? {
203 return Err(ExtractionError::SecurityViolation {
204 reason: "password-protected ZIP archives are not supported".into(),
205 });
206 }
207
208 Ok(Self { inner })
209 }
210
211 fn is_password_protected(archive: &mut ZipReader<R>) -> Result<bool> {
217 const SAMPLE_SIZE: usize = 100;
218 let total_entries = archive.len();
219
220 if total_entries <= SAMPLE_SIZE * 3 {
221 for i in 0..total_entries {
222 if Self::check_entry_encrypted(archive, i)? {
223 return Ok(true);
224 }
225 }
226 return Ok(false);
227 }
228
229 for i in 0..SAMPLE_SIZE {
231 if Self::check_entry_encrypted(archive, i)? {
232 return Ok(true);
233 }
234 }
235
236 let middle_start = (total_entries / 2).saturating_sub(SAMPLE_SIZE / 2);
238 let middle_end = middle_start + SAMPLE_SIZE;
239 for i in middle_start..middle_end.min(total_entries) {
240 if Self::check_entry_encrypted(archive, i)? {
241 return Ok(true);
242 }
243 }
244
245 let tail_start = total_entries.saturating_sub(SAMPLE_SIZE);
247 if tail_start > middle_end {
248 for i in tail_start..total_entries {
249 if Self::check_entry_encrypted(archive, i)? {
250 return Ok(true);
251 }
252 }
253 }
254
255 Ok(false)
256 }
257
258 #[inline]
259 fn check_entry_encrypted(archive: &mut ZipReader<R>, index: usize) -> Result<bool> {
260 let file = archive.by_index(index).map_err(|e| {
261 ExtractionError::InvalidArchive(format!(
262 "failed to check entry {index} for encryption: {e}"
263 ))
264 })?;
265
266 Ok(file.encrypted())
267 }
268
269 fn process_entry(
271 &mut self,
272 index: usize,
273 validator: &mut EntryValidator,
274 dest: &DestDir,
275 report: &mut ExtractionReport,
276 copy_buffer: &mut CopyBuffer,
277 dir_cache: &mut common::DirCache,
278 ) -> Result<()> {
279 let (path, entry_type, uncompressed_size, compressed_size, mode) = {
281 let mut zip_file = self.inner.by_index(index).map_err(|e| {
282 ExtractionError::InvalidArchive(format!("failed to read entry {index}: {e}"))
283 })?;
284
285 if zip_file.encrypted() {
286 return Err(ExtractionError::SecurityViolation {
287 reason: format!("encrypted entry detected: {}", zip_file.name()),
288 });
289 }
290
291 let path = PathBuf::from(zip_file.name());
294 let (uncompressed_size, compressed_size) = ZipEntryAdapter::get_sizes(&zip_file);
295 let mode = zip_file.unix_mode();
296
297 let entry_type = ZipEntryAdapter::to_entry_type(&mut zip_file)?;
298
299 let compression = ZipEntryAdapter::get_compression_method(&zip_file);
300 if matches!(compression, CompressionMethod::Unsupported) {
301 return Err(ExtractionError::SecurityViolation {
302 reason: format!(
303 "unsupported compression method: {:?}",
304 zip_file.compression()
305 ),
306 });
307 }
308
309 (path, entry_type, uncompressed_size, compressed_size, mode)
310 };
311
312 let validated = validator.validate_entry(
313 &path,
314 &entry_type,
315 uncompressed_size,
316 Some(compressed_size),
317 mode,
318 )?;
319
320 match validated.entry_type {
321 ValidatedEntryType::File => {
322 let mut zip_file = self.inner.by_index(index).map_err(|e| {
323 ExtractionError::InvalidArchive(format!("failed to read entry {index}: {e}"))
324 })?;
325 Self::extract_file(
326 &mut zip_file,
327 &validated,
328 dest,
329 report,
330 uncompressed_size,
331 copy_buffer,
332 dir_cache,
333 )?;
334 }
335
336 ValidatedEntryType::Directory => {
337 common::create_directory(&validated, dest, report, dir_cache)?;
338 }
339
340 ValidatedEntryType::Symlink(safe_symlink) => {
341 common::create_symlink(&safe_symlink, dest, report, dir_cache)?;
342 }
343
344 ValidatedEntryType::Hardlink { .. } => {
345 return Err(ExtractionError::SecurityViolation {
346 reason: "hardlinks are not supported in ZIP format".into(),
347 });
348 }
349 }
350
351 Ok(())
352 }
353
354 fn extract_file(
356 zip_file: &mut zip::read::ZipFile<'_, R>,
357 validated: &crate::security::validator::ValidatedEntry,
358 dest: &DestDir,
359 report: &mut ExtractionReport,
360 file_size: u64,
361 copy_buffer: &mut CopyBuffer,
362 dir_cache: &mut common::DirCache,
363 ) -> Result<()> {
364 common::extract_file_generic(
365 zip_file,
366 validated,
367 dest,
368 report,
369 Some(file_size),
370 copy_buffer,
371 dir_cache,
372 )
373 }
374}
375
376impl<R: Read + Seek> ArchiveFormat for ZipArchive<R> {
377 fn extract(&mut self, output_dir: &Path, config: &SecurityConfig) -> Result<ExtractionReport> {
378 let start = Instant::now();
379
380 let dest = DestDir::new(output_dir.to_path_buf())?;
381
382 let mut validator = EntryValidator::new(config, &dest);
384
385 let mut report = ExtractionReport::new();
386
387 let mut copy_buffer = CopyBuffer::new();
389
390 let mut dir_cache = common::DirCache::new();
391
392 let entry_count = self.inner.len();
393
394 for i in 0..entry_count {
395 self.process_entry(
396 i,
397 &mut validator,
398 &dest,
399 &mut report,
400 &mut copy_buffer,
401 &mut dir_cache,
402 )?;
403 }
404
405 report.duration = start.elapsed();
406
407 Ok(report)
408 }
409
410 fn format_name(&self) -> &'static str {
411 "zip"
412 }
413}
414
415struct ZipEntryAdapter;
417
418impl ZipEntryAdapter {
419 fn to_entry_type<R: Read>(zip_file: &mut zip::read::ZipFile<'_, R>) -> Result<EntryType> {
424 if zip_file.is_dir() {
425 return Ok(EntryType::Directory);
426 }
427
428 if Self::is_symlink(zip_file) {
430 let target = Self::read_symlink_target(zip_file)?;
431 return Ok(EntryType::Symlink { target });
432 }
433
434 Ok(EntryType::File)
435 }
436
437 fn is_symlink<R: Read>(zip_file: &zip::read::ZipFile<'_, R>) -> bool {
439 zip_file.unix_mode().is_some_and(|mode| {
440 const S_IFMT: u32 = 0o170_000;
441 const S_IFLNK: u32 = 0o120_000;
442 (mode & S_IFMT) == S_IFLNK
443 })
444 }
445
446 fn read_symlink_target<R: Read>(zip_file: &mut zip::read::ZipFile<'_, R>) -> Result<PathBuf> {
448 const MAX_SYMLINK_TARGET_SIZE: u64 = 4096;
450
451 let size = zip_file.size();
452 if size > MAX_SYMLINK_TARGET_SIZE {
453 return Err(ExtractionError::SecurityViolation {
454 reason: format!(
455 "symlink target too large: {size} bytes (max {MAX_SYMLINK_TARGET_SIZE})"
456 ),
457 });
458 }
459
460 #[allow(clippy::cast_possible_truncation)]
463 let mut target_bytes = Vec::with_capacity(size as usize);
464 zip_file
465 .take(MAX_SYMLINK_TARGET_SIZE)
466 .read_to_end(&mut target_bytes)
467 .map_err(|e| {
468 ExtractionError::InvalidArchive(format!("failed to read symlink target: {e}"))
469 })?;
470
471 let target_str = std::str::from_utf8(&target_bytes).map_err(|_| {
472 ExtractionError::InvalidArchive("symlink target is not valid UTF-8".into())
473 })?;
474
475 Ok(PathBuf::from(target_str))
476 }
477
478 fn get_compression_method<R: Read>(zip_file: &zip::read::ZipFile<'_, R>) -> CompressionMethod {
480 match zip_file.compression() {
481 zip::CompressionMethod::Stored => CompressionMethod::Stored,
482 zip::CompressionMethod::Deflated => CompressionMethod::Deflate,
483 zip::CompressionMethod::Bzip2 => CompressionMethod::Bzip2,
484 zip::CompressionMethod::Zstd => CompressionMethod::Zstd,
485 _ => CompressionMethod::Unsupported,
486 }
487 }
488
489 fn get_sizes<R: Read>(zip_file: &zip::read::ZipFile<'_, R>) -> (u64, u64) {
491 (zip_file.size(), zip_file.compressed_size())
492 }
493}
494
495#[derive(Debug, Clone, Copy)]
497enum CompressionMethod {
498 Stored,
499 Deflate,
500 Bzip2,
501 Zstd,
502 Unsupported,
503}
504
505#[cfg(test)]
506#[allow(
507 clippy::unwrap_used,
508 clippy::expect_used,
509 clippy::items_after_statements,
510 clippy::uninlined_format_args,
511 clippy::field_reassign_with_default
512)]
513mod tests {
514 use super::*;
515 use crate::test_utils::create_test_zip;
516 use std::io::Cursor;
517 use std::io::Write;
518 use tempfile::TempDir;
519 use zip::write::SimpleFileOptions;
520 use zip::write::ZipWriter;
521
522 #[test]
523 fn test_zip_archive_new() {
524 let zip_data = create_test_zip(vec![]);
525 let cursor = Cursor::new(zip_data);
526 let archive = ZipArchive::new(cursor).unwrap();
527 assert_eq!(archive.format_name(), "zip");
528 }
529
530 #[test]
531 fn test_extract_empty_archive() {
532 let zip_data = create_test_zip(vec![]);
533 let cursor = Cursor::new(zip_data);
534 let mut archive = ZipArchive::new(cursor).unwrap();
535
536 let temp = TempDir::new().unwrap();
537 let config = SecurityConfig::default();
538
539 let report = archive.extract(temp.path(), &config).unwrap();
540
541 assert_eq!(report.files_extracted, 0);
542 assert_eq!(report.directories_created, 0);
543 }
544
545 #[test]
546 fn test_extract_simple_file() {
547 let zip_data = create_test_zip(vec![("file.txt", b"hello world")]);
548 let cursor = Cursor::new(zip_data);
549 let mut archive = ZipArchive::new(cursor).unwrap();
550
551 let temp = TempDir::new().unwrap();
552 let config = SecurityConfig::default();
553
554 let report = archive.extract(temp.path(), &config).unwrap();
555
556 assert_eq!(report.files_extracted, 1);
557 assert!(temp.path().join("file.txt").exists());
558
559 let content = std::fs::read_to_string(temp.path().join("file.txt")).unwrap();
560 assert_eq!(content, "hello world");
561 }
562
563 #[test]
564 fn test_extract_multiple_files() {
565 let zip_data = create_test_zip(vec![
566 ("file1.txt", b"content1"),
567 ("file2.txt", b"content2"),
568 ("file3.txt", b"content3"),
569 ]);
570 let cursor = Cursor::new(zip_data);
571 let mut archive = ZipArchive::new(cursor).unwrap();
572
573 let temp = TempDir::new().unwrap();
574 let config = SecurityConfig::default();
575
576 let report = archive.extract(temp.path(), &config).unwrap();
577
578 assert_eq!(report.files_extracted, 3);
579 }
580
581 #[test]
582 fn test_extract_nested_structure() {
583 let zip_data = create_test_zip(vec![("dir1/dir2/file.txt", b"nested")]);
584 let cursor = Cursor::new(zip_data);
585 let mut archive = ZipArchive::new(cursor).unwrap();
586
587 let temp = TempDir::new().unwrap();
588 let config = SecurityConfig::default();
589
590 let report = archive.extract(temp.path(), &config).unwrap();
591
592 assert_eq!(report.files_extracted, 1);
593 assert!(temp.path().join("dir1/dir2/file.txt").exists());
594 }
595
596 #[test]
597 fn test_extract_with_deflate_compression() {
598 let buffer = Vec::new();
599 let mut zip = ZipWriter::new(Cursor::new(buffer));
600
601 let options =
602 SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
603
604 zip.start_file("compressed.txt", options).unwrap();
605 zip.write_all(b"This text will be compressed with DEFLATE")
606 .unwrap();
607
608 let zip_data = zip.finish().unwrap().into_inner();
609 let cursor = Cursor::new(zip_data);
610 let mut archive = ZipArchive::new(cursor).unwrap();
611
612 let temp = TempDir::new().unwrap();
613 let config = SecurityConfig::default();
614
615 let report = archive.extract(temp.path(), &config).unwrap();
616
617 assert_eq!(report.files_extracted, 1);
618
619 let content = std::fs::read_to_string(temp.path().join("compressed.txt")).unwrap();
620 assert_eq!(content, "This text will be compressed with DEFLATE");
621 }
622
623 #[test]
624 fn test_extract_with_bzip2_compression() {
625 let buffer = Vec::new();
626 let mut zip = ZipWriter::new(Cursor::new(buffer));
627
628 let options =
629 SimpleFileOptions::default().compression_method(zip::CompressionMethod::Bzip2);
630
631 zip.start_file("bzip2.txt", options).unwrap();
632 zip.write_all(b"This text will be compressed with BZIP2")
633 .unwrap();
634
635 let zip_data = zip.finish().unwrap().into_inner();
636 let cursor = Cursor::new(zip_data);
637 let mut archive = ZipArchive::new(cursor).unwrap();
638
639 let temp = TempDir::new().unwrap();
640 let config = SecurityConfig::default();
641
642 let report = archive.extract(temp.path(), &config).unwrap();
643
644 assert_eq!(report.files_extracted, 1);
645 }
646
647 #[test]
648 fn test_extract_with_zstd_compression() {
649 let buffer = Vec::new();
650 let mut zip = ZipWriter::new(Cursor::new(buffer));
651
652 let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Zstd);
653
654 zip.start_file("zstd.txt", options).unwrap();
655 zip.write_all(b"This text will be compressed with ZSTD")
656 .unwrap();
657
658 let zip_data = zip.finish().unwrap().into_inner();
659 let cursor = Cursor::new(zip_data);
660 let mut archive = ZipArchive::new(cursor).unwrap();
661
662 let temp = TempDir::new().unwrap();
663 let config = SecurityConfig::default();
664
665 let report = archive.extract(temp.path(), &config).unwrap();
666
667 assert_eq!(report.files_extracted, 1);
668 }
669
670 #[test]
671 fn test_extract_directory_entry() {
672 let buffer = Vec::new();
673 let mut zip = ZipWriter::new(Cursor::new(buffer));
674
675 let options = SimpleFileOptions::default();
677 zip.add_directory("mydir/", options).unwrap();
678
679 let zip_data = zip.finish().unwrap().into_inner();
680 let cursor = Cursor::new(zip_data);
681 let mut archive = ZipArchive::new(cursor).unwrap();
682
683 let temp = TempDir::new().unwrap();
684 let config = SecurityConfig::default();
685
686 let report = archive.extract(temp.path(), &config).unwrap();
687
688 assert_eq!(report.directories_created, 1);
689 assert!(temp.path().join("mydir").is_dir());
690 }
691
692 #[test]
693 fn test_extract_empty_file() {
694 let zip_data = create_test_zip(vec![("empty.txt", b"")]);
695 let cursor = Cursor::new(zip_data);
696 let mut archive = ZipArchive::new(cursor).unwrap();
697
698 let temp = TempDir::new().unwrap();
699 let config = SecurityConfig::default();
700
701 let report = archive.extract(temp.path(), &config).unwrap();
702
703 assert_eq!(report.files_extracted, 1);
704 assert!(temp.path().join("empty.txt").exists());
705
706 let metadata = std::fs::metadata(temp.path().join("empty.txt")).unwrap();
707 assert_eq!(metadata.len(), 0);
708 }
709
710 #[test]
711 fn test_quota_file_size_exceeded() {
712 let zip_data = create_test_zip(vec![("large.bin", &vec![0u8; 1000])]);
713 let cursor = Cursor::new(zip_data);
714 let mut archive = ZipArchive::new(cursor).unwrap();
715
716 let temp = TempDir::new().unwrap();
717 let mut config = SecurityConfig::default();
718 config.max_file_size = 100; let result = archive.extract(temp.path(), &config);
721
722 assert!(result.is_err());
723 }
724
725 #[test]
726 fn test_quota_file_count_exceeded() {
727 let zip_data = create_test_zip(vec![
728 ("file1.txt", b"data"),
729 ("file2.txt", b"data"),
730 ("file3.txt", b"data"),
731 ]);
732 let cursor = Cursor::new(zip_data);
733 let mut archive = ZipArchive::new(cursor).unwrap();
734
735 let temp = TempDir::new().unwrap();
736 let mut config = SecurityConfig::default();
737 config.max_file_count = 2; let result = archive.extract(temp.path(), &config);
740
741 assert!(result.is_err());
742 }
743
744 #[test]
745 fn test_path_traversal_rejected() {
746 let zip_data = create_test_zip(vec![("../etc/passwd", b"malicious")]);
747 let cursor = Cursor::new(zip_data);
748 let mut archive = ZipArchive::new(cursor).unwrap();
749
750 let temp = TempDir::new().unwrap();
751 let config = SecurityConfig::default();
752
753 let result = archive.extract(temp.path(), &config);
754
755 assert!(result.is_err());
756 assert!(matches!(
757 result.unwrap_err(),
758 ExtractionError::PathTraversal { .. }
759 ));
760 }
761
762 #[test]
763 fn test_absolute_path_rejected() {
764 let zip_data = create_test_zip(vec![("/etc/shadow", b"malicious")]);
765 let cursor = Cursor::new(zip_data);
766 let mut archive = ZipArchive::new(cursor).unwrap();
767
768 let temp = TempDir::new().unwrap();
769 let config = SecurityConfig::default();
770
771 let result = archive.extract(temp.path(), &config);
772
773 assert!(result.is_err());
774 }
775
776 #[test]
777 fn test_zip_bomb_detection() {
778 let buffer = Vec::new();
780 let mut zip = ZipWriter::new(Cursor::new(buffer));
781
782 let options =
783 SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
784
785 zip.start_file("bomb.txt", options).unwrap();
786 zip.write_all(&vec![0u8; 100_000]).unwrap();
788
789 let zip_data = zip.finish().unwrap().into_inner();
790 let cursor = Cursor::new(zip_data);
791 let mut archive = ZipArchive::new(cursor).unwrap();
792
793 let temp = TempDir::new().unwrap();
794 let mut config = SecurityConfig::default();
795 config.max_compression_ratio = 10.0; let result = archive.extract(temp.path(), &config);
798
799 assert!(result.is_err());
801 }
802
803 #[test]
804 #[cfg(unix)]
805 fn test_file_permissions_preserved() {
806 use std::os::unix::fs::PermissionsExt;
807
808 let buffer = Vec::new();
809 let mut zip = ZipWriter::new(Cursor::new(buffer));
810
811 let options = SimpleFileOptions::default().unix_permissions(0o755);
812 zip.start_file("script.sh", options).unwrap();
813 zip.write_all(b"#!/bin/sh\n").unwrap();
814
815 let zip_data = zip.finish().unwrap().into_inner();
816 let cursor = Cursor::new(zip_data);
817 let mut archive = ZipArchive::new(cursor).unwrap();
818
819 let temp = TempDir::new().unwrap();
820 let config = SecurityConfig::default();
821
822 let report = archive.extract(temp.path(), &config).unwrap();
823
824 assert_eq!(report.files_extracted, 1);
825
826 let metadata = std::fs::metadata(temp.path().join("script.sh")).unwrap();
827 let permissions = metadata.permissions();
828 assert_eq!(permissions.mode() & 0o777, 0o755);
829 }
830
831 #[test]
832 #[cfg(unix)]
833 fn test_permissions_sanitized_setuid_removed() {
834 use std::os::unix::fs::PermissionsExt;
835
836 let buffer = Vec::new();
837 let mut zip = ZipWriter::new(Cursor::new(buffer));
838
839 let options = SimpleFileOptions::default().unix_permissions(0o4755); zip.start_file("binary", options).unwrap();
841 zip.write_all(b"data").unwrap();
842
843 let zip_data = zip.finish().unwrap().into_inner();
844 let cursor = Cursor::new(zip_data);
845 let mut archive = ZipArchive::new(cursor).unwrap();
846
847 let temp = TempDir::new().unwrap();
848 let config = SecurityConfig::default();
849
850 let _report = archive.extract(temp.path(), &config).unwrap();
851
852 let metadata = std::fs::metadata(temp.path().join("binary")).unwrap();
853 let permissions = metadata.permissions();
854 assert_eq!(permissions.mode() & 0o7777, 0o755);
856 }
857
858 #[test]
859 #[cfg(unix)]
860 fn test_permissions_sanitized_setgid_removed() {
861 use std::os::unix::fs::PermissionsExt;
862
863 let buffer = Vec::new();
865 let mut zip = ZipWriter::new(Cursor::new(buffer));
866
867 let options = SimpleFileOptions::default().unix_permissions(0o2755); zip.start_file("binary", options).unwrap();
869 zip.write_all(b"data").unwrap();
870
871 let zip_data = zip.finish().unwrap().into_inner();
872 let cursor = Cursor::new(zip_data);
873 let mut archive = ZipArchive::new(cursor).unwrap();
874
875 let temp = TempDir::new().unwrap();
876 let config = SecurityConfig::default();
877
878 let _report = archive.extract(temp.path(), &config).unwrap();
879
880 let metadata = std::fs::metadata(temp.path().join("binary")).unwrap();
881 let permissions = metadata.permissions();
882 assert_eq!(permissions.mode() & 0o7777, 0o755);
884 }
885
886 #[test]
887 #[cfg(unix)]
888 fn test_permissions_sanitized_setuid_setgid_removed() {
889 use std::os::unix::fs::PermissionsExt;
890
891 let buffer = Vec::new();
893 let mut zip = ZipWriter::new(Cursor::new(buffer));
894
895 let options = SimpleFileOptions::default().unix_permissions(0o6755); zip.start_file("binary", options).unwrap();
897 zip.write_all(b"data").unwrap();
898
899 let zip_data = zip.finish().unwrap().into_inner();
900 let cursor = Cursor::new(zip_data);
901 let mut archive = ZipArchive::new(cursor).unwrap();
902
903 let temp = TempDir::new().unwrap();
904 let config = SecurityConfig::default();
905
906 let _report = archive.extract(temp.path(), &config).unwrap();
907
908 let metadata = std::fs::metadata(temp.path().join("binary")).unwrap();
909 let permissions = metadata.permissions();
910 assert_eq!(permissions.mode() & 0o7777, 0o755);
912 }
913
914 #[test]
924 #[cfg(unix)]
925 #[ignore = "zip crate does not preserve file type bits in unix_permissions()"]
926 fn test_extract_symlink_via_unix_attributes() {
927 let buffer = Vec::new();
928 let mut zip = ZipWriter::new(Cursor::new(buffer));
929
930 let options = SimpleFileOptions::default().unix_permissions(0o644);
932 zip.start_file("target.txt", options).unwrap();
933 zip.write_all(b"data").unwrap();
934
935 const S_IFLNK: u32 = 0o120_000; let symlink_mode = S_IFLNK | 0o777; let options = SimpleFileOptions::default().unix_permissions(symlink_mode);
942 zip.start_file("link.txt", options).unwrap();
943 zip.write_all(b"target.txt").unwrap(); let zip_data = zip.finish().unwrap().into_inner();
946 let cursor = Cursor::new(zip_data);
947 let mut archive = ZipArchive::new(cursor).unwrap();
948
949 let temp = TempDir::new().unwrap();
950 let mut config = SecurityConfig::default();
951 config.allowed.symlinks = true;
952
953 let report = archive.extract(temp.path(), &config).unwrap();
954
955 assert_eq!(report.files_extracted, 1, "should have 1 regular file");
956 assert_eq!(report.symlinks_created, 1, "should have 1 symlink");
957
958 let link_path = temp.path().join("link.txt");
960 assert!(link_path.exists(), "symlink should exist");
961
962 let metadata = std::fs::symlink_metadata(&link_path).unwrap();
964 assert!(metadata.is_symlink(), "link.txt should be a symlink");
965 }
966
967 #[test]
969 #[cfg(unix)]
970 #[ignore = "zip crate does not preserve file type bits in unix_permissions()"]
971 fn test_symlink_disabled_by_default() {
972 let buffer = Vec::new();
973 let mut zip = ZipWriter::new(Cursor::new(buffer));
974
975 const S_IFLNK: u32 = 0o120_000;
977 let symlink_mode = S_IFLNK | 0o777;
978
979 let options = SimpleFileOptions::default().unix_permissions(symlink_mode);
980 zip.start_file("link.txt", options).unwrap();
981 zip.write_all(b"target.txt").unwrap();
982
983 let zip_data = zip.finish().unwrap().into_inner();
984 let cursor = Cursor::new(zip_data);
985 let mut archive = ZipArchive::new(cursor).unwrap();
986
987 let temp = TempDir::new().unwrap();
988 let config = SecurityConfig::default(); let result = archive.extract(temp.path(), &config);
991
992 assert!(
994 result.is_err(),
995 "extraction should fail when symlinks are disabled"
996 );
997
998 match result {
1000 Err(ExtractionError::SecurityViolation { reason }) => {
1001 assert!(
1002 reason.contains("symlinks not allowed") || reason.contains("symlink"),
1003 "error should mention symlinks: {reason}"
1004 );
1005 }
1006 Err(other) => panic!("expected SecurityViolation, got: {other:?}"),
1007 Ok(_) => panic!("expected error, got success"),
1008 }
1009 }
1010
1011 #[test]
1013 #[cfg(unix)]
1014 #[ignore = "debug test showing zip crate limitation"]
1015 fn test_debug_zip_unix_mode() {
1016 let buffer = Vec::new();
1018 let mut zip = ZipWriter::new(Cursor::new(buffer));
1019
1020 const S_IFLNK: u32 = 0o120_000;
1021 let symlink_mode = S_IFLNK | 0o777;
1022
1023 let options = SimpleFileOptions::default().unix_permissions(symlink_mode);
1024 zip.start_file("link.txt", options).unwrap();
1025 zip.write_all(b"target.txt").unwrap();
1026
1027 let zip_data = zip.finish().unwrap().into_inner();
1028
1029 let mut reader = zip::ZipArchive::new(Cursor::new(zip_data)).unwrap();
1031 let file = reader.by_index(0).unwrap();
1032
1033 if let Some(mode) = file.unix_mode() {
1034 eprintln!("Mode retrieved: {:o} (decimal: {})", mode, mode);
1035 eprintln!("Expected symlink mode: {:o}", symlink_mode);
1036
1037 const S_IFMT: u32 = 0o170_000;
1038 const S_IFLNK_CHECK: u32 = 0o120_000;
1039 eprintln!("File type bits: {:o}", mode & S_IFMT);
1040 eprintln!("Is symlink: {}", (mode & S_IFMT) == S_IFLNK_CHECK);
1041 } else {
1042 panic!("No Unix mode set!");
1043 }
1044 }
1045
1046 #[test]
1047 fn test_hardlink_rejected() {
1048 let zip_data = create_test_zip(vec![("file.txt", b"content")]);
1057 let cursor = Cursor::new(zip_data);
1058 let archive = ZipArchive::new(cursor).unwrap();
1059
1060 assert_eq!(archive.format_name(), "zip");
1062
1063 }
1069
1070 #[test]
1071 fn test_compression_method_detection() {
1072 let buffer = Vec::new();
1074 let mut zip = ZipWriter::new(Cursor::new(buffer));
1075
1076 let stored =
1077 SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
1078 zip.start_file("stored.txt", stored).unwrap();
1079 zip.write_all(b"stored").unwrap();
1080
1081 let deflated =
1082 SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
1083 zip.start_file("deflated.txt", deflated).unwrap();
1084 zip.write_all(b"deflated").unwrap();
1085
1086 let zip_data = zip.finish().unwrap().into_inner();
1087 let cursor = Cursor::new(zip_data);
1088 let mut archive = ZipArchive::new(cursor).unwrap();
1089
1090 let temp = TempDir::new().unwrap();
1091 let config = SecurityConfig::default();
1092
1093 let report = archive.extract(temp.path(), &config).unwrap();
1094
1095 assert_eq!(report.files_extracted, 2);
1096 }
1097
1098 #[test]
1099 fn test_bytes_written_tracking() {
1100 let zip_data = create_test_zip(vec![
1101 ("file1.txt", b"hello"), ("file2.txt", b"world!!!"), ]);
1104 let cursor = Cursor::new(zip_data);
1105 let mut archive = ZipArchive::new(cursor).unwrap();
1106
1107 let temp = TempDir::new().unwrap();
1108 let config = SecurityConfig::default();
1109
1110 let report = archive.extract(temp.path(), &config).unwrap();
1111
1112 assert_eq!(report.bytes_written, 13);
1113 }
1114
1115 #[test]
1116 fn test_duration_tracking() {
1117 let zip_data = create_test_zip(vec![("file.txt", b"data")]);
1118 let cursor = Cursor::new(zip_data);
1119 let mut archive = ZipArchive::new(cursor).unwrap();
1120
1121 let temp = TempDir::new().unwrap();
1122 let config = SecurityConfig::default();
1123
1124 let report = archive.extract(temp.path(), &config).unwrap();
1125
1126 assert!(report.duration.as_nanos() > 0);
1128 }
1129
1130 #[test]
1131 fn test_invalid_zip_archive() {
1132 let invalid_data = b"not a zip file";
1133 let cursor = Cursor::new(invalid_data);
1134 let result = ZipArchive::new(cursor);
1135
1136 assert!(result.is_err());
1137 }
1138
1139 #[test]
1140 fn test_entry_type_detection_file() {
1141 let zip_data = create_test_zip(vec![("regular.txt", b"content")]);
1142 let cursor = Cursor::new(zip_data);
1143 let mut archive = ZipArchive::new(cursor).unwrap();
1144
1145 let temp = TempDir::new().unwrap();
1146 let config = SecurityConfig::default();
1147
1148 let report = archive.extract(temp.path(), &config).unwrap();
1149
1150 assert_eq!(report.files_extracted, 1);
1151 assert_eq!(report.directories_created, 0);
1152 assert_eq!(report.symlinks_created, 0);
1153 }
1154
1155 #[test]
1156 fn test_entry_type_detection_directory() {
1157 let buffer = Vec::new();
1158 let mut zip = ZipWriter::new(Cursor::new(buffer));
1159
1160 let options = SimpleFileOptions::default();
1161 zip.add_directory("testdir/", options).unwrap();
1162
1163 let zip_data = zip.finish().unwrap().into_inner();
1164 let cursor = Cursor::new(zip_data);
1165 let mut archive = ZipArchive::new(cursor).unwrap();
1166
1167 let temp = TempDir::new().unwrap();
1168 let config = SecurityConfig::default();
1169
1170 let report = archive.extract(temp.path(), &config).unwrap();
1171
1172 assert_eq!(report.files_extracted, 0);
1173 assert_eq!(report.directories_created, 1);
1174 }
1175
1176 #[test]
1177 fn test_nested_directories_created_automatically() {
1178 let zip_data = create_test_zip(vec![("a/b/c/file.txt", b"nested")]);
1181 let cursor = Cursor::new(zip_data);
1182 let mut archive = ZipArchive::new(cursor).unwrap();
1183
1184 let temp = TempDir::new().unwrap();
1185 let config = SecurityConfig::default();
1186
1187 let _report = archive.extract(temp.path(), &config).unwrap();
1188
1189 assert!(temp.path().join("a/b/c/file.txt").exists());
1190 assert!(temp.path().join("a").is_dir());
1191 assert!(temp.path().join("a/b").is_dir());
1192 assert!(temp.path().join("a/b/c").is_dir());
1193 }
1194
1195 #[test]
1196 fn test_large_file_extraction() {
1197 let large_data = vec![0xAB; 1024 * 1024];
1199 let zip_data = create_test_zip(vec![("large.bin", &large_data)]);
1200 let cursor = Cursor::new(zip_data);
1201 let mut archive = ZipArchive::new(cursor).unwrap();
1202
1203 let temp = TempDir::new().unwrap();
1204 let config = SecurityConfig::default();
1205
1206 let report = archive.extract(temp.path(), &config).unwrap();
1207
1208 assert_eq!(report.files_extracted, 1);
1209
1210 let extracted = std::fs::read(temp.path().join("large.bin")).unwrap();
1211 assert_eq!(extracted.len(), 1024 * 1024);
1212 }
1213
1214 #[test]
1215 fn test_many_files_extraction() {
1216 let entries: Vec<_> = (0..100)
1218 .map(|i| (format!("file{i}.txt"), format!("content{i}").into_bytes()))
1219 .collect();
1220
1221 let buffer = Vec::new();
1222 let mut zip = ZipWriter::new(Cursor::new(buffer));
1223
1224 for (name, data) in &entries {
1225 let options = SimpleFileOptions::default();
1226 zip.start_file(name, options).unwrap();
1227 zip.write_all(data).unwrap();
1228 }
1229
1230 let zip_data = zip.finish().unwrap().into_inner();
1231 let cursor = Cursor::new(zip_data);
1232 let mut archive = ZipArchive::new(cursor).unwrap();
1233
1234 let temp = TempDir::new().unwrap();
1235 let config = SecurityConfig::default();
1236
1237 let report = archive.extract(temp.path(), &config).unwrap();
1238
1239 assert_eq!(report.files_extracted, 100);
1240 }
1241
1242 #[test]
1243 fn test_quota_total_size_exceeded() {
1244 let zip_data = create_test_zip(vec![
1245 ("file1.txt", &vec![0u8; 600]),
1246 ("file2.txt", &vec![0u8; 600]),
1247 ]);
1248 let cursor = Cursor::new(zip_data);
1249 let mut archive = ZipArchive::new(cursor).unwrap();
1250
1251 let temp = TempDir::new().unwrap();
1252 let mut config = SecurityConfig::default();
1253 config.max_total_size = 1000; let result = archive.extract(temp.path(), &config);
1256
1257 assert!(result.is_err());
1258 }
1259
1260 #[test]
1261 fn test_special_characters_in_filename() {
1262 let zip_data = create_test_zip(vec![
1263 ("file with spaces.txt", b"content"),
1264 ("file-with-dashes.txt", b"content"),
1265 ("file_with_underscores.txt", b"content"),
1266 ]);
1267 let cursor = Cursor::new(zip_data);
1268 let mut archive = ZipArchive::new(cursor).unwrap();
1269
1270 let temp = TempDir::new().unwrap();
1271 let config = SecurityConfig::default();
1272
1273 let report = archive.extract(temp.path(), &config).unwrap();
1274
1275 assert_eq!(report.files_extracted, 3);
1276 assert!(temp.path().join("file with spaces.txt").exists());
1277 }
1278}