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