exarch_core/
api.rs

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