1use std::path::Path;
4
5use crate::ArchiveError;
6use crate::ExtractionReport;
7use crate::NoopProgress;
8use crate::ProgressCallback;
9use crate::Result;
10use crate::SecurityConfig;
11use crate::config::ExtractionOptions;
12use crate::creation::CreationConfig;
13use crate::creation::CreationReport;
14use crate::formats::detect::ArchiveType;
15use crate::formats::detect::detect_format;
16use crate::formats::detect::is_zip_family_alias;
17use crate::inspection::ArchiveManifest;
18use crate::inspection::VerificationReport;
19
20pub fn extract_archive<P: AsRef<Path>, Q: AsRef<Path>>(
53 archive_path: P,
54 output_dir: Q,
55 config: &SecurityConfig,
56) -> Result<ExtractionReport> {
57 let mut noop = NoopProgress;
58 extract_archive_with_progress(archive_path, output_dir, config, &mut noop)
59}
60
61pub fn extract_archive_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
98 archive_path: P,
99 output_dir: Q,
100 config: &SecurityConfig,
101 progress: &mut dyn ProgressCallback,
102) -> Result<ExtractionReport> {
103 let options = ExtractionOptions::default();
104 extract_archive_with_options_and_progress(archive_path, output_dir, config, &options, progress)
105}
106
107fn extract_impl<P: AsRef<Path>, Q: AsRef<Path>>(
108 archive_path: P,
109 output_dir: Q,
110 config: &SecurityConfig,
111 options: &ExtractionOptions,
112 progress: &mut dyn ProgressCallback,
113) -> Result<ExtractionReport> {
114 config.validate()?;
115
116 let archive_path = archive_path.as_ref();
117 let output_dir = output_dir.as_ref();
118
119 let format = detect_format(archive_path)?;
121
122 match format {
124 ArchiveType::Tar => {
125 extract_tar_with_decoder(archive_path, output_dir, config, options, progress, Ok)
126 }
127 ArchiveType::TarGz => {
128 extract_tar_with_decoder(archive_path, output_dir, config, options, progress, |r| {
129 Ok(flate2::read::GzDecoder::new(r))
130 })
131 }
132 ArchiveType::TarBz2 => {
133 extract_tar_with_decoder(archive_path, output_dir, config, options, progress, |r| {
134 Ok(bzip2::read::BzDecoder::new(r))
135 })
136 }
137 ArchiveType::TarXz => {
138 extract_tar_with_decoder(archive_path, output_dir, config, options, progress, |r| {
139 Ok(xz2::read::XzDecoder::new(r))
140 })
141 }
142 ArchiveType::TarZst => {
143 extract_tar_with_decoder(archive_path, output_dir, config, options, progress, |r| {
144 Ok(zstd::stream::read::Decoder::new(r)?)
145 })
146 }
147 ArchiveType::Zip => extract_zip(archive_path, output_dir, config, options, progress),
148 ArchiveType::SevenZ => extract_7z(archive_path, output_dir, config, options, progress),
149 }
150}
151
152pub fn extract_archive_with_options_and_progress<P: AsRef<Path>, Q: AsRef<Path>>(
200 archive_path: P,
201 output_dir: Q,
202 config: &SecurityConfig,
203 options: &ExtractionOptions,
204 progress: &mut dyn ProgressCallback,
205) -> Result<ExtractionReport> {
206 if options.atomic {
207 extract_atomic(archive_path, output_dir, config, options, progress)
208 } else {
209 extract_impl(archive_path, output_dir, config, options, progress)
210 }
211}
212
213pub fn extract_archive_with_options<P: AsRef<Path>, Q: AsRef<Path>>(
244 archive_path: P,
245 output_dir: Q,
246 config: &SecurityConfig,
247 options: &ExtractionOptions,
248) -> Result<ExtractionReport> {
249 let mut noop = NoopProgress;
250 extract_archive_with_options_and_progress(archive_path, output_dir, config, options, &mut noop)
251}
252
253fn extract_atomic<P: AsRef<Path>, Q: AsRef<Path>>(
254 archive_path: P,
255 output_dir: Q,
256 config: &SecurityConfig,
257 options: &ExtractionOptions,
258 progress: &mut dyn ProgressCallback,
259) -> Result<ExtractionReport> {
260 let output_dir = output_dir.as_ref();
261
262 let canonical_output = if output_dir.exists() {
266 output_dir.canonicalize().map_err(ArchiveError::Io)?
267 } else {
268 output_dir.to_path_buf()
269 };
270
271 let parent = canonical_output
272 .parent()
273 .ok_or_else(|| ArchiveError::InvalidConfiguration {
274 reason: "output directory has no parent".into(),
275 })?;
276
277 std::fs::create_dir_all(parent).map_err(ArchiveError::Io)?;
278
279 let temp_dir = tempfile::tempdir_in(parent).map_err(|e| {
280 ArchiveError::Io(std::io::Error::new(
281 e.kind(),
282 format!(
283 "failed to create temp directory in {}: {e}",
284 parent.display()
285 ),
286 ))
287 })?;
288
289 let result = extract_impl(archive_path, temp_dir.path(), config, options, progress);
290
291 match result {
292 Ok(report) => {
293 let temp_path = temp_dir.keep();
295 std::fs::rename(&temp_path, output_dir).map_err(|e| {
296 let _ = std::fs::remove_dir_all(&temp_path);
298 if e.kind() == std::io::ErrorKind::AlreadyExists {
300 ArchiveError::OutputExists {
301 path: output_dir.to_path_buf(),
302 }
303 } else {
304 ArchiveError::Io(std::io::Error::new(
305 e.kind(),
306 format!("failed to rename temp dir to {}: {e}", output_dir.display()),
307 ))
308 }
309 })?;
310
311 Ok(report)
312 }
313 Err(e) => {
314 Err(e)
316 }
317 }
318}
319
320fn extract_tar_with_decoder<R, F>(
328 archive_path: &Path,
329 output_dir: &Path,
330 config: &SecurityConfig,
331 options: &ExtractionOptions,
332 progress: &mut dyn ProgressCallback,
333 make_decoder: F,
334) -> Result<ExtractionReport>
335where
336 R: std::io::Read,
337 F: FnOnce(std::io::BufReader<std::fs::File>) -> Result<R>,
338{
339 use crate::formats::TarArchive;
340 use crate::formats::traits::ArchiveFormat;
341
342 let file = std::fs::File::open(archive_path)?;
343 let reader = std::io::BufReader::new(file);
344 let decoder = make_decoder(reader)?;
345 let mut archive = TarArchive::new(decoder);
346 archive.extract(output_dir, config, options, progress)
347}
348
349fn extract_zip(
350 archive_path: &Path,
351 output_dir: &Path,
352 config: &SecurityConfig,
353 options: &ExtractionOptions,
354 progress: &mut dyn ProgressCallback,
355) -> Result<ExtractionReport> {
356 use crate::formats::ZipArchive;
357 use crate::formats::traits::ArchiveFormat;
358 use std::fs::File;
359
360 let file = File::open(archive_path)?;
361 let mut archive = ZipArchive::new(file)?;
362 archive.extract(output_dir, config, options, progress)
363}
364
365fn extract_7z(
366 archive_path: &Path,
367 output_dir: &Path,
368 config: &SecurityConfig,
369 options: &ExtractionOptions,
370 progress: &mut dyn ProgressCallback,
371) -> Result<ExtractionReport> {
372 use crate::formats::SevenZArchive;
373 use crate::formats::traits::ArchiveFormat;
374 use std::fs::File;
375
376 let file = File::open(archive_path)?;
377 let mut archive = SevenZArchive::new(file)?;
378 archive.extract(output_dir, config, options, progress)
379}
380
381pub fn create_archive<P: AsRef<Path>, Q: AsRef<Path>>(
414 output_path: P,
415 sources: &[Q],
416 config: &CreationConfig,
417) -> Result<CreationReport> {
418 let mut noop = NoopProgress;
419 create_archive_with_progress(output_path, sources, config, &mut noop)
420}
421
422pub fn create_archive_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
463 output_path: P,
464 sources: &[Q],
465 config: &CreationConfig,
466 progress: &mut dyn ProgressCallback,
467) -> Result<CreationReport> {
468 config.validate()?;
469
470 let output = output_path.as_ref();
471
472 if config.format.is_none() {
481 reject_zip_family_creation(output)?;
482 }
483
484 let format = determine_creation_format(output, config)?;
486
487 let source_refs: Vec<&Path> = sources.iter().map(AsRef::as_ref).collect();
488 let creator = creator_for_format(format)?;
489 creator.create(output, &source_refs, config, progress)
490}
491
492fn creator_for_format(
493 format: ArchiveType,
494) -> Result<Box<dyn crate::formats::traits::FormatCreator>> {
495 match format {
496 ArchiveType::Tar => Ok(Box::new(crate::creation::TarCreator)),
497 ArchiveType::TarGz => Ok(Box::new(crate::creation::TarGzCreator)),
498 ArchiveType::TarBz2 => Ok(Box::new(crate::creation::TarBz2Creator)),
499 ArchiveType::TarXz => Ok(Box::new(crate::creation::TarXzCreator)),
500 ArchiveType::TarZst => Ok(Box::new(crate::creation::TarZstCreator)),
501 ArchiveType::Zip => Ok(Box::new(crate::creation::ZipCreator)),
502 ArchiveType::SevenZ => Err(ArchiveError::InvalidConfiguration {
503 reason: "7z archive creation is not supported".into(),
504 }),
505 }
506}
507
508pub fn list_archive<P: AsRef<Path>>(
543 archive_path: P,
544 config: &SecurityConfig,
545) -> Result<ArchiveManifest> {
546 crate::inspection::list_archive(archive_path, config)
547}
548
549pub fn verify_archive<P: AsRef<Path>>(
593 archive_path: P,
594 config: &SecurityConfig,
595) -> Result<VerificationReport> {
596 crate::inspection::verify_archive(archive_path, config)
597}
598
599fn reject_zip_family_creation(output: &Path) -> Result<()> {
605 let Some(ext) = output.extension().and_then(|e| e.to_str()) else {
606 return Ok(());
607 };
608 if is_zip_family_alias(ext) {
609 let ext_lower = ext.to_ascii_lowercase();
610 return Err(ArchiveError::InvalidArchive(format!(
611 "creation for .{ext_lower} isn't supported: the format is ZIP-based but \
612 requires extra structure (signing, manifests, ordering) that exarch \
613 doesn't produce. Use .zip, or set CreationConfig::format = Some(\
614 exarch_core::formats::detect::ArchiveType::Zip) to override."
615 )));
616 }
617 Ok(())
618}
619
620fn determine_creation_format(output: &Path, config: &CreationConfig) -> Result<ArchiveType> {
622 if let Some(format) = config.format {
624 return Ok(format);
625 }
626
627 detect_format(output)
629}
630
631#[cfg(test)]
632#[allow(clippy::unwrap_used)]
633mod tests {
634 use super::*;
635 use std::path::PathBuf;
636
637 #[test]
638 fn test_extract_archive_nonexistent_file() {
639 let config = SecurityConfig::default();
640 let result = extract_archive(
641 PathBuf::from("nonexistent_test.tar"),
642 PathBuf::from("/tmp/test"),
643 &config,
644 );
645 assert!(result.is_err());
647 }
648
649 #[test]
650 fn test_determine_creation_format_tar() {
651 let config = CreationConfig::default();
652 let path = PathBuf::from("archive.tar");
653 let format = determine_creation_format(&path, &config).unwrap();
654 assert_eq!(format, ArchiveType::Tar);
655 }
656
657 #[test]
658 fn test_determine_creation_format_tar_gz() {
659 let config = CreationConfig::default();
660 let path = PathBuf::from("archive.tar.gz");
661 let format = determine_creation_format(&path, &config).unwrap();
662 assert_eq!(format, ArchiveType::TarGz);
663
664 let path2 = PathBuf::from("archive.tgz");
665 let format2 = determine_creation_format(&path2, &config).unwrap();
666 assert_eq!(format2, ArchiveType::TarGz);
667 }
668
669 #[test]
670 fn test_determine_creation_format_tar_bz2() {
671 let config = CreationConfig::default();
672 let path = PathBuf::from("archive.tar.bz2");
673 let format = determine_creation_format(&path, &config).unwrap();
674 assert_eq!(format, ArchiveType::TarBz2);
675 }
676
677 #[test]
678 fn test_determine_creation_format_tar_xz() {
679 let config = CreationConfig::default();
680 let path = PathBuf::from("archive.tar.xz");
681 let format = determine_creation_format(&path, &config).unwrap();
682 assert_eq!(format, ArchiveType::TarXz);
683 }
684
685 #[test]
686 fn test_determine_creation_format_tar_zst() {
687 let config = CreationConfig::default();
688 let path = PathBuf::from("archive.tar.zst");
689 let format = determine_creation_format(&path, &config).unwrap();
690 assert_eq!(format, ArchiveType::TarZst);
691 }
692
693 #[test]
694 fn test_determine_creation_format_zip() {
695 let config = CreationConfig::default();
696 let path = PathBuf::from("archive.zip");
697 let format = determine_creation_format(&path, &config).unwrap();
698 assert_eq!(format, ArchiveType::Zip);
699 }
700
701 #[test]
702 fn test_determine_creation_format_explicit() {
703 let config = CreationConfig::default().with_format(Some(ArchiveType::TarGz));
704 let path = PathBuf::from("archive.xyz");
705 let format = determine_creation_format(&path, &config).unwrap();
706 assert_eq!(format, ArchiveType::TarGz);
707 }
708
709 #[test]
710 fn test_determine_creation_format_unknown() {
711 let config = CreationConfig::default();
712 let path = PathBuf::from("archive.rar");
713 let result = determine_creation_format(&path, &config);
714 assert!(result.is_err());
715 }
716
717 #[test]
718 fn test_extract_archive_7z_not_implemented() {
719 let dest = tempfile::TempDir::new().unwrap();
720 let path = PathBuf::from("test.7z");
721
722 let result = extract_archive(&path, dest.path(), &SecurityConfig::default());
723
724 assert!(result.is_err());
725 }
726
727 #[test]
728 fn test_create_archive_invalid_compression_level_rejected_before_io() {
729 let dest = tempfile::TempDir::new().unwrap();
730 let archive_path = dest.path().join("output.tar.gz");
731 let config = CreationConfig {
732 compression_level: Some(15),
733 ..CreationConfig::default()
734 };
735 let result = create_archive(&archive_path, &[] as &[&str], &config);
736 assert!(
737 matches!(
738 result,
739 Err(ArchiveError::InvalidCompressionLevel { level: 15 })
740 ),
741 "expected InvalidCompressionLevel, got {result:?}",
742 );
743 assert!(!archive_path.exists(), "output file must not be created");
745 }
746
747 #[test]
748 fn test_create_archive_zip_family_not_supported() {
749 let dest = tempfile::TempDir::new().unwrap();
753 for ext in ["apk", "whl", "EPUB"] {
754 let archive_path = dest.path().join(format!("output.{ext}"));
755 let result = create_archive(&archive_path, &[] as &[&str], &CreationConfig::default());
756 assert!(
757 matches!(result, Err(ArchiveError::InvalidArchive(_))),
758 ".{ext} should be rejected, got {result:?}",
759 );
760 }
761 }
762
763 #[test]
764 fn test_create_archive_zip_family_override_bypasses_guard() {
765 let dest = tempfile::TempDir::new().unwrap();
769 let src = dest.path().join("source.txt");
770 std::fs::write(&src, b"hello").unwrap();
771 let archive_path = dest.path().join("output.apk");
772 let config = CreationConfig::default().with_format(Some(ArchiveType::Zip));
773 let result = create_archive(&archive_path, &[&src], &config);
774 assert!(
775 result.is_ok(),
776 "explicit format override should bypass the guard, got {result:?}",
777 );
778 }
779
780 #[test]
781 fn test_create_archive_7z_not_supported() {
782 let dest = tempfile::TempDir::new().unwrap();
783 let archive_path = dest.path().join("output.7z");
784
785 let result = create_archive(&archive_path, &[] as &[&str], &CreationConfig::default());
786
787 assert!(result.is_err());
788 assert!(matches!(
789 result.unwrap_err(),
790 ArchiveError::InvalidConfiguration { .. }
791 ));
792 }
793
794 #[test]
795 fn test_extract_archive_with_options_and_progress_non_atomic_delegates_to_normal() {
796 let dest = tempfile::TempDir::new().unwrap();
797 let options = ExtractionOptions {
798 atomic: false,
799 skip_duplicates: true,
800 };
801 let result = extract_archive_with_options_and_progress(
802 PathBuf::from("nonexistent.tar.gz"),
803 dest.path(),
804 &SecurityConfig::default(),
805 &options,
806 &mut NoopProgress,
807 );
808 assert!(result.is_err());
809 }
810
811 #[test]
812 fn test_extract_archive_with_options_delegates() {
813 let dest = tempfile::TempDir::new().unwrap();
814 let options = ExtractionOptions {
815 atomic: false,
816 skip_duplicates: true,
817 };
818 let result = extract_archive_with_options(
819 PathBuf::from("nonexistent.tar.gz"),
820 dest.path(),
821 &SecurityConfig::default(),
822 &options,
823 );
824 assert!(result.is_err());
825 }
826
827 #[test]
828 fn test_extract_atomic_success() {
829 use crate::create_archive;
830 use crate::creation::CreationConfig;
831
832 let archive_dir = tempfile::TempDir::new().unwrap();
834 let archive_path = archive_dir.path().join("test.tar.gz");
835
836 let src_dir = tempfile::TempDir::new().unwrap();
838 std::fs::write(src_dir.path().join("hello.txt"), b"hello world").unwrap();
839 create_archive(&archive_path, &[src_dir.path()], &CreationConfig::default()).unwrap();
840
841 let parent = tempfile::TempDir::new().unwrap();
842 let output_dir = parent.path().join("extracted");
843
844 let options = ExtractionOptions {
845 atomic: true,
846 skip_duplicates: true,
847 };
848 let result = extract_archive_with_options(
849 &archive_path,
850 &output_dir,
851 &SecurityConfig::default(),
852 &options,
853 );
854
855 assert!(result.is_ok());
856 assert!(output_dir.exists());
857 let temp_entries: Vec<_> = std::fs::read_dir(parent.path()).unwrap().collect();
859 assert_eq!(
860 temp_entries.len(),
861 1,
862 "Expected only the output dir, found temp remnants"
863 );
864 }
865
866 #[test]
867 fn test_extract_atomic_failure_cleans_up() {
868 let parent = tempfile::TempDir::new().unwrap();
869 let output_dir = parent.path().join("extracted");
870
871 let options = ExtractionOptions {
872 atomic: true,
873 skip_duplicates: true,
874 };
875 let result = extract_archive_with_options(
876 PathBuf::from("nonexistent_archive.tar.gz"),
877 &output_dir,
878 &SecurityConfig::default(),
879 &options,
880 );
881
882 assert!(result.is_err());
883 assert!(!output_dir.exists());
885 let temp_entries: Vec<_> = std::fs::read_dir(parent.path()).unwrap().collect();
887 assert!(
888 temp_entries.is_empty(),
889 "Temp dir not cleaned up after failure"
890 );
891 }
892
893 #[test]
894 fn test_extract_atomic_output_already_exists_fails() {
895 use crate::create_archive;
896 use crate::creation::CreationConfig;
897
898 let parent = tempfile::TempDir::new().unwrap();
899 let output_dir = parent.path().join("extracted");
900 std::fs::create_dir_all(&output_dir).unwrap();
901 std::fs::write(output_dir.join("existing.txt"), b"old content").unwrap();
904
905 let archive_dir = tempfile::TempDir::new().unwrap();
906 let archive_path = archive_dir.path().join("test.tar.gz");
907 let src_dir = tempfile::TempDir::new().unwrap();
908 std::fs::write(src_dir.path().join("new.txt"), b"new content").unwrap();
909 create_archive(&archive_path, &[src_dir.path()], &CreationConfig::default()).unwrap();
910
911 let options = ExtractionOptions {
912 atomic: true,
913 skip_duplicates: true,
914 };
915 let result = extract_archive_with_options(
916 &archive_path,
917 &output_dir,
918 &SecurityConfig::default(),
919 &options,
920 );
921
922 assert!(result.is_err());
924 assert!(output_dir.join("existing.txt").exists());
926 }
927
928 #[test]
930 fn test_progress_callback_invoked_during_extraction() {
931 use crate::ProgressCallback;
932 use std::path::Path;
933
934 struct TrackingProgress {
935 started: usize,
936 completed: usize,
937 finished: bool,
938 }
939
940 impl ProgressCallback for TrackingProgress {
941 fn on_entry_start(&mut self, _path: &Path, _total: usize, _current: usize) {
942 self.started += 1;
943 }
944
945 fn on_bytes_written(&mut self, _bytes: u64) {}
946
947 fn on_entry_complete(&mut self, _path: &Path) {
948 self.completed += 1;
949 }
950
951 fn on_complete(&mut self) {
952 self.finished = true;
953 }
954 }
955
956 let archive_dir = tempfile::TempDir::new().unwrap();
957 let archive_path = archive_dir.path().join("test.tar.gz");
958 let src_dir = tempfile::TempDir::new().unwrap();
959 std::fs::write(src_dir.path().join("a.txt"), b"hello").unwrap();
960 std::fs::write(src_dir.path().join("b.txt"), b"world").unwrap();
961 create_archive(&archive_path, &[src_dir.path()], &CreationConfig::default()).unwrap();
962
963 let dest = tempfile::TempDir::new().unwrap();
964 let mut progress = TrackingProgress {
965 started: 0,
966 completed: 0,
967 finished: false,
968 };
969
970 let report = extract_archive_with_progress(
971 &archive_path,
972 dest.path(),
973 &SecurityConfig::default(),
974 &mut progress,
975 )
976 .unwrap();
977
978 assert!(report.files_extracted >= 2, "expected at least 2 files");
979 assert!(progress.started >= 2, "on_entry_start not called");
980 assert!(progress.completed >= 2, "on_entry_complete not called");
981 assert!(progress.finished, "on_complete not called");
982 }
983
984 #[test]
986 fn test_progress_callback_invoked_during_zip_extraction() {
987 use crate::ProgressCallback;
988 use std::path::Path;
989
990 struct TrackingProgress {
991 started: usize,
992 completed: usize,
993 finished: bool,
994 }
995
996 impl ProgressCallback for TrackingProgress {
997 fn on_entry_start(&mut self, _path: &Path, _total: usize, _current: usize) {
998 self.started += 1;
999 }
1000
1001 fn on_bytes_written(&mut self, _bytes: u64) {}
1002
1003 fn on_entry_complete(&mut self, _path: &Path) {
1004 self.completed += 1;
1005 }
1006
1007 fn on_complete(&mut self) {
1008 self.finished = true;
1009 }
1010 }
1011
1012 let tmp = tempfile::TempDir::new().unwrap();
1013 let archive_path = tmp.path().join("test.zip");
1014 let src_dir = tempfile::TempDir::new().unwrap();
1015 std::fs::write(src_dir.path().join("x.txt"), b"foo").unwrap();
1016 std::fs::write(src_dir.path().join("y.txt"), b"bar").unwrap();
1017 let config = CreationConfig::default().with_format(Some(ArchiveType::Zip));
1018 create_archive(&archive_path, &[src_dir.path()], &config).unwrap();
1019
1020 let dest = tempfile::TempDir::new().unwrap();
1021 let mut progress = TrackingProgress {
1022 started: 0,
1023 completed: 0,
1024 finished: false,
1025 };
1026 let report = extract_archive_with_progress(
1027 &archive_path,
1028 dest.path(),
1029 &SecurityConfig::default(),
1030 &mut progress,
1031 )
1032 .unwrap();
1033
1034 assert!(report.files_extracted >= 2, "expected at least 2 files");
1035 assert!(progress.started >= 2, "on_entry_start not called for ZIP");
1036 assert!(
1037 progress.completed >= 2,
1038 "on_entry_complete not called for ZIP"
1039 );
1040 assert!(progress.finished, "on_complete not called for ZIP");
1041 }
1042
1043 #[test]
1045 fn test_progress_callback_invoked_during_sevenz_extraction() {
1046 use crate::ProgressCallback;
1047 use std::path::Path;
1048
1049 struct TrackingProgress {
1050 started: usize,
1051 completed: usize,
1052 finished: bool,
1053 }
1054
1055 impl ProgressCallback for TrackingProgress {
1056 fn on_entry_start(&mut self, _path: &Path, _total: usize, _current: usize) {
1057 self.started += 1;
1058 }
1059
1060 fn on_bytes_written(&mut self, _bytes: u64) {}
1061
1062 fn on_entry_complete(&mut self, _path: &Path) {
1063 self.completed += 1;
1064 }
1065
1066 fn on_complete(&mut self) {
1067 self.finished = true;
1068 }
1069 }
1070
1071 let fixture =
1072 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/simple.7z");
1073
1074 let dest = tempfile::TempDir::new().unwrap();
1075 let mut progress = TrackingProgress {
1076 started: 0,
1077 completed: 0,
1078 finished: false,
1079 };
1080 let report = extract_archive_with_progress(
1081 &fixture,
1082 dest.path(),
1083 &SecurityConfig::default(),
1084 &mut progress,
1085 )
1086 .unwrap();
1087
1088 assert!(
1089 report.files_extracted >= 1,
1090 "expected at least 1 file from simple.7z"
1091 );
1092 assert!(progress.started >= 1, "on_entry_start not called for 7z");
1093 assert!(
1094 progress.completed >= 1,
1095 "on_entry_complete not called for 7z"
1096 );
1097 assert!(progress.finished, "on_complete not called for 7z");
1098 }
1099}