use crate::ExtractionError;
use crate::ProgressCallback;
use crate::Result;
use crate::creation::compression::compression_level_to_bzip2;
use crate::creation::compression::compression_level_to_flate2;
use crate::creation::compression::compression_level_to_xz;
use crate::creation::compression::compression_level_to_zstd;
use crate::creation::config::CreationConfig;
use crate::creation::filters;
use crate::creation::progress::ProgressReader;
use crate::creation::report::CreationReport;
use crate::creation::walker::EntryType;
use crate::creation::walker::FilteredWalker;
use crate::creation::walker::collect_entries;
use crate::io::CountingWriter;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use tar::Builder;
use tar::Header;
#[allow(dead_code)] pub fn create_tar<P: AsRef<Path>, Q: AsRef<Path>>(
output: P,
sources: &[Q],
config: &CreationConfig,
) -> Result<CreationReport> {
let file = File::create(output.as_ref())?;
create_tar_internal(file, sources, config)
}
#[allow(dead_code)] pub fn create_tar_gz<P: AsRef<Path>, Q: AsRef<Path>>(
output: P,
sources: &[Q],
config: &CreationConfig,
) -> Result<CreationReport> {
let file = File::create(output.as_ref())?;
let level = compression_level_to_flate2(config.compression_level);
let encoder = flate2::write::GzEncoder::new(file, level);
create_tar_internal(encoder, sources, config)
}
#[allow(dead_code)] pub fn create_tar_bz2<P: AsRef<Path>, Q: AsRef<Path>>(
output: P,
sources: &[Q],
config: &CreationConfig,
) -> Result<CreationReport> {
let file = File::create(output.as_ref())?;
let level = compression_level_to_bzip2(config.compression_level);
let encoder = bzip2::write::BzEncoder::new(file, level);
create_tar_internal(encoder, sources, config)
}
#[allow(dead_code)] pub fn create_tar_xz<P: AsRef<Path>, Q: AsRef<Path>>(
output: P,
sources: &[Q],
config: &CreationConfig,
) -> Result<CreationReport> {
let file = File::create(output.as_ref())?;
let level = compression_level_to_xz(config.compression_level);
let encoder = xz2::write::XzEncoder::new(file, level);
create_tar_internal(encoder, sources, config)
}
#[allow(dead_code)] pub fn create_tar_zst<P: AsRef<Path>, Q: AsRef<Path>>(
output: P,
sources: &[Q],
config: &CreationConfig,
) -> Result<CreationReport> {
let file = File::create(output.as_ref())?;
let level = compression_level_to_zstd(config.compression_level);
let mut encoder = zstd::Encoder::new(file, level)?;
encoder.include_checksum(true)?;
let report = create_tar_internal(encoder, sources, config)?;
Ok(report)
}
#[allow(dead_code)] pub fn create_tar_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
output: P,
sources: &[Q],
config: &CreationConfig,
progress: &mut dyn ProgressCallback,
) -> Result<CreationReport> {
let file = File::create(output.as_ref())?;
create_tar_internal_with_progress(file, sources, config, progress)
}
#[allow(dead_code)] pub fn create_tar_gz_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
output: P,
sources: &[Q],
config: &CreationConfig,
progress: &mut dyn ProgressCallback,
) -> Result<CreationReport> {
let file = File::create(output.as_ref())?;
let level = compression_level_to_flate2(config.compression_level);
let encoder = flate2::write::GzEncoder::new(file, level);
create_tar_internal_with_progress(encoder, sources, config, progress)
}
#[allow(dead_code)] pub fn create_tar_bz2_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
output: P,
sources: &[Q],
config: &CreationConfig,
progress: &mut dyn ProgressCallback,
) -> Result<CreationReport> {
let file = File::create(output.as_ref())?;
let level = compression_level_to_bzip2(config.compression_level);
let encoder = bzip2::write::BzEncoder::new(file, level);
create_tar_internal_with_progress(encoder, sources, config, progress)
}
#[allow(dead_code)] pub fn create_tar_xz_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
output: P,
sources: &[Q],
config: &CreationConfig,
progress: &mut dyn ProgressCallback,
) -> Result<CreationReport> {
let file = File::create(output.as_ref())?;
let level = compression_level_to_xz(config.compression_level);
let encoder = xz2::write::XzEncoder::new(file, level);
create_tar_internal_with_progress(encoder, sources, config, progress)
}
#[allow(dead_code)] pub fn create_tar_zst_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
output: P,
sources: &[Q],
config: &CreationConfig,
progress: &mut dyn ProgressCallback,
) -> Result<CreationReport> {
let file = File::create(output.as_ref())?;
let level = compression_level_to_zstd(config.compression_level);
let mut encoder = zstd::Encoder::new(file, level)?;
encoder.include_checksum(true)?;
let report = create_tar_internal_with_progress(encoder, sources, config, progress)?;
Ok(report)
}
fn create_tar_internal_with_progress<W: Write, P: AsRef<Path>>(
writer: W,
sources: &[P],
config: &CreationConfig,
progress: &mut dyn ProgressCallback,
) -> Result<CreationReport> {
let counting_writer = CountingWriter::new(writer);
let mut builder = Builder::new(counting_writer);
let mut report = CreationReport::default();
let start = std::time::Instant::now();
let entries = collect_entries(sources, config)?;
let total_entries = entries.len();
let mut current_entry = 0usize;
let mut buffer = vec![0u8; 64 * 1024];
for entry in &entries {
current_entry += 1;
match &entry.entry_type {
EntryType::File => {
progress.on_entry_start(&entry.archive_path, total_entries, current_entry);
add_file_to_tar_with_progress_impl(
&mut builder,
&entry.path,
&entry.archive_path,
config,
&mut report,
progress,
&mut buffer,
)?;
progress.on_entry_complete(&entry.archive_path);
}
EntryType::Directory => {
progress.on_entry_start(&entry.archive_path, total_entries, current_entry);
report.directories_added += 1;
progress.on_entry_complete(&entry.archive_path);
}
EntryType::Symlink { target } => {
progress.on_entry_start(&entry.archive_path, total_entries, current_entry);
if config.follow_symlinks {
add_file_to_tar_with_progress_impl(
&mut builder,
&entry.path,
&entry.archive_path,
config,
&mut report,
progress,
&mut buffer,
)?;
} else {
add_symlink_to_tar(&mut builder, &entry.archive_path, target, &mut report)?;
}
progress.on_entry_complete(&entry.archive_path);
}
}
}
builder.finish()?;
let mut counting_writer = builder.into_inner()?;
counting_writer.flush()?;
report.bytes_compressed = counting_writer.total_bytes();
report.duration = start.elapsed();
progress.on_complete();
Ok(report)
}
fn create_tar_internal<W: Write, P: AsRef<Path>>(
writer: W,
sources: &[P],
config: &CreationConfig,
) -> Result<CreationReport> {
let counting_writer = CountingWriter::new(writer);
let mut builder = Builder::new(counting_writer);
let mut report = CreationReport::default();
let start = std::time::Instant::now();
for source in sources {
let path = source.as_ref();
if !path.exists() {
return Err(ExtractionError::SourceNotFound {
path: path.to_path_buf(),
});
}
if path.is_dir() {
add_directory_to_tar(&mut builder, path, config, &mut report)?;
} else {
let archive_path =
filters::compute_archive_path(path, path.parent().unwrap_or(path), config)?;
add_file_to_tar(&mut builder, path, &archive_path, config, &mut report)?;
}
}
builder.finish()?;
let mut counting_writer = builder.into_inner()?;
counting_writer.flush()?;
report.bytes_compressed = counting_writer.total_bytes();
report.duration = start.elapsed();
Ok(report)
}
fn add_directory_to_tar<W: Write>(
builder: &mut Builder<W>,
dir: &Path,
config: &CreationConfig,
report: &mut CreationReport,
) -> Result<()> {
let walker = FilteredWalker::new(dir, config);
for entry in walker.walk() {
let entry = entry?;
match entry.entry_type {
EntryType::File => {
add_file_to_tar(builder, &entry.path, &entry.archive_path, config, report)?;
}
EntryType::Directory => {
report.directories_added += 1;
}
EntryType::Symlink { target } => {
if config.follow_symlinks {
add_file_to_tar(builder, &entry.path, &entry.archive_path, config, report)?;
} else {
add_symlink_to_tar(builder, &entry.archive_path, &target, report)?;
}
}
}
}
Ok(())
}
fn add_file_to_tar<W: Write>(
builder: &mut Builder<W>,
file_path: &Path,
archive_path: &Path,
config: &CreationConfig,
report: &mut CreationReport,
) -> Result<()> {
let mut file = File::open(file_path)?;
let metadata = file.metadata()?;
let size = metadata.len();
let mut header = Header::new_gnu();
header.set_size(size);
header.set_cksum();
if config.preserve_permissions {
set_permissions(&mut header, &metadata);
}
builder.append_data(&mut header, archive_path, &mut file)?;
report.files_added += 1;
report.bytes_written += size;
Ok(())
}
fn add_file_to_tar_with_progress_impl<W: Write>(
builder: &mut Builder<W>,
file_path: &Path,
archive_path: &Path,
config: &CreationConfig,
report: &mut CreationReport,
progress: &mut dyn ProgressCallback,
_buffer: &mut [u8],
) -> Result<()> {
let file = File::open(file_path)?;
let metadata = file.metadata()?;
let size = metadata.len();
let mut header = Header::new_gnu();
header.set_size(size);
header.set_cksum();
if config.preserve_permissions {
set_permissions(&mut header, &metadata);
}
let mut tracked_file = ProgressReader::new(file, progress);
builder.append_data(&mut header, archive_path, &mut tracked_file)?;
report.files_added += 1;
report.bytes_written += size;
Ok(())
}
#[cfg(unix)]
fn add_symlink_to_tar<W: Write>(
builder: &mut Builder<W>,
link_path: &Path,
target: &Path,
report: &mut CreationReport,
) -> Result<()> {
let mut header = Header::new_gnu();
header.set_entry_type(tar::EntryType::Symlink);
header.set_size(0);
header.set_cksum();
builder.append_link(&mut header, link_path, target)?;
report.symlinks_added += 1;
Ok(())
}
#[cfg(not(unix))]
fn add_symlink_to_tar<W: Write>(
_builder: &mut Builder<W>,
_link_path: &Path,
_target: &Path,
report: &mut CreationReport,
) -> Result<()> {
report.files_skipped += 1;
report.add_warning("Symlinks not supported on this platform");
Ok(())
}
#[cfg(unix)]
fn set_permissions(header: &mut Header, metadata: &std::fs::Metadata) {
use std::os::unix::fs::MetadataExt;
let mode = metadata.mode();
header.set_mode(mode);
header.set_uid(u64::from(metadata.uid()));
header.set_gid(u64::from(metadata.gid()));
#[allow(clippy::cast_sign_loss)] let mtime = metadata.mtime().max(0) as u64;
header.set_mtime(mtime);
}
#[cfg(not(unix))]
fn set_permissions(header: &mut Header, metadata: &std::fs::Metadata) {
let mode = if metadata.permissions().readonly() {
0o444
} else {
0o644
};
header.set_mode(mode);
if let Ok(modified) = metadata.modified() {
if let Ok(duration) = modified.duration_since(std::time::UNIX_EPOCH) {
header.set_mtime(duration.as_secs());
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
use crate::SecurityConfig;
use crate::api::extract_archive;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_create_tar_single_file() {
let temp = TempDir::new().unwrap();
let output = temp.path().join("output.tar");
let source_dir = TempDir::new().unwrap();
fs::write(source_dir.path().join("test.txt"), "Hello TAR").unwrap();
let config = CreationConfig::default()
.with_exclude_patterns(vec![])
.with_include_hidden(true);
let report = create_tar(&output, &[source_dir.path().join("test.txt")], &config).unwrap();
assert_eq!(report.files_added, 1);
assert!(report.bytes_written > 0);
assert!(output.exists());
}
#[test]
fn test_create_tar_directory() {
let temp = TempDir::new().unwrap();
let output = temp.path().join("output.tar");
let source_dir = TempDir::new().unwrap();
fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
fs::write(source_dir.path().join("file2.txt"), "content2").unwrap();
fs::create_dir(source_dir.path().join("subdir")).unwrap();
fs::write(source_dir.path().join("subdir/file3.txt"), "content3").unwrap();
let config = CreationConfig::default()
.with_exclude_patterns(vec![])
.with_include_hidden(true);
let report = create_tar(&output, &[source_dir.path()], &config).unwrap();
assert_eq!(report.files_added, 3);
assert_eq!(report.directories_added, 2);
assert!(output.exists());
}
#[test]
fn test_create_tar_gz_compression() {
let temp = TempDir::new().unwrap();
let output = temp.path().join("output.tar.gz");
let source_dir = TempDir::new().unwrap();
fs::write(source_dir.path().join("test.txt"), "a".repeat(1000)).unwrap();
let config = CreationConfig::default()
.with_exclude_patterns(vec![])
.with_compression_level(9);
let report = create_tar_gz(&output, &[source_dir.path()], &config).unwrap();
assert_eq!(report.files_added, 1);
assert!(output.exists());
let data = fs::read(&output).unwrap();
assert_eq!(&data[0..2], &[0x1f, 0x8b]); }
#[test]
fn test_create_tar_bz2_compression() {
let temp = TempDir::new().unwrap();
let output = temp.path().join("output.tar.bz2");
let source_dir = TempDir::new().unwrap();
fs::write(source_dir.path().join("test.txt"), "bzip2 test").unwrap();
let config = CreationConfig::default().with_exclude_patterns(vec![]);
let report = create_tar_bz2(&output, &[source_dir.path()], &config).unwrap();
assert_eq!(report.files_added, 1);
assert!(output.exists());
let data = fs::read(&output).unwrap();
assert_eq!(&data[0..3], b"BZh"); }
#[test]
fn test_create_tar_xz_compression() {
let temp = TempDir::new().unwrap();
let output = temp.path().join("output.tar.xz");
let source_dir = TempDir::new().unwrap();
fs::write(source_dir.path().join("test.txt"), "xz test").unwrap();
let config = CreationConfig::default().with_exclude_patterns(vec![]);
let report = create_tar_xz(&output, &[source_dir.path()], &config).unwrap();
assert_eq!(report.files_added, 1);
assert!(output.exists());
let data = fs::read(&output).unwrap();
assert_eq!(&data[0..6], &[0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]); }
#[test]
fn test_create_tar_zst_compression() {
let temp = TempDir::new().unwrap();
let output = temp.path().join("output.tar.zst");
let source_dir = TempDir::new().unwrap();
fs::write(source_dir.path().join("test.txt"), "zstd test").unwrap();
let config = CreationConfig::default().with_exclude_patterns(vec![]);
let report = create_tar_zst(&output, &[source_dir.path()], &config).unwrap();
assert_eq!(report.files_added, 1);
assert!(output.exists());
let data = fs::read(&output).unwrap();
assert!(data.len() >= 4, "output file should have data");
assert_eq!(&data[0..4], &[0x28, 0xB5, 0x2F, 0xFD]); }
#[test]
fn test_create_tar_compression_levels() {
let temp = TempDir::new().unwrap();
let source_dir = TempDir::new().unwrap();
fs::write(source_dir.path().join("test.txt"), "a".repeat(10000)).unwrap();
for level in [1, 6, 9] {
let output = temp.path().join(format!("output_{level}.tar.gz"));
let config = CreationConfig::default()
.with_exclude_patterns(vec![])
.with_compression_level(level);
let report = create_tar_gz(&output, &[source_dir.path()], &config).unwrap();
assert_eq!(report.files_added, 1);
assert!(output.exists());
}
}
#[test]
#[cfg(unix)]
fn test_create_tar_preserves_permissions() {
use std::os::unix::fs::PermissionsExt;
let temp = TempDir::new().unwrap();
let output = temp.path().join("output.tar");
let source_dir = TempDir::new().unwrap();
let file_path = source_dir.path().join("test.txt");
fs::write(&file_path, "content").unwrap();
fs::set_permissions(&file_path, fs::Permissions::from_mode(0o755)).unwrap();
let config = CreationConfig::default()
.with_exclude_patterns(vec![])
.with_preserve_permissions(true);
let report = create_tar(&output, &[source_dir.path()], &config).unwrap();
assert_eq!(report.files_added, 1);
let extract_dir = TempDir::new().unwrap();
let security_config = SecurityConfig::default();
extract_archive(&output, extract_dir.path(), &security_config).unwrap();
let extracted = extract_dir.path().join("test.txt");
let perms = fs::metadata(&extracted).unwrap().permissions();
assert_eq!(perms.mode() & 0o777, 0o755);
}
#[test]
fn test_create_tar_report_statistics() {
let temp = TempDir::new().unwrap();
let output = temp.path().join("output.tar");
let source_dir = TempDir::new().unwrap();
fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
fs::write(source_dir.path().join("file2.txt"), "content2").unwrap();
fs::create_dir(source_dir.path().join("subdir")).unwrap();
fs::write(source_dir.path().join("subdir/file3.txt"), "content3").unwrap();
let config = CreationConfig::default()
.with_exclude_patterns(vec![])
.with_include_hidden(true);
let report = create_tar(&output, &[source_dir.path()], &config).unwrap();
assert_eq!(report.files_added, 3);
assert!(report.directories_added >= 1);
assert_eq!(report.files_skipped, 0);
assert!(!report.has_warnings());
assert!(report.duration.as_nanos() > 0);
}
#[test]
fn test_create_tar_roundtrip() {
let temp = TempDir::new().unwrap();
let output = temp.path().join("output.tar.gz");
let source_dir = TempDir::new().unwrap();
fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
fs::create_dir(source_dir.path().join("subdir")).unwrap();
fs::write(source_dir.path().join("subdir/file2.txt"), "content2").unwrap();
let config = CreationConfig::default()
.with_exclude_patterns(vec![])
.with_include_hidden(true);
let report = create_tar_gz(&output, &[source_dir.path()], &config).unwrap();
assert!(report.files_added >= 2);
let extract_dir = TempDir::new().unwrap();
let security_config = SecurityConfig::default();
extract_archive(&output, extract_dir.path(), &security_config).unwrap();
let extracted1 = fs::read_to_string(extract_dir.path().join("file1.txt")).unwrap();
assert_eq!(extracted1, "content1");
let extracted2 = fs::read_to_string(extract_dir.path().join("subdir/file2.txt")).unwrap();
assert_eq!(extracted2, "content2");
}
#[test]
fn test_create_tar_source_not_found() {
let temp = TempDir::new().unwrap();
let output = temp.path().join("output.tar");
let config = CreationConfig::default();
let result = create_tar(&output, &[Path::new("/nonexistent/path")], &config);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ExtractionError::SourceNotFound { .. }
));
}
#[test]
fn test_compression_level_to_flate2() {
let level = compression_level_to_flate2(None);
assert_eq!(level, flate2::Compression::default());
let level = compression_level_to_flate2(Some(1));
assert_eq!(level, flate2::Compression::fast());
let level = compression_level_to_flate2(Some(9));
assert_eq!(level, flate2::Compression::best());
let level = compression_level_to_flate2(Some(5));
assert_eq!(level, flate2::Compression::new(5));
}
#[test]
fn test_compression_level_to_zstd() {
assert_eq!(compression_level_to_zstd(None), 3);
assert_eq!(compression_level_to_zstd(Some(1)), 1);
assert_eq!(compression_level_to_zstd(Some(6)), 3);
assert_eq!(compression_level_to_zstd(Some(7)), 10);
assert_eq!(compression_level_to_zstd(Some(9)), 19);
}
#[test]
fn test_create_tar_with_progress_callback() {
#[derive(Debug, Default, Clone)]
struct TestProgress {
entries_started: Vec<String>,
entries_completed: Vec<String>,
bytes_written: u64,
completed: bool,
}
impl ProgressCallback for TestProgress {
fn on_entry_start(&mut self, path: &Path, _total: usize, _current: usize) {
self.entries_started
.push(path.to_string_lossy().to_string());
}
fn on_bytes_written(&mut self, bytes: u64) {
self.bytes_written += bytes;
}
fn on_entry_complete(&mut self, path: &Path) {
self.entries_completed
.push(path.to_string_lossy().to_string());
}
fn on_complete(&mut self) {
self.completed = true;
}
}
let temp = TempDir::new().unwrap();
let output = temp.path().join("output.tar");
let source_dir = TempDir::new().unwrap();
fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
fs::write(source_dir.path().join("file2.txt"), "content2").unwrap();
fs::create_dir(source_dir.path().join("subdir")).unwrap();
fs::write(source_dir.path().join("subdir/file3.txt"), "content3").unwrap();
let config = CreationConfig::default()
.with_exclude_patterns(vec![])
.with_include_hidden(true);
let mut progress = TestProgress::default();
let report =
create_tar_with_progress(&output, &[source_dir.path()], &config, &mut progress)
.unwrap();
assert_eq!(report.files_added, 3);
assert!(report.directories_added >= 1);
assert!(
progress.entries_started.len() >= 3,
"Expected at least 3 entry starts, got {}",
progress.entries_started.len()
);
assert!(
progress.entries_completed.len() >= 3,
"Expected at least 3 entry completions, got {}",
progress.entries_completed.len()
);
assert!(
progress.bytes_written > 0,
"Expected bytes written > 0, got {}",
progress.bytes_written
);
assert!(progress.completed, "Expected on_complete to be called");
let has_file1 = progress
.entries_started
.iter()
.any(|p| p.contains("file1.txt"));
let has_file2 = progress
.entries_started
.iter()
.any(|p| p.contains("file2.txt"));
let has_file3 = progress
.entries_started
.iter()
.any(|p| p.contains("file3.txt"));
assert!(has_file1, "Expected file1.txt in progress callbacks");
assert!(has_file2, "Expected file2.txt in progress callbacks");
assert!(has_file3, "Expected file3.txt in progress callbacks");
}
}