exarch_core/
api.rs

1//! High-level public API for archive extraction, creation, and inspection.
2
3use std::path::Path;
4
5use crate::ExtractionError;
6use crate::ExtractionReport;
7use crate::NoopProgress;
8use crate::ProgressCallback;
9use crate::Result;
10use crate::SecurityConfig;
11use crate::creation::CreationConfig;
12use crate::creation::CreationReport;
13use crate::formats::detect::ArchiveType;
14use crate::formats::detect::detect_format;
15use crate::inspection::ArchiveManifest;
16use crate::inspection::VerificationReport;
17
18/// Extracts an archive to the specified output directory.
19///
20/// This is the main high-level API for extracting archives with security
21/// validation. The archive format is automatically detected.
22///
23/// # Arguments
24///
25/// * `archive_path` - Path to the archive file
26/// * `output_dir` - Directory where files will be extracted
27/// * `config` - Security configuration for the extraction
28///
29/// # Errors
30///
31/// Returns an error if:
32/// - Archive file cannot be opened
33/// - Archive format is unsupported
34/// - Security validation fails
35/// - I/O operations fail
36///
37/// # Examples
38///
39/// ```no_run
40/// use exarch_core::SecurityConfig;
41/// use exarch_core::extract_archive;
42///
43/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
44/// let config = SecurityConfig::default();
45/// let report = extract_archive("archive.tar.gz", "/tmp/output", &config)?;
46/// println!("Extracted {} files", report.files_extracted);
47/// # Ok(())
48/// # }
49/// ```
50pub fn extract_archive<P: AsRef<Path>, Q: AsRef<Path>>(
51    archive_path: P,
52    output_dir: Q,
53    config: &SecurityConfig,
54) -> Result<ExtractionReport> {
55    let mut noop = NoopProgress;
56    extract_archive_with_progress(archive_path, output_dir, config, &mut noop)
57}
58
59/// Extracts an archive with progress reporting.
60///
61/// Same as `extract_archive` but accepts a `ProgressCallback` for
62/// real-time progress updates during extraction.
63///
64/// # Arguments
65///
66/// * `archive_path` - Path to the archive file
67/// * `output_dir` - Directory where files will be extracted
68/// * `config` - Security configuration for the extraction
69/// * `progress` - Callback for progress updates
70///
71/// # Errors
72///
73/// Returns an error if:
74/// - Archive file cannot be opened
75/// - Archive format is unsupported
76/// - Security validation fails
77/// - I/O operations fail
78///
79/// # Examples
80///
81/// ```no_run
82/// use exarch_core::NoopProgress;
83/// use exarch_core::SecurityConfig;
84/// use exarch_core::extract_archive_with_progress;
85///
86/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
87/// let config = SecurityConfig::default();
88/// let mut progress = NoopProgress;
89/// let report =
90///     extract_archive_with_progress("archive.tar.gz", "/tmp/output", &config, &mut progress)?;
91/// println!("Extracted {} files", report.files_extracted);
92/// # Ok(())
93/// # }
94/// ```
95pub fn extract_archive_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
96    archive_path: P,
97    output_dir: Q,
98    config: &SecurityConfig,
99    _progress: &mut dyn ProgressCallback,
100) -> Result<ExtractionReport> {
101    let archive_path = archive_path.as_ref();
102    let output_dir = output_dir.as_ref();
103
104    // Detect archive format from file extension
105    let format = detect_format(archive_path)?;
106
107    // Dispatch to format-specific extraction
108    match format {
109        ArchiveType::Tar => extract_tar(archive_path, output_dir, config),
110        ArchiveType::TarGz => extract_tar_gz(archive_path, output_dir, config),
111        ArchiveType::TarBz2 => extract_tar_bz2(archive_path, output_dir, config),
112        ArchiveType::TarXz => extract_tar_xz(archive_path, output_dir, config),
113        ArchiveType::TarZst => extract_tar_zst(archive_path, output_dir, config),
114        ArchiveType::Zip => extract_zip(archive_path, output_dir, config),
115        ArchiveType::SevenZ => extract_7z(archive_path, output_dir, config),
116    }
117}
118
119fn extract_tar(
120    archive_path: &Path,
121    output_dir: &Path,
122    config: &SecurityConfig,
123) -> Result<ExtractionReport> {
124    use crate::formats::TarArchive;
125    use crate::formats::traits::ArchiveFormat;
126    use std::fs::File;
127    use std::io::BufReader;
128
129    let file = File::open(archive_path)?;
130    let reader = BufReader::new(file);
131    let mut archive = TarArchive::new(reader);
132    archive.extract(output_dir, config)
133}
134
135fn extract_tar_gz(
136    archive_path: &Path,
137    output_dir: &Path,
138    config: &SecurityConfig,
139) -> Result<ExtractionReport> {
140    use crate::formats::TarArchive;
141    use crate::formats::traits::ArchiveFormat;
142    use flate2::read::GzDecoder;
143    use std::fs::File;
144    use std::io::BufReader;
145
146    let file = File::open(archive_path)?;
147    let reader = BufReader::new(file);
148    let decoder = GzDecoder::new(reader);
149    let mut archive = TarArchive::new(decoder);
150    archive.extract(output_dir, config)
151}
152
153fn extract_tar_bz2(
154    archive_path: &Path,
155    output_dir: &Path,
156    config: &SecurityConfig,
157) -> Result<ExtractionReport> {
158    use crate::formats::TarArchive;
159    use crate::formats::traits::ArchiveFormat;
160    use bzip2::read::BzDecoder;
161    use std::fs::File;
162    use std::io::BufReader;
163
164    let file = File::open(archive_path)?;
165    let reader = BufReader::new(file);
166    let decoder = BzDecoder::new(reader);
167    let mut archive = TarArchive::new(decoder);
168    archive.extract(output_dir, config)
169}
170
171fn extract_tar_xz(
172    archive_path: &Path,
173    output_dir: &Path,
174    config: &SecurityConfig,
175) -> Result<ExtractionReport> {
176    use crate::formats::TarArchive;
177    use crate::formats::traits::ArchiveFormat;
178    use std::fs::File;
179    use std::io::BufReader;
180    use xz2::read::XzDecoder;
181
182    let file = File::open(archive_path)?;
183    let reader = BufReader::new(file);
184    let decoder = XzDecoder::new(reader);
185    let mut archive = TarArchive::new(decoder);
186    archive.extract(output_dir, config)
187}
188
189fn extract_tar_zst(
190    archive_path: &Path,
191    output_dir: &Path,
192    config: &SecurityConfig,
193) -> Result<ExtractionReport> {
194    use crate::formats::TarArchive;
195    use crate::formats::traits::ArchiveFormat;
196    use std::fs::File;
197    use std::io::BufReader;
198    use zstd::stream::read::Decoder as ZstdDecoder;
199
200    let file = File::open(archive_path)?;
201    let reader = BufReader::new(file);
202    let decoder = ZstdDecoder::new(reader)?;
203    let mut archive = TarArchive::new(decoder);
204    archive.extract(output_dir, config)
205}
206
207fn extract_zip(
208    archive_path: &Path,
209    output_dir: &Path,
210    config: &SecurityConfig,
211) -> Result<ExtractionReport> {
212    use crate::formats::ZipArchive;
213    use crate::formats::traits::ArchiveFormat;
214    use std::fs::File;
215
216    let file = File::open(archive_path)?;
217    let mut archive = ZipArchive::new(file)?;
218    archive.extract(output_dir, config)
219}
220
221fn extract_7z(
222    archive_path: &Path,
223    output_dir: &Path,
224    config: &SecurityConfig,
225) -> Result<ExtractionReport> {
226    use crate::formats::SevenZArchive;
227    use crate::formats::traits::ArchiveFormat;
228    use std::fs::File;
229
230    let file = File::open(archive_path)?;
231    let mut archive = SevenZArchive::new(file)?;
232    archive.extract(output_dir, config)
233}
234
235/// Creates an archive from source files and directories.
236///
237/// Format is auto-detected from output file extension, or can be
238/// explicitly set via `config.format`.
239///
240/// # Arguments
241///
242/// * `output_path` - Path to the output archive file
243/// * `sources` - Source files and directories to include
244/// * `config` - Creation configuration
245///
246/// # Errors
247///
248/// Returns an error if:
249/// - Cannot determine archive format
250/// - Source files don't exist
251/// - I/O operations fail
252/// - Configuration is invalid
253///
254/// # Examples
255///
256/// ```no_run
257/// use exarch_core::create_archive;
258/// use exarch_core::creation::CreationConfig;
259///
260/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
261/// let config = CreationConfig::default();
262/// let report = create_archive("output.tar.gz", &["src/", "Cargo.toml"], &config)?;
263/// println!("Created archive with {} files", report.files_added);
264/// # Ok(())
265/// # }
266/// ```
267pub fn create_archive<P: AsRef<Path>, Q: AsRef<Path>>(
268    output_path: P,
269    sources: &[Q],
270    config: &CreationConfig,
271) -> Result<CreationReport> {
272    let mut noop = NoopProgress;
273    create_archive_with_progress(output_path, sources, config, &mut noop)
274}
275
276/// Creates an archive with progress reporting.
277///
278/// Same as `create_archive` but accepts a `ProgressCallback` for
279/// real-time progress updates during creation.
280///
281/// # Arguments
282///
283/// * `output_path` - Path to the output archive file
284/// * `sources` - Source files and directories to include
285/// * `config` - Creation configuration
286/// * `progress` - Callback for progress updates
287///
288/// # Errors
289///
290/// Returns an error if:
291/// - Cannot determine archive format
292/// - Source files don't exist
293/// - I/O operations fail
294/// - Configuration is invalid
295///
296/// # Examples
297///
298/// ```no_run
299/// use exarch_core::NoopProgress;
300/// use exarch_core::create_archive_with_progress;
301/// use exarch_core::creation::CreationConfig;
302///
303/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
304/// let config = CreationConfig::default();
305/// let mut progress = NoopProgress;
306/// let report = create_archive_with_progress(
307///     "output.tar.gz",
308///     &["src/", "Cargo.toml"],
309///     &config,
310///     &mut progress,
311/// )?;
312/// println!("Created archive with {} files", report.files_added);
313/// # Ok(())
314/// # }
315/// ```
316pub fn create_archive_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
317    output_path: P,
318    sources: &[Q],
319    config: &CreationConfig,
320    progress: &mut dyn ProgressCallback,
321) -> Result<CreationReport> {
322    let output = output_path.as_ref();
323
324    // Determine format from extension or config
325    let format = determine_creation_format(output, config)?;
326
327    // Dispatch to format-specific creator with progress
328    match format {
329        ArchiveType::Tar => {
330            crate::creation::tar::create_tar_with_progress(output, sources, config, progress)
331        }
332        ArchiveType::TarGz => {
333            crate::creation::tar::create_tar_gz_with_progress(output, sources, config, progress)
334        }
335        ArchiveType::TarBz2 => {
336            crate::creation::tar::create_tar_bz2_with_progress(output, sources, config, progress)
337        }
338        ArchiveType::TarXz => {
339            crate::creation::tar::create_tar_xz_with_progress(output, sources, config, progress)
340        }
341        ArchiveType::TarZst => {
342            crate::creation::tar::create_tar_zst_with_progress(output, sources, config, progress)
343        }
344        ArchiveType::Zip => {
345            crate::creation::zip::create_zip_with_progress(output, sources, config, progress)
346        }
347        ArchiveType::SevenZ => Err(ExtractionError::InvalidArchive(
348            "7z archive creation not yet supported".into(),
349        )),
350    }
351}
352
353/// Lists archive contents without extracting.
354///
355/// Returns a manifest containing metadata for all entries in the archive.
356/// No files are written to disk during this operation.
357///
358/// # Arguments
359///
360/// * `archive_path` - Path to archive file
361/// * `config` - Security configuration (quota limits apply)
362///
363/// # Errors
364///
365/// Returns error if:
366/// - Archive file cannot be opened
367/// - Archive format is unsupported or corrupted
368/// - Quota limits exceeded (file count, total size)
369///
370/// # Examples
371///
372/// ```no_run
373/// use exarch_core::SecurityConfig;
374/// use exarch_core::list_archive;
375///
376/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
377/// let config = SecurityConfig::default();
378/// let manifest = list_archive("archive.tar.gz", &config)?;
379///
380/// println!("Archive contains {} files", manifest.total_entries);
381/// for entry in manifest.entries {
382///     println!("{}: {} bytes", entry.path.display(), entry.size);
383/// }
384/// # Ok(())
385/// # }
386/// ```
387pub fn list_archive<P: AsRef<Path>>(
388    archive_path: P,
389    config: &SecurityConfig,
390) -> Result<ArchiveManifest> {
391    crate::inspection::list_archive(archive_path, config)
392}
393
394/// Verifies archive integrity and security without extracting.
395///
396/// Performs comprehensive validation:
397/// - Integrity checks (structure, checksums)
398/// - Security checks (path traversal, zip bombs, CVEs)
399/// - Policy checks (file types, permissions)
400///
401/// # Arguments
402///
403/// * `archive_path` - Path to archive file
404/// * `config` - Security configuration for validation
405///
406/// # Errors
407///
408/// Returns error if:
409/// - Archive file cannot be opened
410/// - Archive is severely corrupted (cannot read structure)
411///
412/// Security violations are reported in `VerificationReport.issues`,
413/// not as errors.
414///
415/// # Examples
416///
417/// ```no_run
418/// use exarch_core::SecurityConfig;
419/// use exarch_core::VerificationStatus;
420/// use exarch_core::verify_archive;
421///
422/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
423/// let config = SecurityConfig::default();
424/// let report = verify_archive("archive.tar.gz", &config)?;
425///
426/// if report.status == VerificationStatus::Pass {
427///     println!("Archive is safe to extract");
428/// } else {
429///     eprintln!("Security issues found:");
430///     for issue in report.issues {
431///         eprintln!("  [{}] {}", issue.severity, issue.message);
432///     }
433/// }
434/// # Ok(())
435/// # }
436/// ```
437pub fn verify_archive<P: AsRef<Path>>(
438    archive_path: P,
439    config: &SecurityConfig,
440) -> Result<VerificationReport> {
441    crate::inspection::verify_archive(archive_path, config)
442}
443
444/// Determines archive format from output path or config.
445fn determine_creation_format(output: &Path, config: &CreationConfig) -> Result<ArchiveType> {
446    // If format explicitly set in config, use it
447    if let Some(format) = config.format {
448        return Ok(format);
449    }
450
451    // Auto-detect from extension
452    detect_format(output)
453}
454
455#[cfg(test)]
456#[allow(clippy::unwrap_used)]
457mod tests {
458    use super::*;
459    use std::path::PathBuf;
460
461    #[test]
462    fn test_extract_archive_nonexistent_file() {
463        let config = SecurityConfig::default();
464        let result = extract_archive(
465            PathBuf::from("nonexistent_test.tar"),
466            PathBuf::from("/tmp/test"),
467            &config,
468        );
469        // Should fail because file doesn't exist
470        assert!(result.is_err());
471    }
472
473    #[test]
474    fn test_determine_creation_format_tar() {
475        let config = CreationConfig::default();
476        let path = PathBuf::from("archive.tar");
477        let format = determine_creation_format(&path, &config).unwrap();
478        assert_eq!(format, ArchiveType::Tar);
479    }
480
481    #[test]
482    fn test_determine_creation_format_tar_gz() {
483        let config = CreationConfig::default();
484        let path = PathBuf::from("archive.tar.gz");
485        let format = determine_creation_format(&path, &config).unwrap();
486        assert_eq!(format, ArchiveType::TarGz);
487
488        let path2 = PathBuf::from("archive.tgz");
489        let format2 = determine_creation_format(&path2, &config).unwrap();
490        assert_eq!(format2, ArchiveType::TarGz);
491    }
492
493    #[test]
494    fn test_determine_creation_format_tar_bz2() {
495        let config = CreationConfig::default();
496        let path = PathBuf::from("archive.tar.bz2");
497        let format = determine_creation_format(&path, &config).unwrap();
498        assert_eq!(format, ArchiveType::TarBz2);
499    }
500
501    #[test]
502    fn test_determine_creation_format_tar_xz() {
503        let config = CreationConfig::default();
504        let path = PathBuf::from("archive.tar.xz");
505        let format = determine_creation_format(&path, &config).unwrap();
506        assert_eq!(format, ArchiveType::TarXz);
507    }
508
509    #[test]
510    fn test_determine_creation_format_tar_zst() {
511        let config = CreationConfig::default();
512        let path = PathBuf::from("archive.tar.zst");
513        let format = determine_creation_format(&path, &config).unwrap();
514        assert_eq!(format, ArchiveType::TarZst);
515    }
516
517    #[test]
518    fn test_determine_creation_format_zip() {
519        let config = CreationConfig::default();
520        let path = PathBuf::from("archive.zip");
521        let format = determine_creation_format(&path, &config).unwrap();
522        assert_eq!(format, ArchiveType::Zip);
523    }
524
525    #[test]
526    fn test_determine_creation_format_explicit() {
527        let config = CreationConfig::default().with_format(Some(ArchiveType::TarGz));
528        let path = PathBuf::from("archive.xyz");
529        let format = determine_creation_format(&path, &config).unwrap();
530        assert_eq!(format, ArchiveType::TarGz);
531    }
532
533    #[test]
534    fn test_determine_creation_format_unknown() {
535        let config = CreationConfig::default();
536        let path = PathBuf::from("archive.rar");
537        let result = determine_creation_format(&path, &config);
538        assert!(result.is_err());
539    }
540
541    #[test]
542    fn test_extract_archive_7z_not_implemented() {
543        let dest = tempfile::TempDir::new().unwrap();
544        let path = PathBuf::from("test.7z");
545
546        let result = extract_archive(&path, dest.path(), &SecurityConfig::default());
547
548        assert!(result.is_err());
549    }
550
551    #[test]
552    fn test_create_archive_7z_not_supported() {
553        let dest = tempfile::TempDir::new().unwrap();
554        let archive_path = dest.path().join("output.7z");
555
556        let result = create_archive(&archive_path, &[] as &[&str], &CreationConfig::default());
557
558        assert!(result.is_err());
559        assert!(matches!(
560            result.unwrap_err(),
561            ExtractionError::InvalidArchive(_)
562        ));
563    }
564}