tar_framing/lib.rs
1//! Low level framing of tar streams.
2//!
3//! This crate provides two APIs:
4//!
5//! - [`stream`] is a low-level, lossless per-block framing API.
6//! - [`logical`] is a medium-level, assembled member reader API.
7//!
8//! [`stream`] provides the basic static machine enforcement for a tar
9//! stream, including ensuring that any given stream is either strictly
10//! pax *or* GNU and not a mix of the two. [`logical`] is layered on top
11//! of [`stream`] and provides APIs for accessing the "effective" metadata
12//! for each assembled member.
13//!
14//! This crate tries to faithfully extract pax or GNU entries without mixing the
15//! two. See the sections below for compatibility notes.
16//!
17//! ## pax compatibility
18//!
19//! When decoding pax-formatted tar streams, tar-framing attempts to conform to
20//! pax as specified in [POSIX.1-2024], i.e. "issue 8" of the POSIX specification.
21//! See the [pax specification] for full details.
22//!
23//! However, there are a few small deviations from a pedantic reading of [POSIX.1-2024]
24//! that are worth noting:
25//!
26//! - tar-framing permits a `ctime` pax record, despite not being specified in [POSIX.1-2024].
27//! The ctime record was removed from pax in [POSIX.1-2004] (which is itself a minor edit
28//! of POSIX.1-2001). However, many real-world pax archives still contain it, and its
29//! presence does not compromise or introduce ambiguity during framing.
30//!
31//! - tar-framing permits implementation-defined vendor pax records to contain arbitrary
32//! bytes, including invalid UTF-8. POSIX requires extended-header values to default
33//! to UTF-8, but real-world writers including star, GNU tar, and libarchive use raw
34//! `SCHILY.xattr.*` values. Standard and reserved pax record values remain strict UTF-8.
35//!
36//! - tar-framing rejects directory entries (typeflag `'5'`) that present a nonzero size
37//! in their ustar header or pax `size` record. pax says that this size should be treated
38//! as a filesystem allocation hint rather than a physical size, but real-world parsers vary
39//! widely in how they handle it (some ignore it, others skip over that number of bytes, etc.).
40//!
41//! - tar-framing does not change a member type because of a path suffix. For example, it preserves
42//! a trailing slash in an effective member path. Before ustar, tar used a trailing slash to
43//! identify a directory. pax does not require an implementation to use this behavior. Therefore,
44//! a path suffix can conflict with the header type. The extraction layer in tar-codec rejects
45//! this conflict.
46//!
47//! - tar-framing rejects negative timestamps as well as timestamps that would exceed the
48//! precision of a `u64`. pax allows both of these, although it notes that portable timestamps
49//! cannot be negative and that tools may reject such timestamps.
50//!
51//! - tar-framing silently removes fractional components from parsed timestamps. Timestamps
52//! are truncated to second precision.
53//!
54//! - tar-framing rejects typeflags that are not explicitly defined in pax. pax says to handle
55//! these as regular files (i.e. assuming their size is a physical size), but this has marginal
56//! benefit in practice.
57//!
58//! - tar-framing rejects `hdrcharset` pax records that aren't UTF-8 or `BINARY`. pax says
59//! that "additional names may be agreed between the originator and the recipient," but
60//! we are the recipient and we don't accept any other `hdrcharset` names.
61//!
62//! ## GNU compatibility
63//!
64//! When decoding GNU-formatted tar streams, tar-framing attempts to follow the
65//! ["Basic Tar Format"] in the GNU docs. Specifically, tar-framing attempts
66//! to follow the rules for the "old GNU" format, i.e. GNU tar's non-pax format.
67//!
68//! tar-framing intentionally only supports a subset of the GNU tar format:
69//!
70//! - The GNU "longname" and "longlink" (`'L'` and `'K'`) typeflags are supported,
71//! with similar path-precedence semantics as their pax record equivalents.
72//!
73//! - Other GNU-specific typeflags are **not** supported whatsoever, and produce
74//! a framing error. This includes sparse files (`'S'`) and multivolume headers
75//! (`'M'`).
76//!
77//! - tar-framing accepts the GNU-specific "base-256" encoding for numbers, but rejects
78//! negative encodings as well as any value that would exceed the precision of a `u64`.
79//! tar-framing also allows "base-256" encodings where the numeric value _would_ fit
80//! into an octal encoding in the alloted buffer/byte span; GNU technically says that
81//! this is reserved for future use.
82//!
83//! ## General compatibility
84//!
85//! Because pax and GNU both use ustar as their baseline, any compatibility aspect of pax
86//! that is derived from ustar also applies during GNU tar decoding.
87//!
88//! tar-framing accepts wholly NUL `mode`, `uid`, `gid`, and `mtime` fields by default for
89//! compatibility with real-world writers in both families. These fields are represented as
90//! missing rather than assigned a value. This can be disabled with
91//! [`StreamPolicy::allow_all_nul_numeric_fields`].
92//!
93//! By default, tar-framing follows a strict reading of pax and requires the `uname` and `gname`
94//! header fields to be NUL terminated (the `magic` field is also required to be NUL terminated, but
95//! is so by definition). However, some encoders will incorrectly pack a 32-byte value into
96//! these fields, rather than honoring the 31-byte + NUL terminator limit.
97//! CPython's `tarfile` in particular does this.
98//! This can be disabled with [`StreamPolicy::allow_header_uname_and_gname_without_terminator`].
99//!
100//! Separately, higher-level crates (like tar-codec) may choose to apply additional
101//! restrictions when processing logical archive members. For example, a consumer
102//! of tar-framing may choose to reject vendor-specific pax records, or member names
103//! that contain forbidden characters, or any other additional restriction.
104//!
105//! [POSIX.1-2024]: https://pubs.opengroup.org/onlinepubs/9799919799/
106//! [pax specification]: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/pax.html
107//! [POSIX.1-2004]: https://pubs.opengroup.org/onlinepubs/009695399/toc.htm
108//! ["Basic Tar Format"]: https://www.gnu.org/software/tar/manual/html_node/Standard.html
109
110use std::fmt;
111
112mod error;
113pub mod header;
114pub mod logical;
115mod pax;
116pub mod stream;
117#[cfg(test)]
118mod test_support;
119pub mod write;
120
121pub use error::{FrameError, FrameErrorInner};
122pub use pax::{
123 HdrCharset, PaxError, PaxExtension, PaxKeyword, PaxRecord, PaxState, PaxString, PaxValue,
124};
125pub use stream::StreamPolicy;
126
127/// The size of a logical tar record.
128pub const BLOCK_SIZE: usize = 512;
129
130/// The default maximum size in bytes of one local or global pax extension.
131///
132/// This is 256 KiB.
133pub const DEFAULT_MAX_PAX_EXTENSION_SIZE: u64 = 256 * 1024;
134
135/// The default maximum total payload size of all global pax extensions.
136///
137/// This limit applies to the full archive. It does not reset after an ordinary
138/// member.
139///
140/// This is 1 MiB.
141pub const DEFAULT_MAX_GLOBAL_PAX_EXTENSIONS_SIZE: u64 = 4 * DEFAULT_MAX_PAX_EXTENSION_SIZE;
142
143/// The default maximum size in bytes of one GNU metadata extension.
144///
145/// This is 128 KiB.
146pub const DEFAULT_MAX_GNU_EXTENSION_SIZE: u64 = 128 * 1024;
147
148/// A single tar block.
149pub type Block = [u8; BLOCK_SIZE];
150
151/// An automatically detected, mutually exclusive tar archive family.
152#[derive(Clone, Copy, Debug, Eq, PartialEq)]
153pub enum ArchiveFormat {
154 /// pax ustar headers with optional pax extended headers.
155 Pax,
156 /// Old GNU tar headers with optional `L` and `K` extension entries.
157 Gnu,
158}
159
160impl fmt::Display for ArchiveFormat {
161 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
162 match self {
163 Self::Pax => formatter.write_str("pax"),
164 Self::Gnu => formatter.write_str("GNU"),
165 }
166 }
167}
168
169/// The scope of a pax extended header.
170#[derive(Clone, Copy, Debug, Eq, PartialEq)]
171pub enum PaxKind {
172 /// A typeflag `x` header applying to the next ordinary member.
173 Local,
174 /// A typeflag `g` header updating persistent global values.
175 Global,
176}
177
178/// The supported GNU metadata extension kinds.
179#[derive(Clone, Copy, Debug, Eq, PartialEq)]
180pub enum GnuKind {
181 /// A typeflag `L` extension giving a long name for the next member.
182 LongName,
183 /// A typeflag `K` extension giving a long link name for the next member.
184 LongLink,
185}
186
187/// A supported ordinary ustar member type.
188///
189/// These are shared across both pax and GNU tar streams.
190#[derive(Clone, Copy, Debug, Eq, PartialEq)]
191pub enum UstarKind {
192 /// A regular file (`'0'` or NUL).
193 Regular,
194 /// A hard link (`'1'`).
195 HardLink,
196 /// A symbolic link (`'2'`).
197 SymbolicLink,
198 /// A character device (`'3'`).
199 CharacterDevice,
200 /// A block device (`'4'`).
201 BlockDevice,
202 /// A directory (`'5'`).
203 Directory,
204 /// A FIFO (`'6'`).
205 Fifo,
206 /// A contiguous file (`'7'`).
207 Contiguous,
208}