1use std::path::Path;
4
5use crate::ExtractionError;
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_impl(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 => extract_tar(archive_path, output_dir, config, options, progress),
125 ArchiveType::TarGz => extract_tar_gz(archive_path, output_dir, config, options, progress),
126 ArchiveType::TarBz2 => extract_tar_bz2(archive_path, output_dir, config, options, progress),
127 ArchiveType::TarXz => extract_tar_xz(archive_path, output_dir, config, options, progress),
128 ArchiveType::TarZst => extract_tar_zst(archive_path, output_dir, config, options, progress),
129 ArchiveType::Zip => extract_zip(archive_path, output_dir, config, options, progress),
130 ArchiveType::SevenZ => extract_7z(archive_path, output_dir, config, options, progress),
131 }
132}
133
134pub fn extract_archive_with_options_and_progress<P: AsRef<Path>, Q: AsRef<Path>>(
156 archive_path: P,
157 output_dir: Q,
158 config: &SecurityConfig,
159 options: &ExtractionOptions,
160 progress: &mut dyn ProgressCallback,
161) -> Result<ExtractionReport> {
162 if options.atomic {
163 extract_atomic(archive_path, output_dir, config, options, progress)
164 } else {
165 extract_impl(archive_path, output_dir, config, options, progress)
166 }
167}
168
169pub fn extract_archive_with_options<P: AsRef<Path>, Q: AsRef<Path>>(
183 archive_path: P,
184 output_dir: Q,
185 config: &SecurityConfig,
186 options: &ExtractionOptions,
187) -> Result<ExtractionReport> {
188 let mut noop = NoopProgress;
189 extract_archive_with_options_and_progress(archive_path, output_dir, config, options, &mut noop)
190}
191
192fn extract_atomic<P: AsRef<Path>, Q: AsRef<Path>>(
193 archive_path: P,
194 output_dir: Q,
195 config: &SecurityConfig,
196 options: &ExtractionOptions,
197 progress: &mut dyn ProgressCallback,
198) -> Result<ExtractionReport> {
199 let output_dir = output_dir.as_ref();
200
201 let canonical_output = if output_dir.exists() {
205 output_dir.canonicalize().map_err(ExtractionError::Io)?
206 } else {
207 output_dir.to_path_buf()
208 };
209
210 let parent =
211 canonical_output
212 .parent()
213 .ok_or_else(|| ExtractionError::InvalidConfiguration {
214 reason: "output directory has no parent".into(),
215 })?;
216
217 std::fs::create_dir_all(parent).map_err(ExtractionError::Io)?;
218
219 let temp_dir = tempfile::tempdir_in(parent).map_err(|e| {
220 ExtractionError::Io(std::io::Error::new(
221 e.kind(),
222 format!(
223 "failed to create temp directory in {}: {e}",
224 parent.display()
225 ),
226 ))
227 })?;
228
229 let result = extract_impl(archive_path, temp_dir.path(), config, options, progress);
230
231 match result {
232 Ok(report) => {
233 let temp_path = temp_dir.keep();
235 std::fs::rename(&temp_path, output_dir).map_err(|e| {
236 let _ = std::fs::remove_dir_all(&temp_path);
238 if e.kind() == std::io::ErrorKind::AlreadyExists {
240 ExtractionError::OutputExists {
241 path: output_dir.to_path_buf(),
242 }
243 } else {
244 ExtractionError::Io(std::io::Error::new(
245 e.kind(),
246 format!("failed to rename temp dir to {}: {e}", output_dir.display()),
247 ))
248 }
249 })?;
250
251 Ok(report)
252 }
253 Err(e) => {
254 Err(e)
256 }
257 }
258}
259
260fn extract_tar(
261 archive_path: &Path,
262 output_dir: &Path,
263 config: &SecurityConfig,
264 options: &ExtractionOptions,
265 progress: &mut dyn ProgressCallback,
266) -> Result<ExtractionReport> {
267 use crate::formats::TarArchive;
268 use crate::formats::traits::ArchiveFormat;
269 use std::fs::File;
270 use std::io::BufReader;
271
272 let file = File::open(archive_path)?;
273 let reader = BufReader::new(file);
274 let mut archive = TarArchive::new(reader);
275 archive.extract(output_dir, config, options, progress)
276}
277
278fn extract_tar_gz(
279 archive_path: &Path,
280 output_dir: &Path,
281 config: &SecurityConfig,
282 options: &ExtractionOptions,
283 progress: &mut dyn ProgressCallback,
284) -> Result<ExtractionReport> {
285 use crate::formats::TarArchive;
286 use crate::formats::traits::ArchiveFormat;
287 use flate2::read::GzDecoder;
288 use std::fs::File;
289 use std::io::BufReader;
290
291 let file = File::open(archive_path)?;
292 let reader = BufReader::new(file);
293 let decoder = GzDecoder::new(reader);
294 let mut archive = TarArchive::new(decoder);
295 archive.extract(output_dir, config, options, progress)
296}
297
298fn extract_tar_bz2(
299 archive_path: &Path,
300 output_dir: &Path,
301 config: &SecurityConfig,
302 options: &ExtractionOptions,
303 progress: &mut dyn ProgressCallback,
304) -> Result<ExtractionReport> {
305 use crate::formats::TarArchive;
306 use crate::formats::traits::ArchiveFormat;
307 use bzip2::read::BzDecoder;
308 use std::fs::File;
309 use std::io::BufReader;
310
311 let file = File::open(archive_path)?;
312 let reader = BufReader::new(file);
313 let decoder = BzDecoder::new(reader);
314 let mut archive = TarArchive::new(decoder);
315 archive.extract(output_dir, config, options, progress)
316}
317
318fn extract_tar_xz(
319 archive_path: &Path,
320 output_dir: &Path,
321 config: &SecurityConfig,
322 options: &ExtractionOptions,
323 progress: &mut dyn ProgressCallback,
324) -> Result<ExtractionReport> {
325 use crate::formats::TarArchive;
326 use crate::formats::traits::ArchiveFormat;
327 use std::fs::File;
328 use std::io::BufReader;
329 use xz2::read::XzDecoder;
330
331 let file = File::open(archive_path)?;
332 let reader = BufReader::new(file);
333 let decoder = XzDecoder::new(reader);
334 let mut archive = TarArchive::new(decoder);
335 archive.extract(output_dir, config, options, progress)
336}
337
338fn extract_tar_zst(
339 archive_path: &Path,
340 output_dir: &Path,
341 config: &SecurityConfig,
342 options: &ExtractionOptions,
343 progress: &mut dyn ProgressCallback,
344) -> Result<ExtractionReport> {
345 use crate::formats::TarArchive;
346 use crate::formats::traits::ArchiveFormat;
347 use std::fs::File;
348 use std::io::BufReader;
349 use zstd::stream::read::Decoder as ZstdDecoder;
350
351 let file = File::open(archive_path)?;
352 let reader = BufReader::new(file);
353 let decoder = ZstdDecoder::new(reader)?;
354 let mut archive = TarArchive::new(decoder);
355 archive.extract(output_dir, config, options, progress)
356}
357
358fn extract_zip(
359 archive_path: &Path,
360 output_dir: &Path,
361 config: &SecurityConfig,
362 options: &ExtractionOptions,
363 progress: &mut dyn ProgressCallback,
364) -> Result<ExtractionReport> {
365 use crate::formats::ZipArchive;
366 use crate::formats::traits::ArchiveFormat;
367 use std::fs::File;
368
369 let file = File::open(archive_path)?;
370 let mut archive = ZipArchive::new(file)?;
371 archive.extract(output_dir, config, options, progress)
372}
373
374fn extract_7z(
375 archive_path: &Path,
376 output_dir: &Path,
377 config: &SecurityConfig,
378 options: &ExtractionOptions,
379 progress: &mut dyn ProgressCallback,
380) -> Result<ExtractionReport> {
381 use crate::formats::SevenZArchive;
382 use crate::formats::traits::ArchiveFormat;
383 use std::fs::File;
384
385 let file = File::open(archive_path)?;
386 let mut archive = SevenZArchive::new(file)?;
387 archive.extract(output_dir, config, options, progress)
388}
389
390pub fn create_archive<P: AsRef<Path>, Q: AsRef<Path>>(
423 output_path: P,
424 sources: &[Q],
425 config: &CreationConfig,
426) -> Result<CreationReport> {
427 let mut noop = NoopProgress;
428 create_archive_with_progress(output_path, sources, config, &mut noop)
429}
430
431pub fn create_archive_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
472 output_path: P,
473 sources: &[Q],
474 config: &CreationConfig,
475 progress: &mut dyn ProgressCallback,
476) -> Result<CreationReport> {
477 config.validate()?;
478
479 let output = output_path.as_ref();
480
481 if config.format.is_none() {
490 reject_zip_family_creation(output)?;
491 }
492
493 let format = determine_creation_format(output, config)?;
495
496 let source_refs: Vec<&Path> = sources.iter().map(AsRef::as_ref).collect();
497 let creator = creator_for_format(format)?;
498 creator.create(output, &source_refs, config, progress)
499}
500
501fn creator_for_format(
502 format: ArchiveType,
503) -> Result<Box<dyn crate::formats::traits::FormatCreator>> {
504 match format {
505 ArchiveType::Tar => Ok(Box::new(crate::creation::TarCreator)),
506 ArchiveType::TarGz => Ok(Box::new(crate::creation::TarGzCreator)),
507 ArchiveType::TarBz2 => Ok(Box::new(crate::creation::TarBz2Creator)),
508 ArchiveType::TarXz => Ok(Box::new(crate::creation::TarXzCreator)),
509 ArchiveType::TarZst => Ok(Box::new(crate::creation::TarZstCreator)),
510 ArchiveType::Zip => Ok(Box::new(crate::creation::ZipCreator)),
511 ArchiveType::SevenZ => Err(ExtractionError::UnsupportedFormat),
512 }
513}
514
515pub fn list_archive<P: AsRef<Path>>(
550 archive_path: P,
551 config: &SecurityConfig,
552) -> Result<ArchiveManifest> {
553 crate::inspection::list_archive(archive_path, config)
554}
555
556pub fn verify_archive<P: AsRef<Path>>(
600 archive_path: P,
601 config: &SecurityConfig,
602) -> Result<VerificationReport> {
603 crate::inspection::verify_archive(archive_path, config)
604}
605
606fn reject_zip_family_creation(output: &Path) -> Result<()> {
612 let Some(ext) = output.extension().and_then(|e| e.to_str()) else {
613 return Ok(());
614 };
615 if is_zip_family_alias(ext) {
616 let ext_lower = ext.to_ascii_lowercase();
617 return Err(ExtractionError::InvalidArchive(format!(
618 "creation for .{ext_lower} isn't supported: the format is ZIP-based but \
619 requires extra structure (signing, manifests, ordering) that exarch \
620 doesn't produce. Use .zip, or set CreationConfig::format = Some(\
621 exarch_core::formats::detect::ArchiveType::Zip) to override."
622 )));
623 }
624 Ok(())
625}
626
627fn determine_creation_format(output: &Path, config: &CreationConfig) -> Result<ArchiveType> {
629 if let Some(format) = config.format {
631 return Ok(format);
632 }
633
634 detect_format(output)
636}
637
638#[cfg(test)]
639#[allow(clippy::unwrap_used)]
640mod tests {
641 use super::*;
642 use std::path::PathBuf;
643
644 #[test]
645 fn test_extract_archive_nonexistent_file() {
646 let config = SecurityConfig::default();
647 let result = extract_archive(
648 PathBuf::from("nonexistent_test.tar"),
649 PathBuf::from("/tmp/test"),
650 &config,
651 );
652 assert!(result.is_err());
654 }
655
656 #[test]
657 fn test_determine_creation_format_tar() {
658 let config = CreationConfig::default();
659 let path = PathBuf::from("archive.tar");
660 let format = determine_creation_format(&path, &config).unwrap();
661 assert_eq!(format, ArchiveType::Tar);
662 }
663
664 #[test]
665 fn test_determine_creation_format_tar_gz() {
666 let config = CreationConfig::default();
667 let path = PathBuf::from("archive.tar.gz");
668 let format = determine_creation_format(&path, &config).unwrap();
669 assert_eq!(format, ArchiveType::TarGz);
670
671 let path2 = PathBuf::from("archive.tgz");
672 let format2 = determine_creation_format(&path2, &config).unwrap();
673 assert_eq!(format2, ArchiveType::TarGz);
674 }
675
676 #[test]
677 fn test_determine_creation_format_tar_bz2() {
678 let config = CreationConfig::default();
679 let path = PathBuf::from("archive.tar.bz2");
680 let format = determine_creation_format(&path, &config).unwrap();
681 assert_eq!(format, ArchiveType::TarBz2);
682 }
683
684 #[test]
685 fn test_determine_creation_format_tar_xz() {
686 let config = CreationConfig::default();
687 let path = PathBuf::from("archive.tar.xz");
688 let format = determine_creation_format(&path, &config).unwrap();
689 assert_eq!(format, ArchiveType::TarXz);
690 }
691
692 #[test]
693 fn test_determine_creation_format_tar_zst() {
694 let config = CreationConfig::default();
695 let path = PathBuf::from("archive.tar.zst");
696 let format = determine_creation_format(&path, &config).unwrap();
697 assert_eq!(format, ArchiveType::TarZst);
698 }
699
700 #[test]
701 fn test_determine_creation_format_zip() {
702 let config = CreationConfig::default();
703 let path = PathBuf::from("archive.zip");
704 let format = determine_creation_format(&path, &config).unwrap();
705 assert_eq!(format, ArchiveType::Zip);
706 }
707
708 #[test]
709 fn test_determine_creation_format_explicit() {
710 let config = CreationConfig::default().with_format(Some(ArchiveType::TarGz));
711 let path = PathBuf::from("archive.xyz");
712 let format = determine_creation_format(&path, &config).unwrap();
713 assert_eq!(format, ArchiveType::TarGz);
714 }
715
716 #[test]
717 fn test_determine_creation_format_unknown() {
718 let config = CreationConfig::default();
719 let path = PathBuf::from("archive.rar");
720 let result = determine_creation_format(&path, &config);
721 assert!(result.is_err());
722 }
723
724 #[test]
725 fn test_extract_archive_7z_not_implemented() {
726 let dest = tempfile::TempDir::new().unwrap();
727 let path = PathBuf::from("test.7z");
728
729 let result = extract_archive(&path, dest.path(), &SecurityConfig::default());
730
731 assert!(result.is_err());
732 }
733
734 #[test]
735 fn test_create_archive_invalid_compression_level_rejected_before_io() {
736 let dest = tempfile::TempDir::new().unwrap();
737 let archive_path = dest.path().join("output.tar.gz");
738 let config = CreationConfig {
739 compression_level: Some(15),
740 ..CreationConfig::default()
741 };
742 let result = create_archive(&archive_path, &[] as &[&str], &config);
743 assert!(
744 matches!(
745 result,
746 Err(ExtractionError::InvalidCompressionLevel { level: 15 })
747 ),
748 "expected InvalidCompressionLevel, got {result:?}",
749 );
750 assert!(!archive_path.exists(), "output file must not be created");
752 }
753
754 #[test]
755 fn test_create_archive_zip_family_not_supported() {
756 let dest = tempfile::TempDir::new().unwrap();
760 for ext in ["apk", "whl", "EPUB"] {
761 let archive_path = dest.path().join(format!("output.{ext}"));
762 let result = create_archive(&archive_path, &[] as &[&str], &CreationConfig::default());
763 assert!(
764 matches!(result, Err(ExtractionError::InvalidArchive(_))),
765 ".{ext} should be rejected, got {result:?}",
766 );
767 }
768 }
769
770 #[test]
771 fn test_create_archive_zip_family_override_bypasses_guard() {
772 let dest = tempfile::TempDir::new().unwrap();
776 let src = dest.path().join("source.txt");
777 std::fs::write(&src, b"hello").unwrap();
778 let archive_path = dest.path().join("output.apk");
779 let config = CreationConfig::default().with_format(Some(ArchiveType::Zip));
780 let result = create_archive(&archive_path, &[&src], &config);
781 assert!(
782 result.is_ok(),
783 "explicit format override should bypass the guard, got {result:?}",
784 );
785 }
786
787 #[test]
788 fn test_create_archive_7z_not_supported() {
789 let dest = tempfile::TempDir::new().unwrap();
790 let archive_path = dest.path().join("output.7z");
791
792 let result = create_archive(&archive_path, &[] as &[&str], &CreationConfig::default());
793
794 assert!(result.is_err());
795 assert!(matches!(
796 result.unwrap_err(),
797 ExtractionError::UnsupportedFormat
798 ));
799 }
800
801 #[test]
802 fn test_extract_archive_with_options_and_progress_non_atomic_delegates_to_normal() {
803 let dest = tempfile::TempDir::new().unwrap();
804 let options = ExtractionOptions {
805 atomic: false,
806 skip_duplicates: true,
807 };
808 let result = extract_archive_with_options_and_progress(
809 PathBuf::from("nonexistent.tar.gz"),
810 dest.path(),
811 &SecurityConfig::default(),
812 &options,
813 &mut NoopProgress,
814 );
815 assert!(result.is_err());
816 }
817
818 #[test]
819 fn test_extract_archive_with_options_delegates() {
820 let dest = tempfile::TempDir::new().unwrap();
821 let options = ExtractionOptions {
822 atomic: false,
823 skip_duplicates: true,
824 };
825 let result = extract_archive_with_options(
826 PathBuf::from("nonexistent.tar.gz"),
827 dest.path(),
828 &SecurityConfig::default(),
829 &options,
830 );
831 assert!(result.is_err());
832 }
833
834 #[test]
835 fn test_extract_atomic_success() {
836 use crate::create_archive;
837 use crate::creation::CreationConfig;
838
839 let archive_dir = tempfile::TempDir::new().unwrap();
841 let archive_path = archive_dir.path().join("test.tar.gz");
842
843 let src_dir = tempfile::TempDir::new().unwrap();
845 std::fs::write(src_dir.path().join("hello.txt"), b"hello world").unwrap();
846 create_archive(&archive_path, &[src_dir.path()], &CreationConfig::default()).unwrap();
847
848 let parent = tempfile::TempDir::new().unwrap();
849 let output_dir = parent.path().join("extracted");
850
851 let options = ExtractionOptions {
852 atomic: true,
853 skip_duplicates: true,
854 };
855 let result = extract_archive_with_options(
856 &archive_path,
857 &output_dir,
858 &SecurityConfig::default(),
859 &options,
860 );
861
862 assert!(result.is_ok());
863 assert!(output_dir.exists());
864 let temp_entries: Vec<_> = std::fs::read_dir(parent.path()).unwrap().collect();
866 assert_eq!(
867 temp_entries.len(),
868 1,
869 "Expected only the output dir, found temp remnants"
870 );
871 }
872
873 #[test]
874 fn test_extract_atomic_failure_cleans_up() {
875 let parent = tempfile::TempDir::new().unwrap();
876 let output_dir = parent.path().join("extracted");
877
878 let options = ExtractionOptions {
879 atomic: true,
880 skip_duplicates: true,
881 };
882 let result = extract_archive_with_options(
883 PathBuf::from("nonexistent_archive.tar.gz"),
884 &output_dir,
885 &SecurityConfig::default(),
886 &options,
887 );
888
889 assert!(result.is_err());
890 assert!(!output_dir.exists());
892 let temp_entries: Vec<_> = std::fs::read_dir(parent.path()).unwrap().collect();
894 assert!(
895 temp_entries.is_empty(),
896 "Temp dir not cleaned up after failure"
897 );
898 }
899
900 #[test]
901 fn test_extract_atomic_output_already_exists_fails() {
902 use crate::create_archive;
903 use crate::creation::CreationConfig;
904
905 let parent = tempfile::TempDir::new().unwrap();
906 let output_dir = parent.path().join("extracted");
907 std::fs::create_dir_all(&output_dir).unwrap();
908 std::fs::write(output_dir.join("existing.txt"), b"old content").unwrap();
911
912 let archive_dir = tempfile::TempDir::new().unwrap();
913 let archive_path = archive_dir.path().join("test.tar.gz");
914 let src_dir = tempfile::TempDir::new().unwrap();
915 std::fs::write(src_dir.path().join("new.txt"), b"new content").unwrap();
916 create_archive(&archive_path, &[src_dir.path()], &CreationConfig::default()).unwrap();
917
918 let options = ExtractionOptions {
919 atomic: true,
920 skip_duplicates: true,
921 };
922 let result = extract_archive_with_options(
923 &archive_path,
924 &output_dir,
925 &SecurityConfig::default(),
926 &options,
927 );
928
929 assert!(result.is_err());
931 assert!(output_dir.join("existing.txt").exists());
933 }
934
935 #[test]
937 fn test_progress_callback_invoked_during_extraction() {
938 use crate::ProgressCallback;
939 use std::path::Path;
940
941 struct TrackingProgress {
942 started: usize,
943 completed: usize,
944 finished: bool,
945 }
946
947 impl ProgressCallback for TrackingProgress {
948 fn on_entry_start(&mut self, _path: &Path, _total: usize, _current: usize) {
949 self.started += 1;
950 }
951
952 fn on_bytes_written(&mut self, _bytes: u64) {}
953
954 fn on_entry_complete(&mut self, _path: &Path) {
955 self.completed += 1;
956 }
957
958 fn on_complete(&mut self) {
959 self.finished = true;
960 }
961 }
962
963 let archive_dir = tempfile::TempDir::new().unwrap();
964 let archive_path = archive_dir.path().join("test.tar.gz");
965 let src_dir = tempfile::TempDir::new().unwrap();
966 std::fs::write(src_dir.path().join("a.txt"), b"hello").unwrap();
967 std::fs::write(src_dir.path().join("b.txt"), b"world").unwrap();
968 create_archive(&archive_path, &[src_dir.path()], &CreationConfig::default()).unwrap();
969
970 let dest = tempfile::TempDir::new().unwrap();
971 let mut progress = TrackingProgress {
972 started: 0,
973 completed: 0,
974 finished: false,
975 };
976
977 let report = extract_archive_with_progress(
978 &archive_path,
979 dest.path(),
980 &SecurityConfig::default(),
981 &mut progress,
982 )
983 .unwrap();
984
985 assert!(report.files_extracted >= 2, "expected at least 2 files");
986 assert!(progress.started >= 2, "on_entry_start not called");
987 assert!(progress.completed >= 2, "on_entry_complete not called");
988 assert!(progress.finished, "on_complete not called");
989 }
990
991 #[test]
993 fn test_progress_callback_invoked_during_zip_extraction() {
994 use crate::ProgressCallback;
995 use std::path::Path;
996
997 struct TrackingProgress {
998 started: usize,
999 completed: usize,
1000 finished: bool,
1001 }
1002
1003 impl ProgressCallback for TrackingProgress {
1004 fn on_entry_start(&mut self, _path: &Path, _total: usize, _current: usize) {
1005 self.started += 1;
1006 }
1007
1008 fn on_bytes_written(&mut self, _bytes: u64) {}
1009
1010 fn on_entry_complete(&mut self, _path: &Path) {
1011 self.completed += 1;
1012 }
1013
1014 fn on_complete(&mut self) {
1015 self.finished = true;
1016 }
1017 }
1018
1019 let tmp = tempfile::TempDir::new().unwrap();
1020 let archive_path = tmp.path().join("test.zip");
1021 let src_dir = tempfile::TempDir::new().unwrap();
1022 std::fs::write(src_dir.path().join("x.txt"), b"foo").unwrap();
1023 std::fs::write(src_dir.path().join("y.txt"), b"bar").unwrap();
1024 let config = CreationConfig::default().with_format(Some(ArchiveType::Zip));
1025 create_archive(&archive_path, &[src_dir.path()], &config).unwrap();
1026
1027 let dest = tempfile::TempDir::new().unwrap();
1028 let mut progress = TrackingProgress {
1029 started: 0,
1030 completed: 0,
1031 finished: false,
1032 };
1033 let report = extract_archive_with_progress(
1034 &archive_path,
1035 dest.path(),
1036 &SecurityConfig::default(),
1037 &mut progress,
1038 )
1039 .unwrap();
1040
1041 assert!(report.files_extracted >= 2, "expected at least 2 files");
1042 assert!(progress.started >= 2, "on_entry_start not called for ZIP");
1043 assert!(
1044 progress.completed >= 2,
1045 "on_entry_complete not called for ZIP"
1046 );
1047 assert!(progress.finished, "on_complete not called for ZIP");
1048 }
1049
1050 #[test]
1052 fn test_progress_callback_invoked_during_sevenz_extraction() {
1053 use crate::ProgressCallback;
1054 use std::path::Path;
1055
1056 struct TrackingProgress {
1057 started: usize,
1058 completed: usize,
1059 finished: bool,
1060 }
1061
1062 impl ProgressCallback for TrackingProgress {
1063 fn on_entry_start(&mut self, _path: &Path, _total: usize, _current: usize) {
1064 self.started += 1;
1065 }
1066
1067 fn on_bytes_written(&mut self, _bytes: u64) {}
1068
1069 fn on_entry_complete(&mut self, _path: &Path) {
1070 self.completed += 1;
1071 }
1072
1073 fn on_complete(&mut self) {
1074 self.finished = true;
1075 }
1076 }
1077
1078 let fixture =
1079 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/simple.7z");
1080
1081 let dest = tempfile::TempDir::new().unwrap();
1082 let mut progress = TrackingProgress {
1083 started: 0,
1084 completed: 0,
1085 finished: false,
1086 };
1087 let report = extract_archive_with_progress(
1088 &fixture,
1089 dest.path(),
1090 &SecurityConfig::default(),
1091 &mut progress,
1092 )
1093 .unwrap();
1094
1095 assert!(
1096 report.files_extracted >= 1,
1097 "expected at least 1 file from simple.7z"
1098 );
1099 assert!(progress.started >= 1, "on_entry_start not called for 7z");
1100 assert!(
1101 progress.completed >= 1,
1102 "on_entry_complete not called for 7z"
1103 );
1104 assert!(progress.finished, "on_complete not called for 7z");
1105 }
1106}