1use crate::ProgressCallback;
7use crate::Result;
8use crate::config::Validated;
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::progress::ProgressReader;
15use crate::creation::progress::ProgressTracker;
16use crate::creation::report::CreationReport;
17use crate::creation::walker::EntryType;
18use crate::creation::walker::collect_entries;
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<Validated>,
102 progress: &mut dyn ProgressCallback,
103) -> Result<CreationReport> {
104 let file = File::create(output.as_ref())?;
105 let (mut report, file) = create_tar_internal_with_progress(file, sources, config, progress)?;
106 drop(file);
107 report.bytes_compressed = std::fs::metadata(output.as_ref())?.len();
108 Ok(report)
109}
110
111pub fn create_tar_gz_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
122 output: P,
123 sources: &[Q],
124 config: &CreationConfig<Validated>,
125 progress: &mut dyn ProgressCallback,
126) -> Result<CreationReport> {
127 let file = File::create(output.as_ref())?;
128 let level = compression_level_to_flate2(config.compression_level);
129 let encoder = flate2::write::GzEncoder::new(file, level);
130 let (mut report, encoder) =
131 create_tar_internal_with_progress(encoder, sources, config, progress)?;
132 encoder.finish()?;
133 report.bytes_compressed = std::fs::metadata(output.as_ref())?.len();
134 Ok(report)
135}
136
137pub fn create_tar_bz2_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
148 output: P,
149 sources: &[Q],
150 config: &CreationConfig<Validated>,
151 progress: &mut dyn ProgressCallback,
152) -> Result<CreationReport> {
153 let file = File::create(output.as_ref())?;
154 let level = compression_level_to_bzip2(config.compression_level);
155 let encoder = bzip2::write::BzEncoder::new(file, level);
156 let (mut report, encoder) =
157 create_tar_internal_with_progress(encoder, sources, config, progress)?;
158 encoder.finish()?;
159 report.bytes_compressed = std::fs::metadata(output.as_ref())?.len();
160 Ok(report)
161}
162
163pub fn create_tar_xz_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
174 output: P,
175 sources: &[Q],
176 config: &CreationConfig<Validated>,
177 progress: &mut dyn ProgressCallback,
178) -> Result<CreationReport> {
179 let file = File::create(output.as_ref())?;
180 let level = compression_level_to_xz(config.compression_level);
181 let encoder = xz2::write::XzEncoder::new(file, level);
182 let (mut report, encoder) =
183 create_tar_internal_with_progress(encoder, sources, config, progress)?;
184 encoder.finish()?;
185 report.bytes_compressed = std::fs::metadata(output.as_ref())?.len();
186 Ok(report)
187}
188
189pub fn create_tar_zst_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
200 output: P,
201 sources: &[Q],
202 config: &CreationConfig<Validated>,
203 progress: &mut dyn ProgressCallback,
204) -> Result<CreationReport> {
205 let file = File::create(output.as_ref())?;
206 let level = compression_level_to_zstd(config.compression_level);
207 let mut encoder = zstd::Encoder::new(file, level)?;
208 encoder.include_checksum(true)?;
209
210 let (mut report, encoder) =
211 create_tar_internal_with_progress(encoder, sources, config, progress)?;
212 encoder.finish()?;
213
214 report.bytes_compressed = std::fs::metadata(output.as_ref())?.len();
215 Ok(report)
216}
217
218fn create_tar_internal_with_progress<W: Write, P: AsRef<Path>>(
223 writer: W,
224 sources: &[P],
225 config: &CreationConfig<Validated>,
226 progress: &mut dyn ProgressCallback,
227) -> Result<(CreationReport, W)> {
228 let mut builder = Builder::new(writer);
229 let mut report = CreationReport::default();
230 let start = std::time::Instant::now();
231
232 let entries = collect_entries(sources, config)?;
234 let total_entries = entries.len();
235
236 let mut tracker = ProgressTracker::new(progress, total_entries);
237
238 for entry in &entries {
239 match &entry.entry_type {
240 EntryType::File => {
241 tracker.on_entry_start(&entry.archive_path);
242 add_file_to_tar_with_progress_impl(
243 &mut builder,
244 &entry.path,
245 &entry.archive_path,
246 config,
247 &mut report,
248 tracker.callback(),
249 )?;
250 tracker.on_entry_complete(&entry.archive_path);
251 }
252 EntryType::Directory => {
253 tracker.on_entry_start(&entry.archive_path);
254 add_directory_to_tar(
255 &mut builder,
256 &entry.path,
257 &entry.archive_path,
258 config,
259 &mut report,
260 )?;
261 tracker.on_entry_complete(&entry.archive_path);
262 }
263 EntryType::Symlink { target } => {
264 tracker.on_entry_start(&entry.archive_path);
265 if config.follow_symlinks {
266 add_file_to_tar_with_progress_impl(
267 &mut builder,
268 &entry.path,
269 &entry.archive_path,
270 config,
271 &mut report,
272 tracker.callback(),
273 )?;
274 } else {
275 add_symlink_to_tar(&mut builder, &entry.archive_path, target, &mut report)?;
276 }
277 tracker.on_entry_complete(&entry.archive_path);
278 }
279 }
280 }
281
282 builder.finish()?;
284
285 let mut writer = builder.into_inner()?;
286 writer.flush()?;
287
288 report.duration = start.elapsed();
289
290 tracker.on_complete();
291
292 Ok((report, writer))
293}
294
295fn add_directory_to_tar<W: Write>(
301 builder: &mut Builder<W>,
302 dir_path: &Path,
303 archive_path: &Path,
304 config: &CreationConfig<Validated>,
305 report: &mut CreationReport,
306) -> Result<()> {
307 if archive_path.as_os_str().is_empty() {
308 return Ok(());
309 }
310
311 let mut header = Header::new_gnu();
312 header.set_entry_type(tar::EntryType::Directory);
313 header.set_size(0);
314
315 if config.preserve_permissions {
316 let metadata = std::fs::metadata(dir_path)?;
317 set_permissions(&mut header, &metadata);
318 } else {
319 header.set_mode(0o755);
325 header.set_uid(0);
326 header.set_gid(0);
327 header.set_mtime(0);
328 }
329
330 header.set_cksum();
331 builder.append_data(&mut header, archive_path, std::io::empty())?;
332
333 report.directories_added += 1;
334
335 Ok(())
336}
337
338fn add_file_to_tar_with_progress_impl<W: Write>(
340 builder: &mut Builder<W>,
341 file_path: &Path,
342 archive_path: &Path,
343 config: &CreationConfig<Validated>,
344 report: &mut CreationReport,
345 progress: &mut dyn ProgressCallback,
346) -> Result<()> {
347 let file = File::open(file_path)?;
348 let metadata = file.metadata()?;
349 let size = metadata.len();
350
351 let mut header = Header::new_gnu();
352 header.set_size(size);
353 header.set_cksum();
354
355 if config.preserve_permissions {
356 set_permissions(&mut header, &metadata);
357 }
358
359 let mut tracked_file = ProgressReader::new(file, progress);
360 builder.append_data(&mut header, archive_path, &mut tracked_file)?;
361
362 report.files_added += 1;
363 report.bytes_written += size;
364
365 Ok(())
366}
367
368#[cfg(unix)]
370fn add_symlink_to_tar<W: Write>(
371 builder: &mut Builder<W>,
372 link_path: &Path,
373 target: &Path,
374 report: &mut CreationReport,
375) -> Result<()> {
376 let mut header = Header::new_gnu();
377 header.set_entry_type(tar::EntryType::Symlink);
378 header.set_size(0);
379 header.set_cksum();
380
381 builder.append_link(&mut header, link_path, target)?;
382
383 report.symlinks_added += 1;
384
385 Ok(())
386}
387
388#[cfg(not(unix))]
389fn add_symlink_to_tar<W: Write>(
390 _builder: &mut Builder<W>,
391 _link_path: &Path,
392 _target: &Path,
393 report: &mut CreationReport,
394) -> Result<()> {
395 report.files_skipped = report.files_skipped.saturating_add(1);
397 report.add_warning("Symlinks not supported on this platform");
398 Ok(())
399}
400
401#[cfg(unix)]
403fn set_permissions(header: &mut Header, metadata: &std::fs::Metadata) {
404 use std::os::unix::fs::MetadataExt;
405 let mode = metadata.mode();
406 header.set_mode(mode);
407 header.set_uid(u64::from(metadata.uid()));
408 header.set_gid(u64::from(metadata.gid()));
409 #[allow(clippy::cast_sign_loss)] let mtime = metadata.mtime().max(0) as u64;
412 header.set_mtime(mtime);
413}
414
415#[cfg(not(unix))]
416fn set_permissions(header: &mut Header, metadata: &std::fs::Metadata) {
417 let mode = if metadata.permissions().readonly() {
419 0o444
420 } else {
421 0o644
422 };
423 header.set_mode(mode);
424
425 if let Ok(modified) = metadata.modified() {
427 if let Ok(duration) = modified.duration_since(std::time::UNIX_EPOCH) {
428 header.set_mtime(duration.as_secs());
429 }
430 }
431}
432
433pub struct TarCreator;
435
436pub struct TarGzCreator;
438
439pub struct TarBz2Creator;
441
442pub struct TarXzCreator;
444
445pub struct TarZstCreator;
447
448impl crate::formats::traits::FormatCreator for TarCreator {
449 fn create(
450 &self,
451 output: &Path,
452 sources: &[&Path],
453 config: &CreationConfig<Validated>,
454 progress: &mut dyn ProgressCallback,
455 ) -> crate::Result<crate::creation::CreationReport> {
456 create_tar_with_progress(output, sources, config, progress)
457 }
458
459 fn format_name(&self) -> &'static str {
460 "tar"
461 }
462}
463
464impl crate::formats::traits::FormatCreator for TarGzCreator {
465 fn create(
466 &self,
467 output: &Path,
468 sources: &[&Path],
469 config: &CreationConfig<Validated>,
470 progress: &mut dyn ProgressCallback,
471 ) -> crate::Result<crate::creation::CreationReport> {
472 create_tar_gz_with_progress(output, sources, config, progress)
473 }
474
475 fn format_name(&self) -> &'static str {
476 "tar.gz"
477 }
478}
479
480impl crate::formats::traits::FormatCreator for TarBz2Creator {
481 fn create(
482 &self,
483 output: &Path,
484 sources: &[&Path],
485 config: &CreationConfig<Validated>,
486 progress: &mut dyn ProgressCallback,
487 ) -> crate::Result<crate::creation::CreationReport> {
488 create_tar_bz2_with_progress(output, sources, config, progress)
489 }
490
491 fn format_name(&self) -> &'static str {
492 "tar.bz2"
493 }
494}
495
496impl crate::formats::traits::FormatCreator for TarXzCreator {
497 fn create(
498 &self,
499 output: &Path,
500 sources: &[&Path],
501 config: &CreationConfig<Validated>,
502 progress: &mut dyn ProgressCallback,
503 ) -> crate::Result<crate::creation::CreationReport> {
504 create_tar_xz_with_progress(output, sources, config, progress)
505 }
506
507 fn format_name(&self) -> &'static str {
508 "tar.xz"
509 }
510}
511
512impl crate::formats::traits::FormatCreator for TarZstCreator {
513 fn create(
514 &self,
515 output: &Path,
516 sources: &[&Path],
517 config: &CreationConfig<Validated>,
518 progress: &mut dyn ProgressCallback,
519 ) -> crate::Result<crate::creation::CreationReport> {
520 create_tar_zst_with_progress(output, sources, config, progress)
521 }
522
523 fn format_name(&self) -> &'static str {
524 "tar.zst"
525 }
526}
527
528#[cfg(test)]
529#[allow(clippy::unwrap_used)] mod tests {
531 use super::*;
532 use crate::ArchiveError;
533 use crate::SecurityConfig;
534 use crate::api::create_archive;
535 use crate::api::extract_archive;
536 use crate::formats::detect::ArchiveType;
537 use std::assert_matches;
538 use std::fs;
539 use tempfile::TempDir;
540
541 #[test]
542 fn test_create_tar_single_file() {
543 let temp = TempDir::new().unwrap();
544 let output = temp.path().join("output.tar");
545
546 let source_dir = TempDir::new().unwrap();
547 fs::write(source_dir.path().join("test.txt"), "Hello TAR").unwrap();
548
549 let config = CreationConfig::default()
550 .with_exclude_patterns(vec![])
551 .with_include_hidden(true)
552 .with_format(Some(ArchiveType::Tar));
553
554 let report = create_archive(
555 &output,
556 &[source_dir.path().join("test.txt").as_path()],
557 &config,
558 )
559 .unwrap();
560
561 assert_eq!(report.files_added, 1);
562 assert!(report.bytes_written > 0);
563 assert!(output.exists());
564 }
565
566 #[test]
567 fn test_create_tar_directory() {
568 let temp = TempDir::new().unwrap();
569 let output = temp.path().join("output.tar");
570
571 let source_dir = TempDir::new().unwrap();
572 fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
573 fs::write(source_dir.path().join("file2.txt"), "content2").unwrap();
574 fs::create_dir(source_dir.path().join("subdir")).unwrap();
575 fs::write(source_dir.path().join("subdir/file3.txt"), "content3").unwrap();
576
577 let config = CreationConfig::default()
578 .with_exclude_patterns(vec![])
579 .with_include_hidden(true)
580 .with_format(Some(ArchiveType::Tar));
581
582 let report = create_archive(&output, &[source_dir.path()], &config).unwrap();
583
584 assert_eq!(report.files_added, 3);
585 assert_eq!(report.directories_added, 1);
588 assert!(output.exists());
589 }
590
591 #[test]
592 fn test_create_tar_gz_compression() {
593 let temp = TempDir::new().unwrap();
594 let output = temp.path().join("output.tar.gz");
595
596 let source_dir = TempDir::new().unwrap();
597 fs::write(source_dir.path().join("test.txt"), "a".repeat(1000)).unwrap();
598
599 let config = CreationConfig::default()
600 .with_exclude_patterns(vec![])
601 .with_compression_level(9)
602 .unwrap()
603 .with_format(Some(ArchiveType::TarGz));
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_eq!(&data[0..2], &[0x1f, 0x8b]); }
613
614 #[test]
615 fn test_create_tar_bz2_compression() {
616 let temp = TempDir::new().unwrap();
617 let output = temp.path().join("output.tar.bz2");
618
619 let source_dir = TempDir::new().unwrap();
620 fs::write(source_dir.path().join("test.txt"), "bzip2 test").unwrap();
621
622 let config = CreationConfig::default()
623 .with_exclude_patterns(vec![])
624 .with_format(Some(ArchiveType::TarBz2));
625
626 let report = create_archive(&output, &[source_dir.path()], &config).unwrap();
627
628 assert_eq!(report.files_added, 1);
629 assert!(output.exists());
630
631 let data = fs::read(&output).unwrap();
632 assert_eq!(&data[0..3], b"BZh"); }
634
635 #[test]
636 fn test_create_tar_xz_compression() {
637 let temp = TempDir::new().unwrap();
638 let output = temp.path().join("output.tar.xz");
639
640 let source_dir = TempDir::new().unwrap();
641 fs::write(source_dir.path().join("test.txt"), "xz test").unwrap();
642
643 let config = CreationConfig::default()
644 .with_exclude_patterns(vec![])
645 .with_format(Some(ArchiveType::TarXz));
646
647 let report = create_archive(&output, &[source_dir.path()], &config).unwrap();
648
649 assert_eq!(report.files_added, 1);
650 assert!(output.exists());
651
652 let data = fs::read(&output).unwrap();
653 assert_eq!(&data[0..6], &[0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]); }
655
656 #[test]
657 fn test_create_tar_zst_compression() {
658 let temp = TempDir::new().unwrap();
659 let output = temp.path().join("output.tar.zst");
660
661 let source_dir = TempDir::new().unwrap();
662 fs::write(source_dir.path().join("test.txt"), "zstd test").unwrap();
663
664 let config = CreationConfig::default()
665 .with_exclude_patterns(vec![])
666 .with_format(Some(ArchiveType::TarZst));
667
668 let report = create_archive(&output, &[source_dir.path()], &config).unwrap();
669
670 assert_eq!(report.files_added, 1);
671 assert!(output.exists());
672
673 let data = fs::read(&output).unwrap();
674 assert!(data.len() >= 4, "output file should have data");
675 assert_eq!(&data[0..4], &[0x28, 0xB5, 0x2F, 0xFD]); }
677
678 #[test]
686 fn test_create_tar_compression_levels() {
687 let temp = TempDir::new().unwrap();
688
689 let source_dir = TempDir::new().unwrap();
690 fs::write(source_dir.path().join("test.txt"), "a".repeat(10000)).unwrap();
691
692 for format in [
693 ArchiveType::TarGz,
694 ArchiveType::TarBz2,
695 ArchiveType::TarXz,
696 ArchiveType::TarZst,
697 ] {
698 for level in 1..=9 {
699 let output = temp.path().join(format!("output_{format:?}_{level}.tar"));
700 let config = CreationConfig::default()
701 .with_exclude_patterns(vec![])
702 .with_compression_level(level)
703 .unwrap()
704 .with_format(Some(format));
705
706 let report = create_archive(&output, &[source_dir.path()], &config).unwrap();
707 assert_eq!(report.files_added, 1, "{format:?} level {level}");
708 assert!(output.exists(), "{format:?} level {level}");
709 }
710 }
711 }
712
713 #[test]
714 #[cfg(unix)]
715 fn test_create_tar_preserves_permissions() {
716 use std::os::unix::fs::PermissionsExt;
717
718 let temp = TempDir::new().unwrap();
719 let output = temp.path().join("output.tar");
720
721 let source_dir = TempDir::new().unwrap();
722 let file_path = source_dir.path().join("test.txt");
723 fs::write(&file_path, "content").unwrap();
724 fs::set_permissions(&file_path, fs::Permissions::from_mode(0o755)).unwrap();
725
726 let config = CreationConfig::default()
727 .with_exclude_patterns(vec![])
728 .with_preserve_permissions(true)
729 .with_format(Some(ArchiveType::Tar));
730
731 let report = create_archive(&output, &[source_dir.path()], &config).unwrap();
732 assert_eq!(report.files_added, 1);
733
734 let extract_dir = TempDir::new().unwrap();
735 let security_config = SecurityConfig::default();
736 extract_archive(&output, extract_dir.path(), &security_config).unwrap();
737
738 let extracted = extract_dir.path().join("test.txt");
739 let perms = fs::metadata(&extracted).unwrap().permissions();
740 assert_eq!(perms.mode() & 0o777, 0o755);
741 }
742
743 #[test]
748 #[cfg(unix)]
749 fn test_create_tar_directory_mode_without_preserve_permissions() {
750 use std::os::unix::fs::PermissionsExt;
751
752 let temp = TempDir::new().unwrap();
753 let output = temp.path().join("output.tar");
754
755 let source_dir = TempDir::new().unwrap();
756 let subdir = source_dir.path().join("subdir");
757 fs::create_dir(&subdir).unwrap();
758 fs::set_permissions(&subdir, fs::Permissions::from_mode(0o700)).unwrap();
762
763 let config = CreationConfig::default()
764 .with_exclude_patterns(vec![])
765 .with_preserve_permissions(false)
766 .with_format(Some(ArchiveType::Tar));
767
768 let report = create_archive(&output, &[source_dir.path()], &config).unwrap();
769 assert_eq!(report.directories_added, 1);
770
771 let extract_dir = TempDir::new().unwrap();
772 let security_config = SecurityConfig::default();
773 extract_archive(&output, extract_dir.path(), &security_config).unwrap();
774
775 let extracted = extract_dir.path().join("subdir");
776 let mode = fs::metadata(&extracted).unwrap().permissions().mode() & 0o777;
777 assert_eq!(
778 mode, 0o755,
779 "directory entries without preserve_permissions must use a traversable \
780 deterministic default, not mode 0"
781 );
782 }
783
784 #[test]
785 fn test_create_tar_report_statistics() {
786 let temp = TempDir::new().unwrap();
787 let output = temp.path().join("output.tar");
788
789 let source_dir = TempDir::new().unwrap();
790 fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
791 fs::write(source_dir.path().join("file2.txt"), "content2").unwrap();
792 fs::create_dir(source_dir.path().join("subdir")).unwrap();
793 fs::write(source_dir.path().join("subdir/file3.txt"), "content3").unwrap();
794
795 let config = CreationConfig::default()
796 .with_exclude_patterns(vec![])
797 .with_include_hidden(true)
798 .with_format(Some(ArchiveType::Tar));
799
800 let report = create_archive(&output, &[source_dir.path()], &config).unwrap();
801
802 assert_eq!(report.files_added, 3);
803 assert!(report.directories_added >= 1);
804 assert_eq!(report.files_skipped, 0);
805 assert!(!report.has_warnings());
806 assert!(report.duration.as_nanos() > 0);
807 }
808
809 #[test]
810 fn test_create_tar_roundtrip() {
811 let temp = TempDir::new().unwrap();
812 let output = temp.path().join("output.tar.gz");
813
814 let source_dir = TempDir::new().unwrap();
815 fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
816 fs::create_dir(source_dir.path().join("subdir")).unwrap();
817 fs::write(source_dir.path().join("subdir/file2.txt"), "content2").unwrap();
818
819 let config = CreationConfig::default()
820 .with_exclude_patterns(vec![])
821 .with_include_hidden(true)
822 .with_format(Some(ArchiveType::TarGz));
823
824 let report = create_archive(&output, &[source_dir.path()], &config).unwrap();
825 assert!(report.files_added >= 2);
826
827 let extract_dir = TempDir::new().unwrap();
828 let security_config = SecurityConfig::default();
829 extract_archive(&output, extract_dir.path(), &security_config).unwrap();
830
831 let extracted1 = fs::read_to_string(extract_dir.path().join("file1.txt")).unwrap();
832 assert_eq!(extracted1, "content1");
833
834 let extracted2 = fs::read_to_string(extract_dir.path().join("subdir/file2.txt")).unwrap();
835 assert_eq!(extracted2, "content2");
836 }
837
838 #[test]
839 fn test_create_tar_source_not_found() {
840 let temp = TempDir::new().unwrap();
841 let output = temp.path().join("output.tar");
842
843 let config = CreationConfig::default().with_format(Some(ArchiveType::Tar));
844 let result = create_archive(&output, &[Path::new("/nonexistent/path")], &config);
845
846 assert!(result.is_err());
847 assert_matches!(result.unwrap_err(), ArchiveError::SourceNotFound { .. });
848 }
849
850 #[test]
851 fn test_compression_level_to_flate2() {
852 let level = compression_level_to_flate2(None);
854 assert_eq!(level, flate2::Compression::default());
855
856 let level = compression_level_to_flate2(Some(1));
858 assert_eq!(level, flate2::Compression::fast());
859
860 let level = compression_level_to_flate2(Some(9));
862 assert_eq!(level, flate2::Compression::best());
863
864 let level = compression_level_to_flate2(Some(5));
866 assert_eq!(level, flate2::Compression::new(5));
867 }
868
869 #[test]
870 fn test_compression_level_to_zstd() {
871 assert_eq!(compression_level_to_zstd(None), 3);
872 assert_eq!(compression_level_to_zstd(Some(1)), 1);
873 assert_eq!(compression_level_to_zstd(Some(6)), 3);
874 assert_eq!(compression_level_to_zstd(Some(7)), 10);
875 assert_eq!(compression_level_to_zstd(Some(9)), 19);
876 }
877
878 struct FailWriter {
883 written: usize,
884 fail_after: usize,
885 }
886
887 impl FailWriter {
888 fn new(fail_after: usize) -> Self {
889 Self {
890 written: 0,
891 fail_after,
892 }
893 }
894 }
895
896 impl Write for FailWriter {
897 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
898 if self.written >= self.fail_after {
899 return Err(std::io::Error::new(
900 std::io::ErrorKind::WriteZero,
901 "simulated write failure",
902 ));
903 }
904 let allowed = (self.fail_after - self.written).min(buf.len());
905 self.written += allowed;
906 Ok(allowed)
907 }
908
909 fn flush(&mut self) -> std::io::Result<()> {
910 Ok(())
911 }
912 }
913
914 #[test]
921 fn test_zstd_encoder_finish_error_propagated() {
922 let source_dir = TempDir::new().unwrap();
923 fs::write(source_dir.path().join("a.txt"), "hello").unwrap();
924
925 let config = CreationConfig::default()
926 .with_exclude_patterns(vec![])
927 .with_format(Some(ArchiveType::TarZst))
928 .validate()
929 .unwrap();
930
931 let fail_writer = FailWriter::new(8);
934 let level = compression_level_to_zstd(config.compression_level);
935 let mut encoder = zstd::Encoder::new(fail_writer, level).unwrap();
936 encoder.include_checksum(true).unwrap();
937
938 let mut noop = crate::NoopProgress;
939 let result =
940 create_tar_internal_with_progress(encoder, &[source_dir.path()], &config, &mut noop);
941
942 let is_err = match result {
945 Err(_) => true,
946 Ok((_, enc)) => enc.finish().is_err(),
947 };
948 assert!(
949 is_err,
950 "expected an error from zstd encoder when underlying writer fails"
951 );
952 }
953
954 #[test]
955 fn test_create_tar_with_progress_callback() {
956 #[derive(Debug, Default, Clone)]
957 struct TestProgress {
958 entries_started: Vec<String>,
959 entries_completed: Vec<String>,
960 bytes_written: u64,
961 completed: bool,
962 }
963
964 impl ProgressCallback for TestProgress {
965 fn on_entry_start(&mut self, path: &Path, _total: usize, _current: usize) {
966 self.entries_started
967 .push(path.to_string_lossy().to_string());
968 }
969
970 fn on_bytes_written(&mut self, bytes: u64) {
971 self.bytes_written += bytes;
972 }
973
974 fn on_entry_complete(&mut self, path: &Path) {
975 self.entries_completed
976 .push(path.to_string_lossy().to_string());
977 }
978
979 fn on_complete(&mut self) {
980 self.completed = true;
981 }
982 }
983
984 let temp = TempDir::new().unwrap();
985 let output = temp.path().join("output.tar");
986
987 let source_dir = TempDir::new().unwrap();
989 fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
990 fs::write(source_dir.path().join("file2.txt"), "content2").unwrap();
991 fs::create_dir(source_dir.path().join("subdir")).unwrap();
992 fs::write(source_dir.path().join("subdir/file3.txt"), "content3").unwrap();
993
994 let config = CreationConfig::default()
995 .with_exclude_patterns(vec![])
996 .with_include_hidden(true)
997 .validate()
998 .unwrap();
999
1000 let mut progress = TestProgress::default();
1001
1002 let report =
1003 create_tar_with_progress(&output, &[source_dir.path()], &config, &mut progress)
1004 .unwrap();
1005
1006 assert_eq!(report.files_added, 3);
1008 assert!(report.directories_added >= 1);
1009
1010 assert!(
1012 progress.entries_started.len() >= 3,
1013 "Expected at least 3 entry starts, got {}",
1014 progress.entries_started.len()
1015 );
1016 assert!(
1017 progress.entries_completed.len() >= 3,
1018 "Expected at least 3 entry completions, got {}",
1019 progress.entries_completed.len()
1020 );
1021 assert!(
1022 progress.bytes_written > 0,
1023 "Expected bytes written > 0, got {}",
1024 progress.bytes_written
1025 );
1026 assert!(progress.completed, "Expected on_complete to be called");
1027
1028 let has_file1 = progress
1030 .entries_started
1031 .iter()
1032 .any(|p| p.contains("file1.txt"));
1033 let has_file2 = progress
1034 .entries_started
1035 .iter()
1036 .any(|p| p.contains("file2.txt"));
1037 let has_file3 = progress
1038 .entries_started
1039 .iter()
1040 .any(|p| p.contains("file3.txt"));
1041
1042 assert!(has_file1, "Expected file1.txt in progress callbacks");
1043 assert!(has_file2, "Expected file2.txt in progress callbacks");
1044 assert!(has_file3, "Expected file3.txt in progress callbacks");
1045 }
1046
1047 #[test]
1055 fn test_create_tar_zst_with_progress_calls_finish() {
1056 let temp = TempDir::new().unwrap();
1057 let output = temp.path().join("output.tar.zst");
1058
1059 let source_dir = TempDir::new().unwrap();
1060 fs::write(source_dir.path().join("test.txt"), "zstd progress finish").unwrap();
1061
1062 let config = CreationConfig::default()
1063 .with_exclude_patterns(vec![])
1064 .validate()
1065 .unwrap();
1066 let mut noop = crate::NoopProgress;
1067 let report =
1068 create_tar_zst_with_progress(&output, &[source_dir.path()], &config, &mut noop)
1069 .unwrap();
1070
1071 assert_eq!(report.files_added, 1);
1072 assert!(output.exists());
1073
1074 let data = fs::read(&output).unwrap();
1076 assert!(data.len() >= 4);
1077 assert_eq!(&data[0..4], &[0x28, 0xB5, 0x2F, 0xFD]);
1078 }
1079}