1use 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#[allow(dead_code)] pub 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#[allow(dead_code)] pub 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#[allow(dead_code)] pub 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#[allow(dead_code)] pub 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#[allow(dead_code)] pub 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 Ok(report)
200}
201
202#[allow(dead_code)] pub 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#[allow(dead_code)] pub 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#[allow(dead_code)] pub 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#[allow(dead_code)] pub 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#[allow(dead_code)] pub 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
382fn 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 let entries = collect_entries(sources, config)?;
396 let total_entries = entries.len();
397
398 let mut current_entry = 0usize;
401
402 let mut buffer = vec![0u8; 64 * 1024]; 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 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
461fn 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 if !path.exists() {
479 return Err(ExtractionError::SourceNotFound {
480 path: path.to_path_buf(),
481 });
482 }
483
484 if path.is_dir() {
486 add_directory_to_tar(&mut builder, path, config, &mut report)?;
487 } else {
488 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 builder.finish()?;
497
498 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
508fn 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 report.directories_added += 1;
527 }
528 EntryType::Symlink { target } => {
529 if config.follow_symlinks {
530 add_file_to_tar(builder, &entry.path, &entry.archive_path, config, report)?;
532 } else {
533 add_symlink_to_tar(builder, &entry.archive_path, &target, report)?;
535 }
536 }
537 }
538 }
539
540 Ok(())
541}
542
543fn 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 let mut header = Header::new_gnu();
557 header.set_size(size);
558 header.set_cksum();
559
560 if config.preserve_permissions {
562 set_permissions(&mut header, &metadata);
563 }
564
565 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
574fn 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 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#[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 report.files_skipped += 1;
638 report.add_warning("Symlinks not supported on this platform");
639 Ok(())
640}
641
642#[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 #[allow(clippy::cast_sign_loss)] 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 let mode = if metadata.permissions().readonly() {
660 0o444
661 } else {
662 0o644
663 };
664 header.set_mode(mode);
665
666 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)] mod 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 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 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 assert_eq!(report.files_added, 3);
723 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 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 let data = fs::read(&output).unwrap();
748 assert_eq!(&data[0..2], &[0x1f, 0x8b]); }
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 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 let data = fs::read(&output).unwrap();
769 assert_eq!(&data[0..3], b"BZh"); }
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 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 let data = fs::read(&output).unwrap();
790 assert_eq!(&data[0..6], &[0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]); }
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 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 let data = fs::read(&output).unwrap();
811 assert!(data.len() >= 4, "output file should have data");
813 assert_eq!(&data[0..4], &[0x28, 0xB5, 0x2F, 0xFD]); }
815
816 #[test]
817 fn test_create_tar_compression_levels() {
818 let temp = TempDir::new().unwrap();
819
820 let source_dir = TempDir::new().unwrap();
822 fs::write(source_dir.path().join("test.txt"), "a".repeat(10000)).unwrap();
823
824 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 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 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 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 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 let report = create_tar_gz(&output, &[source_dir.path()], &config).unwrap();
910 assert!(report.files_added >= 2);
911
912 let extract_dir = TempDir::new().unwrap();
914 let security_config = SecurityConfig::default();
915 extract_archive(&output, extract_dir.path(), &security_config).unwrap();
916
917 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 let level = compression_level_to_flate2(None);
944 assert_eq!(level, flate2::Compression::default());
945
946 let level = compression_level_to_flate2(Some(1));
948 assert_eq!(level, flate2::Compression::fast());
949
950 let level = compression_level_to_flate2(Some(9));
952 assert_eq!(level, flate2::Compression::best());
953
954 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 #[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 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 assert_eq!(report.files_added, 3);
1022 assert!(report.directories_added >= 1);
1023
1024 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 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}