Skip to main content

tar_no_std/
header.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//! TAR header definition taken from <https://www.gnu.org/software/tar/manual/html_node/Standard.html>.
25//! A Tar-archive is a collection of 512-byte sized blocks. Unfortunately there are several
26//! TAR-like archive specifications. An Overview can be found here:
27//! <https://www.gnu.org/software/tar/manual/html_node/Formats.html#Formats>
28//!
29//! This library focuses on extracting files from the GNU Tar format.
30
31#![allow(non_upper_case_globals)]
32
33use crate::{BLOCKSIZE, NAME_LEN, PREFIX_LEN, TarFormatDecimal, TarFormatOctal, TarFormatString};
34use core::error::Error;
35use core::fmt::{Debug, Display, Formatter};
36use core::num::ParseIntError;
37
38const CKSUM_OFFSET: usize = 148;
39const CKSUM_LEN: usize = 8;
40
41/// Errors that may happen when parsing the [`ModeFlags`].
42#[derive(Debug)]
43pub enum ModeError {
44    ParseInt(ParseIntError),
45    IllegalMode,
46}
47
48impl Display for ModeError {
49    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
50        Debug::fmt(self, f)
51    }
52}
53
54impl Error for ModeError {
55    fn source(&self) -> Option<&(dyn Error + 'static)> {
56        match self {
57            Self::ParseInt(e) => Some(e),
58            Self::IllegalMode => None,
59        }
60    }
61}
62
63/// Wrapper around the UNIX file permissions given in octal ASCII.
64#[derive(Copy, Clone, PartialEq, Eq)]
65#[repr(transparent)]
66pub struct Mode(TarFormatOctal<8>);
67
68impl Mode {
69    /// Parses the [`ModeFlags`] from the mode string.
70    ///
71    /// # Errors
72    /// Returns [`ModeError`] for invalid values.
73    pub fn to_flags(self) -> Result<ModeFlags, ModeError> {
74        let bits = self.0.as_number::<u64>().map_err(ModeError::ParseInt)?;
75        ModeFlags::from_bits(bits & 0o7777).ok_or(ModeError::IllegalMode)
76    }
77}
78
79impl Debug for Mode {
80    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
81        Debug::fmt(&self.to_flags(), f)
82    }
83}
84
85#[derive(Copy, Clone, Debug, PartialOrd, PartialEq, Eq)]
86pub struct InvalidTypeFlagError(u8);
87
88impl Display for InvalidTypeFlagError {
89    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
90        f.write_fmt(format_args!("{:x} is not a valid TypeFlag", self.0))
91    }
92}
93
94impl core::error::Error for InvalidTypeFlagError {}
95
96#[derive(Copy, Clone, PartialOrd, PartialEq, Eq)]
97pub struct TypeFlagRaw(u8);
98
99impl TypeFlagRaw {
100    /// Tries to parse the underlying value as [`TypeFlag`]. This fails if the
101    /// Tar file is corrupt and the type is invalid.
102    ///
103    /// # Errors
104    /// Returns [`InvalidTypeFlagError`] for invalid values.
105    pub fn try_to_type_flag(self) -> Result<TypeFlag, InvalidTypeFlagError> {
106        TypeFlag::try_from(self)
107    }
108}
109
110impl Debug for TypeFlagRaw {
111    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
112        Debug::fmt(&self.try_to_type_flag(), f)
113    }
114}
115
116/// Describes the kind of payload, that follows after a
117/// [`PosixHeader`]. The properties of this payload are
118/// described inside the header.
119#[derive(Debug, Copy, Clone, PartialEq, Eq)]
120#[repr(u8)]
121#[allow(unused)]
122pub enum TypeFlag {
123    /// Represents a regular file. In order to be compatible with older versions of tar, a typeflag
124    /// value of AREGTYPE should be silently recognized as a regular file. New archives should be
125    /// created using REGTYPE. Also, for backward compatibility, tar treats a regular file whose
126    /// name ends with a slash as a directory.
127    REGTYPE = b'0',
128    /// Represents a regular file. In order to be compatible with older versions of tar, a typeflag
129    /// value of AREGTYPE should be silently recognized as a regular file. New archives should be
130    /// created using REGTYPE. Also, for backward compatibility, tar treats a regular file whose
131    /// name ends with a slash as a directory.
132    AREGTYPE = b'\0',
133    /// This flag represents a file linked to another file, of any type, previously archived. Such
134    /// files are identified in Unix by each file having the same device and inode number. The
135    /// linked-to name is specified in the linkname field with a trailing null.
136    LINK = b'1',
137    /// This represents a symbolic link to another file. The linked-to name is specified in the
138    /// linkname field with a trailing null.
139    SYMTYPE = b'2',
140    /// Represents character special files and block special files respectively. In this case the
141    /// devmajor and devminor fields will contain the major and minor device numbers respectively.
142    /// Operating systems may map the device specifications to their own local specification, or
143    /// may ignore the entry.
144    CHRTYPE = b'3',
145    /// Represents character special files and block special files respectively. In this case the
146    /// devmajor and devminor fields will contain the major and minor device numbers respectively.
147    /// Operating systems may map the device specifications to their own local specification, or
148    /// may ignore the entry.
149    BLKTYPE = b'4',
150    /// This flag specifies a directory or sub-directory. The directory name in the name field
151    /// should end with a slash. On systems where disk allocation is performed on a directory
152    /// basis, the size field will contain the maximum number of bytes (which may be rounded to
153    /// the nearest disk block allocation unit) which the directory may hold. A size field of zero
154    /// indicates no such limiting. Systems which do not support limiting in this manner should
155    /// ignore the size field.
156    DIRTYPE = b'5',
157    /// This specifies a FIFO special file. Note that the archiving of a FIFO file archives the
158    /// existence of this file and not its contents.
159    FIFOTYPE = b'6',
160    /// This specifies a contiguous file, which is the same as a normal file except that, in
161    /// operating systems which support it, all its space is allocated contiguously on the disk.
162    /// Operating systems which do not allow contiguous allocation should silently treat this type
163    /// as a normal file.
164    CONTTYPE = b'7',
165    /// POSIX PAX extended header applying to the next entry in the archive.
166    XHDTYPE = b'x',
167    /// POSIX PAX global extended header applying to following entries.
168    XGLTYPE = b'g',
169}
170
171impl TypeFlag {
172    /// Whether we have a regular file.
173    #[must_use]
174    pub fn is_regular_file(self) -> bool {
175        // Equivalent. See spec.
176        self == Self::AREGTYPE || self == Self::REGTYPE
177    }
178
179    /// Whether this entry has payload blocks described by the size field.
180    pub(crate) fn has_payload(self) -> bool {
181        self.is_regular_file() || matches!(self, Self::CONTTYPE | Self::XHDTYPE | Self::XGLTYPE)
182    }
183}
184
185impl TryFrom<TypeFlagRaw> for TypeFlag {
186    type Error = InvalidTypeFlagError;
187
188    fn try_from(value: TypeFlagRaw) -> Result<Self, Self::Error> {
189        match value.0 {
190            b'0' => Ok(Self::REGTYPE),
191            b'\0' => Ok(Self::AREGTYPE),
192            b'1' => Ok(Self::LINK),
193            b'2' => Ok(Self::SYMTYPE),
194            b'3' => Ok(Self::CHRTYPE),
195            b'4' => Ok(Self::BLKTYPE),
196            b'5' => Ok(Self::DIRTYPE),
197            b'6' => Ok(Self::FIFOTYPE),
198            b'7' => Ok(Self::CONTTYPE),
199            b'x' => Ok(Self::XHDTYPE),
200            b'g' => Ok(Self::XGLTYPE),
201            e => Err(InvalidTypeFlagError(e)),
202        }
203    }
204}
205
206bitflags::bitflags! {
207    /// UNIX file permissions in octal format.
208    #[repr(transparent)]
209    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
210    pub struct ModeFlags: u64 {
211        /// Set UID on execution.
212        const SetUID = 0o4000;
213        /// Set GID on execution.
214        const SetGID = 0o2000;
215        /// Reserved.
216        const TSVTX = 0o1000;
217        /// Owner read.
218        const OwnerRead = 0o400;
219        /// Owner write.
220        const OwnerWrite = 0o200;
221        /// Owner execute.
222        const OwnerExec = 0o100;
223        /// Group read.
224        const GroupRead = 0o040;
225        /// Group write.
226        const GroupWrite = 0o020;
227        /// Group execute.
228        const GroupExec = 0o010;
229        /// Others read.
230        const OthersRead = 0o004;
231        /// Others read.
232        const OthersWrite = 0o002;
233        /// Others execute.
234        const OthersExec = 0o001;
235    }
236}
237
238/// Header of the TAR format as specified by POSIX (POSIX 1003.1-1990).
239///
240/// "New" GNU Tar versions use this archive format by default.
241/// (<https://www.gnu.org/software/tar/manual/html_node/Formats.html#Formats>).
242///
243/// Each file is started by such a header, that describes the size and
244/// the file name. After that, the file content stands in chunks of 512 bytes.
245/// The number of bytes can be derived from the file size.
246///
247/// This is also mostly compatible with the "Ustar"-header and the "GNU format".
248/// Because this library mainly targets the filename, the data, and basic
249/// metadata, we don't need advanced checks for specific extensions.
250#[derive(Debug, Copy, Clone, PartialEq, Eq)]
251#[repr(C, packed)]
252pub struct PosixHeader {
253    pub name: TarFormatString<NAME_LEN>,
254    pub mode: Mode,
255    pub uid: TarFormatOctal<8>,
256    pub gid: TarFormatOctal<8>,
257    // confusing; size is stored as ASCII string
258    pub size: TarFormatOctal<12>,
259    pub mtime: TarFormatDecimal<12>,
260    pub cksum: TarFormatOctal<8>,
261    pub typeflag: TypeFlagRaw,
262    /// Name. There is always a null byte, therefore
263    /// the max len is 99.
264    pub linkname: TarFormatString<NAME_LEN>,
265    pub magic: TarFormatString<6>,
266    pub version: TarFormatString<2>,
267    /// Username. There is always a null byte, therefore
268    /// the max len is N-1.
269    pub uname: TarFormatString<32>,
270    /// Groupname. There is always a null byte, therefore
271    /// the max len is N-1.
272    pub gname: TarFormatString<32>,
273    pub dev_major: TarFormatOctal<8>,
274    pub dev_minor: TarFormatOctal<8>,
275    pub prefix: TarFormatString<PREFIX_LEN>,
276    // padding => to BLOCKSIZE bytes
277    pub _pad: [u8; 12],
278}
279
280impl PosixHeader {
281    /// Returns the number of blocks that are required to read the whole file
282    /// content. Returns an error, if the file size can't be parsed from the
283    /// header.
284    ///
285    /// # Errors
286    /// Returns a [`ParseIntError`] error if the size can't be parsed.
287    pub fn payload_block_count(&self) -> Result<usize, ParseIntError> {
288        let parsed_size = self.size.as_number::<usize>()?;
289        Ok(parsed_size.div_ceil(BLOCKSIZE))
290    }
291
292    pub(crate) fn has_valid_checksum(&self) -> bool {
293        self.cksum
294            .as_number::<u64>()
295            .is_ok_and(|cksum| cksum == self.computed_checksum())
296    }
297
298    pub(crate) fn computed_checksum(&self) -> u64 {
299        let ptr = core::ptr::addr_of!(*self);
300        let ptr = ptr.cast::<u8>();
301
302        // SAFETY: we know that the data is at least BLOCKSIZE bytes long.
303        let self_bytes = unsafe { core::slice::from_raw_parts(ptr, BLOCKSIZE) };
304
305        self_bytes
306            .iter()
307            .enumerate()
308            .map(|(i, &byte)| {
309                if (CKSUM_OFFSET..CKSUM_OFFSET + CKSUM_LEN).contains(&i) {
310                    u64::from(b' ')
311                } else {
312                    u64::from(byte)
313                }
314            })
315            .sum()
316    }
317
318    /// A Tar archive is terminated, if an end-of-archive entry, which consists
319    /// of two 512 blocks of zero bytes, is found.
320    #[must_use]
321    pub fn is_zero_block(&self) -> bool {
322        let ptr = core::ptr::addr_of!(*self);
323        let ptr = ptr.cast::<u8>();
324
325        // SAFETY: we know that the data is at least BLOCKSIZE bytes long.
326        let self_bytes = unsafe { core::slice::from_raw_parts(ptr, BLOCKSIZE) };
327        self_bytes.iter().filter(|x| **x == 0).count() == BLOCKSIZE
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use crate::BLOCKSIZE;
334    use crate::header::{PosixHeader, TypeFlag};
335    use std::mem::size_of;
336
337    /// Returns the [`PosixHeader`] at the beginning of the Tar archive.
338    fn bytes_to_archive(tar_archive_data: &[u8]) -> &PosixHeader {
339        // SAFETY: In the test we know the length is sufficient.
340        unsafe { (tar_archive_data.as_ptr().cast::<PosixHeader>()).as_ref() }.unwrap()
341    }
342
343    #[test]
344    fn test_display_header() {
345        let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_default.tar"));
346        assert_eq!(archive.name.as_str(), Ok("bye_world_513b.txt"));
347        println!("{archive:#?}'");
348    }
349
350    #[test]
351    fn test_payload_block_count() {
352        // first file is "bye_world_513b.txt" => we expect two data blocks
353        let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_default.tar"));
354        assert_eq!(archive.payload_block_count(), Ok(2));
355    }
356
357    #[test]
358    fn test_show_tar_header_magics() {
359        let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_default.tar"));
360        println!(
361            "default: magic='{:?}', version='{:?}'",
362            archive.magic, archive.version
363        );
364        let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_gnu.tar"));
365        println!(
366            "gnu: magic='{:?}', version='{:?}'",
367            archive.magic, archive.version
368        );
369        let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_oldgnu.tar"));
370        println!(
371            "oldgnu: magic='{:?}', version='{:?}'",
372            archive.magic, archive.version
373        );
374        let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_pax.tar"));
375        println!(
376            "pax: magic='{:?}', version='{:?}'",
377            archive.magic, archive.version
378        );
379        let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_posix.tar"));
380        println!(
381            "posix: magic='{:?}', version='{:?}'",
382            archive.magic, archive.version
383        );
384        let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_ustar.tar"));
385        println!(
386            "ustar: magic='{:?}', version='{:?}'",
387            archive.magic, archive.version
388        );
389        let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_v7.tar"));
390        println!(
391            "v7: magic='{:?}', version='{:?}'",
392            archive.magic, archive.version
393        );
394    }
395
396    #[test]
397    fn test_parse_tar_header_filename() {
398        let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_default.tar"));
399        assert_eq!(
400            archive.typeflag.try_to_type_flag(),
401            Ok(TypeFlag::REGTYPE),
402            "the first entry is a regular file!"
403        );
404        assert_eq!(archive.name.as_str(), Ok("bye_world_513b.txt"));
405
406        let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_gnu.tar"));
407        assert_eq!(
408            archive.typeflag.try_to_type_flag(),
409            Ok(TypeFlag::REGTYPE),
410            "the first entry is a regular file!"
411        );
412        assert_eq!(archive.name.as_str(), Ok("bye_world_513b.txt"));
413
414        let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_oldgnu.tar"));
415        assert_eq!(
416            archive.typeflag.try_to_type_flag(),
417            Ok(TypeFlag::REGTYPE),
418            "the first entry is a regular file!"
419        );
420        assert_eq!(archive.name.as_str(), Ok("bye_world_513b.txt"));
421
422        let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_ustar.tar"));
423        assert_eq!(
424            archive.typeflag.try_to_type_flag(),
425            Ok(TypeFlag::REGTYPE),
426            "the first entry is a regular file!"
427        );
428        assert_eq!(archive.name.as_str(), Ok("bye_world_513b.txt"));
429
430        let archive = bytes_to_archive(include_bytes!("../tests/gnu_tar_v7.tar"));
431        // ARegType: legacy
432        assert_eq!(
433            archive.typeflag.try_to_type_flag(),
434            Ok(TypeFlag::AREGTYPE),
435            "the first entry is a regular file!"
436        );
437        assert_eq!(archive.name.as_str(), Ok("bye_world_513b.txt"));
438    }
439
440    #[test]
441    fn test_parse_pax_headers() {
442        for archive_data in [
443            include_bytes!("../tests/gnu_tar_pax.tar") as &[u8],
444            include_bytes!("../tests/gnu_tar_posix.tar") as &[u8],
445        ] {
446            let archive = bytes_to_archive(archive_data);
447            assert_eq!(archive.typeflag.try_to_type_flag(), Ok(TypeFlag::XHDTYPE));
448        }
449    }
450
451    #[test]
452    fn test_size() {
453        assert_eq!(BLOCKSIZE, size_of::<PosixHeader>());
454    }
455}