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::inspection::ArchiveManifest;
17use crate::inspection::VerificationReport;
18
19pub fn extract_archive<P: AsRef<Path>, Q: AsRef<Path>>(
52 archive_path: P,
53 output_dir: Q,
54 config: &SecurityConfig,
55) -> Result<ExtractionReport> {
56 let mut noop = NoopProgress;
57 extract_archive_with_progress(archive_path, output_dir, config, &mut noop)
58}
59
60pub fn extract_archive_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
97 archive_path: P,
98 output_dir: Q,
99 config: &SecurityConfig,
100 progress: &mut dyn ProgressCallback,
101) -> Result<ExtractionReport> {
102 let options = ExtractionOptions::default();
103 extract_archive_with_progress_and_options(archive_path, output_dir, config, &options, progress)
104}
105
106fn extract_archive_with_progress_and_options<P: AsRef<Path>, Q: AsRef<Path>>(
107 archive_path: P,
108 output_dir: Q,
109 config: &SecurityConfig,
110 options: &ExtractionOptions,
111 _progress: &mut dyn ProgressCallback,
112) -> Result<ExtractionReport> {
113 let archive_path = archive_path.as_ref();
114 let output_dir = output_dir.as_ref();
115
116 let format = detect_format(archive_path)?;
118
119 match format {
121 ArchiveType::Tar => extract_tar(archive_path, output_dir, config, options),
122 ArchiveType::TarGz => extract_tar_gz(archive_path, output_dir, config, options),
123 ArchiveType::TarBz2 => extract_tar_bz2(archive_path, output_dir, config, options),
124 ArchiveType::TarXz => extract_tar_xz(archive_path, output_dir, config, options),
125 ArchiveType::TarZst => extract_tar_zst(archive_path, output_dir, config, options),
126 ArchiveType::Zip => extract_zip(archive_path, output_dir, config, options),
127 ArchiveType::SevenZ => extract_7z(archive_path, output_dir, config, options),
128 }
129}
130
131pub fn extract_archive_full<P: AsRef<Path>, Q: AsRef<Path>>(
153 archive_path: P,
154 output_dir: Q,
155 config: &SecurityConfig,
156 options: &ExtractionOptions,
157 progress: &mut dyn ProgressCallback,
158) -> Result<ExtractionReport> {
159 if options.atomic {
160 extract_atomic(archive_path, output_dir, config, options, progress)
161 } else {
162 extract_archive_with_progress_and_options(
163 archive_path,
164 output_dir,
165 config,
166 options,
167 progress,
168 )
169 }
170}
171
172pub fn extract_archive_with_options<P: AsRef<Path>, Q: AsRef<Path>>(
185 archive_path: P,
186 output_dir: Q,
187 config: &SecurityConfig,
188 options: &ExtractionOptions,
189) -> Result<ExtractionReport> {
190 let mut noop = NoopProgress;
191 extract_archive_full(archive_path, output_dir, config, options, &mut noop)
192}
193
194fn extract_atomic<P: AsRef<Path>, Q: AsRef<Path>>(
195 archive_path: P,
196 output_dir: Q,
197 config: &SecurityConfig,
198 options: &ExtractionOptions,
199 progress: &mut dyn ProgressCallback,
200) -> Result<ExtractionReport> {
201 let output_dir = output_dir.as_ref();
202
203 let canonical_output = if output_dir.exists() {
207 output_dir.canonicalize().map_err(ExtractionError::Io)?
208 } else {
209 output_dir.to_path_buf()
210 };
211
212 let parent =
213 canonical_output
214 .parent()
215 .ok_or_else(|| ExtractionError::InvalidConfiguration {
216 reason: "output directory has no parent".into(),
217 })?;
218
219 std::fs::create_dir_all(parent).map_err(ExtractionError::Io)?;
220
221 let temp_dir = tempfile::tempdir_in(parent).map_err(|e| {
222 ExtractionError::Io(std::io::Error::new(
223 e.kind(),
224 format!(
225 "failed to create temp directory in {}: {e}",
226 parent.display()
227 ),
228 ))
229 })?;
230
231 let result = extract_archive_with_progress_and_options(
232 archive_path,
233 temp_dir.path(),
234 config,
235 options,
236 progress,
237 );
238
239 match result {
240 Ok(report) => {
241 let temp_path = temp_dir.keep();
243 std::fs::rename(&temp_path, output_dir).map_err(|e| {
244 let _ = std::fs::remove_dir_all(&temp_path);
246 if e.kind() == std::io::ErrorKind::AlreadyExists {
248 ExtractionError::OutputExists {
249 path: output_dir.to_path_buf(),
250 }
251 } else {
252 ExtractionError::Io(std::io::Error::new(
253 e.kind(),
254 format!("failed to rename temp dir to {}: {e}", output_dir.display()),
255 ))
256 }
257 })?;
258
259 Ok(report)
260 }
261 Err(e) => {
262 Err(e)
264 }
265 }
266}
267
268fn extract_tar(
269 archive_path: &Path,
270 output_dir: &Path,
271 config: &SecurityConfig,
272 options: &ExtractionOptions,
273) -> Result<ExtractionReport> {
274 use crate::formats::TarArchive;
275 use crate::formats::traits::ArchiveFormat;
276 use std::fs::File;
277 use std::io::BufReader;
278
279 let file = File::open(archive_path)?;
280 let reader = BufReader::new(file);
281 let mut archive = TarArchive::new(reader);
282 archive.extract(output_dir, config, options)
283}
284
285fn extract_tar_gz(
286 archive_path: &Path,
287 output_dir: &Path,
288 config: &SecurityConfig,
289 options: &ExtractionOptions,
290) -> Result<ExtractionReport> {
291 use crate::formats::TarArchive;
292 use crate::formats::traits::ArchiveFormat;
293 use flate2::read::GzDecoder;
294 use std::fs::File;
295 use std::io::BufReader;
296
297 let file = File::open(archive_path)?;
298 let reader = BufReader::new(file);
299 let decoder = GzDecoder::new(reader);
300 let mut archive = TarArchive::new(decoder);
301 archive.extract(output_dir, config, options)
302}
303
304fn extract_tar_bz2(
305 archive_path: &Path,
306 output_dir: &Path,
307 config: &SecurityConfig,
308 options: &ExtractionOptions,
309) -> Result<ExtractionReport> {
310 use crate::formats::TarArchive;
311 use crate::formats::traits::ArchiveFormat;
312 use bzip2::read::BzDecoder;
313 use std::fs::File;
314 use std::io::BufReader;
315
316 let file = File::open(archive_path)?;
317 let reader = BufReader::new(file);
318 let decoder = BzDecoder::new(reader);
319 let mut archive = TarArchive::new(decoder);
320 archive.extract(output_dir, config, options)
321}
322
323fn extract_tar_xz(
324 archive_path: &Path,
325 output_dir: &Path,
326 config: &SecurityConfig,
327 options: &ExtractionOptions,
328) -> Result<ExtractionReport> {
329 use crate::formats::TarArchive;
330 use crate::formats::traits::ArchiveFormat;
331 use std::fs::File;
332 use std::io::BufReader;
333 use xz2::read::XzDecoder;
334
335 let file = File::open(archive_path)?;
336 let reader = BufReader::new(file);
337 let decoder = XzDecoder::new(reader);
338 let mut archive = TarArchive::new(decoder);
339 archive.extract(output_dir, config, options)
340}
341
342fn extract_tar_zst(
343 archive_path: &Path,
344 output_dir: &Path,
345 config: &SecurityConfig,
346 options: &ExtractionOptions,
347) -> Result<ExtractionReport> {
348 use crate::formats::TarArchive;
349 use crate::formats::traits::ArchiveFormat;
350 use std::fs::File;
351 use std::io::BufReader;
352 use zstd::stream::read::Decoder as ZstdDecoder;
353
354 let file = File::open(archive_path)?;
355 let reader = BufReader::new(file);
356 let decoder = ZstdDecoder::new(reader)?;
357 let mut archive = TarArchive::new(decoder);
358 archive.extract(output_dir, config, options)
359}
360
361fn extract_zip(
362 archive_path: &Path,
363 output_dir: &Path,
364 config: &SecurityConfig,
365 options: &ExtractionOptions,
366) -> Result<ExtractionReport> {
367 use crate::formats::ZipArchive;
368 use crate::formats::traits::ArchiveFormat;
369 use std::fs::File;
370
371 let file = File::open(archive_path)?;
372 let mut archive = ZipArchive::new(file)?;
373 archive.extract(output_dir, config, options)
374}
375
376fn extract_7z(
377 archive_path: &Path,
378 output_dir: &Path,
379 config: &SecurityConfig,
380 options: &ExtractionOptions,
381) -> Result<ExtractionReport> {
382 use crate::formats::SevenZArchive;
383 use crate::formats::traits::ArchiveFormat;
384 use std::fs::File;
385
386 let file = File::open(archive_path)?;
387 let mut archive = SevenZArchive::new(file)?;
388 archive.extract(output_dir, config, options)
389}
390
391pub fn create_archive<P: AsRef<Path>, Q: AsRef<Path>>(
424 output_path: P,
425 sources: &[Q],
426 config: &CreationConfig,
427) -> Result<CreationReport> {
428 let mut noop = NoopProgress;
429 create_archive_with_progress(output_path, sources, config, &mut noop)
430}
431
432pub fn create_archive_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
473 output_path: P,
474 sources: &[Q],
475 config: &CreationConfig,
476 progress: &mut dyn ProgressCallback,
477) -> Result<CreationReport> {
478 let output = output_path.as_ref();
479
480 let format = determine_creation_format(output, config)?;
482
483 match format {
485 ArchiveType::Tar => {
486 crate::creation::tar::create_tar_with_progress(output, sources, config, progress)
487 }
488 ArchiveType::TarGz => {
489 crate::creation::tar::create_tar_gz_with_progress(output, sources, config, progress)
490 }
491 ArchiveType::TarBz2 => {
492 crate::creation::tar::create_tar_bz2_with_progress(output, sources, config, progress)
493 }
494 ArchiveType::TarXz => {
495 crate::creation::tar::create_tar_xz_with_progress(output, sources, config, progress)
496 }
497 ArchiveType::TarZst => {
498 crate::creation::tar::create_tar_zst_with_progress(output, sources, config, progress)
499 }
500 ArchiveType::Zip => {
501 crate::creation::zip::create_zip_with_progress(output, sources, config, progress)
502 }
503 ArchiveType::SevenZ => Err(ExtractionError::InvalidArchive(
504 "7z archive creation not yet supported".into(),
505 )),
506 }
507}
508
509pub fn list_archive<P: AsRef<Path>>(
544 archive_path: P,
545 config: &SecurityConfig,
546) -> Result<ArchiveManifest> {
547 crate::inspection::list_archive(archive_path, config)
548}
549
550pub fn verify_archive<P: AsRef<Path>>(
594 archive_path: P,
595 config: &SecurityConfig,
596) -> Result<VerificationReport> {
597 crate::inspection::verify_archive(archive_path, config)
598}
599
600fn determine_creation_format(output: &Path, config: &CreationConfig) -> Result<ArchiveType> {
602 if let Some(format) = config.format {
604 return Ok(format);
605 }
606
607 detect_format(output)
609}
610
611#[cfg(test)]
612#[allow(clippy::unwrap_used)]
613mod tests {
614 use super::*;
615 use std::path::PathBuf;
616
617 #[test]
618 fn test_extract_archive_nonexistent_file() {
619 let config = SecurityConfig::default();
620 let result = extract_archive(
621 PathBuf::from("nonexistent_test.tar"),
622 PathBuf::from("/tmp/test"),
623 &config,
624 );
625 assert!(result.is_err());
627 }
628
629 #[test]
630 fn test_determine_creation_format_tar() {
631 let config = CreationConfig::default();
632 let path = PathBuf::from("archive.tar");
633 let format = determine_creation_format(&path, &config).unwrap();
634 assert_eq!(format, ArchiveType::Tar);
635 }
636
637 #[test]
638 fn test_determine_creation_format_tar_gz() {
639 let config = CreationConfig::default();
640 let path = PathBuf::from("archive.tar.gz");
641 let format = determine_creation_format(&path, &config).unwrap();
642 assert_eq!(format, ArchiveType::TarGz);
643
644 let path2 = PathBuf::from("archive.tgz");
645 let format2 = determine_creation_format(&path2, &config).unwrap();
646 assert_eq!(format2, ArchiveType::TarGz);
647 }
648
649 #[test]
650 fn test_determine_creation_format_tar_bz2() {
651 let config = CreationConfig::default();
652 let path = PathBuf::from("archive.tar.bz2");
653 let format = determine_creation_format(&path, &config).unwrap();
654 assert_eq!(format, ArchiveType::TarBz2);
655 }
656
657 #[test]
658 fn test_determine_creation_format_tar_xz() {
659 let config = CreationConfig::default();
660 let path = PathBuf::from("archive.tar.xz");
661 let format = determine_creation_format(&path, &config).unwrap();
662 assert_eq!(format, ArchiveType::TarXz);
663 }
664
665 #[test]
666 fn test_determine_creation_format_tar_zst() {
667 let config = CreationConfig::default();
668 let path = PathBuf::from("archive.tar.zst");
669 let format = determine_creation_format(&path, &config).unwrap();
670 assert_eq!(format, ArchiveType::TarZst);
671 }
672
673 #[test]
674 fn test_determine_creation_format_zip() {
675 let config = CreationConfig::default();
676 let path = PathBuf::from("archive.zip");
677 let format = determine_creation_format(&path, &config).unwrap();
678 assert_eq!(format, ArchiveType::Zip);
679 }
680
681 #[test]
682 fn test_determine_creation_format_explicit() {
683 let config = CreationConfig::default().with_format(Some(ArchiveType::TarGz));
684 let path = PathBuf::from("archive.xyz");
685 let format = determine_creation_format(&path, &config).unwrap();
686 assert_eq!(format, ArchiveType::TarGz);
687 }
688
689 #[test]
690 fn test_determine_creation_format_unknown() {
691 let config = CreationConfig::default();
692 let path = PathBuf::from("archive.rar");
693 let result = determine_creation_format(&path, &config);
694 assert!(result.is_err());
695 }
696
697 #[test]
698 fn test_extract_archive_7z_not_implemented() {
699 let dest = tempfile::TempDir::new().unwrap();
700 let path = PathBuf::from("test.7z");
701
702 let result = extract_archive(&path, dest.path(), &SecurityConfig::default());
703
704 assert!(result.is_err());
705 }
706
707 #[test]
708 fn test_create_archive_7z_not_supported() {
709 let dest = tempfile::TempDir::new().unwrap();
710 let archive_path = dest.path().join("output.7z");
711
712 let result = create_archive(&archive_path, &[] as &[&str], &CreationConfig::default());
713
714 assert!(result.is_err());
715 assert!(matches!(
716 result.unwrap_err(),
717 ExtractionError::InvalidArchive(_)
718 ));
719 }
720
721 #[test]
722 fn test_extract_archive_full_non_atomic_delegates_to_normal() {
723 let dest = tempfile::TempDir::new().unwrap();
724 let options = ExtractionOptions {
725 atomic: false,
726 skip_duplicates: true,
727 };
728 let result = extract_archive_full(
729 PathBuf::from("nonexistent.tar.gz"),
730 dest.path(),
731 &SecurityConfig::default(),
732 &options,
733 &mut NoopProgress,
734 );
735 assert!(result.is_err());
736 }
737
738 #[test]
739 fn test_extract_archive_with_options_delegates() {
740 let dest = tempfile::TempDir::new().unwrap();
741 let options = ExtractionOptions {
742 atomic: false,
743 skip_duplicates: true,
744 };
745 let result = extract_archive_with_options(
746 PathBuf::from("nonexistent.tar.gz"),
747 dest.path(),
748 &SecurityConfig::default(),
749 &options,
750 );
751 assert!(result.is_err());
752 }
753
754 #[test]
755 fn test_extract_atomic_success() {
756 use crate::create_archive;
757 use crate::creation::CreationConfig;
758
759 let archive_dir = tempfile::TempDir::new().unwrap();
761 let archive_path = archive_dir.path().join("test.tar.gz");
762
763 let src_dir = tempfile::TempDir::new().unwrap();
765 std::fs::write(src_dir.path().join("hello.txt"), b"hello world").unwrap();
766 create_archive(&archive_path, &[src_dir.path()], &CreationConfig::default()).unwrap();
767
768 let parent = tempfile::TempDir::new().unwrap();
769 let output_dir = parent.path().join("extracted");
770
771 let options = ExtractionOptions {
772 atomic: true,
773 skip_duplicates: true,
774 };
775 let result = extract_archive_with_options(
776 &archive_path,
777 &output_dir,
778 &SecurityConfig::default(),
779 &options,
780 );
781
782 assert!(result.is_ok());
783 assert!(output_dir.exists());
784 let temp_entries: Vec<_> = std::fs::read_dir(parent.path()).unwrap().collect();
786 assert_eq!(
787 temp_entries.len(),
788 1,
789 "Expected only the output dir, found temp remnants"
790 );
791 }
792
793 #[test]
794 fn test_extract_atomic_failure_cleans_up() {
795 let parent = tempfile::TempDir::new().unwrap();
796 let output_dir = parent.path().join("extracted");
797
798 let options = ExtractionOptions {
799 atomic: true,
800 skip_duplicates: true,
801 };
802 let result = extract_archive_with_options(
803 PathBuf::from("nonexistent_archive.tar.gz"),
804 &output_dir,
805 &SecurityConfig::default(),
806 &options,
807 );
808
809 assert!(result.is_err());
810 assert!(!output_dir.exists());
812 let temp_entries: Vec<_> = std::fs::read_dir(parent.path()).unwrap().collect();
814 assert!(
815 temp_entries.is_empty(),
816 "Temp dir not cleaned up after failure"
817 );
818 }
819
820 #[test]
821 fn test_extract_atomic_output_already_exists_fails() {
822 use crate::create_archive;
823 use crate::creation::CreationConfig;
824
825 let parent = tempfile::TempDir::new().unwrap();
826 let output_dir = parent.path().join("extracted");
827 std::fs::create_dir_all(&output_dir).unwrap();
828 std::fs::write(output_dir.join("existing.txt"), b"old content").unwrap();
831
832 let archive_dir = tempfile::TempDir::new().unwrap();
833 let archive_path = archive_dir.path().join("test.tar.gz");
834 let src_dir = tempfile::TempDir::new().unwrap();
835 std::fs::write(src_dir.path().join("new.txt"), b"new content").unwrap();
836 create_archive(&archive_path, &[src_dir.path()], &CreationConfig::default()).unwrap();
837
838 let options = ExtractionOptions {
839 atomic: true,
840 skip_duplicates: true,
841 };
842 let result = extract_archive_with_options(
843 &archive_path,
844 &output_dir,
845 &SecurityConfig::default(),
846 &options,
847 );
848
849 assert!(result.is_err());
851 assert!(output_dir.join("existing.txt").exists());
853 }
854}