1use crate::header::PosixHeader;
28use crate::tar_format_types::TarFormatString;
29use crate::{BLOCKSIZE, POSIX_1003_MAX_FILENAME_LEN};
30#[cfg(feature = "alloc")]
31use alloc::boxed::Box;
32use core::fmt::{Debug, Display, Formatter};
33use core::str::Utf8Error;
34use log::warn;
35
36pub const MIN_BLOCK_COUNT: usize = 3;
40
41pub struct ArchiveEntry<'a> {
44 filename: TarFormatString<POSIX_1003_MAX_FILENAME_LEN>,
45 data: &'a [u8],
46 size: usize,
47 posix_header: &'a PosixHeader,
48}
49
50#[allow(unused)]
51impl<'a> ArchiveEntry<'a> {
52 const fn new(
53 filename: TarFormatString<POSIX_1003_MAX_FILENAME_LEN>,
54 data: &'a [u8],
55 posix_header: &'a PosixHeader,
56 ) -> Self {
57 ArchiveEntry {
58 filename,
59 data,
60 size: data.len(),
61 posix_header,
62 }
63 }
64
65 #[must_use]
68 pub const fn filename(&self) -> TarFormatString<{ POSIX_1003_MAX_FILENAME_LEN }> {
69 self.filename
70 }
71
72 #[must_use]
74 pub const fn data(&self) -> &'a [u8] {
75 self.data
76 }
77
78 #[allow(clippy::missing_const_for_fn)]
83 pub fn data_as_str(&self) -> Result<&'a str, Utf8Error> {
84 core::str::from_utf8(self.data)
85 }
86
87 #[must_use]
89 pub const fn size(&self) -> usize {
90 self.size
91 }
92
93 #[must_use]
95 pub const fn posix_header(&self) -> &PosixHeader {
96 self.posix_header
97 }
98}
99
100impl Debug for ArchiveEntry<'_> {
101 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
102 f.debug_struct("ArchiveEntry")
103 .field("filename", &self.filename().as_str())
104 .field("size", &self.size())
105 .field("data", &"<bytes>")
106 .finish()
107 }
108}
109
110#[derive(Copy, Clone, Debug, PartialEq, Eq)]
112pub enum CorruptDataError {
113 EmptyArchive,
115 InvalidBlockSize,
117 TooShort {
119 byte_count: usize,
121 block_count: usize,
123 },
124 InvalidChecksum {
126 block_index: usize,
128 },
129 InvalidTypeFlag {
131 block_index: usize,
133 },
134 InvalidPayloadSize {
136 block_index: usize,
138 },
139 PayloadExtendsBeyondArchive {
141 block_index: usize,
143 },
144 MissingTerminator,
146}
147
148impl Display for CorruptDataError {
149 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
150 match self {
151 Self::EmptyArchive => f.write_str("archive contains no data"),
152 Self::InvalidBlockSize => f.write_str("archive length is not a multiple of 512 bytes"),
153 Self::TooShort {
154 byte_count,
155 block_count,
156 } => write!(
157 f,
158 "archive is too short: {byte_count} bytes ({block_count} blocks), expected at least {MIN_BLOCK_COUNT} blocks"
159 ),
160 Self::InvalidChecksum { block_index } => {
161 write!(f, "header at block {block_index} has an invalid checksum")
162 }
163 Self::InvalidTypeFlag { block_index } => {
164 write!(f, "header at block {block_index} has an invalid type flag")
165 }
166 Self::InvalidPayloadSize { block_index } => {
167 write!(
168 f,
169 "header at block {block_index} has an invalid payload size"
170 )
171 }
172 Self::PayloadExtendsBeyondArchive { block_index } => write!(
173 f,
174 "payload described by header at block {block_index} extends beyond the archive"
175 ),
176 Self::MissingTerminator => f.write_str("archive does not end with two zero blocks"),
177 }
178 }
179}
180
181impl core::error::Error for CorruptDataError {}
182
183#[cfg(feature = "alloc")]
191#[derive(Clone, Debug, PartialEq, Eq)]
192pub struct TarArchive {
193 data: Box<[u8]>,
194}
195
196#[cfg(feature = "alloc")]
197impl TarArchive {
198 pub fn new(data: Box<[u8]>) -> Result<Self, CorruptDataError> {
214 TarArchiveRef::validate(&data).map(|_| Self { data })
215 }
216
217 #[must_use]
221 pub fn entries(&self) -> ArchiveEntryIterator<'_> {
222 ArchiveEntryIterator::new(self.data.as_ref())
223 }
224
225 #[must_use]
230 pub fn headers(&self) -> ArchiveHeaderIterator<'_> {
231 ArchiveHeaderIterator::new(self.data.as_ref())
232 }
233}
234
235#[cfg(feature = "alloc")]
236#[allow(clippy::fallible_impl_from)]
237impl From<Box<[u8]>> for TarArchive {
238 fn from(data: Box<[u8]>) -> Self {
239 Self::new(data).unwrap()
240 }
241}
242
243#[cfg(feature = "alloc")]
244impl From<TarArchive> for Box<[u8]> {
245 fn from(ar: TarArchive) -> Self {
246 ar.data
247 }
248}
249
250#[derive(Clone, Debug, PartialEq, Eq)]
253pub struct TarArchiveRef<'a> {
254 data: &'a [u8],
255}
256
257#[allow(unused)]
258impl<'a> TarArchiveRef<'a> {
259 pub fn new(data: &'a [u8]) -> Result<Self, CorruptDataError> {
273 Self::validate(data).map(|()| Self { data })
274 }
275
276 fn validate(data: &'a [u8]) -> Result<(), CorruptDataError> {
282 if data.is_empty() {
283 return Err(CorruptDataError::EmptyArchive);
284 }
285 if data.len() % BLOCKSIZE != 0 {
286 return Err(CorruptDataError::InvalidBlockSize);
287 }
288 if data.len() / BLOCKSIZE < MIN_BLOCK_COUNT {
289 return Err(CorruptDataError::TooShort {
290 byte_count: data.len(),
291 block_count: data.len() / BLOCKSIZE,
292 });
293 }
294
295 Self::validate_headers(data)
296 }
297
298 fn validate_headers(data: &'a [u8]) -> Result<(), CorruptDataError> {
308 let header_iter = ArchiveHeaderIterator::new(data);
309 let total_block_count = data.len() / BLOCKSIZE;
310 let mut block_index = 0;
311
312 loop {
313 if block_index >= total_block_count {
314 return Err(CorruptDataError::MissingTerminator);
315 }
316
317 let hdr = header_iter.block_as_header(block_index);
318 if hdr.is_zero_block() {
319 return (block_index + 1 < total_block_count
320 && header_iter.block_as_header(block_index + 1).is_zero_block())
321 .then_some(())
322 .ok_or(CorruptDataError::MissingTerminator);
323 }
324
325 if !hdr.has_valid_checksum() {
326 return Err(CorruptDataError::InvalidChecksum { block_index });
327 }
328
329 let typeflag = hdr
330 .typeflag
331 .try_to_type_flag()
332 .map_err(|_| CorruptDataError::InvalidTypeFlag { block_index })?;
333 let mut next_block_index = block_index
334 .checked_add(1)
335 .ok_or(CorruptDataError::PayloadExtendsBeyondArchive { block_index })?;
336 if typeflag.has_payload() {
337 let payload_block_count = hdr
338 .payload_block_count()
339 .map_err(|_| CorruptDataError::InvalidPayloadSize { block_index })?;
340 next_block_index = next_block_index
341 .checked_add(payload_block_count)
342 .ok_or(CorruptDataError::PayloadExtendsBeyondArchive { block_index })?;
343 }
344 if next_block_index > total_block_count {
345 return Err(CorruptDataError::PayloadExtendsBeyondArchive { block_index });
346 }
347 block_index = next_block_index;
348 }
349 }
350
351 #[must_use]
355 pub fn entries(&self) -> ArchiveEntryIterator<'a> {
356 ArchiveEntryIterator::new(self.data)
357 }
358
359 #[must_use]
364 pub fn headers(&self) -> ArchiveHeaderIterator<'a> {
365 ArchiveHeaderIterator::new(self.data)
366 }
367}
368
369#[cfg_attr(feature = "alloc", doc = " [`TarArchive::headers`] or")]
375#[derive(Debug)]
377pub struct ArchiveHeaderIterator<'a> {
378 archive_data: &'a [u8],
379 next_hdr_block_index: usize,
380}
381
382impl<'a> ArchiveHeaderIterator<'a> {
383 #[must_use]
384 fn new(archive: &'a [u8]) -> Self {
385 assert!(!archive.is_empty());
386 assert_eq!(archive.len() % BLOCKSIZE, 0);
387 Self {
388 archive_data: archive,
389 next_hdr_block_index: 0,
390 }
391 }
392
393 const fn block_as_header(&self, block_index: usize) -> &'a PosixHeader {
395 let blocks = self.archive_data.len() / BLOCKSIZE;
396 assert!(block_index < blocks);
397
398 let ptr = self
399 .archive_data
400 .as_ptr()
401 .wrapping_add(block_index * BLOCKSIZE)
402 .cast::<PosixHeader>();
403 unsafe { ptr.as_ref().unwrap() }
406 }
407}
408
409type BlockIndex = usize;
410
411impl<'a> Iterator for ArchiveHeaderIterator<'a> {
412 type Item = (BlockIndex, &'a PosixHeader);
413
414 fn next(&mut self) -> Option<Self::Item> {
416 let total_block_count = self.archive_data.len() / BLOCKSIZE;
417 if self.next_hdr_block_index >= total_block_count {
418 return None;
419 }
420
421 let hdr = self.block_as_header(self.next_hdr_block_index);
422 let block_index = self.next_hdr_block_index;
423
424 if hdr.is_zero_block() {
427 return None;
428 }
429
430 self.next_hdr_block_index += 1;
432
433 let typeflag = hdr
437 .typeflag
438 .try_to_type_flag()
439 .expect("type flag should be valid after successful validation");
440 if typeflag.has_payload() {
441 let payload_block_count = hdr
442 .payload_block_count()
443 .expect("payload size should be valid after successful validation");
444 self.next_hdr_block_index += payload_block_count;
445 }
446
447 Some((block_index, hdr))
448 }
449}
450
451#[derive(Debug)]
462pub struct ArchiveEntryIterator<'a>(ArchiveHeaderIterator<'a>);
463
464impl<'a> ArchiveEntryIterator<'a> {
465 fn new(archive: &'a [u8]) -> Self {
466 Self(ArchiveHeaderIterator::new(archive))
467 }
468
469 fn next_hdr(&mut self) -> Option<(BlockIndex, &'a PosixHeader)> {
470 self.0.next()
471 }
472}
473
474impl<'a> Iterator for ArchiveEntryIterator<'a> {
475 type Item = ArchiveEntry<'a>;
476
477 fn next(&mut self) -> Option<Self::Item> {
478 let (mut block_index, mut hdr) = self.next_hdr()?;
479
480 while !hdr
483 .typeflag
484 .try_to_type_flag()
485 .expect("type flag should be valid after successful validation")
486 .is_regular_file()
487 {
488 warn!(
489 "Skipping entry of type {:?} (not supported yet)",
490 hdr.typeflag
491 );
492
493 (block_index, hdr) = self.next_hdr()?;
495 }
496
497 let payload_size: usize = hdr
498 .size
499 .as_number()
500 .expect("payload size should be valid after successful validation");
501
502 let idx_first_data_block = block_index + 1;
503 let idx_begin = idx_first_data_block * BLOCKSIZE;
504 let idx_end_exclusive = idx_begin + payload_size;
505
506 let file_bytes = &self.0.archive_data[idx_begin..idx_end_exclusive];
507
508 let mut filename =
509 TarFormatString::<POSIX_1003_MAX_FILENAME_LEN>::new([0; POSIX_1003_MAX_FILENAME_LEN]);
510
511 if (
514 hdr.magic.as_str(),
515 hdr.version.as_str(),
516 hdr.prefix.is_empty(),
517 ) == (Ok("ustar"), Ok("00"), false)
518 {
519 filename.append(&hdr.prefix);
520 filename.append(&TarFormatString::<1>::new(*b"/"));
521 }
522 filename.append(&hdr.name);
523 Some(ArchiveEntry::new(filename, file_bytes, hdr))
524 }
525}
526
527#[cfg(test)]
528mod tests {
529 use super::*;
530 use crate::TarFormatOctal;
531 use std::vec::Vec;
532
533 #[test]
534 #[rustfmt::skip]
535 fn test_constructor_returns_error() {
536 assert_eq!(
537 TarArchiveRef::new(&[0]),
538 Err(CorruptDataError::InvalidBlockSize)
539 );
540 assert_eq!(
541 TarArchiveRef::new(&[]),
542 Err(CorruptDataError::EmptyArchive)
543 );
544 assert_eq!(
545 TarArchiveRef::new(&[0; BLOCKSIZE]),
546 Err(CorruptDataError::TooShort {
547 byte_count: BLOCKSIZE,
548 block_count: 1,
549 })
550 );
551 assert!(TarArchiveRef::new(&[0; BLOCKSIZE * MIN_BLOCK_COUNT]).is_ok());
552
553 #[cfg(feature = "alloc")]
554 {
555 assert_eq!(
556 TarArchive::new(vec![].into_boxed_slice()),
557 Err(CorruptDataError::EmptyArchive)
558 );
559 assert_eq!(
560 TarArchive::new(vec![0].into_boxed_slice()),
561 Err(CorruptDataError::InvalidBlockSize)
562 );
563 assert!(TarArchive::new(vec![0; BLOCKSIZE * MIN_BLOCK_COUNT].into_boxed_slice()).is_ok());
564 };
565 }
566
567 #[test]
568 fn test_header_iterator() {
569 let archive = include_bytes!("../tests/gnu_tar_default.tar");
570 let iter = TarArchiveRef::new(archive)
571 .expect("test archive should pass validation")
572 .headers();
573 let names = iter
574 .map(|(_i, hdr)| hdr.name.as_str().unwrap())
575 .collect::<Vec<_>>();
576
577 assert_eq!(
578 names.as_slice(),
579 &[
580 "bye_world_513b.txt",
581 "hello_world_513b.txt",
582 "hello_world.txt",
583 ]
584 );
585 }
586
587 #[test]
589 fn test_print_archive_headers() {
590 let data = include_bytes!("../tests/gnu_tar_default.tar");
591
592 let iter = TarArchiveRef::new(data)
593 .expect("test archive should pass validation")
594 .headers();
595 let entries = iter.map(|(_, hdr)| hdr).collect::<Vec<_>>();
596 println!("{entries:#?}");
597 }
598
599 #[test]
601 fn test_print_archive_list() {
602 let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_default.tar")).unwrap();
603 let entries = archive.entries().collect::<Vec<_>>();
604 println!("{entries:#?}");
605 }
606
607 #[test]
616 fn test_weird_fuzzing_tarballs() {
617 let main_tarball =
622 TarArchiveRef::new(include_bytes!("../tests/weird_fuzzing_tarballs.tar"))
623 .expect("archive containing fuzzing inputs should pass validation");
624
625 let mut input_count = 0;
629 for fuzzing_input in main_tarball.entries() {
630 let result = TarArchiveRef::new(fuzzing_input.data());
631 assert!(
632 matches!(result, Err(CorruptDataError::InvalidChecksum { .. })),
635 "fuzzing input {:?} should fail checksum validation: {result:?}",
636 fuzzing_input.filename(),
637 );
638 input_count += 1;
639 }
640 assert_eq!(input_count, 32);
641 }
642
643 #[test]
645 fn test_archive_entries() {
646 let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_default.tar")).unwrap();
647 let entries = archive.entries().collect::<Vec<_>>();
648 assert_archive_content(&entries);
649
650 let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_gnu.tar")).unwrap();
651 let entries = archive.entries().collect::<Vec<_>>();
652 assert_archive_content(&entries);
653
654 let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_oldgnu.tar")).unwrap();
655 let entries = archive.entries().collect::<Vec<_>>();
656 assert_archive_content(&entries);
657
658 let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_pax.tar")).unwrap();
661 let entries = archive.entries().collect::<Vec<_>>();
662 assert_archive_content(&entries);
663
664 let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_posix.tar")).unwrap();
665 let entries = archive.entries().collect::<Vec<_>>();
666 assert_archive_content(&entries);
667
668 let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_ustar.tar")).unwrap();
669 let entries = archive.entries().collect::<Vec<_>>();
670 assert_archive_content(&entries);
671
672 let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_v7.tar")).unwrap();
673 let entries = archive.entries().collect::<Vec<_>>();
674 assert_archive_content(&entries);
675 }
676
677 #[test]
679 fn test_archive_with_long_dir_entries() {
680 let archive =
683 TarArchiveRef::new(include_bytes!("../tests/gnu_tar_ustar_long.tar")).unwrap();
684 let entries = archive.entries().collect::<Vec<_>>();
685
686 assert_eq!(entries.len(), 2);
687 assert_entry_content(
689 &entries[0],
690 "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678/ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ",
691 7,
692 );
693 assert_entry_content(
695 &entries[1],
696 "01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234/ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ",
697 7,
698 );
699 }
700
701 #[test]
702 fn test_archive_with_deep_dir_entries() {
703 let archive =
706 TarArchiveRef::new(include_bytes!("../tests/gnu_tar_ustar_deep.tar")).unwrap();
707 let entries = archive.entries().collect::<Vec<_>>();
708
709 assert_eq!(entries.len(), 1);
710 assert_entry_content(
711 &entries[0],
712 "0123456789/0123456789/0123456789/0123456789/0123456789/0123456789/0123456789/0123456789/0123456789/0123456789/0123456789/0123456789/empty",
713 0,
714 );
715 }
716
717 #[test]
718 fn test_default_archive_with_dir_entries() {
719 let archive =
722 TarArchiveRef::new(include_bytes!("../tests/gnu_tar_default_with_dir.tar")).unwrap();
723 let entries = archive.entries().collect::<Vec<_>>();
724
725 assert_archive_with_dir_content(&entries);
726 }
727
728 #[test]
729 fn test_ustar_archive_with_dir_entries() {
730 let archive =
733 TarArchiveRef::new(include_bytes!("../tests/mac_tar_ustar_with_dir.tar")).unwrap();
734 let entries = archive.entries().collect::<Vec<_>>();
735
736 assert_archive_with_dir_content(&entries);
737 }
738
739 #[test]
740 fn test_data_fills_entire_block() {
741 let mut data = [0_u8; 4 * BLOCKSIZE];
743
744 {
746 data[BLOCKSIZE..BLOCKSIZE * 2].fill(0xff);
747 }
748
749 {
751 let hdr = unsafe { data.as_mut_ptr().cast::<PosixHeader>().as_mut().unwrap() };
753 let blocksize_octal = "1000\0\0\0\0\0\0\0\0" ;
754 let blocksize_octal_bytes: [u8; 12] = {
755 let mut val = [0; 12];
756 val.copy_from_slice(blocksize_octal.as_bytes());
757 val
758 };
759 hdr.size = TarFormatOctal::new(blocksize_octal_bytes);
760 write_checksum(hdr);
761 }
762 let archive = TarArchiveRef::new(data.as_slice()).unwrap();
763 let entries = archive.entries().collect::<Vec<_>>();
764 assert_eq!(entries.len(), 1);
765 assert!(entries[0].data.iter().all(|&v| v == 0xff));
766 }
767
768 #[test]
769 fn test_constructor_rejects_invalid_header_checksum() {
770 let mut data = include_bytes!("../tests/gnu_tar_default.tar").to_vec();
771 data[0] ^= 0xff;
772
773 assert_eq!(
774 TarArchiveRef::new(data.as_slice()),
775 Err(CorruptDataError::InvalidChecksum { block_index: 0 })
776 );
777 }
778
779 #[test]
780 fn test_constructor_rejects_invalid_type_flag() {
781 let mut data = include_bytes!("../tests/gnu_tar_default.tar").to_vec();
782 data[156] = b'?';
783 write_first_header_checksum(&mut data);
784
785 assert_eq!(
786 TarArchiveRef::new(data.as_slice()),
787 Err(CorruptDataError::InvalidTypeFlag { block_index: 0 })
788 );
789 }
790
791 #[test]
792 fn test_constructor_rejects_invalid_payload_size() {
793 let mut data = include_bytes!("../tests/gnu_tar_default.tar").to_vec();
794 data[124] = 0xff;
795 write_first_header_checksum(&mut data);
796
797 assert_eq!(
798 TarArchiveRef::new(data.as_slice()),
799 Err(CorruptDataError::InvalidPayloadSize { block_index: 0 })
800 );
801 }
802
803 #[test]
804 fn test_constructor_rejects_payload_beyond_archive() {
805 let mut data = include_bytes!("../tests/gnu_tar_default.tar").to_vec();
806 data[124..136].copy_from_slice(b"77777777777\0");
807 write_first_header_checksum(&mut data);
808
809 assert_eq!(
810 TarArchiveRef::new(data.as_slice()),
811 Err(CorruptDataError::PayloadExtendsBeyondArchive { block_index: 0 })
812 );
813 }
814
815 #[test]
816 fn test_constructor_rejects_missing_end_marker() {
817 let mut data = [0; BLOCKSIZE * MIN_BLOCK_COUNT];
818 data[BLOCKSIZE] = 1;
819
820 assert_eq!(
821 TarArchiveRef::new(&data),
822 Err(CorruptDataError::MissingTerminator)
823 );
824 }
825
826 #[cfg(feature = "alloc")]
828 #[test]
829 fn test_archive_entries_alloc() {
830 let data = include_bytes!("../tests/gnu_tar_default.tar")
831 .to_vec()
832 .into_boxed_slice();
833 let archive = TarArchive::new(data.clone()).unwrap();
834 let entries = archive.entries().collect::<Vec<_>>();
835 assert_archive_content(&entries);
836
837 assert_eq!(data, archive.into());
839 }
840
841 fn assert_entry_content(entry: &ArchiveEntry, filename: &str, size: usize) {
843 assert_eq!(entry.filename().as_str(), Ok(filename));
844 assert_eq!(entry.size(), size);
845 assert_eq!(entry.data().len(), size);
846 }
847
848 fn write_checksum(hdr: &mut PosixHeader) {
849 let checksum = format!("{:06o}\0 ", hdr.computed_checksum());
850 let mut checksum_bytes = [0; 8];
851 checksum_bytes.copy_from_slice(checksum.as_bytes());
852 hdr.cksum = TarFormatOctal::new(checksum_bytes);
853 }
854
855 fn write_first_header_checksum(data: &mut [u8]) {
856 let hdr = unsafe { data.as_mut_ptr().cast::<PosixHeader>().as_mut().unwrap() };
858 write_checksum(hdr);
859 }
860
861 fn assert_archive_content(entries: &[ArchiveEntry]) {
865 use crate::ModeFlags;
866 let permissions = ModeFlags::OwnerRead
867 | ModeFlags::OwnerWrite
868 | ModeFlags::OwnerExec
869 | ModeFlags::GroupRead
870 | ModeFlags::GroupWrite
871 | ModeFlags::GroupExec
872 | ModeFlags::OthersRead
873 | ModeFlags::OthersWrite
874 | ModeFlags::OthersExec;
875 let rw_rw_r__ = ModeFlags::OwnerRead
876 | ModeFlags::OwnerWrite
877 | ModeFlags::GroupRead
878 | ModeFlags::GroupWrite
879 | ModeFlags::OthersRead;
880 #[allow(non_snake_case)]
882 let rw_r__r__ = ModeFlags::OwnerRead
883 | ModeFlags::OwnerWrite
884 | ModeFlags::GroupRead
885 | ModeFlags::OthersRead;
886
887 assert_eq!(entries.len(), 3);
888
889 assert_entry_content(&entries[0], "bye_world_513b.txt", 513);
890 assert_eq!(
891 entries[0].data_as_str().expect("Should be valid UTF-8"),
892 include_str!("../tests/bye_world_513b.txt").replace("\r\n", "\n")
894 );
895 assert_eq!(
896 entries[0]
897 .posix_header()
898 .mode
899 .to_flags()
900 .unwrap()
901 .intersection(permissions),
902 rw_rw_r__
903 );
904
905 assert_entry_content(&entries[1], "hello_world_513b.txt", 513);
908 assert_eq!(
909 entries[1].data_as_str().expect("Should be valid UTF-8"),
910 include_str!("../tests/hello_world_513b.txt").replace("\r\n", "\n")
912 );
913 assert_eq!(
914 entries[1]
915 .posix_header()
916 .mode
917 .to_flags()
918 .unwrap()
919 .intersection(permissions),
920 rw_rw_r__
921 );
922
923 assert_entry_content(&entries[2], "hello_world.txt", 12);
924 assert_eq!(
925 entries[2].data_as_str().expect("Should be valid UTF-8"),
926 "Hello World\n",
927 "file content must match"
928 );
929 assert_eq!(
930 entries[2]
931 .posix_header()
932 .mode
933 .to_flags()
934 .unwrap()
935 .intersection(permissions),
936 rw_r__r__
937 );
938 }
939
940 fn assert_archive_with_dir_content(entries: &[ArchiveEntry]) {
944 assert_eq!(entries.len(), 3);
945
946 assert_entry_content(&entries[0], "tests/hello_world.txt", 12);
947 assert_eq!(
948 entries[0].data_as_str().expect("Should be valid UTF-8"),
949 "Hello World\n",
950 "file content must match"
951 );
952
953 assert_entry_content(&entries[1], "tests/bye_world_513b.txt", 513);
956 assert_eq!(
957 entries[1].data_as_str().expect("Should be valid UTF-8"),
958 include_str!("../tests/bye_world_513b.txt").replace("\r\n", "\n")
960 );
961
962 assert_entry_content(&entries[2], "tests/hello_world_513b.txt", 513);
963 assert_eq!(
964 entries[2].data_as_str().expect("Should be valid UTF-8"),
965 include_str!("../tests/hello_world_513b.txt").replace("\r\n", "\n")
967 );
968 }
969}