1use crate::ProgressCallback;
7use crate::Result;
8use crate::creation::compression::compression_level_to_bzip2;
9use crate::creation::compression::compression_level_to_flate2;
10use crate::creation::compression::compression_level_to_xz;
11use crate::creation::compression::compression_level_to_zstd;
12use crate::creation::config::CreationConfig;
13use crate::creation::progress::ProgressReader;
14use crate::creation::report::CreationReport;
15use crate::creation::walker::EntryType;
16use crate::creation::walker::collect_entries;
17use crate::io::CountingWriter;
18use std::fs::File;
19use std::io::Write;
20use std::path::Path;
21use tar::Builder;
22use tar::Header;
23
24pub fn create_tar_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
98 output: P,
99 sources: &[Q],
100 config: &CreationConfig,
101 progress: &mut dyn ProgressCallback,
102) -> Result<CreationReport> {
103 let file = File::create(output.as_ref())?;
104 let (report, _) = create_tar_internal_with_progress(file, sources, config, progress)?;
105 Ok(report)
106}
107
108pub fn create_tar_gz_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
119 output: P,
120 sources: &[Q],
121 config: &CreationConfig,
122 progress: &mut dyn ProgressCallback,
123) -> Result<CreationReport> {
124 let file = File::create(output.as_ref())?;
125 let level = compression_level_to_flate2(config.compression_level);
126 let encoder = flate2::write::GzEncoder::new(file, level);
127 let (report, _) = create_tar_internal_with_progress(encoder, sources, config, progress)?;
128 Ok(report)
129}
130
131pub fn create_tar_bz2_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
142 output: P,
143 sources: &[Q],
144 config: &CreationConfig,
145 progress: &mut dyn ProgressCallback,
146) -> Result<CreationReport> {
147 let file = File::create(output.as_ref())?;
148 let level = compression_level_to_bzip2(config.compression_level);
149 let encoder = bzip2::write::BzEncoder::new(file, level);
150 let (report, _) = create_tar_internal_with_progress(encoder, sources, config, progress)?;
151 Ok(report)
152}
153
154pub fn create_tar_xz_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
165 output: P,
166 sources: &[Q],
167 config: &CreationConfig,
168 progress: &mut dyn ProgressCallback,
169) -> Result<CreationReport> {
170 let file = File::create(output.as_ref())?;
171 let level = compression_level_to_xz(config.compression_level);
172 let encoder = xz2::write::XzEncoder::new(file, level);
173 let (report, _) = create_tar_internal_with_progress(encoder, sources, config, progress)?;
174 Ok(report)
175}
176
177pub fn create_tar_zst_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
188 output: P,
189 sources: &[Q],
190 config: &CreationConfig,
191 progress: &mut dyn ProgressCallback,
192) -> Result<CreationReport> {
193 let file = File::create(output.as_ref())?;
194 let level = compression_level_to_zstd(config.compression_level);
195 let mut encoder = zstd::Encoder::new(file, level)?;
196 encoder.include_checksum(true)?;
197
198 let (report, encoder) = create_tar_internal_with_progress(encoder, sources, config, progress)?;
199 encoder.finish()?;
200
201 Ok(report)
202}
203
204fn create_tar_internal_with_progress<W: Write, P: AsRef<Path>>(
209 writer: W,
210 sources: &[P],
211 config: &CreationConfig,
212 progress: &mut dyn ProgressCallback,
213) -> Result<(CreationReport, W)> {
214 let counting_writer = CountingWriter::new(writer);
215 let mut builder = Builder::new(counting_writer);
216 let mut report = CreationReport::default();
217 let start = std::time::Instant::now();
218
219 let entries = collect_entries(sources, config)?;
221 let total_entries = entries.len();
222
223 let mut current_entry = 0usize;
226
227 let mut buffer = vec![0u8; 64 * 1024]; for entry in &entries {
231 current_entry += 1;
232
233 match &entry.entry_type {
234 EntryType::File => {
235 progress.on_entry_start(&entry.archive_path, total_entries, current_entry);
236 add_file_to_tar_with_progress_impl(
237 &mut builder,
238 &entry.path,
239 &entry.archive_path,
240 config,
241 &mut report,
242 progress,
243 &mut buffer,
244 )?;
245 progress.on_entry_complete(&entry.archive_path);
246 }
247 EntryType::Directory => {
248 progress.on_entry_start(&entry.archive_path, total_entries, current_entry);
249 report.directories_added += 1;
250 progress.on_entry_complete(&entry.archive_path);
251 }
252 EntryType::Symlink { target } => {
253 progress.on_entry_start(&entry.archive_path, total_entries, current_entry);
254 if config.follow_symlinks {
255 add_file_to_tar_with_progress_impl(
256 &mut builder,
257 &entry.path,
258 &entry.archive_path,
259 config,
260 &mut report,
261 progress,
262 &mut buffer,
263 )?;
264 } else {
265 add_symlink_to_tar(&mut builder, &entry.archive_path, target, &mut report)?;
266 }
267 progress.on_entry_complete(&entry.archive_path);
268 }
269 }
270 }
271
272 builder.finish()?;
274
275 let mut counting_writer = builder.into_inner()?;
276 counting_writer.flush()?;
277
278 report.bytes_compressed = counting_writer.total_bytes();
279 report.duration = start.elapsed();
280
281 progress.on_complete();
282
283 Ok((report, counting_writer.into_inner()))
284}
285
286fn add_file_to_tar_with_progress_impl<W: Write>(
289 builder: &mut Builder<W>,
290 file_path: &Path,
291 archive_path: &Path,
292 config: &CreationConfig,
293 report: &mut CreationReport,
294 progress: &mut dyn ProgressCallback,
295 _buffer: &mut [u8],
296) -> Result<()> {
297 let file = File::open(file_path)?;
298 let metadata = file.metadata()?;
299 let size = metadata.len();
300
301 let mut header = Header::new_gnu();
302 header.set_size(size);
303 header.set_cksum();
304
305 if config.preserve_permissions {
306 set_permissions(&mut header, &metadata);
307 }
308
309 let mut tracked_file = ProgressReader::new(file, progress);
313 builder.append_data(&mut header, archive_path, &mut tracked_file)?;
314
315 report.files_added += 1;
316 report.bytes_written += size;
317
318 Ok(())
319}
320
321#[cfg(unix)]
323fn add_symlink_to_tar<W: Write>(
324 builder: &mut Builder<W>,
325 link_path: &Path,
326 target: &Path,
327 report: &mut CreationReport,
328) -> Result<()> {
329 let mut header = Header::new_gnu();
330 header.set_entry_type(tar::EntryType::Symlink);
331 header.set_size(0);
332 header.set_cksum();
333
334 builder.append_link(&mut header, link_path, target)?;
335
336 report.symlinks_added += 1;
337
338 Ok(())
339}
340
341#[cfg(not(unix))]
342fn add_symlink_to_tar<W: Write>(
343 _builder: &mut Builder<W>,
344 _link_path: &Path,
345 _target: &Path,
346 report: &mut CreationReport,
347) -> Result<()> {
348 report.files_skipped += 1;
350 report.add_warning("Symlinks not supported on this platform");
351 Ok(())
352}
353
354#[cfg(unix)]
356fn set_permissions(header: &mut Header, metadata: &std::fs::Metadata) {
357 use std::os::unix::fs::MetadataExt;
358 let mode = metadata.mode();
359 header.set_mode(mode);
360 header.set_uid(u64::from(metadata.uid()));
361 header.set_gid(u64::from(metadata.gid()));
362 #[allow(clippy::cast_sign_loss)] let mtime = metadata.mtime().max(0) as u64;
365 header.set_mtime(mtime);
366}
367
368#[cfg(not(unix))]
369fn set_permissions(header: &mut Header, metadata: &std::fs::Metadata) {
370 let mode = if metadata.permissions().readonly() {
372 0o444
373 } else {
374 0o644
375 };
376 header.set_mode(mode);
377
378 if let Ok(modified) = metadata.modified() {
380 if let Ok(duration) = modified.duration_since(std::time::UNIX_EPOCH) {
381 header.set_mtime(duration.as_secs());
382 }
383 }
384}
385
386pub struct TarCreator;
388
389pub struct TarGzCreator;
391
392pub struct TarBz2Creator;
394
395pub struct TarXzCreator;
397
398pub struct TarZstCreator;
400
401impl crate::formats::traits::FormatCreator for TarCreator {
402 fn create(
403 &self,
404 output: &Path,
405 sources: &[&Path],
406 config: &CreationConfig,
407 progress: &mut dyn ProgressCallback,
408 ) -> crate::Result<crate::creation::CreationReport> {
409 create_tar_with_progress(output, sources, config, progress)
410 }
411
412 fn format_name(&self) -> &'static str {
413 "tar"
414 }
415}
416
417impl crate::formats::traits::FormatCreator for TarGzCreator {
418 fn create(
419 &self,
420 output: &Path,
421 sources: &[&Path],
422 config: &CreationConfig,
423 progress: &mut dyn ProgressCallback,
424 ) -> crate::Result<crate::creation::CreationReport> {
425 create_tar_gz_with_progress(output, sources, config, progress)
426 }
427
428 fn format_name(&self) -> &'static str {
429 "tar.gz"
430 }
431}
432
433impl crate::formats::traits::FormatCreator for TarBz2Creator {
434 fn create(
435 &self,
436 output: &Path,
437 sources: &[&Path],
438 config: &CreationConfig,
439 progress: &mut dyn ProgressCallback,
440 ) -> crate::Result<crate::creation::CreationReport> {
441 create_tar_bz2_with_progress(output, sources, config, progress)
442 }
443
444 fn format_name(&self) -> &'static str {
445 "tar.bz2"
446 }
447}
448
449impl crate::formats::traits::FormatCreator for TarXzCreator {
450 fn create(
451 &self,
452 output: &Path,
453 sources: &[&Path],
454 config: &CreationConfig,
455 progress: &mut dyn ProgressCallback,
456 ) -> crate::Result<crate::creation::CreationReport> {
457 create_tar_xz_with_progress(output, sources, config, progress)
458 }
459
460 fn format_name(&self) -> &'static str {
461 "tar.xz"
462 }
463}
464
465impl crate::formats::traits::FormatCreator for TarZstCreator {
466 fn create(
467 &self,
468 output: &Path,
469 sources: &[&Path],
470 config: &CreationConfig,
471 progress: &mut dyn ProgressCallback,
472 ) -> crate::Result<crate::creation::CreationReport> {
473 create_tar_zst_with_progress(output, sources, config, progress)
474 }
475
476 fn format_name(&self) -> &'static str {
477 "tar.zst"
478 }
479}
480
481#[cfg(test)]
482#[allow(clippy::unwrap_used)] mod tests {
484 use super::*;
485 use crate::ExtractionError;
486 use crate::SecurityConfig;
487 use crate::api::create_archive;
488 use crate::api::extract_archive;
489 use crate::formats::detect::ArchiveType;
490 use std::fs;
491 use tempfile::TempDir;
492
493 #[test]
494 fn test_create_tar_single_file() {
495 let temp = TempDir::new().unwrap();
496 let output = temp.path().join("output.tar");
497
498 let source_dir = TempDir::new().unwrap();
499 fs::write(source_dir.path().join("test.txt"), "Hello TAR").unwrap();
500
501 let config = CreationConfig::default()
502 .with_exclude_patterns(vec![])
503 .with_include_hidden(true)
504 .with_format(Some(ArchiveType::Tar));
505
506 let report = create_archive(
507 &output,
508 &[source_dir.path().join("test.txt").as_path()],
509 &config,
510 )
511 .unwrap();
512
513 assert_eq!(report.files_added, 1);
514 assert!(report.bytes_written > 0);
515 assert!(output.exists());
516 }
517
518 #[test]
519 fn test_create_tar_directory() {
520 let temp = TempDir::new().unwrap();
521 let output = temp.path().join("output.tar");
522
523 let source_dir = TempDir::new().unwrap();
524 fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
525 fs::write(source_dir.path().join("file2.txt"), "content2").unwrap();
526 fs::create_dir(source_dir.path().join("subdir")).unwrap();
527 fs::write(source_dir.path().join("subdir/file3.txt"), "content3").unwrap();
528
529 let config = CreationConfig::default()
530 .with_exclude_patterns(vec![])
531 .with_include_hidden(true)
532 .with_format(Some(ArchiveType::Tar));
533
534 let report = create_archive(&output, &[source_dir.path()], &config).unwrap();
535
536 assert_eq!(report.files_added, 3);
537 assert_eq!(report.directories_added, 2);
538 assert!(output.exists());
539 }
540
541 #[test]
542 fn test_create_tar_gz_compression() {
543 let temp = TempDir::new().unwrap();
544 let output = temp.path().join("output.tar.gz");
545
546 let source_dir = TempDir::new().unwrap();
547 fs::write(source_dir.path().join("test.txt"), "a".repeat(1000)).unwrap();
548
549 let config = CreationConfig::default()
550 .with_exclude_patterns(vec![])
551 .with_compression_level(9)
552 .with_format(Some(ArchiveType::TarGz));
553
554 let report = create_archive(&output, &[source_dir.path()], &config).unwrap();
555
556 assert_eq!(report.files_added, 1);
557 assert!(output.exists());
558
559 let data = fs::read(&output).unwrap();
560 assert_eq!(&data[0..2], &[0x1f, 0x8b]); }
562
563 #[test]
564 fn test_create_tar_bz2_compression() {
565 let temp = TempDir::new().unwrap();
566 let output = temp.path().join("output.tar.bz2");
567
568 let source_dir = TempDir::new().unwrap();
569 fs::write(source_dir.path().join("test.txt"), "bzip2 test").unwrap();
570
571 let config = CreationConfig::default()
572 .with_exclude_patterns(vec![])
573 .with_format(Some(ArchiveType::TarBz2));
574
575 let report = create_archive(&output, &[source_dir.path()], &config).unwrap();
576
577 assert_eq!(report.files_added, 1);
578 assert!(output.exists());
579
580 let data = fs::read(&output).unwrap();
581 assert_eq!(&data[0..3], b"BZh"); }
583
584 #[test]
585 fn test_create_tar_xz_compression() {
586 let temp = TempDir::new().unwrap();
587 let output = temp.path().join("output.tar.xz");
588
589 let source_dir = TempDir::new().unwrap();
590 fs::write(source_dir.path().join("test.txt"), "xz test").unwrap();
591
592 let config = CreationConfig::default()
593 .with_exclude_patterns(vec![])
594 .with_format(Some(ArchiveType::TarXz));
595
596 let report = create_archive(&output, &[source_dir.path()], &config).unwrap();
597
598 assert_eq!(report.files_added, 1);
599 assert!(output.exists());
600
601 let data = fs::read(&output).unwrap();
602 assert_eq!(&data[0..6], &[0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]); }
604
605 #[test]
606 fn test_create_tar_zst_compression() {
607 let temp = TempDir::new().unwrap();
608 let output = temp.path().join("output.tar.zst");
609
610 let source_dir = TempDir::new().unwrap();
611 fs::write(source_dir.path().join("test.txt"), "zstd test").unwrap();
612
613 let config = CreationConfig::default()
614 .with_exclude_patterns(vec![])
615 .with_format(Some(ArchiveType::TarZst));
616
617 let report = create_archive(&output, &[source_dir.path()], &config).unwrap();
618
619 assert_eq!(report.files_added, 1);
620 assert!(output.exists());
621
622 let data = fs::read(&output).unwrap();
623 assert!(data.len() >= 4, "output file should have data");
624 assert_eq!(&data[0..4], &[0x28, 0xB5, 0x2F, 0xFD]); }
626
627 #[test]
628 fn test_create_tar_compression_levels() {
629 let temp = TempDir::new().unwrap();
630
631 let source_dir = TempDir::new().unwrap();
632 fs::write(source_dir.path().join("test.txt"), "a".repeat(10000)).unwrap();
633
634 for level in [1, 6, 9] {
635 let output = temp.path().join(format!("output_{level}.tar.gz"));
636 let config = CreationConfig::default()
637 .with_exclude_patterns(vec![])
638 .with_compression_level(level)
639 .with_format(Some(ArchiveType::TarGz));
640
641 let report = create_archive(&output, &[source_dir.path()], &config).unwrap();
642 assert_eq!(report.files_added, 1);
643 assert!(output.exists());
644 }
645 }
646
647 #[test]
648 #[cfg(unix)]
649 fn test_create_tar_preserves_permissions() {
650 use std::os::unix::fs::PermissionsExt;
651
652 let temp = TempDir::new().unwrap();
653 let output = temp.path().join("output.tar");
654
655 let source_dir = TempDir::new().unwrap();
656 let file_path = source_dir.path().join("test.txt");
657 fs::write(&file_path, "content").unwrap();
658 fs::set_permissions(&file_path, fs::Permissions::from_mode(0o755)).unwrap();
659
660 let config = CreationConfig::default()
661 .with_exclude_patterns(vec![])
662 .with_preserve_permissions(true)
663 .with_format(Some(ArchiveType::Tar));
664
665 let report = create_archive(&output, &[source_dir.path()], &config).unwrap();
666 assert_eq!(report.files_added, 1);
667
668 let extract_dir = TempDir::new().unwrap();
669 let security_config = SecurityConfig::default();
670 extract_archive(&output, extract_dir.path(), &security_config).unwrap();
671
672 let extracted = extract_dir.path().join("test.txt");
673 let perms = fs::metadata(&extracted).unwrap().permissions();
674 assert_eq!(perms.mode() & 0o777, 0o755);
675 }
676
677 #[test]
678 fn test_create_tar_report_statistics() {
679 let temp = TempDir::new().unwrap();
680 let output = temp.path().join("output.tar");
681
682 let source_dir = TempDir::new().unwrap();
683 fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
684 fs::write(source_dir.path().join("file2.txt"), "content2").unwrap();
685 fs::create_dir(source_dir.path().join("subdir")).unwrap();
686 fs::write(source_dir.path().join("subdir/file3.txt"), "content3").unwrap();
687
688 let config = CreationConfig::default()
689 .with_exclude_patterns(vec![])
690 .with_include_hidden(true)
691 .with_format(Some(ArchiveType::Tar));
692
693 let report = create_archive(&output, &[source_dir.path()], &config).unwrap();
694
695 assert_eq!(report.files_added, 3);
696 assert!(report.directories_added >= 1);
697 assert_eq!(report.files_skipped, 0);
698 assert!(!report.has_warnings());
699 assert!(report.duration.as_nanos() > 0);
700 }
701
702 #[test]
703 fn test_create_tar_roundtrip() {
704 let temp = TempDir::new().unwrap();
705 let output = temp.path().join("output.tar.gz");
706
707 let source_dir = TempDir::new().unwrap();
708 fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
709 fs::create_dir(source_dir.path().join("subdir")).unwrap();
710 fs::write(source_dir.path().join("subdir/file2.txt"), "content2").unwrap();
711
712 let config = CreationConfig::default()
713 .with_exclude_patterns(vec![])
714 .with_include_hidden(true)
715 .with_format(Some(ArchiveType::TarGz));
716
717 let report = create_archive(&output, &[source_dir.path()], &config).unwrap();
718 assert!(report.files_added >= 2);
719
720 let extract_dir = TempDir::new().unwrap();
721 let security_config = SecurityConfig::default();
722 extract_archive(&output, extract_dir.path(), &security_config).unwrap();
723
724 let extracted1 = fs::read_to_string(extract_dir.path().join("file1.txt")).unwrap();
725 assert_eq!(extracted1, "content1");
726
727 let extracted2 = fs::read_to_string(extract_dir.path().join("subdir/file2.txt")).unwrap();
728 assert_eq!(extracted2, "content2");
729 }
730
731 #[test]
732 fn test_create_tar_source_not_found() {
733 let temp = TempDir::new().unwrap();
734 let output = temp.path().join("output.tar");
735
736 let config = CreationConfig::default().with_format(Some(ArchiveType::Tar));
737 let result = create_archive(&output, &[Path::new("/nonexistent/path")], &config);
738
739 assert!(result.is_err());
740 assert!(matches!(
741 result.unwrap_err(),
742 ExtractionError::SourceNotFound { .. }
743 ));
744 }
745
746 #[test]
747 fn test_compression_level_to_flate2() {
748 let level = compression_level_to_flate2(None);
750 assert_eq!(level, flate2::Compression::default());
751
752 let level = compression_level_to_flate2(Some(1));
754 assert_eq!(level, flate2::Compression::fast());
755
756 let level = compression_level_to_flate2(Some(9));
758 assert_eq!(level, flate2::Compression::best());
759
760 let level = compression_level_to_flate2(Some(5));
762 assert_eq!(level, flate2::Compression::new(5));
763 }
764
765 #[test]
766 fn test_compression_level_to_zstd() {
767 assert_eq!(compression_level_to_zstd(None), 3);
768 assert_eq!(compression_level_to_zstd(Some(1)), 1);
769 assert_eq!(compression_level_to_zstd(Some(6)), 3);
770 assert_eq!(compression_level_to_zstd(Some(7)), 10);
771 assert_eq!(compression_level_to_zstd(Some(9)), 19);
772 }
773
774 struct FailWriter {
779 written: usize,
780 fail_after: usize,
781 }
782
783 impl FailWriter {
784 fn new(fail_after: usize) -> Self {
785 Self {
786 written: 0,
787 fail_after,
788 }
789 }
790 }
791
792 impl Write for FailWriter {
793 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
794 if self.written >= self.fail_after {
795 return Err(std::io::Error::new(
796 std::io::ErrorKind::WriteZero,
797 "simulated write failure",
798 ));
799 }
800 let allowed = (self.fail_after - self.written).min(buf.len());
801 self.written += allowed;
802 Ok(allowed)
803 }
804
805 fn flush(&mut self) -> std::io::Result<()> {
806 Ok(())
807 }
808 }
809
810 #[test]
817 fn test_zstd_encoder_finish_error_propagated() {
818 let source_dir = TempDir::new().unwrap();
819 fs::write(source_dir.path().join("a.txt"), "hello").unwrap();
820
821 let config = CreationConfig::default()
822 .with_exclude_patterns(vec![])
823 .with_format(Some(ArchiveType::TarZst));
824
825 let fail_writer = FailWriter::new(8);
828 let level = compression_level_to_zstd(config.compression_level);
829 let mut encoder = zstd::Encoder::new(fail_writer, level).unwrap();
830 encoder.include_checksum(true).unwrap();
831
832 let mut noop = crate::NoopProgress;
833 let result =
834 create_tar_internal_with_progress(encoder, &[source_dir.path()], &config, &mut noop);
835
836 let is_err = match result {
839 Err(_) => true,
840 Ok((_, enc)) => enc.finish().is_err(),
841 };
842 assert!(
843 is_err,
844 "expected an error from zstd encoder when underlying writer fails"
845 );
846 }
847
848 #[test]
849 fn test_create_tar_with_progress_callback() {
850 #[derive(Debug, Default, Clone)]
851 struct TestProgress {
852 entries_started: Vec<String>,
853 entries_completed: Vec<String>,
854 bytes_written: u64,
855 completed: bool,
856 }
857
858 impl ProgressCallback for TestProgress {
859 fn on_entry_start(&mut self, path: &Path, _total: usize, _current: usize) {
860 self.entries_started
861 .push(path.to_string_lossy().to_string());
862 }
863
864 fn on_bytes_written(&mut self, bytes: u64) {
865 self.bytes_written += bytes;
866 }
867
868 fn on_entry_complete(&mut self, path: &Path) {
869 self.entries_completed
870 .push(path.to_string_lossy().to_string());
871 }
872
873 fn on_complete(&mut self) {
874 self.completed = true;
875 }
876 }
877
878 let temp = TempDir::new().unwrap();
879 let output = temp.path().join("output.tar");
880
881 let source_dir = TempDir::new().unwrap();
883 fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
884 fs::write(source_dir.path().join("file2.txt"), "content2").unwrap();
885 fs::create_dir(source_dir.path().join("subdir")).unwrap();
886 fs::write(source_dir.path().join("subdir/file3.txt"), "content3").unwrap();
887
888 let config = CreationConfig::default()
889 .with_exclude_patterns(vec![])
890 .with_include_hidden(true);
891
892 let mut progress = TestProgress::default();
893
894 let report =
895 create_tar_with_progress(&output, &[source_dir.path()], &config, &mut progress)
896 .unwrap();
897
898 assert_eq!(report.files_added, 3);
900 assert!(report.directories_added >= 1);
901
902 assert!(
904 progress.entries_started.len() >= 3,
905 "Expected at least 3 entry starts, got {}",
906 progress.entries_started.len()
907 );
908 assert!(
909 progress.entries_completed.len() >= 3,
910 "Expected at least 3 entry completions, got {}",
911 progress.entries_completed.len()
912 );
913 assert!(
914 progress.bytes_written > 0,
915 "Expected bytes written > 0, got {}",
916 progress.bytes_written
917 );
918 assert!(progress.completed, "Expected on_complete to be called");
919
920 let has_file1 = progress
922 .entries_started
923 .iter()
924 .any(|p| p.contains("file1.txt"));
925 let has_file2 = progress
926 .entries_started
927 .iter()
928 .any(|p| p.contains("file2.txt"));
929 let has_file3 = progress
930 .entries_started
931 .iter()
932 .any(|p| p.contains("file3.txt"));
933
934 assert!(has_file1, "Expected file1.txt in progress callbacks");
935 assert!(has_file2, "Expected file2.txt in progress callbacks");
936 assert!(has_file3, "Expected file3.txt in progress callbacks");
937 }
938
939 #[test]
947 fn test_create_tar_zst_with_progress_calls_finish() {
948 let temp = TempDir::new().unwrap();
949 let output = temp.path().join("output.tar.zst");
950
951 let source_dir = TempDir::new().unwrap();
952 fs::write(source_dir.path().join("test.txt"), "zstd progress finish").unwrap();
953
954 let config = CreationConfig::default().with_exclude_patterns(vec![]);
955 let mut noop = crate::NoopProgress;
956 let report =
957 create_tar_zst_with_progress(&output, &[source_dir.path()], &config, &mut noop)
958 .unwrap();
959
960 assert_eq!(report.files_added, 1);
961 assert!(output.exists());
962
963 let data = fs::read(&output).unwrap();
965 assert!(data.len() >= 4);
966 assert_eq!(&data[0..4], &[0x28, 0xB5, 0x2F, 0xFD]);
967 }
968}