Skip to main content

tar_no_std/
archive.rs

1/*
2MIT License
3
4Copyright (c) 2025 Philipp Schuster
5
6Permission is hereby granted, free of charge, to any person obtaining a copy
7of this software and associated documentation files (the "Software"), to deal
8in the Software without restriction, including without limitation the rights
9to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10copies of the Software, and to permit persons to whom the Software is
11furnished to do so, subject to the following conditions:
12
13The above copyright notice and this permission notice shall be included in all
14copies or substantial portions of the Software.
15
16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22SOFTWARE.
23*/
24//! Module for [`TarArchiveRef`]. If the `alloc`-feature is enabled, this crate
25//! also exports `TarArchive`, which owns data on the heap.
26
27use 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
36/// Minimum amount of blocks that an archive must have to be considered sane.
37/// - one header block
38/// - two terminating zero blocks
39pub const MIN_BLOCK_COUNT: usize = 3;
40
41/// Describes an entry in an archive.
42/// Currently only supports files but no directories.
43pub 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    /// Filename of the entry with a maximum of 100 characters (including the
66    /// terminating NULL-byte).
67    #[must_use]
68    pub const fn filename(&self) -> TarFormatString<{ POSIX_1003_MAX_FILENAME_LEN }> {
69        self.filename
70    }
71
72    /// Data of the file.
73    #[must_use]
74    pub const fn data(&self) -> &'a [u8] {
75        self.data
76    }
77
78    /// Data of the file as string slice, if data is valid UTF-8.
79    ///
80    /// # Errors
81    /// Returns a [`Utf8Error`] error for invalid strings.
82    #[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    /// Filesize in bytes.
88    #[must_use]
89    pub const fn size(&self) -> usize {
90        self.size
91    }
92
93    /// Returns the [`PosixHeader`] for the entry.
94    #[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/// Describes why archive validation failed.
111#[derive(Copy, Clone, Debug, PartialEq, Eq)]
112pub enum CorruptDataError {
113    /// The archive contains no data.
114    EmptyArchive,
115    /// The archive length is not a multiple of the 512-byte block size.
116    InvalidBlockSize,
117    /// The archive is shorter than [`MIN_BLOCK_COUNT`] blocks.
118    TooShort {
119        /// Total number of bytes in the archive.
120        byte_count: usize,
121        /// Number of complete 512-byte blocks in the archive.
122        block_count: usize,
123    },
124    /// The header at `block_index` has an invalid checksum.
125    InvalidChecksum {
126        /// Index of the invalid header block.
127        block_index: usize,
128    },
129    /// The header at `block_index` has an unsupported type flag.
130    InvalidTypeFlag {
131        /// Index of the invalid header block.
132        block_index: usize,
133    },
134    /// The payload size in the header at `block_index` is invalid.
135    InvalidPayloadSize {
136        /// Index of the header with the invalid payload size.
137        block_index: usize,
138    },
139    /// A payload starting at `block_index` extends past the archive.
140    PayloadExtendsBeyondArchive {
141        /// Index of the header that describes the payload.
142        block_index: usize,
143    },
144    /// The archive does not end with two zero blocks.
145    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/// An owning, validated Tar archive.
184///
185/// Unlike [`TarArchiveRef`], this type takes ownership of the archive bytes.
186/// [`TarArchive::new`] validates the supplied data before constructing the
187/// archive.
188///
189/// This is only available with the `alloc` feature of this crate.
190#[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    /// Creates an owning wrapper around validated Tar archive bytes.
199    ///
200    /// The supplied data must have a valid block layout, contain at least the
201    /// minimum number of blocks, and end with two zero blocks. Each archive
202    /// header must have a valid checksum and type flag, and every payload size
203    /// must lead to a header or the terminating zero blocks within the archive.
204    ///
205    /// Validation checks the archive structure required for safe iteration. It
206    /// does not guarantee that every supported entry can be consumed without
207    /// further format-specific limitations; see [`ArchiveEntryIterator`].
208    ///
209    /// Returns an error, if the sanity checks report problems.
210    ///
211    /// # Errors
212    /// Returns [`CorruptDataError`] if validation fails.
213    pub fn new(data: Box<[u8]>) -> Result<Self, CorruptDataError> {
214        TarArchiveRef::validate(&data).map(|_| Self { data })
215    }
216
217    /// Iterates over the regular files in the Tar archive.
218    ///
219    /// See [`ArchiveEntryIterator`] for format support and limitations.
220    #[must_use]
221    pub fn entries(&self) -> ArchiveEntryIterator<'_> {
222        ArchiveEntryIterator::new(self.data.as_ref())
223    }
224
225    /// Iterates over the headers in the Tar archive.
226    ///
227    /// PAX extended headers are returned as normal [`PosixHeader`] values,
228    /// while their payload blocks are skipped before the next iteration.
229    #[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/// Wrapper type around bytes, which represents a Tar archive. To iterate the
251/// entries, use [`TarArchiveRef::entries`].
252#[derive(Clone, Debug, PartialEq, Eq)]
253pub struct TarArchiveRef<'a> {
254    data: &'a [u8],
255}
256
257#[allow(unused)]
258impl<'a> TarArchiveRef<'a> {
259    /// Creates a borrowed wrapper around validated Tar archive bytes.
260    ///
261    /// The supplied data must have a valid block layout, contain at least the
262    /// minimum number of blocks, and end with two zero blocks. Each archive
263    /// header must have a valid checksum and type flag, and every payload size
264    /// must lead to a header or the terminating zero blocks within the archive.
265    ///
266    /// Validation checks the archive structure required for safe iteration. It
267    /// does not guarantee that every supported entry can be consumed without
268    /// further format-specific limitations; see [`ArchiveEntryIterator`].
269    ///
270    /// # Errors
271    /// Returns [`CorruptDataError`] if validation fails.
272    pub fn new(data: &'a [u8]) -> Result<Self, CorruptDataError> {
273        Self::validate(data).map(|()| Self { data })
274    }
275
276    /// Validates the archive's overall block layout and header sequence.
277    ///
278    /// The archive must not be empty, must have a length that is a multiple of
279    /// [`BLOCKSIZE`], and must contain at least [`MIN_BLOCK_COUNT`] blocks.
280    /// Header-specific validation is delegated to [`Self::validate_headers`].
281    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    /// Validates the archive's header sequence and terminator.
299    ///
300    /// Rejects invalid checksums, type flags, and payload sizes, as well as a
301    /// missing double-zero terminator.
302    /*
303     * Do not use ArchiveHeaderIterator's Iterator implementation here. It
304     * assumes validated data, whereas validation must reject malformed headers
305     * and only accept an explicit double-zero terminator.
306     */
307    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    /// Iterates over the regular files in the Tar archive.
352    ///
353    /// See [`ArchiveEntryIterator`] for format support and limitations.
354    #[must_use]
355    pub fn entries(&self) -> ArchiveEntryIterator<'a> {
356        ArchiveEntryIterator::new(self.data)
357    }
358
359    /// Iterates over the headers in the Tar archive.
360    ///
361    /// PAX extended headers are returned as normal [`PosixHeader`] values,
362    /// while their payload blocks are skipped before the next iteration.
363    #[must_use]
364    pub fn headers(&self) -> ArchiveHeaderIterator<'a> {
365        ArchiveHeaderIterator::new(self.data)
366    }
367}
368
369/// Iterates over the headers of a validated Tar archive.
370///
371/// PAX extended headers are returned as normal [`PosixHeader`] values, while
372/// their payload blocks are skipped before the next iteration. Obtain this
373/// iterator with
374#[cfg_attr(feature = "alloc", doc = " [`TarArchive::headers`] or")]
375/// [`TarArchiveRef::headers`].
376#[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    /// Parse the memory at the given block as [`PosixHeader`].
394    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        // SAFETY: We asserted that the block is in bound and the memory is
404        // valid.
405        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    /// Returns the next header and advances past its payload blocks.
415    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        // Validation guarantees a double-zero terminator. The first zero block
425        // marks the end of the archive.
426        if hdr.is_zero_block() {
427            return None;
428        }
429
430        // Start at next block on next iteration.
431        self.next_hdr_block_index += 1;
432
433        // We only update the block index for types that have a payload.
434        // In directory entries, for example, the size field has other
435        // semantics. See spec.
436        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/// Iterator over the files of the archive.
452///
453/// Only regular files are yielded. Directories, links, PAX extended headers,
454/// and other recognized special types ([`crate::TypeFlag`]) are skipped.
455///
456/// This permits reading PAX archives that use extended records only for
457/// optional metadata, such as high-precision timestamps. PAX metadata is
458/// skipped rather than applied, so filenames and sizes must remain available
459/// in the regular file headers. Directory paths encoded in those names are
460/// preserved.
461#[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        // Ignore directory entries, i.e. yield only regular files. Works as
481        // filenames in tarballs are fully specified, e.g. dirA/dirB/file1
482        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            // Update properties.
494            (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        // POXIS_1003 long filename check
512        // https://docs.scinet.utoronto.ca/index.php/(POSIX_1003.1_USTAR)
513        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    /// The test here is that no panics occur.
588    #[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    /// The test here is that no panics occur.
600    #[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    /// Tests various weird (= invalid, corrupt) tarballs that are bundled
608    /// within this file. The tarball(s) originate from a fuzzing process from a
609    /// GitHub contributor [0].
610    ///
611    /// The outer archive is valid, while every nested fuzzer input must fail
612    /// checksum validation without panicking.
613    ///
614    /// [0] https://github.com/phip1611/tar-no-std/issues/12#issuecomment-2092632090
615    #[test]
616    fn test_weird_fuzzing_tarballs() {
617        /*std::env::set_var("RUST_LOG", "trace");
618        std::env::set_var("RUST_LOG_STYLE", "always");
619        env_logger::init();*/
620
621        let main_tarball =
622            TarArchiveRef::new(include_bytes!("../tests/weird_fuzzing_tarballs.tar"))
623                .expect("archive containing fuzzing inputs should pass validation");
624
625        // Every corpus entry corrupts a header checksum. Check the validation
626        // category without coupling this test to the exact header that is
627        // reached first.
628        let mut input_count = 0;
629        for fuzzing_input in main_tarball.entries() {
630            let result = TarArchiveRef::new(fuzzing_input.data());
631            assert!(
632                // TODO we should fix the checksum of at least some of these
633                // to exercise more code paths.
634                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    /// Tests to read the entries from existing archives in various Tar flavors.
644    #[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        // PAX metadata is ignored; these files also have usable regular
659        // headers.
660        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    /// Tests to read the entries from an existing tarball with a directory in it
678    #[test]
679    fn test_archive_with_long_dir_entries() {
680        // tarball created with:
681        //     $ cd tests; gtar --format=ustar -cf gnu_tar_ustar_long.tar 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234/ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ
682        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        // Maximum length of a directory and name when the directory itself is tar'd
688        assert_entry_content(
689            &entries[0],
690            "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678/ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ",
691            7,
692        );
693        // Maximum length of a directory and name when only the file is tar'd.
694        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        // tarball created with:
704        //     $ cd tests; gtar --format=ustar -cf gnu_tar_ustar_deep.tar 0123456789
705        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        // tarball created with:
720        //     $ gtar -cf tests/gnu_tar_default_with_dir.tar --exclude '*.tar' --exclude '012345678*' tests
721        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        // tarball created with:
731        //     $(osx) tar -cf tests/mac_tar_ustar_with_dir.tar --format=ustar --exclude '*.tar' --exclude '012345678*' tests
732        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        // header, data block, 2 zero blocks
742        let mut data = [0_u8; 4 * BLOCKSIZE];
743
744        // Fill payload: We have a full block
745        {
746            data[BLOCKSIZE..BLOCKSIZE * 2].fill(0xff);
747        }
748
749        // Write header
750        {
751            // SAFETY: We know that the header is at the beginning of the data.
752            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" /* BLOCKSIZE */;
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    /// Like [`test_archive_entries`] but with additional `alloc` functionality.
827    #[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        // Test that the archive can be transformed into owned heap data.
838        assert_eq!(data, archive.into());
839    }
840
841    /// Test that the entry's contents match the expected content.
842    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        // SAFETY: all callers provide at least one complete header block.
857        let hdr = unsafe { data.as_mut_ptr().cast::<PosixHeader>().as_mut().unwrap() };
858        write_checksum(hdr);
859    }
860
861    /// Tests that the parsed archive matches the expected order. The tarballs
862    /// the tests directory were created once by me with files in the order
863    /// specified in this test.
864    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        // Rust complains otherwise, but this is intentionally written this way.
881        #[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            // .replace: Ensure that the test also works on Windows
893            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        // Test that an entry that needs two 512 byte data blocks is read
906        // properly.
907        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            // .replace: Ensure that the test also works on Windows
911            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    /// Tests that the parsed archive matches the expected order and the filename includes
941    /// the directory name. The tarballs the tests directory were created once by me with files
942    /// in the order specified in this test.
943    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        // Test that an entry that needs two 512 byte data blocks is read
954        // properly.
955        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            // .replace: Ensure that the test also works on Windows
959            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            // .replace: Ensure that the test also works on Windows
966            include_str!("../tests/hello_world_513b.txt").replace("\r\n", "\n")
967        );
968    }
969}