use std::path::Path;
use crate::ExtractionError;
use crate::ExtractionReport;
use crate::NoopProgress;
use crate::ProgressCallback;
use crate::Result;
use crate::SecurityConfig;
use crate::config::ExtractionOptions;
use crate::creation::CreationConfig;
use crate::creation::CreationReport;
use crate::formats::detect::ArchiveType;
use crate::formats::detect::detect_format;
use crate::inspection::ArchiveManifest;
use crate::inspection::VerificationReport;
pub fn extract_archive<P: AsRef<Path>, Q: AsRef<Path>>(
archive_path: P,
output_dir: Q,
config: &SecurityConfig,
) -> Result<ExtractionReport> {
let mut noop = NoopProgress;
extract_archive_with_progress(archive_path, output_dir, config, &mut noop)
}
pub fn extract_archive_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
archive_path: P,
output_dir: Q,
config: &SecurityConfig,
progress: &mut dyn ProgressCallback,
) -> Result<ExtractionReport> {
let options = ExtractionOptions::default();
extract_archive_with_progress_and_options(archive_path, output_dir, config, &options, progress)
}
fn extract_archive_with_progress_and_options<P: AsRef<Path>, Q: AsRef<Path>>(
archive_path: P,
output_dir: Q,
config: &SecurityConfig,
options: &ExtractionOptions,
_progress: &mut dyn ProgressCallback,
) -> Result<ExtractionReport> {
let archive_path = archive_path.as_ref();
let output_dir = output_dir.as_ref();
let format = detect_format(archive_path)?;
match format {
ArchiveType::Tar => extract_tar(archive_path, output_dir, config, options),
ArchiveType::TarGz => extract_tar_gz(archive_path, output_dir, config, options),
ArchiveType::TarBz2 => extract_tar_bz2(archive_path, output_dir, config, options),
ArchiveType::TarXz => extract_tar_xz(archive_path, output_dir, config, options),
ArchiveType::TarZst => extract_tar_zst(archive_path, output_dir, config, options),
ArchiveType::Zip => extract_zip(archive_path, output_dir, config, options),
ArchiveType::SevenZ => extract_7z(archive_path, output_dir, config, options),
}
}
pub fn extract_archive_full<P: AsRef<Path>, Q: AsRef<Path>>(
archive_path: P,
output_dir: Q,
config: &SecurityConfig,
options: &ExtractionOptions,
progress: &mut dyn ProgressCallback,
) -> Result<ExtractionReport> {
if options.atomic {
extract_atomic(archive_path, output_dir, config, options, progress)
} else {
extract_archive_with_progress_and_options(
archive_path,
output_dir,
config,
options,
progress,
)
}
}
pub fn extract_archive_with_options<P: AsRef<Path>, Q: AsRef<Path>>(
archive_path: P,
output_dir: Q,
config: &SecurityConfig,
options: &ExtractionOptions,
) -> Result<ExtractionReport> {
let mut noop = NoopProgress;
extract_archive_full(archive_path, output_dir, config, options, &mut noop)
}
fn extract_atomic<P: AsRef<Path>, Q: AsRef<Path>>(
archive_path: P,
output_dir: Q,
config: &SecurityConfig,
options: &ExtractionOptions,
progress: &mut dyn ProgressCallback,
) -> Result<ExtractionReport> {
let output_dir = output_dir.as_ref();
let canonical_output = if output_dir.exists() {
output_dir.canonicalize().map_err(ExtractionError::Io)?
} else {
output_dir.to_path_buf()
};
let parent =
canonical_output
.parent()
.ok_or_else(|| ExtractionError::InvalidConfiguration {
reason: "output directory has no parent".into(),
})?;
std::fs::create_dir_all(parent).map_err(ExtractionError::Io)?;
let temp_dir = tempfile::tempdir_in(parent).map_err(|e| {
ExtractionError::Io(std::io::Error::new(
e.kind(),
format!(
"failed to create temp directory in {}: {e}",
parent.display()
),
))
})?;
let result = extract_archive_with_progress_and_options(
archive_path,
temp_dir.path(),
config,
options,
progress,
);
match result {
Ok(report) => {
let temp_path = temp_dir.keep();
std::fs::rename(&temp_path, output_dir).map_err(|e| {
let _ = std::fs::remove_dir_all(&temp_path);
if e.kind() == std::io::ErrorKind::AlreadyExists {
ExtractionError::OutputExists {
path: output_dir.to_path_buf(),
}
} else {
ExtractionError::Io(std::io::Error::new(
e.kind(),
format!("failed to rename temp dir to {}: {e}", output_dir.display()),
))
}
})?;
Ok(report)
}
Err(e) => {
Err(e)
}
}
}
fn extract_tar(
archive_path: &Path,
output_dir: &Path,
config: &SecurityConfig,
options: &ExtractionOptions,
) -> Result<ExtractionReport> {
use crate::formats::TarArchive;
use crate::formats::traits::ArchiveFormat;
use std::fs::File;
use std::io::BufReader;
let file = File::open(archive_path)?;
let reader = BufReader::new(file);
let mut archive = TarArchive::new(reader);
archive.extract(output_dir, config, options)
}
fn extract_tar_gz(
archive_path: &Path,
output_dir: &Path,
config: &SecurityConfig,
options: &ExtractionOptions,
) -> Result<ExtractionReport> {
use crate::formats::TarArchive;
use crate::formats::traits::ArchiveFormat;
use flate2::read::GzDecoder;
use std::fs::File;
use std::io::BufReader;
let file = File::open(archive_path)?;
let reader = BufReader::new(file);
let decoder = GzDecoder::new(reader);
let mut archive = TarArchive::new(decoder);
archive.extract(output_dir, config, options)
}
fn extract_tar_bz2(
archive_path: &Path,
output_dir: &Path,
config: &SecurityConfig,
options: &ExtractionOptions,
) -> Result<ExtractionReport> {
use crate::formats::TarArchive;
use crate::formats::traits::ArchiveFormat;
use bzip2::read::BzDecoder;
use std::fs::File;
use std::io::BufReader;
let file = File::open(archive_path)?;
let reader = BufReader::new(file);
let decoder = BzDecoder::new(reader);
let mut archive = TarArchive::new(decoder);
archive.extract(output_dir, config, options)
}
fn extract_tar_xz(
archive_path: &Path,
output_dir: &Path,
config: &SecurityConfig,
options: &ExtractionOptions,
) -> Result<ExtractionReport> {
use crate::formats::TarArchive;
use crate::formats::traits::ArchiveFormat;
use std::fs::File;
use std::io::BufReader;
use xz2::read::XzDecoder;
let file = File::open(archive_path)?;
let reader = BufReader::new(file);
let decoder = XzDecoder::new(reader);
let mut archive = TarArchive::new(decoder);
archive.extract(output_dir, config, options)
}
fn extract_tar_zst(
archive_path: &Path,
output_dir: &Path,
config: &SecurityConfig,
options: &ExtractionOptions,
) -> Result<ExtractionReport> {
use crate::formats::TarArchive;
use crate::formats::traits::ArchiveFormat;
use std::fs::File;
use std::io::BufReader;
use zstd::stream::read::Decoder as ZstdDecoder;
let file = File::open(archive_path)?;
let reader = BufReader::new(file);
let decoder = ZstdDecoder::new(reader)?;
let mut archive = TarArchive::new(decoder);
archive.extract(output_dir, config, options)
}
fn extract_zip(
archive_path: &Path,
output_dir: &Path,
config: &SecurityConfig,
options: &ExtractionOptions,
) -> Result<ExtractionReport> {
use crate::formats::ZipArchive;
use crate::formats::traits::ArchiveFormat;
use std::fs::File;
let file = File::open(archive_path)?;
let mut archive = ZipArchive::new(file)?;
archive.extract(output_dir, config, options)
}
fn extract_7z(
archive_path: &Path,
output_dir: &Path,
config: &SecurityConfig,
options: &ExtractionOptions,
) -> Result<ExtractionReport> {
use crate::formats::SevenZArchive;
use crate::formats::traits::ArchiveFormat;
use std::fs::File;
let file = File::open(archive_path)?;
let mut archive = SevenZArchive::new(file)?;
archive.extract(output_dir, config, options)
}
pub fn create_archive<P: AsRef<Path>, Q: AsRef<Path>>(
output_path: P,
sources: &[Q],
config: &CreationConfig,
) -> Result<CreationReport> {
let mut noop = NoopProgress;
create_archive_with_progress(output_path, sources, config, &mut noop)
}
pub fn create_archive_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
output_path: P,
sources: &[Q],
config: &CreationConfig,
progress: &mut dyn ProgressCallback,
) -> Result<CreationReport> {
let output = output_path.as_ref();
let format = determine_creation_format(output, config)?;
match format {
ArchiveType::Tar => {
crate::creation::tar::create_tar_with_progress(output, sources, config, progress)
}
ArchiveType::TarGz => {
crate::creation::tar::create_tar_gz_with_progress(output, sources, config, progress)
}
ArchiveType::TarBz2 => {
crate::creation::tar::create_tar_bz2_with_progress(output, sources, config, progress)
}
ArchiveType::TarXz => {
crate::creation::tar::create_tar_xz_with_progress(output, sources, config, progress)
}
ArchiveType::TarZst => {
crate::creation::tar::create_tar_zst_with_progress(output, sources, config, progress)
}
ArchiveType::Zip => {
crate::creation::zip::create_zip_with_progress(output, sources, config, progress)
}
ArchiveType::SevenZ => Err(ExtractionError::InvalidArchive(
"7z archive creation not yet supported".into(),
)),
}
}
pub fn list_archive<P: AsRef<Path>>(
archive_path: P,
config: &SecurityConfig,
) -> Result<ArchiveManifest> {
crate::inspection::list_archive(archive_path, config)
}
pub fn verify_archive<P: AsRef<Path>>(
archive_path: P,
config: &SecurityConfig,
) -> Result<VerificationReport> {
crate::inspection::verify_archive(archive_path, config)
}
fn determine_creation_format(output: &Path, config: &CreationConfig) -> Result<ArchiveType> {
if let Some(format) = config.format {
return Ok(format);
}
detect_format(output)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_extract_archive_nonexistent_file() {
let config = SecurityConfig::default();
let result = extract_archive(
PathBuf::from("nonexistent_test.tar"),
PathBuf::from("/tmp/test"),
&config,
);
assert!(result.is_err());
}
#[test]
fn test_determine_creation_format_tar() {
let config = CreationConfig::default();
let path = PathBuf::from("archive.tar");
let format = determine_creation_format(&path, &config).unwrap();
assert_eq!(format, ArchiveType::Tar);
}
#[test]
fn test_determine_creation_format_tar_gz() {
let config = CreationConfig::default();
let path = PathBuf::from("archive.tar.gz");
let format = determine_creation_format(&path, &config).unwrap();
assert_eq!(format, ArchiveType::TarGz);
let path2 = PathBuf::from("archive.tgz");
let format2 = determine_creation_format(&path2, &config).unwrap();
assert_eq!(format2, ArchiveType::TarGz);
}
#[test]
fn test_determine_creation_format_tar_bz2() {
let config = CreationConfig::default();
let path = PathBuf::from("archive.tar.bz2");
let format = determine_creation_format(&path, &config).unwrap();
assert_eq!(format, ArchiveType::TarBz2);
}
#[test]
fn test_determine_creation_format_tar_xz() {
let config = CreationConfig::default();
let path = PathBuf::from("archive.tar.xz");
let format = determine_creation_format(&path, &config).unwrap();
assert_eq!(format, ArchiveType::TarXz);
}
#[test]
fn test_determine_creation_format_tar_zst() {
let config = CreationConfig::default();
let path = PathBuf::from("archive.tar.zst");
let format = determine_creation_format(&path, &config).unwrap();
assert_eq!(format, ArchiveType::TarZst);
}
#[test]
fn test_determine_creation_format_zip() {
let config = CreationConfig::default();
let path = PathBuf::from("archive.zip");
let format = determine_creation_format(&path, &config).unwrap();
assert_eq!(format, ArchiveType::Zip);
}
#[test]
fn test_determine_creation_format_explicit() {
let config = CreationConfig::default().with_format(Some(ArchiveType::TarGz));
let path = PathBuf::from("archive.xyz");
let format = determine_creation_format(&path, &config).unwrap();
assert_eq!(format, ArchiveType::TarGz);
}
#[test]
fn test_determine_creation_format_unknown() {
let config = CreationConfig::default();
let path = PathBuf::from("archive.rar");
let result = determine_creation_format(&path, &config);
assert!(result.is_err());
}
#[test]
fn test_extract_archive_7z_not_implemented() {
let dest = tempfile::TempDir::new().unwrap();
let path = PathBuf::from("test.7z");
let result = extract_archive(&path, dest.path(), &SecurityConfig::default());
assert!(result.is_err());
}
#[test]
fn test_create_archive_7z_not_supported() {
let dest = tempfile::TempDir::new().unwrap();
let archive_path = dest.path().join("output.7z");
let result = create_archive(&archive_path, &[] as &[&str], &CreationConfig::default());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ExtractionError::InvalidArchive(_)
));
}
#[test]
fn test_extract_archive_full_non_atomic_delegates_to_normal() {
let dest = tempfile::TempDir::new().unwrap();
let options = ExtractionOptions {
atomic: false,
skip_duplicates: true,
};
let result = extract_archive_full(
PathBuf::from("nonexistent.tar.gz"),
dest.path(),
&SecurityConfig::default(),
&options,
&mut NoopProgress,
);
assert!(result.is_err());
}
#[test]
fn test_extract_archive_with_options_delegates() {
let dest = tempfile::TempDir::new().unwrap();
let options = ExtractionOptions {
atomic: false,
skip_duplicates: true,
};
let result = extract_archive_with_options(
PathBuf::from("nonexistent.tar.gz"),
dest.path(),
&SecurityConfig::default(),
&options,
);
assert!(result.is_err());
}
#[test]
fn test_extract_atomic_success() {
use crate::create_archive;
use crate::creation::CreationConfig;
let archive_dir = tempfile::TempDir::new().unwrap();
let archive_path = archive_dir.path().join("test.tar.gz");
let src_dir = tempfile::TempDir::new().unwrap();
std::fs::write(src_dir.path().join("hello.txt"), b"hello world").unwrap();
create_archive(&archive_path, &[src_dir.path()], &CreationConfig::default()).unwrap();
let parent = tempfile::TempDir::new().unwrap();
let output_dir = parent.path().join("extracted");
let options = ExtractionOptions {
atomic: true,
skip_duplicates: true,
};
let result = extract_archive_with_options(
&archive_path,
&output_dir,
&SecurityConfig::default(),
&options,
);
assert!(result.is_ok());
assert!(output_dir.exists());
let temp_entries: Vec<_> = std::fs::read_dir(parent.path()).unwrap().collect();
assert_eq!(
temp_entries.len(),
1,
"Expected only the output dir, found temp remnants"
);
}
#[test]
fn test_extract_atomic_failure_cleans_up() {
let parent = tempfile::TempDir::new().unwrap();
let output_dir = parent.path().join("extracted");
let options = ExtractionOptions {
atomic: true,
skip_duplicates: true,
};
let result = extract_archive_with_options(
PathBuf::from("nonexistent_archive.tar.gz"),
&output_dir,
&SecurityConfig::default(),
&options,
);
assert!(result.is_err());
assert!(!output_dir.exists());
let temp_entries: Vec<_> = std::fs::read_dir(parent.path()).unwrap().collect();
assert!(
temp_entries.is_empty(),
"Temp dir not cleaned up after failure"
);
}
#[test]
fn test_extract_atomic_output_already_exists_fails() {
use crate::create_archive;
use crate::creation::CreationConfig;
let parent = tempfile::TempDir::new().unwrap();
let output_dir = parent.path().join("extracted");
std::fs::create_dir_all(&output_dir).unwrap();
std::fs::write(output_dir.join("existing.txt"), b"old content").unwrap();
let archive_dir = tempfile::TempDir::new().unwrap();
let archive_path = archive_dir.path().join("test.tar.gz");
let src_dir = tempfile::TempDir::new().unwrap();
std::fs::write(src_dir.path().join("new.txt"), b"new content").unwrap();
create_archive(&archive_path, &[src_dir.path()], &CreationConfig::default()).unwrap();
let options = ExtractionOptions {
atomic: true,
skip_duplicates: true,
};
let result = extract_archive_with_options(
&archive_path,
&output_dir,
&SecurityConfig::default(),
&options,
);
assert!(result.is_err());
assert!(output_dir.join("existing.txt").exists());
}
}