exarch_core/creation/
tar.rs

1//! TAR archive creation with multiple compression formats.
2//!
3//! This module provides functions for creating TAR archives with various
4//! compression options: uncompressed, gzip, bzip2, xz, and zstd.
5
6use crate::ExtractionError;
7use crate::ProgressCallback;
8use crate::Result;
9use crate::creation::compression::compression_level_to_bzip2;
10use crate::creation::compression::compression_level_to_flate2;
11use crate::creation::compression::compression_level_to_xz;
12use crate::creation::compression::compression_level_to_zstd;
13use crate::creation::config::CreationConfig;
14use crate::creation::filters;
15use crate::creation::progress::ProgressReader;
16use crate::creation::report::CreationReport;
17use crate::creation::walker::EntryType;
18use crate::creation::walker::FilteredWalker;
19use crate::creation::walker::collect_entries;
20use crate::io::CountingWriter;
21use std::fs::File;
22use std::io::Write;
23use std::path::Path;
24use tar::Builder;
25use tar::Header;
26
27/// Creates an uncompressed TAR archive.
28///
29/// # Examples
30///
31/// ```no_run
32/// use exarch_core::creation::CreationConfig;
33/// use exarch_core::creation::tar::create_tar;
34/// use std::path::Path;
35///
36/// let config = CreationConfig::default();
37/// let report = create_tar(Path::new("output.tar"), &[Path::new("src")], &config)?;
38/// println!("Added {} files", report.files_added);
39/// # Ok::<(), exarch_core::ExtractionError>(())
40/// ```
41///
42/// # Errors
43///
44/// Returns an error if:
45/// - Source path does not exist
46/// - Output file cannot be created
47/// - I/O error during archive creation
48#[allow(dead_code)] // Will be used by CLI
49pub fn create_tar<P: AsRef<Path>, Q: AsRef<Path>>(
50    output: P,
51    sources: &[Q],
52    config: &CreationConfig,
53) -> Result<CreationReport> {
54    let file = File::create(output.as_ref())?;
55    create_tar_internal(file, sources, config)
56}
57
58/// Creates a gzip-compressed TAR archive (.tar.gz).
59///
60/// # Examples
61///
62/// ```no_run
63/// use exarch_core::creation::CreationConfig;
64/// use exarch_core::creation::tar::create_tar_gz;
65/// use std::path::Path;
66///
67/// let config = CreationConfig::default().with_compression_level(9);
68/// let report = create_tar_gz(
69///     Path::new("output.tar.gz"),
70///     &[Path::new("src"), Path::new("tests")],
71///     &config,
72/// )?;
73/// # Ok::<(), exarch_core::ExtractionError>(())
74/// ```
75///
76/// # Errors
77///
78/// Returns an error if:
79/// - Source path does not exist
80/// - Output file cannot be created
81/// - Compression fails
82/// - I/O error during archive creation
83#[allow(dead_code)] // Will be used by CLI
84pub fn create_tar_gz<P: AsRef<Path>, Q: AsRef<Path>>(
85    output: P,
86    sources: &[Q],
87    config: &CreationConfig,
88) -> Result<CreationReport> {
89    let file = File::create(output.as_ref())?;
90    let level = compression_level_to_flate2(config.compression_level);
91    let encoder = flate2::write::GzEncoder::new(file, level);
92    create_tar_internal(encoder, sources, config)
93}
94
95/// Creates a bzip2-compressed TAR archive (.tar.bz2).
96///
97/// # Examples
98///
99/// ```no_run
100/// use exarch_core::creation::CreationConfig;
101/// use exarch_core::creation::tar::create_tar_bz2;
102/// use std::path::Path;
103///
104/// let config = CreationConfig::default();
105/// let report = create_tar_bz2(Path::new("output.tar.bz2"), &[Path::new("src")], &config)?;
106/// # Ok::<(), exarch_core::ExtractionError>(())
107/// ```
108///
109/// # Errors
110///
111/// Returns an error if:
112/// - Source path does not exist
113/// - Output file cannot be created
114/// - Compression fails
115/// - I/O error during archive creation
116#[allow(dead_code)] // Will be used by CLI
117pub fn create_tar_bz2<P: AsRef<Path>, Q: AsRef<Path>>(
118    output: P,
119    sources: &[Q],
120    config: &CreationConfig,
121) -> Result<CreationReport> {
122    let file = File::create(output.as_ref())?;
123    let level = compression_level_to_bzip2(config.compression_level);
124    let encoder = bzip2::write::BzEncoder::new(file, level);
125    create_tar_internal(encoder, sources, config)
126}
127
128/// Creates an xz-compressed TAR archive (.tar.xz).
129///
130/// # Examples
131///
132/// ```no_run
133/// use exarch_core::creation::CreationConfig;
134/// use exarch_core::creation::tar::create_tar_xz;
135/// use std::path::Path;
136///
137/// let config = CreationConfig::default();
138/// let report = create_tar_xz(Path::new("output.tar.xz"), &[Path::new("src")], &config)?;
139/// # Ok::<(), exarch_core::ExtractionError>(())
140/// ```
141///
142/// # Errors
143///
144/// Returns an error if:
145/// - Source path does not exist
146/// - Output file cannot be created
147/// - Compression fails
148/// - I/O error during archive creation
149#[allow(dead_code)] // Will be used by CLI
150pub fn create_tar_xz<P: AsRef<Path>, Q: AsRef<Path>>(
151    output: P,
152    sources: &[Q],
153    config: &CreationConfig,
154) -> Result<CreationReport> {
155    let file = File::create(output.as_ref())?;
156    let level = compression_level_to_xz(config.compression_level);
157    let encoder = xz2::write::XzEncoder::new(file, level);
158    create_tar_internal(encoder, sources, config)
159}
160
161/// Creates a zstd-compressed TAR archive (.tar.zst).
162///
163/// # Examples
164///
165/// ```no_run
166/// use exarch_core::creation::CreationConfig;
167/// use exarch_core::creation::tar::create_tar_zst;
168/// use std::path::Path;
169///
170/// let config = CreationConfig::default();
171/// let report = create_tar_zst(Path::new("output.tar.zst"), &[Path::new("src")], &config)?;
172/// # Ok::<(), exarch_core::ExtractionError>(())
173/// ```
174///
175/// # Errors
176///
177/// Returns an error if:
178/// - Source path does not exist
179/// - Output file cannot be created
180/// - Compression fails
181/// - I/O error during archive creation
182#[allow(dead_code)] // Will be used by CLI
183pub fn create_tar_zst<P: AsRef<Path>, Q: AsRef<Path>>(
184    output: P,
185    sources: &[Q],
186    config: &CreationConfig,
187) -> Result<CreationReport> {
188    let file = File::create(output.as_ref())?;
189    let level = compression_level_to_zstd(config.compression_level);
190    let mut encoder = zstd::Encoder::new(file, level)?;
191    encoder.include_checksum(true)?;
192
193    let report = create_tar_internal(encoder, sources, config)?;
194
195    // zstd encoder needs explicit finish() to flush data
196    // This is already done by into_inner() in create_tar_internal via
197    // builder.into_inner() But we rely on Drop to finish the encoder
198
199    Ok(report)
200}
201
202/// Creates an uncompressed TAR archive with progress reporting.
203///
204/// This function provides real-time progress updates during archive creation
205/// through callback functions. Useful for displaying progress bars or logging
206/// in interactive applications.
207///
208/// # Parameters
209///
210/// - `output`: Path where the TAR archive will be created
211/// - `sources`: Slice of source paths to include in the archive
212/// - `config`: Configuration controlling filtering, permissions, and archiving
213///   behavior
214/// - `progress`: Mutable reference to a progress callback implementation
215///
216/// # Progress Callbacks
217///
218/// The `progress` callback receives four types of events:
219///
220/// 1. `on_entry_start`: Called before processing each file/directory
221/// 2. `on_bytes_written`: Called for each chunk of data written (typically
222///    every 64 KB)
223/// 3. `on_entry_complete`: Called after successfully processing an entry
224/// 4. `on_complete`: Called once when the entire archive is finished
225///
226/// Note: Callbacks are invoked frequently during large file processing. For
227/// better performance with very large files, consider batching updates.
228///
229/// # Examples
230///
231/// ```no_run
232/// use exarch_core::ProgressCallback;
233/// use exarch_core::creation::CreationConfig;
234/// use exarch_core::creation::tar::create_tar_with_progress;
235/// use std::path::Path;
236///
237/// struct SimpleProgress;
238///
239/// impl ProgressCallback for SimpleProgress {
240///     fn on_entry_start(&mut self, path: &Path, total: usize, current: usize) {
241///         println!("[{}/{}] Processing: {}", current, total, path.display());
242///     }
243///
244///     fn on_bytes_written(&mut self, bytes: u64) {
245///         // Called frequently - consider rate limiting
246///     }
247///
248///     fn on_entry_complete(&mut self, path: &Path) {
249///         println!("Completed: {}", path.display());
250///     }
251///
252///     fn on_complete(&mut self) {
253///         println!("Archive creation complete!");
254///     }
255/// }
256///
257/// let config = CreationConfig::default();
258/// let mut progress = SimpleProgress;
259/// let report = create_tar_with_progress(
260///     Path::new("output.tar"),
261///     &[Path::new("src")],
262///     &config,
263///     &mut progress,
264/// )?;
265/// # Ok::<(), exarch_core::ExtractionError>(())
266/// ```
267///
268/// # Errors
269///
270/// Returns an error if:
271/// - Source path does not exist
272/// - Output file cannot be created
273/// - I/O error during archive creation
274/// - File metadata cannot be read
275#[allow(dead_code)] // Will be used by CLI
276pub fn create_tar_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
277    output: P,
278    sources: &[Q],
279    config: &CreationConfig,
280    progress: &mut dyn ProgressCallback,
281) -> Result<CreationReport> {
282    let file = File::create(output.as_ref())?;
283    create_tar_internal_with_progress(file, sources, config, progress)
284}
285
286/// Creates a gzip-compressed TAR archive with progress reporting.
287///
288/// Identical to [`create_tar_with_progress`] but applies gzip compression.
289/// See that function for detailed documentation on progress callbacks and
290/// usage.
291///
292/// # Errors
293///
294/// Returns an error if output file cannot be created, compression fails, or I/O
295/// operations fail.
296#[allow(dead_code)] // Will be used by CLI
297pub fn create_tar_gz_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
298    output: P,
299    sources: &[Q],
300    config: &CreationConfig,
301    progress: &mut dyn ProgressCallback,
302) -> Result<CreationReport> {
303    let file = File::create(output.as_ref())?;
304    let level = compression_level_to_flate2(config.compression_level);
305    let encoder = flate2::write::GzEncoder::new(file, level);
306    create_tar_internal_with_progress(encoder, sources, config, progress)
307}
308
309/// Creates a bzip2-compressed TAR archive with progress reporting.
310///
311/// Identical to [`create_tar_with_progress`] but applies bzip2 compression.
312/// See that function for detailed documentation on progress callbacks and
313/// usage.
314///
315/// # Errors
316///
317/// Returns an error if output file cannot be created, compression fails, or I/O
318/// operations fail.
319#[allow(dead_code)] // Will be used by CLI
320pub fn create_tar_bz2_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
321    output: P,
322    sources: &[Q],
323    config: &CreationConfig,
324    progress: &mut dyn ProgressCallback,
325) -> Result<CreationReport> {
326    let file = File::create(output.as_ref())?;
327    let level = compression_level_to_bzip2(config.compression_level);
328    let encoder = bzip2::write::BzEncoder::new(file, level);
329    create_tar_internal_with_progress(encoder, sources, config, progress)
330}
331
332/// Creates an xz-compressed TAR archive with progress reporting.
333///
334/// Identical to [`create_tar_with_progress`] but applies xz compression.
335/// See that function for detailed documentation on progress callbacks and
336/// usage.
337///
338/// # Errors
339///
340/// Returns an error if output file cannot be created, compression fails, or I/O
341/// operations fail.
342#[allow(dead_code)] // Will be used by CLI
343pub fn create_tar_xz_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
344    output: P,
345    sources: &[Q],
346    config: &CreationConfig,
347    progress: &mut dyn ProgressCallback,
348) -> Result<CreationReport> {
349    let file = File::create(output.as_ref())?;
350    let level = compression_level_to_xz(config.compression_level);
351    let encoder = xz2::write::XzEncoder::new(file, level);
352    create_tar_internal_with_progress(encoder, sources, config, progress)
353}
354
355/// Creates a zstd-compressed TAR archive with progress reporting.
356///
357/// Identical to [`create_tar_with_progress`] but applies zstd compression.
358/// See that function for detailed documentation on progress callbacks and
359/// usage.
360///
361/// # Errors
362///
363/// Returns an error if output file cannot be created, compression fails, or I/O
364/// operations fail.
365#[allow(dead_code)] // Will be used by CLI
366pub fn create_tar_zst_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
367    output: P,
368    sources: &[Q],
369    config: &CreationConfig,
370    progress: &mut dyn ProgressCallback,
371) -> Result<CreationReport> {
372    let file = File::create(output.as_ref())?;
373    let level = compression_level_to_zstd(config.compression_level);
374    let mut encoder = zstd::Encoder::new(file, level)?;
375    encoder.include_checksum(true)?;
376
377    let report = create_tar_internal_with_progress(encoder, sources, config, progress)?;
378
379    Ok(report)
380}
381
382/// Internal function that creates TAR with any writer and progress reporting.
383fn create_tar_internal_with_progress<W: Write, P: AsRef<Path>>(
384    writer: W,
385    sources: &[P],
386    config: &CreationConfig,
387    progress: &mut dyn ProgressCallback,
388) -> Result<CreationReport> {
389    let counting_writer = CountingWriter::new(writer);
390    let mut builder = Builder::new(counting_writer);
391    let mut report = CreationReport::default();
392    let start = std::time::Instant::now();
393
394    // Single-pass collection of entries (avoids double directory traversal)
395    let entries = collect_entries(sources, config)?;
396    let total_entries = entries.len();
397
398    // Manual entry counting (we can't use ProgressTracker because we need to
399    // pass progress to nested functions for byte-level progress)
400    let mut current_entry = 0usize;
401
402    // Reusable buffer for file copying (fixes HIGH #2)
403    let mut buffer = vec![0u8; 64 * 1024]; // 64 KB
404
405    for entry in &entries {
406        current_entry += 1;
407
408        match &entry.entry_type {
409            EntryType::File => {
410                progress.on_entry_start(&entry.archive_path, total_entries, current_entry);
411                add_file_to_tar_with_progress_impl(
412                    &mut builder,
413                    &entry.path,
414                    &entry.archive_path,
415                    config,
416                    &mut report,
417                    progress,
418                    &mut buffer,
419                )?;
420                progress.on_entry_complete(&entry.archive_path);
421            }
422            EntryType::Directory => {
423                progress.on_entry_start(&entry.archive_path, total_entries, current_entry);
424                report.directories_added += 1;
425                progress.on_entry_complete(&entry.archive_path);
426            }
427            EntryType::Symlink { target } => {
428                progress.on_entry_start(&entry.archive_path, total_entries, current_entry);
429                if config.follow_symlinks {
430                    add_file_to_tar_with_progress_impl(
431                        &mut builder,
432                        &entry.path,
433                        &entry.archive_path,
434                        config,
435                        &mut report,
436                        progress,
437                        &mut buffer,
438                    )?;
439                } else {
440                    add_symlink_to_tar(&mut builder, &entry.archive_path, target, &mut report)?;
441                }
442                progress.on_entry_complete(&entry.archive_path);
443            }
444        }
445    }
446
447    // Finish writing TAR
448    builder.finish()?;
449
450    let mut counting_writer = builder.into_inner()?;
451    counting_writer.flush()?;
452
453    report.bytes_compressed = counting_writer.total_bytes();
454    report.duration = start.elapsed();
455
456    progress.on_complete();
457
458    Ok(report)
459}
460
461/// Internal function that creates TAR with any writer.
462///
463/// Handles the core logic of walking sources and adding entries to the archive.
464fn create_tar_internal<W: Write, P: AsRef<Path>>(
465    writer: W,
466    sources: &[P],
467    config: &CreationConfig,
468) -> Result<CreationReport> {
469    let counting_writer = CountingWriter::new(writer);
470    let mut builder = Builder::new(counting_writer);
471    let mut report = CreationReport::default();
472    let start = std::time::Instant::now();
473
474    for source in sources {
475        let path = source.as_ref();
476
477        // Validate source exists
478        if !path.exists() {
479            return Err(ExtractionError::SourceNotFound {
480                path: path.to_path_buf(),
481            });
482        }
483
484        // Walk directory or add single file
485        if path.is_dir() {
486            add_directory_to_tar(&mut builder, path, config, &mut report)?;
487        } else {
488            // For single files, use filename as archive path
489            let archive_path =
490                filters::compute_archive_path(path, path.parent().unwrap_or(path), config)?;
491            add_file_to_tar(&mut builder, path, &archive_path, config, &mut report)?;
492        }
493    }
494
495    // Finish writing TAR
496    builder.finish()?;
497
498    // Get inner writer and ensure it's properly flushed
499    let mut counting_writer = builder.into_inner()?;
500    counting_writer.flush()?;
501
502    report.bytes_compressed = counting_writer.total_bytes();
503    report.duration = start.elapsed();
504
505    Ok(report)
506}
507
508/// Adds a directory tree to the TAR archive using the walker.
509fn add_directory_to_tar<W: Write>(
510    builder: &mut Builder<W>,
511    dir: &Path,
512    config: &CreationConfig,
513    report: &mut CreationReport,
514) -> Result<()> {
515    let walker = FilteredWalker::new(dir, config);
516
517    for entry in walker.walk() {
518        let entry = entry?;
519
520        match entry.entry_type {
521            EntryType::File => {
522                add_file_to_tar(builder, &entry.path, &entry.archive_path, config, report)?;
523            }
524            EntryType::Directory => {
525                // TAR can create directories implicitly, but we track them
526                report.directories_added += 1;
527            }
528            EntryType::Symlink { target } => {
529                if config.follow_symlinks {
530                    // Walker already resolved symlinks, treat as file
531                    add_file_to_tar(builder, &entry.path, &entry.archive_path, config, report)?;
532                } else {
533                    // Add symlink as-is
534                    add_symlink_to_tar(builder, &entry.archive_path, &target, report)?;
535                }
536            }
537        }
538    }
539
540    Ok(())
541}
542
543/// Adds a single file to the TAR archive.
544fn add_file_to_tar<W: Write>(
545    builder: &mut Builder<W>,
546    file_path: &Path,
547    archive_path: &Path,
548    config: &CreationConfig,
549    report: &mut CreationReport,
550) -> Result<()> {
551    let mut file = File::open(file_path)?;
552    let metadata = file.metadata()?;
553    let size = metadata.len();
554
555    // Create TAR header
556    let mut header = Header::new_gnu();
557    header.set_size(size);
558    header.set_cksum();
559
560    // Set permissions if configured
561    if config.preserve_permissions {
562        set_permissions(&mut header, &metadata);
563    }
564
565    // Add file to archive
566    builder.append_data(&mut header, archive_path, &mut file)?;
567
568    report.files_added += 1;
569    report.bytes_written += size;
570
571    Ok(())
572}
573
574/// Adds a single file to the TAR archive with progress reporting and reusable
575/// buffer.
576fn add_file_to_tar_with_progress_impl<W: Write>(
577    builder: &mut Builder<W>,
578    file_path: &Path,
579    archive_path: &Path,
580    config: &CreationConfig,
581    report: &mut CreationReport,
582    progress: &mut dyn ProgressCallback,
583    _buffer: &mut [u8],
584) -> Result<()> {
585    let file = File::open(file_path)?;
586    let metadata = file.metadata()?;
587    let size = metadata.len();
588
589    let mut header = Header::new_gnu();
590    header.set_size(size);
591    header.set_cksum();
592
593    if config.preserve_permissions {
594        set_permissions(&mut header, &metadata);
595    }
596
597    // Use progress-tracking reader with batched updates (1 MB batches)
598    // Note: tar crate's append_data does its own buffering internally,
599    // so we use ProgressReader wrapper instead of manual buffer
600    let mut tracked_file = ProgressReader::new(file, progress);
601    builder.append_data(&mut header, archive_path, &mut tracked_file)?;
602
603    report.files_added += 1;
604    report.bytes_written += size;
605
606    Ok(())
607}
608
609/// Adds a symlink to the TAR archive.
610#[cfg(unix)]
611fn add_symlink_to_tar<W: Write>(
612    builder: &mut Builder<W>,
613    link_path: &Path,
614    target: &Path,
615    report: &mut CreationReport,
616) -> Result<()> {
617    let mut header = Header::new_gnu();
618    header.set_entry_type(tar::EntryType::Symlink);
619    header.set_size(0);
620    header.set_cksum();
621
622    builder.append_link(&mut header, link_path, target)?;
623
624    report.symlinks_added += 1;
625
626    Ok(())
627}
628
629#[cfg(not(unix))]
630fn add_symlink_to_tar<W: Write>(
631    _builder: &mut Builder<W>,
632    _link_path: &Path,
633    _target: &Path,
634    report: &mut CreationReport,
635) -> Result<()> {
636    // On non-Unix platforms, skip symlinks
637    report.files_skipped += 1;
638    report.add_warning("Symlinks not supported on this platform");
639    Ok(())
640}
641
642/// Sets file permissions in TAR header from metadata.
643#[cfg(unix)]
644fn set_permissions(header: &mut Header, metadata: &std::fs::Metadata) {
645    use std::os::unix::fs::MetadataExt;
646    let mode = metadata.mode();
647    header.set_mode(mode);
648    header.set_uid(u64::from(metadata.uid()));
649    header.set_gid(u64::from(metadata.gid()));
650    // mtime can be negative for dates before epoch, clamp to 0
651    #[allow(clippy::cast_sign_loss)] // Intentional: clamped to non-negative
652    let mtime = metadata.mtime().max(0) as u64;
653    header.set_mtime(mtime);
654}
655
656#[cfg(not(unix))]
657fn set_permissions(header: &mut Header, metadata: &std::fs::Metadata) {
658    // On non-Unix platforms, set basic permissions
659    let mode = if metadata.permissions().readonly() {
660        0o444
661    } else {
662        0o644
663    };
664    header.set_mode(mode);
665
666    // Set modification time
667    if let Ok(modified) = metadata.modified() {
668        if let Ok(duration) = modified.duration_since(std::time::UNIX_EPOCH) {
669            header.set_mtime(duration.as_secs());
670        }
671    }
672}
673
674#[cfg(test)]
675#[allow(clippy::unwrap_used)] // Allow unwrap in tests for brevity
676mod tests {
677    use super::*;
678    use crate::SecurityConfig;
679    use crate::api::extract_archive;
680    use std::fs;
681    use tempfile::TempDir;
682
683    #[test]
684    fn test_create_tar_single_file() {
685        let temp = TempDir::new().unwrap();
686        let output = temp.path().join("output.tar");
687
688        // Create source file
689        let source_dir = TempDir::new().unwrap();
690        fs::write(source_dir.path().join("test.txt"), "Hello TAR").unwrap();
691
692        let config = CreationConfig::default()
693            .with_exclude_patterns(vec![])
694            .with_include_hidden(true);
695
696        let report = create_tar(&output, &[source_dir.path().join("test.txt")], &config).unwrap();
697
698        assert_eq!(report.files_added, 1);
699        assert!(report.bytes_written > 0);
700        assert!(output.exists());
701    }
702
703    #[test]
704    fn test_create_tar_directory() {
705        let temp = TempDir::new().unwrap();
706        let output = temp.path().join("output.tar");
707
708        // Create source directory with multiple files
709        let source_dir = TempDir::new().unwrap();
710        fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
711        fs::write(source_dir.path().join("file2.txt"), "content2").unwrap();
712        fs::create_dir(source_dir.path().join("subdir")).unwrap();
713        fs::write(source_dir.path().join("subdir/file3.txt"), "content3").unwrap();
714
715        let config = CreationConfig::default()
716            .with_exclude_patterns(vec![])
717            .with_include_hidden(true);
718
719        let report = create_tar(&output, &[source_dir.path()], &config).unwrap();
720
721        // Should have exactly 3 files: file1.txt, file2.txt, subdir/file3.txt
722        assert_eq!(report.files_added, 3);
723        // Should have exactly 2 directories: root and subdir
724        assert_eq!(report.directories_added, 2);
725        assert!(output.exists());
726    }
727
728    #[test]
729    fn test_create_tar_gz_compression() {
730        let temp = TempDir::new().unwrap();
731        let output = temp.path().join("output.tar.gz");
732
733        // Create source file
734        let source_dir = TempDir::new().unwrap();
735        fs::write(source_dir.path().join("test.txt"), "a".repeat(1000)).unwrap();
736
737        let config = CreationConfig::default()
738            .with_exclude_patterns(vec![])
739            .with_compression_level(9);
740
741        let report = create_tar_gz(&output, &[source_dir.path()], &config).unwrap();
742
743        assert_eq!(report.files_added, 1);
744        assert!(output.exists());
745
746        // Verify it's a valid gzip file (basic check)
747        let data = fs::read(&output).unwrap();
748        assert_eq!(&data[0..2], &[0x1f, 0x8b]); // gzip magic bytes
749    }
750
751    #[test]
752    fn test_create_tar_bz2_compression() {
753        let temp = TempDir::new().unwrap();
754        let output = temp.path().join("output.tar.bz2");
755
756        // Create source file
757        let source_dir = TempDir::new().unwrap();
758        fs::write(source_dir.path().join("test.txt"), "bzip2 test").unwrap();
759
760        let config = CreationConfig::default().with_exclude_patterns(vec![]);
761
762        let report = create_tar_bz2(&output, &[source_dir.path()], &config).unwrap();
763
764        assert_eq!(report.files_added, 1);
765        assert!(output.exists());
766
767        // Verify it's a valid bzip2 file
768        let data = fs::read(&output).unwrap();
769        assert_eq!(&data[0..3], b"BZh"); // bzip2 magic bytes
770    }
771
772    #[test]
773    fn test_create_tar_xz_compression() {
774        let temp = TempDir::new().unwrap();
775        let output = temp.path().join("output.tar.xz");
776
777        // Create source file
778        let source_dir = TempDir::new().unwrap();
779        fs::write(source_dir.path().join("test.txt"), "xz test").unwrap();
780
781        let config = CreationConfig::default().with_exclude_patterns(vec![]);
782
783        let report = create_tar_xz(&output, &[source_dir.path()], &config).unwrap();
784
785        assert_eq!(report.files_added, 1);
786        assert!(output.exists());
787
788        // Verify it's a valid xz file
789        let data = fs::read(&output).unwrap();
790        assert_eq!(&data[0..6], &[0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]); // xz magic bytes
791    }
792
793    #[test]
794    fn test_create_tar_zst_compression() {
795        let temp = TempDir::new().unwrap();
796        let output = temp.path().join("output.tar.zst");
797
798        // Create source file
799        let source_dir = TempDir::new().unwrap();
800        fs::write(source_dir.path().join("test.txt"), "zstd test").unwrap();
801
802        let config = CreationConfig::default().with_exclude_patterns(vec![]);
803
804        let report = create_tar_zst(&output, &[source_dir.path()], &config).unwrap();
805
806        assert_eq!(report.files_added, 1);
807        assert!(output.exists());
808
809        // Verify it's a valid zstd file
810        let data = fs::read(&output).unwrap();
811        // Check we have at least some data
812        assert!(data.len() >= 4, "output file should have data");
813        assert_eq!(&data[0..4], &[0x28, 0xB5, 0x2F, 0xFD]); // zstd magic bytes
814    }
815
816    #[test]
817    fn test_create_tar_compression_levels() {
818        let temp = TempDir::new().unwrap();
819
820        // Create source with repetitive data (compresses well)
821        let source_dir = TempDir::new().unwrap();
822        fs::write(source_dir.path().join("test.txt"), "a".repeat(10000)).unwrap();
823
824        // Test different compression levels
825        for level in [1, 6, 9] {
826            let output = temp.path().join(format!("output_{level}.tar.gz"));
827            let config = CreationConfig::default()
828                .with_exclude_patterns(vec![])
829                .with_compression_level(level);
830
831            let report = create_tar_gz(&output, &[source_dir.path()], &config).unwrap();
832            assert_eq!(report.files_added, 1);
833            assert!(output.exists());
834        }
835    }
836
837    #[test]
838    #[cfg(unix)]
839    fn test_create_tar_preserves_permissions() {
840        use std::os::unix::fs::PermissionsExt;
841
842        let temp = TempDir::new().unwrap();
843        let output = temp.path().join("output.tar");
844
845        // Create source file with specific permissions
846        let source_dir = TempDir::new().unwrap();
847        let file_path = source_dir.path().join("test.txt");
848        fs::write(&file_path, "content").unwrap();
849        fs::set_permissions(&file_path, fs::Permissions::from_mode(0o755)).unwrap();
850
851        let config = CreationConfig::default()
852            .with_exclude_patterns(vec![])
853            .with_preserve_permissions(true);
854
855        let report = create_tar(&output, &[source_dir.path()], &config).unwrap();
856        assert_eq!(report.files_added, 1);
857
858        // Verify permissions in archive by extracting
859        let extract_dir = TempDir::new().unwrap();
860        let security_config = SecurityConfig::default();
861        extract_archive(&output, extract_dir.path(), &security_config).unwrap();
862
863        let extracted = extract_dir.path().join("test.txt");
864        let perms = fs::metadata(&extracted).unwrap().permissions();
865        assert_eq!(perms.mode() & 0o777, 0o755);
866    }
867
868    #[test]
869    fn test_create_tar_report_statistics() {
870        let temp = TempDir::new().unwrap();
871        let output = temp.path().join("output.tar");
872
873        // Create source directory with known structure
874        let source_dir = TempDir::new().unwrap();
875        fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
876        fs::write(source_dir.path().join("file2.txt"), "content2").unwrap();
877        fs::create_dir(source_dir.path().join("subdir")).unwrap();
878        fs::write(source_dir.path().join("subdir/file3.txt"), "content3").unwrap();
879
880        let config = CreationConfig::default()
881            .with_exclude_patterns(vec![])
882            .with_include_hidden(true);
883
884        let report = create_tar(&output, &[source_dir.path()], &config).unwrap();
885
886        assert_eq!(report.files_added, 3);
887        assert!(report.directories_added >= 1);
888        assert_eq!(report.files_skipped, 0);
889        assert!(!report.has_warnings());
890        assert!(report.duration.as_nanos() > 0);
891    }
892
893    #[test]
894    fn test_create_tar_roundtrip() {
895        let temp = TempDir::new().unwrap();
896        let output = temp.path().join("output.tar.gz");
897
898        // Create source directory
899        let source_dir = TempDir::new().unwrap();
900        fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
901        fs::create_dir(source_dir.path().join("subdir")).unwrap();
902        fs::write(source_dir.path().join("subdir/file2.txt"), "content2").unwrap();
903
904        let config = CreationConfig::default()
905            .with_exclude_patterns(vec![])
906            .with_include_hidden(true);
907
908        // Create archive
909        let report = create_tar_gz(&output, &[source_dir.path()], &config).unwrap();
910        assert!(report.files_added >= 2);
911
912        // Extract archive
913        let extract_dir = TempDir::new().unwrap();
914        let security_config = SecurityConfig::default();
915        extract_archive(&output, extract_dir.path(), &security_config).unwrap();
916
917        // Verify extracted files match originals
918        let extracted1 = fs::read_to_string(extract_dir.path().join("file1.txt")).unwrap();
919        assert_eq!(extracted1, "content1");
920
921        let extracted2 = fs::read_to_string(extract_dir.path().join("subdir/file2.txt")).unwrap();
922        assert_eq!(extracted2, "content2");
923    }
924
925    #[test]
926    fn test_create_tar_source_not_found() {
927        let temp = TempDir::new().unwrap();
928        let output = temp.path().join("output.tar");
929
930        let config = CreationConfig::default();
931        let result = create_tar(&output, &[Path::new("/nonexistent/path")], &config);
932
933        assert!(result.is_err());
934        assert!(matches!(
935            result.unwrap_err(),
936            ExtractionError::SourceNotFound { .. }
937        ));
938    }
939
940    #[test]
941    fn test_compression_level_to_flate2() {
942        // Default
943        let level = compression_level_to_flate2(None);
944        assert_eq!(level, flate2::Compression::default());
945
946        // Fast
947        let level = compression_level_to_flate2(Some(1));
948        assert_eq!(level, flate2::Compression::fast());
949
950        // Best
951        let level = compression_level_to_flate2(Some(9));
952        assert_eq!(level, flate2::Compression::best());
953
954        // Specific level
955        let level = compression_level_to_flate2(Some(5));
956        assert_eq!(level, flate2::Compression::new(5));
957    }
958
959    #[test]
960    fn test_compression_level_to_zstd() {
961        assert_eq!(compression_level_to_zstd(None), 3);
962        assert_eq!(compression_level_to_zstd(Some(1)), 1);
963        assert_eq!(compression_level_to_zstd(Some(6)), 3);
964        assert_eq!(compression_level_to_zstd(Some(7)), 10);
965        assert_eq!(compression_level_to_zstd(Some(9)), 19);
966    }
967
968    // NOTE: Progress tracking reader tests are now in creation/progress.rs
969
970    #[test]
971    fn test_create_tar_with_progress_callback() {
972        #[derive(Debug, Default, Clone)]
973        struct TestProgress {
974            entries_started: Vec<String>,
975            entries_completed: Vec<String>,
976            bytes_written: u64,
977            completed: bool,
978        }
979
980        impl ProgressCallback for TestProgress {
981            fn on_entry_start(&mut self, path: &Path, _total: usize, _current: usize) {
982                self.entries_started
983                    .push(path.to_string_lossy().to_string());
984            }
985
986            fn on_bytes_written(&mut self, bytes: u64) {
987                self.bytes_written += bytes;
988            }
989
990            fn on_entry_complete(&mut self, path: &Path) {
991                self.entries_completed
992                    .push(path.to_string_lossy().to_string());
993            }
994
995            fn on_complete(&mut self) {
996                self.completed = true;
997            }
998        }
999
1000        let temp = TempDir::new().unwrap();
1001        let output = temp.path().join("output.tar");
1002
1003        // Create source directory with multiple files
1004        let source_dir = TempDir::new().unwrap();
1005        fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
1006        fs::write(source_dir.path().join("file2.txt"), "content2").unwrap();
1007        fs::create_dir(source_dir.path().join("subdir")).unwrap();
1008        fs::write(source_dir.path().join("subdir/file3.txt"), "content3").unwrap();
1009
1010        let config = CreationConfig::default()
1011            .with_exclude_patterns(vec![])
1012            .with_include_hidden(true);
1013
1014        let mut progress = TestProgress::default();
1015
1016        let report =
1017            create_tar_with_progress(&output, &[source_dir.path()], &config, &mut progress)
1018                .unwrap();
1019
1020        // Verify report
1021        assert_eq!(report.files_added, 3);
1022        assert!(report.directories_added >= 1);
1023
1024        // Verify callbacks were invoked
1025        assert!(
1026            progress.entries_started.len() >= 3,
1027            "Expected at least 3 entry starts, got {}",
1028            progress.entries_started.len()
1029        );
1030        assert!(
1031            progress.entries_completed.len() >= 3,
1032            "Expected at least 3 entry completions, got {}",
1033            progress.entries_completed.len()
1034        );
1035        assert!(
1036            progress.bytes_written > 0,
1037            "Expected bytes written > 0, got {}",
1038            progress.bytes_written
1039        );
1040        assert!(progress.completed, "Expected on_complete to be called");
1041
1042        // Verify specific entries
1043        let has_file1 = progress
1044            .entries_started
1045            .iter()
1046            .any(|p| p.contains("file1.txt"));
1047        let has_file2 = progress
1048            .entries_started
1049            .iter()
1050            .any(|p| p.contains("file2.txt"));
1051        let has_file3 = progress
1052            .entries_started
1053            .iter()
1054            .any(|p| p.contains("file3.txt"));
1055
1056        assert!(has_file1, "Expected file1.txt in progress callbacks");
1057        assert!(has_file2, "Expected file2.txt in progress callbacks");
1058        assert!(has_file3, "Expected file3.txt in progress callbacks");
1059    }
1060}