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 archive_path = archive_path.as_ref();
103 let output_dir = output_dir.as_ref();
104
105 let format = detect_format(archive_path)?;
107
108 match format {
110 ArchiveType::Tar => extract_tar(archive_path, output_dir, config),
111 ArchiveType::TarGz => extract_tar_gz(archive_path, output_dir, config),
112 ArchiveType::TarBz2 => extract_tar_bz2(archive_path, output_dir, config),
113 ArchiveType::TarXz => extract_tar_xz(archive_path, output_dir, config),
114 ArchiveType::TarZst => extract_tar_zst(archive_path, output_dir, config),
115 ArchiveType::Zip => extract_zip(archive_path, output_dir, config),
116 ArchiveType::SevenZ => extract_7z(archive_path, output_dir, config),
117 }
118}
119
120pub fn extract_archive_full<P: AsRef<Path>, Q: AsRef<Path>>(
142 archive_path: P,
143 output_dir: Q,
144 config: &SecurityConfig,
145 options: &ExtractionOptions,
146 progress: &mut dyn ProgressCallback,
147) -> Result<ExtractionReport> {
148 if options.atomic {
149 extract_atomic(archive_path, output_dir, config, progress)
150 } else {
151 extract_archive_with_progress(archive_path, output_dir, config, progress)
152 }
153}
154
155pub fn extract_archive_with_options<P: AsRef<Path>, Q: AsRef<Path>>(
168 archive_path: P,
169 output_dir: Q,
170 config: &SecurityConfig,
171 options: &ExtractionOptions,
172) -> Result<ExtractionReport> {
173 let mut noop = NoopProgress;
174 extract_archive_full(archive_path, output_dir, config, options, &mut noop)
175}
176
177fn extract_atomic<P: AsRef<Path>, Q: AsRef<Path>>(
178 archive_path: P,
179 output_dir: Q,
180 config: &SecurityConfig,
181 progress: &mut dyn ProgressCallback,
182) -> Result<ExtractionReport> {
183 let output_dir = output_dir.as_ref();
184
185 let canonical_output = if output_dir.exists() {
189 output_dir.canonicalize().map_err(ExtractionError::Io)?
190 } else {
191 output_dir.to_path_buf()
192 };
193
194 let parent =
195 canonical_output
196 .parent()
197 .ok_or_else(|| ExtractionError::InvalidConfiguration {
198 reason: "output directory has no parent".into(),
199 })?;
200
201 std::fs::create_dir_all(parent).map_err(ExtractionError::Io)?;
202
203 let temp_dir = tempfile::tempdir_in(parent).map_err(|e| {
204 ExtractionError::Io(std::io::Error::new(
205 e.kind(),
206 format!(
207 "failed to create temp directory in {}: {e}",
208 parent.display()
209 ),
210 ))
211 })?;
212
213 let result = extract_archive_with_progress(archive_path, temp_dir.path(), config, progress);
214
215 match result {
216 Ok(report) => {
217 let temp_path = temp_dir.keep();
219 std::fs::rename(&temp_path, output_dir).map_err(|e| {
220 let _ = std::fs::remove_dir_all(&temp_path);
222 if e.kind() == std::io::ErrorKind::AlreadyExists {
224 ExtractionError::OutputExists {
225 path: output_dir.to_path_buf(),
226 }
227 } else {
228 ExtractionError::Io(std::io::Error::new(
229 e.kind(),
230 format!("failed to rename temp dir to {}: {e}", output_dir.display()),
231 ))
232 }
233 })?;
234
235 Ok(report)
236 }
237 Err(e) => {
238 Err(e)
240 }
241 }
242}
243
244fn extract_tar(
245 archive_path: &Path,
246 output_dir: &Path,
247 config: &SecurityConfig,
248) -> Result<ExtractionReport> {
249 use crate::formats::TarArchive;
250 use crate::formats::traits::ArchiveFormat;
251 use std::fs::File;
252 use std::io::BufReader;
253
254 let file = File::open(archive_path)?;
255 let reader = BufReader::new(file);
256 let mut archive = TarArchive::new(reader);
257 archive.extract(output_dir, config)
258}
259
260fn extract_tar_gz(
261 archive_path: &Path,
262 output_dir: &Path,
263 config: &SecurityConfig,
264) -> Result<ExtractionReport> {
265 use crate::formats::TarArchive;
266 use crate::formats::traits::ArchiveFormat;
267 use flate2::read::GzDecoder;
268 use std::fs::File;
269 use std::io::BufReader;
270
271 let file = File::open(archive_path)?;
272 let reader = BufReader::new(file);
273 let decoder = GzDecoder::new(reader);
274 let mut archive = TarArchive::new(decoder);
275 archive.extract(output_dir, config)
276}
277
278fn extract_tar_bz2(
279 archive_path: &Path,
280 output_dir: &Path,
281 config: &SecurityConfig,
282) -> Result<ExtractionReport> {
283 use crate::formats::TarArchive;
284 use crate::formats::traits::ArchiveFormat;
285 use bzip2::read::BzDecoder;
286 use std::fs::File;
287 use std::io::BufReader;
288
289 let file = File::open(archive_path)?;
290 let reader = BufReader::new(file);
291 let decoder = BzDecoder::new(reader);
292 let mut archive = TarArchive::new(decoder);
293 archive.extract(output_dir, config)
294}
295
296fn extract_tar_xz(
297 archive_path: &Path,
298 output_dir: &Path,
299 config: &SecurityConfig,
300) -> Result<ExtractionReport> {
301 use crate::formats::TarArchive;
302 use crate::formats::traits::ArchiveFormat;
303 use std::fs::File;
304 use std::io::BufReader;
305 use xz2::read::XzDecoder;
306
307 let file = File::open(archive_path)?;
308 let reader = BufReader::new(file);
309 let decoder = XzDecoder::new(reader);
310 let mut archive = TarArchive::new(decoder);
311 archive.extract(output_dir, config)
312}
313
314fn extract_tar_zst(
315 archive_path: &Path,
316 output_dir: &Path,
317 config: &SecurityConfig,
318) -> Result<ExtractionReport> {
319 use crate::formats::TarArchive;
320 use crate::formats::traits::ArchiveFormat;
321 use std::fs::File;
322 use std::io::BufReader;
323 use zstd::stream::read::Decoder as ZstdDecoder;
324
325 let file = File::open(archive_path)?;
326 let reader = BufReader::new(file);
327 let decoder = ZstdDecoder::new(reader)?;
328 let mut archive = TarArchive::new(decoder);
329 archive.extract(output_dir, config)
330}
331
332fn extract_zip(
333 archive_path: &Path,
334 output_dir: &Path,
335 config: &SecurityConfig,
336) -> Result<ExtractionReport> {
337 use crate::formats::ZipArchive;
338 use crate::formats::traits::ArchiveFormat;
339 use std::fs::File;
340
341 let file = File::open(archive_path)?;
342 let mut archive = ZipArchive::new(file)?;
343 archive.extract(output_dir, config)
344}
345
346fn extract_7z(
347 archive_path: &Path,
348 output_dir: &Path,
349 config: &SecurityConfig,
350) -> Result<ExtractionReport> {
351 use crate::formats::SevenZArchive;
352 use crate::formats::traits::ArchiveFormat;
353 use std::fs::File;
354
355 let file = File::open(archive_path)?;
356 let mut archive = SevenZArchive::new(file)?;
357 archive.extract(output_dir, config)
358}
359
360pub fn create_archive<P: AsRef<Path>, Q: AsRef<Path>>(
393 output_path: P,
394 sources: &[Q],
395 config: &CreationConfig,
396) -> Result<CreationReport> {
397 let mut noop = NoopProgress;
398 create_archive_with_progress(output_path, sources, config, &mut noop)
399}
400
401pub fn create_archive_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
442 output_path: P,
443 sources: &[Q],
444 config: &CreationConfig,
445 progress: &mut dyn ProgressCallback,
446) -> Result<CreationReport> {
447 let output = output_path.as_ref();
448
449 let format = determine_creation_format(output, config)?;
451
452 match format {
454 ArchiveType::Tar => {
455 crate::creation::tar::create_tar_with_progress(output, sources, config, progress)
456 }
457 ArchiveType::TarGz => {
458 crate::creation::tar::create_tar_gz_with_progress(output, sources, config, progress)
459 }
460 ArchiveType::TarBz2 => {
461 crate::creation::tar::create_tar_bz2_with_progress(output, sources, config, progress)
462 }
463 ArchiveType::TarXz => {
464 crate::creation::tar::create_tar_xz_with_progress(output, sources, config, progress)
465 }
466 ArchiveType::TarZst => {
467 crate::creation::tar::create_tar_zst_with_progress(output, sources, config, progress)
468 }
469 ArchiveType::Zip => {
470 crate::creation::zip::create_zip_with_progress(output, sources, config, progress)
471 }
472 ArchiveType::SevenZ => Err(ExtractionError::InvalidArchive(
473 "7z archive creation not yet supported".into(),
474 )),
475 }
476}
477
478pub fn list_archive<P: AsRef<Path>>(
513 archive_path: P,
514 config: &SecurityConfig,
515) -> Result<ArchiveManifest> {
516 crate::inspection::list_archive(archive_path, config)
517}
518
519pub fn verify_archive<P: AsRef<Path>>(
563 archive_path: P,
564 config: &SecurityConfig,
565) -> Result<VerificationReport> {
566 crate::inspection::verify_archive(archive_path, config)
567}
568
569fn determine_creation_format(output: &Path, config: &CreationConfig) -> Result<ArchiveType> {
571 if let Some(format) = config.format {
573 return Ok(format);
574 }
575
576 detect_format(output)
578}
579
580#[cfg(test)]
581#[allow(clippy::unwrap_used)]
582mod tests {
583 use super::*;
584 use std::path::PathBuf;
585
586 #[test]
587 fn test_extract_archive_nonexistent_file() {
588 let config = SecurityConfig::default();
589 let result = extract_archive(
590 PathBuf::from("nonexistent_test.tar"),
591 PathBuf::from("/tmp/test"),
592 &config,
593 );
594 assert!(result.is_err());
596 }
597
598 #[test]
599 fn test_determine_creation_format_tar() {
600 let config = CreationConfig::default();
601 let path = PathBuf::from("archive.tar");
602 let format = determine_creation_format(&path, &config).unwrap();
603 assert_eq!(format, ArchiveType::Tar);
604 }
605
606 #[test]
607 fn test_determine_creation_format_tar_gz() {
608 let config = CreationConfig::default();
609 let path = PathBuf::from("archive.tar.gz");
610 let format = determine_creation_format(&path, &config).unwrap();
611 assert_eq!(format, ArchiveType::TarGz);
612
613 let path2 = PathBuf::from("archive.tgz");
614 let format2 = determine_creation_format(&path2, &config).unwrap();
615 assert_eq!(format2, ArchiveType::TarGz);
616 }
617
618 #[test]
619 fn test_determine_creation_format_tar_bz2() {
620 let config = CreationConfig::default();
621 let path = PathBuf::from("archive.tar.bz2");
622 let format = determine_creation_format(&path, &config).unwrap();
623 assert_eq!(format, ArchiveType::TarBz2);
624 }
625
626 #[test]
627 fn test_determine_creation_format_tar_xz() {
628 let config = CreationConfig::default();
629 let path = PathBuf::from("archive.tar.xz");
630 let format = determine_creation_format(&path, &config).unwrap();
631 assert_eq!(format, ArchiveType::TarXz);
632 }
633
634 #[test]
635 fn test_determine_creation_format_tar_zst() {
636 let config = CreationConfig::default();
637 let path = PathBuf::from("archive.tar.zst");
638 let format = determine_creation_format(&path, &config).unwrap();
639 assert_eq!(format, ArchiveType::TarZst);
640 }
641
642 #[test]
643 fn test_determine_creation_format_zip() {
644 let config = CreationConfig::default();
645 let path = PathBuf::from("archive.zip");
646 let format = determine_creation_format(&path, &config).unwrap();
647 assert_eq!(format, ArchiveType::Zip);
648 }
649
650 #[test]
651 fn test_determine_creation_format_explicit() {
652 let config = CreationConfig::default().with_format(Some(ArchiveType::TarGz));
653 let path = PathBuf::from("archive.xyz");
654 let format = determine_creation_format(&path, &config).unwrap();
655 assert_eq!(format, ArchiveType::TarGz);
656 }
657
658 #[test]
659 fn test_determine_creation_format_unknown() {
660 let config = CreationConfig::default();
661 let path = PathBuf::from("archive.rar");
662 let result = determine_creation_format(&path, &config);
663 assert!(result.is_err());
664 }
665
666 #[test]
667 fn test_extract_archive_7z_not_implemented() {
668 let dest = tempfile::TempDir::new().unwrap();
669 let path = PathBuf::from("test.7z");
670
671 let result = extract_archive(&path, dest.path(), &SecurityConfig::default());
672
673 assert!(result.is_err());
674 }
675
676 #[test]
677 fn test_create_archive_7z_not_supported() {
678 let dest = tempfile::TempDir::new().unwrap();
679 let archive_path = dest.path().join("output.7z");
680
681 let result = create_archive(&archive_path, &[] as &[&str], &CreationConfig::default());
682
683 assert!(result.is_err());
684 assert!(matches!(
685 result.unwrap_err(),
686 ExtractionError::InvalidArchive(_)
687 ));
688 }
689
690 #[test]
691 fn test_extract_archive_full_non_atomic_delegates_to_normal() {
692 let dest = tempfile::TempDir::new().unwrap();
693 let options = ExtractionOptions { atomic: false };
694 let result = extract_archive_full(
695 PathBuf::from("nonexistent.tar.gz"),
696 dest.path(),
697 &SecurityConfig::default(),
698 &options,
699 &mut NoopProgress,
700 );
701 assert!(result.is_err());
702 }
703
704 #[test]
705 fn test_extract_archive_with_options_delegates() {
706 let dest = tempfile::TempDir::new().unwrap();
707 let options = ExtractionOptions { atomic: false };
708 let result = extract_archive_with_options(
709 PathBuf::from("nonexistent.tar.gz"),
710 dest.path(),
711 &SecurityConfig::default(),
712 &options,
713 );
714 assert!(result.is_err());
715 }
716
717 #[test]
718 fn test_extract_atomic_success() {
719 use crate::create_archive;
720 use crate::creation::CreationConfig;
721
722 let archive_dir = tempfile::TempDir::new().unwrap();
724 let archive_path = archive_dir.path().join("test.tar.gz");
725
726 let src_dir = tempfile::TempDir::new().unwrap();
728 std::fs::write(src_dir.path().join("hello.txt"), b"hello world").unwrap();
729 create_archive(&archive_path, &[src_dir.path()], &CreationConfig::default()).unwrap();
730
731 let parent = tempfile::TempDir::new().unwrap();
732 let output_dir = parent.path().join("extracted");
733
734 let options = ExtractionOptions { atomic: true };
735 let result = extract_archive_with_options(
736 &archive_path,
737 &output_dir,
738 &SecurityConfig::default(),
739 &options,
740 );
741
742 assert!(result.is_ok());
743 assert!(output_dir.exists());
744 let temp_entries: Vec<_> = std::fs::read_dir(parent.path()).unwrap().collect();
746 assert_eq!(
747 temp_entries.len(),
748 1,
749 "Expected only the output dir, found temp remnants"
750 );
751 }
752
753 #[test]
754 fn test_extract_atomic_failure_cleans_up() {
755 let parent = tempfile::TempDir::new().unwrap();
756 let output_dir = parent.path().join("extracted");
757
758 let options = ExtractionOptions { atomic: true };
759 let result = extract_archive_with_options(
760 PathBuf::from("nonexistent_archive.tar.gz"),
761 &output_dir,
762 &SecurityConfig::default(),
763 &options,
764 );
765
766 assert!(result.is_err());
767 assert!(!output_dir.exists());
769 let temp_entries: Vec<_> = std::fs::read_dir(parent.path()).unwrap().collect();
771 assert!(
772 temp_entries.is_empty(),
773 "Temp dir not cleaned up after failure"
774 );
775 }
776
777 #[test]
778 fn test_extract_atomic_output_already_exists_fails() {
779 use crate::create_archive;
780 use crate::creation::CreationConfig;
781
782 let parent = tempfile::TempDir::new().unwrap();
783 let output_dir = parent.path().join("extracted");
784 std::fs::create_dir_all(&output_dir).unwrap();
785 std::fs::write(output_dir.join("existing.txt"), b"old content").unwrap();
788
789 let archive_dir = tempfile::TempDir::new().unwrap();
790 let archive_path = archive_dir.path().join("test.tar.gz");
791 let src_dir = tempfile::TempDir::new().unwrap();
792 std::fs::write(src_dir.path().join("new.txt"), b"new content").unwrap();
793 create_archive(&archive_path, &[src_dir.path()], &CreationConfig::default()).unwrap();
794
795 let options = ExtractionOptions { atomic: true };
796 let result = extract_archive_with_options(
797 &archive_path,
798 &output_dir,
799 &SecurityConfig::default(),
800 &options,
801 );
802
803 assert!(result.is_err());
805 assert!(output_dir.join("existing.txt").exists());
807 }
808}