Skip to main content

tar_framing/
stream.rs

1//! Lossless, block-oriented tar streaming.
2//!
3//! This API emits one frame for each accepted non-terminator physical
4//! tar block and preserves each source block verbatim.
5//!
6//! The following Mermaid diagram described the state machine:
7//!
8//! ```mermaid
9//! ---
10//! config:
11//!   layout: elk
12//! ---
13//!
14//! stateDiagram-v2
15//!   state "AwaitingHeader (unclassified)" as Unclassified
16//!   state SelectFormat <<choice>>
17//!
18//!   [*] --> Unclassified
19//!   Unclassified --> AwaitingSecondZero: first zero block
20//!   Unclassified --> SelectFormat: first nonzero header
21//!   SelectFormat --> PosixHeader: ustar identity
22//!   SelectFormat --> GnuHeader: GNU identity
23//!   SelectFormat --> Failed: unsupported identity
24//!
25//!   state "POSIX-pax selected" as Pax {
26//!     direction TB
27//!     state "AwaitingHeader" as PosixBoundary
28//!     state PosixHeader <<choice>>
29//!     state "ReadingMember" as PosixMemberData
30//!
31//!     PosixBoundary --> PosixHeader: next header
32//!     PosixHeader --> ReadingPax: x or g
33//!     PosixHeader --> PosixMemberData: member data
34//!     PosixHeader --> PosixBoundary: empty member
35//!     ReadingPax --> AwaitingUstarHeader: local x payload complete
36//!     ReadingPax --> PosixBoundary: global g payload complete
37//!     AwaitingUstarHeader --> PosixMemberData: member data
38//!     AwaitingUstarHeader --> PosixBoundary: empty member
39//!     PosixMemberData --> PosixBoundary: payload complete
40//!   }
41//!
42//!   state "GNU selected" as Gnu {
43//!     direction TB
44//!     state "AwaitingHeader" as GnuBoundary
45//!     state GnuHeader <<choice>>
46//!     state "ReadingMember" as GnuMemberData
47//!
48//!     GnuBoundary --> GnuHeader: next header
49//!     GnuHeader --> ReadingGnu: L or K data
50//!     GnuHeader --> AwaitingGnuMember: empty L or K
51//!     GnuHeader --> GnuMemberData: member data
52//!     GnuHeader --> GnuBoundary: empty member
53//!     ReadingGnu --> AwaitingGnuMember: metadata payload complete
54//!     AwaitingGnuMember --> ReadingGnu: another L or K data
55//!     AwaitingGnuMember --> AwaitingGnuMember: another empty L or K
56//!     AwaitingGnuMember --> GnuMemberData: member data
57//!     AwaitingGnuMember --> GnuBoundary: empty member
58//!     GnuMemberData --> GnuBoundary: payload complete
59//!   }
60//!
61//! PosixBoundary --> AwaitingSecondZero: first zero block
62//! GnuBoundary --> AwaitingSecondZero: first zero block
63//! AwaitingSecondZero --> Complete: second zero block
64//! AwaitingSecondZero --> Failed: nonzero block or EOF
65//! Complete --> [*]
66//! Failed --> [*]
67//!
68//! note right of Failed
69//!   Validation, ordering, and family-mismatch
70//!   errors also enter Failed; arrows omitted.
71//! end note
72//! ```
73
74use std::{
75    future::poll_fn,
76    ops::Range,
77    pin::Pin,
78    sync::Arc,
79    task::{Context, Poll},
80};
81
82use tokio::io::{AsyncRead, ReadBuf};
83
84use crate::{
85    ArchiveFormat, BLOCK_SIZE, Block, DEFAULT_MAX_GLOBAL_PAX_EXTENSIONS_SIZE,
86    DEFAULT_MAX_GNU_EXTENSION_SIZE, DEFAULT_MAX_PAX_EXTENSION_SIZE, FrameError, FrameErrorInner,
87    GnuKind, HdrCharset, PaxError, PaxKind, PaxRecord, PaxState, PaxValue, UstarKind,
88    header::{
89        CHECKSUM_RANGE, GID_RANGE, GNAME_RANGE, GNU_IDENTITY, IDENTITY_RANGE, MODE_RANGE,
90        MTIME_RANGE, NAME_RANGE, PREFIX_RANGE, SIZE_RANGE, TYPEFLAG_OFFSET, UID_RANGE, UNAME_RANGE,
91        USTAR_IDENTITY, checksum, is_all_nul, parse_number, parse_octal,
92    },
93    pax::{GlobalPaxRecords, PaxRecords, SharedPaxRecords},
94};
95
96type PositionedBlock = (u64, Block);
97
98/// Represents a single non-terminator physical block in a tar stream.
99#[derive(Clone, Debug, Eq, PartialEq)]
100pub enum Frame {
101    /// A local or global pax extended header block.
102    Pax(PaxFrame),
103    /// A GNU long-name or long-link extension header block.
104    Gnu(GnuFrame),
105    /// An ordinary POSIX-ustar or GNU member header block.
106    Header(HeaderFrame),
107    /// A pax or member payload block.
108    Data(DataFrame),
109}
110
111/// A pax extended header block.
112#[derive(Clone, Debug, Eq, PartialEq)]
113pub struct PaxFrame {
114    /// The absolute byte position of this block in the source stream.
115    pub position: u64,
116    /// The lossless header block bytes.
117    pub block: Block,
118    /// Whether this header is local or global.
119    pub kind: PaxKind,
120    /// The number of bytes occupied by the extended header records.
121    pub payload_size: u64,
122}
123
124/// A GNU metadata extension header block.
125#[derive(Clone, Debug, Eq, PartialEq)]
126pub struct GnuFrame {
127    /// The absolute byte position of this block in the source stream.
128    pub position: u64,
129    /// The lossless header block bytes.
130    pub block: Block,
131    /// The GNU extension kind.
132    pub kind: GnuKind,
133    /// The number of metadata payload bytes following the header.
134    pub payload_size: u64,
135}
136
137/// An ordinary physical member header block in the selected archive family.
138///
139/// PAX records remain on their physical payload frames. Use
140/// [`crate::logical::TarReader`] for assembled member metadata.
141#[derive(Clone, Debug, Eq, PartialEq)]
142pub struct HeaderFrame {
143    /// The absolute byte position of this block in the source stream.
144    pub position: u64,
145    /// The lossless header block bytes.
146    pub block: Block,
147    /// The selected archive family of this member header.
148    pub format: ArchiveFormat,
149    /// The member type identified by the header.
150    pub kind: UstarKind,
151    /// The size encoded directly in the ustar or GNU member header field.
152    pub declared_size: u64,
153    /// The size after applying applicable pax `size` records.
154    ///
155    /// This is also the number of payload bytes for which data frames will be
156    /// emitted. Member kinds that cannot carry payload are rejected when either
157    /// their declared or effective size is nonzero.
158    pub effective_size: u64,
159    pub(crate) mode: Option<u64>,
160    pub(crate) uid: Option<u64>,
161    pub(crate) gid: Option<u64>,
162    pub(crate) mtime: Option<u64>,
163}
164
165impl HeaderFrame {
166    fn ustar(
167        position: u64,
168        block: Block,
169        typeflag: u8,
170        declared_size: u64,
171        local_records: Option<&PaxRecords>,
172        global_records: Option<&GlobalPaxRecords>,
173        policy: &StreamPolicy,
174    ) -> Result<Self, FrameError> {
175        let kind = UstarKind::try_from_framed(position, typeflag)?;
176
177        // Some real-world pax writers encode absent ordinary-header metadata as
178        // all NULs. The compatibility policy may accept these empty fields; every
179        // populated fallback remains subject to strict validation.
180        let parse_numeric_field = |field, range: Range<usize>| {
181            Self::parse_numeric_field(
182                position,
183                ArchiveFormat::Pax,
184                field,
185                &block[range],
186                policy.allow_all_nul_numeric_fields,
187            )
188        };
189        let mode = parse_numeric_field("mode", MODE_RANGE)?;
190        let uid = parse_numeric_field("uid", UID_RANGE)?;
191        let gid = parse_numeric_field("gid", GID_RANGE)?;
192        let mtime = parse_numeric_field("mtime", MTIME_RANGE)?;
193
194        Self::validate_owner_names(position, &block, policy)?;
195
196        // POSIX pax deliberately leaves the representation of device numbers unspecified.
197        // We do not consume those fields, so devmajor and devminor remain opaque.
198
199        // Determine our member's actual (i.e. effective) size. This takes any pax `size`
200        // records into account, in addition to the normal header-declared size.
201        let effective_size = PaxState::effective_size(local_records, global_records).map_or(
202            Ok(declared_size),
203            |size| match size {
204                PaxValue::Value(size) => Ok(*size),
205                PaxValue::Deleted => Err(FrameError::deleted_pax_metadata(position, "size")),
206            },
207        )?;
208        validate_pax_member_size(position, kind, declared_size, effective_size)?;
209
210        Ok(Self {
211            position,
212            block,
213            format: ArchiveFormat::Pax,
214            kind,
215            declared_size,
216            effective_size,
217            mode,
218            uid,
219            gid,
220            mtime,
221        })
222    }
223
224    fn gnu(
225        position: u64,
226        block: Block,
227        typeflag: u8,
228        declared_size: u64,
229        require_link_kind: bool,
230        policy: &StreamPolicy,
231    ) -> Result<Self, FrameError> {
232        let kind = UstarKind::try_from_framed(position, typeflag)?;
233        if require_link_kind && !matches!(kind, UstarKind::HardLink | UstarKind::SymbolicLink) {
234            return Err(FrameError::unexpected_order(
235                position,
236                "hard-link or symbolic-link member after GNU long-link extension",
237                "non-link ordinary member",
238            ));
239        }
240        validate_gnu_member_size(position, kind, declared_size)?;
241        let parse_numeric_field = |field, range: Range<usize>| {
242            Self::parse_numeric_field(
243                position,
244                ArchiveFormat::Gnu,
245                field,
246                &block[range],
247                policy.allow_all_nul_numeric_fields,
248            )
249        };
250        let mode = parse_numeric_field("mode", MODE_RANGE)?;
251        let uid = parse_numeric_field("uid", UID_RANGE)?;
252        let gid = parse_numeric_field("gid", GID_RANGE)?;
253        let mtime = parse_numeric_field("mtime", MTIME_RANGE)?;
254        Self::validate_owner_names(position, &block, policy)?;
255
256        Ok(Self {
257            position,
258            block,
259            format: ArchiveFormat::Gnu,
260            kind,
261            declared_size,
262            effective_size: declared_size,
263            mode,
264            uid,
265            gid,
266            mtime,
267        })
268    }
269
270    fn validate_owner_names(
271        position: u64,
272        block: &Block,
273        policy: &StreamPolicy,
274    ) -> Result<(), FrameError> {
275        if policy.allow_header_uname_and_gname_without_terminator {
276            return Ok(());
277        }
278        for (field, range) in [("uname", UNAME_RANGE), ("gname", GNAME_RANGE)] {
279            if !block[range].contains(&0) {
280                return Err(FrameError::at(
281                    position,
282                    FrameErrorInner::UnterminatedUstarStringField { field },
283                ));
284            }
285        }
286        Ok(())
287    }
288
289    fn parse_numeric_field(
290        position: u64,
291        format: ArchiveFormat,
292        field: &'static str,
293        bytes: &[u8],
294        allow_all_nul_numeric_fields: bool,
295    ) -> Result<Option<u64>, FrameError> {
296        if allow_all_nul_numeric_fields && is_all_nul(bytes) {
297            return Ok(None);
298        }
299        parse_number(format, bytes).map(Some).ok_or_else(|| {
300            FrameError::at(
301                position,
302                FrameErrorInner::InvalidNumericField {
303                    field,
304                    found: bytes.to_vec(),
305                },
306            )
307        })
308    }
309
310    pub(crate) fn copy_header_path_into(&self, path: &mut Vec<u8>) {
311        path.clear();
312        let name = trim_nul(&self.block[NAME_RANGE]);
313        if self.format == ArchiveFormat::Gnu {
314            path.extend_from_slice(name);
315            return;
316        }
317        let prefix = trim_nul(&self.block[PREFIX_RANGE]);
318        if !prefix.is_empty() {
319            path.extend_from_slice(prefix);
320            path.push(b'/');
321        }
322        path.extend_from_slice(name);
323    }
324}
325
326/// The payload entry to which a data block belongs.
327#[derive(Clone, Copy, Debug, Eq, PartialEq)]
328pub enum DataOwner {
329    /// Payload bytes belonging to a pax extended header.
330    Pax(PaxKind),
331    /// Payload bytes belonging to a GNU metadata extension.
332    Gnu(GnuKind),
333    /// Payload bytes belonging to an ordinary archive member.
334    Member,
335}
336
337/// A payload physical block.
338///
339/// This can be "real" data for e.g. a file member, or it can be the payload of a pax
340/// or GNU header.
341#[derive(Clone, Debug, Eq, PartialEq)]
342pub struct DataFrame {
343    /// The absolute byte position of this block in the source stream.
344    pub position: u64,
345    /// The lossless payload block bytes, including any final padding.
346    pub block: Block,
347    /// The number of meaningful payload bytes in this block.
348    pub len: usize,
349    /// Whether this block carries metadata-extension or member data.
350    pub owner: DataOwner,
351    /// Parsed records completed by this final pax payload block.
352    ///
353    /// This is `Some` only for the last data block belonging to a local or
354    /// global pax header; other payload data carries `None`.
355    completed_pax_records: Option<SharedPaxRecords>,
356}
357
358impl DataFrame {
359    /// Returns parsed records completed by this final pax payload block.
360    ///
361    /// This returns `Some` only for the last data block belonging to a local
362    /// or global pax header.
363    pub fn completed_pax_records(&self) -> Option<&[PaxRecord]> {
364        self.completed_pax_records
365            .as_deref()
366            .map(PaxRecords::as_slice)
367    }
368
369    pub(crate) fn into_completed_pax_records(self) -> Option<SharedPaxRecords> {
370        self.completed_pax_records
371    }
372}
373
374/// The parser phase required before the next physical frame can be emitted.
375#[derive(Debug)]
376pub(super) enum State {
377    /// No payload is pending; accept a header or the first zero end marker.
378    AwaitingHeader,
379    /// Consume the payload blocks declared by a local or global pax header.
380    ReadingPax {
381        kind: PaxKind,
382        header_position: u64,
383        remaining: u64,
384        payload: Vec<u8>,
385    },
386    /// A local pax header has completed; require its ordinary ustar header.
387    AwaitingUstarHeader { records: SharedPaxRecords },
388    /// Consume uninterpreted payload blocks for a GNU `L` or `K` extension.
389    ReadingGnu {
390        kind: GnuKind,
391        remaining: u64,
392        pending: PendingGnu,
393    },
394    /// GNU metadata is pending; accept another distinct extension or its member.
395    AwaitingGnuMember { pending: PendingGnu },
396    /// Consume the payload blocks declared for an ordinary member.
397    ReadingMember { remaining: u64 },
398    /// The first zero end marker was read; require the second zero block.
399    AwaitingSecondZero,
400    /// A valid two-block end marker was consumed; no further input is examined.
401    Complete,
402    /// An error has been emitted; subsequent polls return end-of-stream.
403    Failed,
404}
405
406#[derive(Clone, Copy, Debug, Default)]
407pub(super) struct PendingGnu {
408    pub(super) long_name: bool,
409    pub(super) long_link: bool,
410}
411
412/// Ordinary-member chunk storage retained across cancellation and API changes.
413#[derive(Default)]
414struct MemberChunk {
415    buffer: Vec<u8>,
416    start_position: u64,
417    physical_len: usize,
418    meaningful_len: usize,
419    state: Option<MemberChunkState>,
420}
421
422#[derive(Clone, Copy)]
423enum MemberChunkState {
424    Reading {
425        member_remaining: u64,
426        filled: usize,
427    },
428    Ready {
429        delivered: usize,
430    },
431}
432
433/// Controls limits and compatibility accepted by a tar frame stream.
434#[derive(Clone, Copy, Debug, Eq, PartialEq)]
435pub struct StreamPolicy {
436    max_pax_extension_size: u64,
437    max_global_pax_extensions_size: u64,
438    allow_all_nul_numeric_fields: bool,
439    allow_header_uname_and_gname_without_terminator: bool,
440    max_gnu_extension_size: u64,
441}
442
443impl Default for StreamPolicy {
444    fn default() -> Self {
445        Self {
446            max_pax_extension_size: DEFAULT_MAX_PAX_EXTENSION_SIZE,
447            max_global_pax_extensions_size: DEFAULT_MAX_GLOBAL_PAX_EXTENSIONS_SIZE,
448            allow_all_nul_numeric_fields: true,
449            allow_header_uname_and_gname_without_terminator: false,
450            max_gnu_extension_size: DEFAULT_MAX_GNU_EXTENSION_SIZE,
451        }
452    }
453}
454
455impl StreamPolicy {
456    /// Sets the maximum size for each pax extension.
457    ///
458    /// A local or global header that declares a larger payload is rejected
459    /// before its payload is consumed. Setting the maximum to zero rejects
460    /// every nonempty extension. Setting it to [`u64::MAX`] removes the
461    /// limit for each extension. The archive-wide limit still applies to global
462    /// extensions.
463    pub fn max_pax_extension_size(mut self, max_pax_extension_size: u64) -> Self {
464        self.max_pax_extension_size = max_pax_extension_size;
465        self
466    }
467
468    /// Sets the maximum total payload size for all global pax extensions.
469    ///
470    /// The total includes each global extension in the archive. It does not reset
471    /// after an ordinary member. A global header is rejected before its payload
472    /// is read if the new total is more than this limit.
473    ///
474    /// A value of zero rejects each nonempty global extension. A value of
475    /// [`u64::MAX`] removes this limit. The limit for each extension still applies.
476    pub fn max_global_pax_extensions_size(mut self, max_global_pax_extensions_size: u64) -> Self {
477        self.max_global_pax_extensions_size = max_global_pax_extensions_size;
478        self
479    }
480
481    /// Configures whether wholly NUL numeric metadata fields may be accepted.
482    ///
483    /// This compatibility option applies to `mode`, `uid`, `gid`, and `mtime`
484    /// in both pax/ustar and GNU ordinary member headers. It is enabled by
485    /// default. Disabling it requires each field to use a valid numeric encoding
486    /// for its archive family.
487    pub fn allow_all_nul_numeric_fields(mut self, allow: bool) -> Self {
488        self.allow_all_nul_numeric_fields = allow;
489        self
490    }
491
492    /// Configures whether ordinary header `uname` and `gname` fields may omit their NUL terminators.
493    ///
494    /// This compatibility option applies to ordinary pax/ustar and GNU member
495    /// headers and is disabled by default. Enabling it accepts all 32 bytes of
496    /// an unterminated field as the name.
497    pub fn allow_header_uname_and_gname_without_terminator(mut self, allow: bool) -> Self {
498        self.allow_header_uname_and_gname_without_terminator = allow;
499        self
500    }
501
502    /// Configures the maximum size accepted for each GNU extension.
503    ///
504    /// A GNU extension member that declares a larger payload is rejected before
505    /// its payload is consumed. Setting the maximum to zero rejects every nonempty
506    /// GNU extension member. Setting it to [`u64::MAX`] removes the per-extension bound.
507    pub fn max_gnu_extension_size(mut self, max_gnu_extension_size: u64) -> Self {
508        self.max_gnu_extension_size = max_gnu_extension_size;
509        self
510    }
511}
512
513/// A strict stream of POSIX-pax or GNU frames sourced from an underlying reader.
514pub struct TarStream<R> {
515    /// Our current stream position.
516    pub(super) position: u64,
517    /// Our interior source.
518    pub(super) inner: R,
519    pub(super) block: Block,
520    pub(super) block_len: usize,
521    pub(super) format: Option<ArchiveFormat>,
522    /// The currently effective global pax records, if any.
523    pub(super) global_pax_records: Option<GlobalPaxRecords>,
524    policy: StreamPolicy,
525    global_pax_extensions_size: u64,
526    member_chunk: MemberChunk,
527    pub(super) state: State,
528}
529
530impl<R> TarStream<R> {
531    /// Creates a new [`TarStream`] from the given reader.
532    pub fn new(reader: R) -> Self {
533        Self {
534            position: 0,
535            inner: reader,
536            block: [0; BLOCK_SIZE],
537            block_len: 0,
538            format: None,
539            global_pax_records: None,
540            policy: StreamPolicy::default(),
541            global_pax_extensions_size: 0,
542            member_chunk: MemberChunk::default(),
543            state: State::AwaitingHeader,
544        }
545    }
546
547    /// Configures the framing policy used by this stream.
548    ///
549    /// Call before reading any frames.
550    pub fn with_policy(mut self, policy: StreamPolicy) -> Self {
551        self.policy = policy;
552        self
553    }
554
555    /// Returns the selected archive family after the first header is read.
556    pub fn format(&self) -> Option<ArchiveFormat> {
557        self.format
558    }
559}
560
561impl<R: AsyncRead + Unpin> TarStream<R> {
562    /// Returns the next non-terminator physical archive frame.
563    ///
564    /// Reaching the end of the archive returns [`None`]. A framing error fuses
565    /// this reader, so every subsequent call also returns [`None`]. Cancelling
566    /// this operation retains any partial block for the next call.
567    pub async fn next_frame(&mut self) -> Result<Option<Frame>, FrameError> {
568        let result = poll_fn(|context| self.poll_next_frame(context)).await;
569        self.fail_on_error(result)
570    }
571
572    /// Reads one ordinary-member payload block without constructing a [`Frame`].
573    ///
574    /// Returns the block's position, lossless bytes, and meaningful length.
575    pub(crate) async fn read_member_block(&mut self) -> Result<(u64, Block, usize), FrameError> {
576        let result = self.read_member_block_inner().await;
577        self.fail_on_error(result)
578    }
579
580    async fn read_member_block_inner(&mut self) -> Result<(u64, Block, usize), FrameError> {
581        if self.member_chunk.state.is_some() {
582            self.complete_member_chunk().await?;
583            return self.take_member_block_from_chunk();
584        }
585        let remaining = match &self.state {
586            State::ReadingMember { remaining } => *remaining,
587            _ => {
588                return Err(FrameError::unexpected_order(
589                    self.position,
590                    "ordinary member payload",
591                    "parser state without member payload",
592                ));
593            }
594        };
595        let (position, block) = match poll_fn(|context| self.poll_read_block(context)).await {
596            Ok(Some(block)) => block,
597            Ok(None) => return Err(self.handle_eof()),
598            Err(error) => return Err(error),
599        };
600        let meaningful_len = remaining.min(BLOCK_SIZE as u64) as usize;
601        self.state = member_payload_state(remaining - meaningful_len as u64);
602        Ok((position, block, meaningful_len))
603    }
604
605    /// Reads aligned ordinary-member payload blocks directly into `buffer`.
606    ///
607    /// This internal path preserves exact physical-block completion checks
608    /// while avoiding lossless [`Frame`] construction for chunk consumers.
609    pub(crate) async fn read_member_chunk(
610        &mut self,
611        buffer: &mut Vec<u8>,
612        target_len: usize,
613    ) -> Result<usize, FrameError> {
614        // A cancelled block read retains its partial physical block here. Finish
615        // and deliver it before starting a direct chunk so no bytes are lost.
616        if self.member_chunk.state.is_none() && self.block_len != 0 {
617            let (_, block, meaningful_len) = self.read_member_block().await?;
618            buffer.clear();
619            buffer.extend_from_slice(&block[..meaningful_len]);
620            return Ok(meaningful_len);
621        }
622        if self.member_chunk.state.is_none() {
623            let result = self.start_member_chunk(buffer, target_len);
624            self.fail_on_error(result)?;
625        }
626        let result = self.complete_member_chunk().await;
627        self.fail_on_error(result)?;
628        let result = self.take_member_chunk(buffer);
629        self.fail_on_error(result)
630    }
631
632    fn fail_on_error<T>(&mut self, result: Result<T, FrameError>) -> Result<T, FrameError> {
633        if result.is_err() {
634            self.state = State::Failed;
635        }
636        result
637    }
638
639    fn start_member_chunk(
640        &mut self,
641        buffer: &mut Vec<u8>,
642        target_len: usize,
643    ) -> Result<(), FrameError> {
644        let member_remaining = match &self.state {
645            State::ReadingMember { remaining } => *remaining,
646            _ => {
647                return Err(FrameError::unexpected_order(
648                    self.position,
649                    "ordinary member payload",
650                    "parser state without member payload",
651                ));
652            }
653        };
654        if self.block_len != 0 {
655            return Err(FrameError::unexpected_order(
656                self.position,
657                "aligned ordinary member payload",
658                "partially buffered physical block",
659            ));
660        }
661
662        let target_len = u64::try_from(target_len.max(BLOCK_SIZE)).map_err(|_| {
663            FrameError::arithmetic_overflow(self.position, "member payload chunk target length")
664        })?;
665        let physical_len = member_remaining
666            .min(target_len)
667            .div_ceil(BLOCK_SIZE as u64)
668            .checked_mul(BLOCK_SIZE as u64)
669            .ok_or_else(|| {
670                FrameError::arithmetic_overflow(
671                    self.position,
672                    "member payload chunk physical length",
673                )
674            })?;
675        let meaningful_len = member_remaining.min(physical_len);
676        let physical_len = usize::try_from(physical_len).map_err(|_| {
677            FrameError::arithmetic_overflow(self.position, "member payload chunk physical length")
678        })?;
679        let meaningful_len = usize::try_from(meaningful_len).map_err(|_| {
680            FrameError::arithmetic_overflow(self.position, "member payload chunk meaningful length")
681        })?;
682
683        // Move the caller's reusable allocation into persistent storage before
684        // reading so cancellation cannot discard partial bytes or progress.
685        self.member_chunk.buffer.clear();
686        std::mem::swap(buffer, &mut self.member_chunk.buffer);
687        if self.member_chunk.buffer.len() != physical_len {
688            self.member_chunk.buffer.resize(physical_len, 0);
689        }
690        self.member_chunk.start_position = self.position;
691        self.member_chunk.physical_len = physical_len;
692        self.member_chunk.meaningful_len = meaningful_len;
693        self.member_chunk.state = Some(MemberChunkState::Reading {
694            member_remaining,
695            filled: 0,
696        });
697        Ok(())
698    }
699
700    async fn complete_member_chunk(&mut self) -> Result<(), FrameError> {
701        loop {
702            let (member_remaining, filled) = match self.member_chunk.state {
703                Some(MemberChunkState::Reading {
704                    member_remaining,
705                    filled,
706                }) => (member_remaining, filled),
707                Some(MemberChunkState::Ready { .. }) => return Ok(()),
708                None => {
709                    return Err(FrameError::unexpected_order(
710                        self.position,
711                        "pending member payload chunk",
712                        "parser state without a pending chunk",
713                    ));
714                }
715            };
716            let start_position = self.member_chunk.start_position;
717            let physical_len = self.member_chunk.physical_len;
718            let meaningful_len = self.member_chunk.meaningful_len;
719            if filled == physical_len {
720                self.position =
721                    checked_position(start_position, physical_len).inspect_err(|_| {
722                        self.member_chunk.state = None;
723                    })?;
724                let remaining = member_remaining
725                    .checked_sub(meaningful_len as u64)
726                    .ok_or_else(|| {
727                        self.member_chunk.state = None;
728                        FrameError::arithmetic_overflow(
729                            start_position,
730                            "remaining member payload length",
731                        )
732                    })?;
733                self.state = member_payload_state(remaining);
734                self.member_chunk.state = Some(MemberChunkState::Ready { delivered: 0 });
735                return Ok(());
736            }
737
738            let read = match poll_fn(|context| {
739                let mut read_buffer =
740                    ReadBuf::new(&mut self.member_chunk.buffer[filled..physical_len]);
741                match Pin::new(&mut self.inner).poll_read(context, &mut read_buffer) {
742                    Poll::Pending => Poll::Pending,
743                    Poll::Ready(Ok(())) => Poll::Ready(Ok(read_buffer.filled().len())),
744                    Poll::Ready(Err(source)) => Poll::Ready(Err(source)),
745                }
746            })
747            .await
748            {
749                Ok(read) => read,
750                Err(source) => {
751                    self.member_chunk.state = None;
752                    let error_position = checked_position(start_position, filled)?;
753                    self.position = checked_position(start_position, filled - filled % BLOCK_SIZE)?;
754                    return Err(FrameError::at(
755                        error_position,
756                        FrameErrorInner::Io { source },
757                    ));
758                }
759            };
760            if read == 0 {
761                self.member_chunk.state = None;
762                let partial_len = filled % BLOCK_SIZE;
763                let completed_len = filled - partial_len;
764                self.position = checked_position(start_position, completed_len)?;
765                if partial_len != 0 {
766                    return Err(FrameError::at(
767                        self.position,
768                        FrameErrorInner::IncompleteBlock { read: partial_len },
769                    ));
770                }
771                let completed_len = u64::try_from(completed_len).map_err(|_| {
772                    FrameError::arithmetic_overflow(
773                        self.position,
774                        "completed member payload chunk length",
775                    )
776                })?;
777                return Err(FrameError::truncated_payload(
778                    self.position,
779                    DataOwner::Member,
780                    member_remaining - member_remaining.min(completed_len),
781                ));
782            }
783            if let Some(MemberChunkState::Reading { filled, .. }) = &mut self.member_chunk.state {
784                *filled += read;
785            }
786        }
787    }
788
789    fn take_member_chunk(&mut self, buffer: &mut Vec<u8>) -> Result<usize, FrameError> {
790        let Some(MemberChunkState::Ready { delivered }) = self.member_chunk.state.take() else {
791            return Err(FrameError::unexpected_order(
792                self.position,
793                "completed member payload chunk",
794                "incomplete member payload chunk",
795            ));
796        };
797        let meaningful_len = self.member_chunk.meaningful_len;
798        let remaining_len = meaningful_len.checked_sub(delivered).ok_or_else(|| {
799            FrameError::arithmetic_overflow(self.position, "undelivered member payload length")
800        })?;
801        if delivered != 0 {
802            self.member_chunk
803                .buffer
804                .copy_within(delivered..meaningful_len, 0);
805        }
806        self.member_chunk.buffer.truncate(remaining_len);
807        std::mem::swap(buffer, &mut self.member_chunk.buffer);
808        Ok(remaining_len)
809    }
810
811    fn take_member_block_from_chunk(&mut self) -> Result<(u64, Block, usize), FrameError> {
812        let Some(MemberChunkState::Ready { delivered }) = self.member_chunk.state else {
813            return Err(FrameError::unexpected_order(
814                self.position,
815                "completed member payload chunk",
816                "incomplete member payload chunk",
817            ));
818        };
819        let start_position = self.member_chunk.start_position;
820        let physical_len = self.member_chunk.physical_len;
821        let total_meaningful_len = self.member_chunk.meaningful_len;
822        let position = checked_position(start_position, delivered).inspect_err(|_| {
823            self.member_chunk.state = None;
824        })?;
825        let mut block = [0; BLOCK_SIZE];
826        block.copy_from_slice(&self.member_chunk.buffer[delivered..delivered + BLOCK_SIZE]);
827        let meaningful_len = total_meaningful_len
828            .checked_sub(delivered)
829            .ok_or_else(|| {
830                self.member_chunk.state = None;
831                FrameError::arithmetic_overflow(self.position, "undelivered member payload length")
832            })?
833            .min(BLOCK_SIZE);
834        let delivered = delivered + BLOCK_SIZE;
835        if delivered == physical_len {
836            self.member_chunk.state = None;
837        } else {
838            self.member_chunk.state = Some(MemberChunkState::Ready { delivered });
839        }
840        Ok((position, block, meaningful_len))
841    }
842
843    fn poll_read_block(
844        &mut self,
845        cx: &mut Context<'_>,
846    ) -> Poll<Result<Option<PositionedBlock>, FrameError>> {
847        while self.block_len < BLOCK_SIZE {
848            let mut read_buf = ReadBuf::new(&mut self.block[self.block_len..]);
849            match Pin::new(&mut self.inner).poll_read(cx, &mut read_buf) {
850                Poll::Pending => return Poll::Pending,
851                Poll::Ready(Err(source)) => {
852                    return Poll::Ready(Err(FrameError::at(
853                        self.position + self.block_len as u64,
854                        FrameErrorInner::Io { source },
855                    )));
856                }
857                Poll::Ready(Ok(())) => {
858                    let read = read_buf.filled().len();
859                    if read == 0 {
860                        if self.block_len == 0 {
861                            return Poll::Ready(Ok(None));
862                        }
863                        return Poll::Ready(Err(FrameError::at(
864                            self.position,
865                            FrameErrorInner::IncompleteBlock {
866                                read: self.block_len,
867                            },
868                        )));
869                    }
870                    self.block_len += read;
871                }
872            }
873        }
874
875        let position = self.position;
876        self.position = self
877            .position
878            .checked_add(BLOCK_SIZE as u64)
879            .ok_or_else(|| FrameError::arithmetic_overflow(position, "stream position"))?;
880        self.block_len = 0;
881        let block = std::mem::replace(&mut self.block, [0; BLOCK_SIZE]);
882        Poll::Ready(Ok(Some((position, block))))
883    }
884
885    fn handle_eof(&mut self) -> FrameError {
886        let inner = match &self.state {
887            State::AwaitingHeader | State::AwaitingSecondZero => FrameErrorInner::MissingEndMarker,
888            State::ReadingPax {
889                kind, remaining, ..
890            } => FrameErrorInner::TruncatedPayload {
891                owner: DataOwner::Pax(*kind),
892                remaining: *remaining,
893            },
894            State::AwaitingUstarHeader { .. } => FrameErrorInner::UnexpectedEof {
895                expected: "ordinary ustar member header after a local pax header",
896            },
897            State::ReadingGnu {
898                kind, remaining, ..
899            } => FrameErrorInner::TruncatedPayload {
900                owner: DataOwner::Gnu(*kind),
901                remaining: *remaining,
902            },
903            State::AwaitingGnuMember { .. } => FrameErrorInner::UnexpectedEof {
904                expected: "ordinary GNU member header after a GNU metadata extension",
905            },
906            State::ReadingMember { remaining } => FrameErrorInner::TruncatedPayload {
907                owner: DataOwner::Member,
908                remaining: *remaining,
909            },
910            State::Complete | State::Failed => FrameErrorInner::UnexpectedEof {
911                expected: "no further input",
912            },
913        };
914        FrameError::at(self.position, inner)
915    }
916
917    fn process_block(&mut self, position: u64, block: Block) -> Result<Option<Frame>, FrameError> {
918        let state = std::mem::replace(&mut self.state, State::Failed);
919        match state {
920            State::AwaitingHeader => {
921                if is_zero_block(&block) {
922                    self.state = State::AwaitingSecondZero;
923                    Ok(None)
924                } else {
925                    self.process_boundary_header(position, block).map(Some)
926                }
927            }
928            State::ReadingPax {
929                kind,
930                header_position,
931                mut remaining,
932                mut payload,
933            } => {
934                let len = remaining.min(BLOCK_SIZE as u64) as usize;
935                payload.extend_from_slice(&block[..len]);
936                remaining -= len as u64;
937                let completed_pax_records = if remaining == 0 {
938                    let records = Arc::new(
939                        PaxRecords::parse(
940                            &payload,
941                            self.global_pax_records
942                                .as_ref()
943                                .map_or(HdrCharset::Utf8, GlobalPaxRecords::hdrcharset),
944                        )
945                        .map_err(|source| {
946                            FrameError::invalid_pax_record(header_position, source)
947                        })?,
948                    );
949                    match kind {
950                        PaxKind::Local => {
951                            self.state = State::AwaitingUstarHeader {
952                                records: records.clone(),
953                            };
954                        }
955                        PaxKind::Global => {
956                            records.apply_global(&mut self.global_pax_records);
957                            self.state = State::AwaitingHeader;
958                        }
959                    }
960                    Some(records)
961                } else {
962                    self.state = State::ReadingPax {
963                        kind,
964                        header_position,
965                        remaining,
966                        payload,
967                    };
968                    None
969                };
970                Ok(Some(Frame::Data(DataFrame {
971                    position,
972                    block,
973                    len,
974                    owner: DataOwner::Pax(kind),
975                    completed_pax_records,
976                })))
977            }
978            State::AwaitingUstarHeader { records } => {
979                if is_zero_block(&block) {
980                    return Err(FrameError::unexpected_order(
981                        position,
982                        "ordinary ustar member header after a local pax header",
983                        "end-of-archive marker",
984                    ));
985                }
986                let parsed = self.parse_format_checked_header(position, &block)?;
987                if matches!(parsed.typeflag, b'x' | b'g') {
988                    return Err(FrameError::unexpected_order(
989                        position,
990                        "ordinary ustar member header after a local pax header",
991                        "another pax extended header",
992                    ));
993                }
994                self.process_ustar_header(position, block, parsed, Some(records))
995                    .map(Some)
996            }
997            State::ReadingGnu {
998                kind,
999                mut remaining,
1000                pending,
1001            } => {
1002                let len = remaining.min(BLOCK_SIZE as u64) as usize;
1003                remaining -= len as u64;
1004                if remaining == 0 {
1005                    self.state = State::AwaitingGnuMember { pending };
1006                } else {
1007                    self.state = State::ReadingGnu {
1008                        kind,
1009                        remaining,
1010                        pending,
1011                    };
1012                }
1013                Ok(Some(Frame::Data(DataFrame {
1014                    position,
1015                    block,
1016                    len,
1017                    owner: DataOwner::Gnu(kind),
1018                    completed_pax_records: None,
1019                })))
1020            }
1021            State::AwaitingGnuMember { pending } => {
1022                if is_zero_block(&block) {
1023                    return Err(FrameError::unexpected_order(
1024                        position,
1025                        "ordinary GNU member header after a GNU metadata extension",
1026                        "end-of-archive marker",
1027                    ));
1028                }
1029                let parsed = self.parse_format_checked_header(position, &block)?;
1030                self.process_gnu_header(position, block, parsed, pending)
1031                    .map(Some)
1032            }
1033            State::ReadingMember { mut remaining } => {
1034                let len = remaining.min(BLOCK_SIZE as u64) as usize;
1035                remaining -= len as u64;
1036                self.state = member_payload_state(remaining);
1037                Ok(Some(Frame::Data(DataFrame {
1038                    position,
1039                    block,
1040                    len,
1041                    owner: DataOwner::Member,
1042                    completed_pax_records: None,
1043                })))
1044            }
1045            State::AwaitingSecondZero => {
1046                if !is_zero_block(&block) {
1047                    return Err(FrameError::at(position, FrameErrorInner::InvalidEndMarker));
1048                }
1049                self.state = State::Complete;
1050                Ok(None)
1051            }
1052            State::Complete => {
1053                self.state = State::Complete;
1054                Ok(None)
1055            }
1056            State::Failed => Ok(None),
1057        }
1058    }
1059
1060    fn process_boundary_header(
1061        &mut self,
1062        position: u64,
1063        block: Block,
1064    ) -> Result<Frame, FrameError> {
1065        let parsed = self.parse_format_checked_header(position, &block)?;
1066        match parsed.format {
1067            ArchiveFormat::Pax => self.process_pax_boundary_header(position, block, parsed),
1068            ArchiveFormat::Gnu => {
1069                self.process_gnu_header(position, block, parsed, PendingGnu::default())
1070            }
1071        }
1072    }
1073
1074    /// Parses a header and enforces the archive's single selected format.
1075    ///
1076    /// The first non-terminator header selects the format; later headers must
1077    /// decode as valid headers of that same family.
1078    fn parse_format_checked_header(
1079        &mut self,
1080        position: u64,
1081        block: &Block,
1082    ) -> Result<ParsedHeader, FrameError> {
1083        let parsed = ParsedHeader::try_from_framed(position, block)?;
1084        if let Some(expected) = self.format
1085            && parsed.format != expected
1086        {
1087            return Err(FrameError::at(
1088                position,
1089                FrameErrorInner::FormatMismatch {
1090                    expected,
1091                    found: parsed.format,
1092                },
1093            ));
1094        }
1095        self.format.get_or_insert(parsed.format);
1096        Ok(parsed)
1097    }
1098
1099    /// Processes a pax/ustar header at an archive-member boundary, where a new
1100    /// pax extension or an ordinary ustar member may begin.
1101    ///
1102    /// Pax extension headers enter [`State::ReadingPax`]; ordinary ustar
1103    /// headers are delegated to [`Self::process_ustar_header`].
1104    fn process_pax_boundary_header(
1105        &mut self,
1106        position: u64,
1107        block: Block,
1108        parsed: ParsedHeader,
1109    ) -> Result<Frame, FrameError> {
1110        match parsed.typeflag {
1111            b'x' => self.process_pax_header(position, block, parsed.size, PaxKind::Local),
1112            b'g' => self.process_pax_header(position, block, parsed.size, PaxKind::Global),
1113            _ => self.process_ustar_header(position, block, parsed, None),
1114        }
1115    }
1116
1117    /// Emits a pax extension header and enters its payload-reading state.
1118    ///
1119    /// This is reached only from the POSIX boundary state, before any local
1120    /// pax records require an ordinary member header.
1121    fn process_pax_header(
1122        &mut self,
1123        position: u64,
1124        block: Block,
1125        payload_size: u64,
1126        kind: PaxKind,
1127    ) -> Result<Frame, FrameError> {
1128        if payload_size > self.policy.max_pax_extension_size {
1129            return Err(FrameError::at(
1130                position,
1131                FrameErrorInner::ExtensionTooLarge {
1132                    format: ArchiveFormat::Pax,
1133                    size: payload_size,
1134                    limit: self.policy.max_pax_extension_size,
1135                },
1136            ));
1137        }
1138        if kind == PaxKind::Global {
1139            let size = self
1140                .global_pax_extensions_size
1141                .checked_add(payload_size)
1142                .ok_or_else(|| {
1143                    FrameError::arithmetic_overflow(position, "global pax extension payload total")
1144                })?;
1145            if size > self.policy.max_global_pax_extensions_size {
1146                return Err(FrameError::at(
1147                    position,
1148                    FrameErrorInner::GlobalPaxExtensionsTooLarge {
1149                        size,
1150                        limit: self.policy.max_global_pax_extensions_size,
1151                    },
1152                ));
1153            }
1154            self.global_pax_extensions_size = size;
1155        }
1156        if payload_size == 0 {
1157            return Err(FrameError::invalid_pax_record(
1158                position,
1159                PaxError::InvalidRecords {
1160                    reason: "extended header payload contains no records",
1161                },
1162            ));
1163        }
1164        self.state = State::ReadingPax {
1165            kind,
1166            header_position: position,
1167            remaining: payload_size,
1168            payload: Vec::new(),
1169        };
1170        Ok(Frame::Pax(PaxFrame {
1171            position,
1172            block,
1173            kind,
1174            payload_size,
1175        }))
1176    }
1177
1178    /// Emits an ordinary ustar member header after applying pax size state.
1179    ///
1180    /// This handles both bare members and members required by
1181    /// [`State::AwaitingUstarHeader`], then enters member data reading when
1182    /// the effective member size requires payload blocks.
1183    fn process_ustar_header(
1184        &mut self,
1185        position: u64,
1186        block: Block,
1187        parsed: ParsedHeader,
1188        local_pax_records: Option<SharedPaxRecords>,
1189    ) -> Result<Frame, FrameError> {
1190        let frame = HeaderFrame::ustar(
1191            position,
1192            block,
1193            parsed.typeflag,
1194            parsed.size,
1195            local_pax_records.as_deref(),
1196            self.global_pax_records.as_ref(),
1197            &self.policy,
1198        )?;
1199        self.state = member_payload_state(frame.effective_size);
1200        Ok(Frame::Header(frame))
1201    }
1202
1203    fn process_gnu_header(
1204        &mut self,
1205        position: u64,
1206        block: Block,
1207        parsed: ParsedHeader,
1208        mut pending: PendingGnu,
1209    ) -> Result<Frame, FrameError> {
1210        let extension = match parsed.typeflag {
1211            b'L' => Some(GnuKind::LongName),
1212            b'K' => Some(GnuKind::LongLink),
1213            _ => None,
1214        };
1215        if let Some(kind) = extension {
1216            let already_seen = match kind {
1217                GnuKind::LongName => &mut pending.long_name,
1218                GnuKind::LongLink => &mut pending.long_link,
1219            };
1220            if *already_seen {
1221                return Err(FrameError::unexpected_order(
1222                    position,
1223                    "ordinary GNU member header or the other GNU metadata extension",
1224                    "duplicate GNU metadata extension",
1225                ));
1226            }
1227            if parsed.size > self.policy.max_gnu_extension_size {
1228                return Err(FrameError::at(
1229                    position,
1230                    FrameErrorInner::ExtensionTooLarge {
1231                        format: ArchiveFormat::Gnu,
1232                        size: parsed.size,
1233                        limit: self.policy.max_gnu_extension_size,
1234                    },
1235                ));
1236            }
1237            *already_seen = true;
1238            self.state = if parsed.size == 0 {
1239                State::AwaitingGnuMember { pending }
1240            } else {
1241                State::ReadingGnu {
1242                    kind,
1243                    remaining: parsed.size,
1244                    pending,
1245                }
1246            };
1247            return Ok(Frame::Gnu(GnuFrame {
1248                position,
1249                block,
1250                kind,
1251                payload_size: parsed.size,
1252            }));
1253        }
1254
1255        let frame = HeaderFrame::gnu(
1256            position,
1257            block,
1258            parsed.typeflag,
1259            parsed.size,
1260            pending.long_link,
1261            &self.policy,
1262        )?;
1263        self.state = member_payload_state(frame.effective_size);
1264        Ok(Frame::Header(frame))
1265    }
1266
1267    fn poll_next_frame(
1268        &mut self,
1269        context: &mut Context<'_>,
1270    ) -> Poll<Result<Option<Frame>, FrameError>> {
1271        loop {
1272            if matches!(self.state, State::Complete | State::Failed) {
1273                return Poll::Ready(Ok(None));
1274            }
1275
1276            let (position, block) = match self.poll_read_block(context) {
1277                Poll::Pending => return Poll::Pending,
1278                Poll::Ready(Ok(Some(block))) => block,
1279                Poll::Ready(Ok(None)) => return Poll::Ready(Err(self.handle_eof())),
1280                Poll::Ready(Err(error)) => return Poll::Ready(Err(error)),
1281            };
1282
1283            match self.process_block(position, block) {
1284                Ok(Some(frame)) => return Poll::Ready(Ok(Some(frame))),
1285                Ok(None) => continue,
1286                Err(error) => return Poll::Ready(Err(error)),
1287            }
1288        }
1289    }
1290}
1291
1292struct ParsedHeader {
1293    format: ArchiveFormat,
1294    typeflag: u8,
1295    size: u64,
1296}
1297
1298/// Converts raw tar input into a typed value while retaining source position
1299/// for any framing error produced by the conversion.
1300trait TryFromFramed<T>: Sized {
1301    fn try_from_framed(position: u64, source: T) -> Result<Self, FrameError>;
1302}
1303
1304fn is_zero_block(block: &Block) -> bool {
1305    block.iter().all(|byte| *byte == 0)
1306}
1307
1308fn trim_nul(bytes: &[u8]) -> &[u8] {
1309    let end = bytes
1310        .iter()
1311        .position(|byte| *byte == 0)
1312        .unwrap_or(bytes.len());
1313    &bytes[..end]
1314}
1315
1316fn member_payload_state(remaining: u64) -> State {
1317    if remaining == 0 {
1318        State::AwaitingHeader
1319    } else {
1320        State::ReadingMember { remaining }
1321    }
1322}
1323
1324fn checked_position(position: u64, len: usize) -> Result<u64, FrameError> {
1325    let len = u64::try_from(len)
1326        .map_err(|_| FrameError::arithmetic_overflow(position, "stream position"))?;
1327    position
1328        .checked_add(len)
1329        .ok_or_else(|| FrameError::arithmetic_overflow(position, "stream position"))
1330}
1331
1332impl TryFromFramed<&Block> for ParsedHeader {
1333    fn try_from_framed(position: u64, block: &Block) -> Result<Self, FrameError> {
1334        let format = match &block[IDENTITY_RANGE] {
1335            identity if identity == USTAR_IDENTITY => ArchiveFormat::Pax,
1336            identity if identity == GNU_IDENTITY => ArchiveFormat::Gnu,
1337            identity => {
1338                return Err(FrameError::at(
1339                    position,
1340                    FrameErrorInner::InvalidIdentity {
1341                        found: identity.try_into().expect("fixed header range"),
1342                    },
1343                ));
1344            }
1345        };
1346
1347        let actual_checksum = checksum(block);
1348        let expected_checksum = parse_octal(&block[CHECKSUM_RANGE]);
1349        if expected_checksum != Some(actual_checksum) {
1350            return Err(FrameError::at(
1351                position,
1352                FrameErrorInner::InvalidChecksum {
1353                    expected: expected_checksum,
1354                    actual: actual_checksum,
1355                },
1356            ));
1357        }
1358
1359        let size_bytes: [u8; 12] = block[SIZE_RANGE].try_into().expect("fixed header range");
1360        let size = parse_number(format, &size_bytes).ok_or_else(|| {
1361            FrameError::at(position, FrameErrorInner::InvalidSize { found: size_bytes })
1362        })?;
1363
1364        Ok(Self {
1365            format,
1366            typeflag: block[TYPEFLAG_OFFSET],
1367            size,
1368        })
1369    }
1370}
1371
1372impl TryFromFramed<u8> for UstarKind {
1373    fn try_from_framed(position: u64, typeflag: u8) -> Result<Self, FrameError> {
1374        match typeflag {
1375            0 | b'0' => Ok(Self::Regular),
1376            b'1' => Ok(Self::HardLink),
1377            b'2' => Ok(Self::SymbolicLink),
1378            b'3' => Ok(Self::CharacterDevice),
1379            b'4' => Ok(Self::BlockDevice),
1380            b'5' => Ok(Self::Directory),
1381            b'6' => Ok(Self::Fifo),
1382            b'7' => Ok(Self::Contiguous),
1383            _ => Err(FrameError::at(
1384                position,
1385                FrameErrorInner::UnsupportedTypeflag { typeflag },
1386            )),
1387        }
1388    }
1389}
1390
1391fn validate_pax_member_size(
1392    position: u64,
1393    kind: UstarKind,
1394    declared_size: u64,
1395    effective_size: u64,
1396) -> Result<(), FrameError> {
1397    match kind {
1398        // PAX permits a nonzero physical hardlink size and allows pax `size`
1399        // records to override it, so the effective size controls framing.
1400        // This is a broadening of what ustar allows; ustar requires
1401        // hardlink members to have `size=0`.
1402        UstarKind::Regular | UstarKind::HardLink | UstarKind::Contiguous => Ok(()),
1403        UstarKind::SymbolicLink
1404        | UstarKind::CharacterDevice
1405        | UstarKind::BlockDevice
1406        | UstarKind::Directory
1407        | UstarKind::Fifo => {
1408            // NOTE: Observe that we're strict about directory entries having
1409            // `size=0`, even though ustar/pax says that they may have a nonzero
1410            // size as an allocation hint (which, in turn, does not affect framing).
1411            // We do this to avoid a common differential where some parsers incorrectly
1412            // honor the directory entry's size during framing.
1413            // TODO: Make this configurable? Doing so seems very risky.
1414            validate_payload_free_size(position, kind, declared_size)?;
1415            validate_payload_free_size(position, kind, effective_size)
1416        }
1417    }
1418}
1419
1420fn validate_gnu_member_size(position: u64, kind: UstarKind, size: u64) -> Result<(), FrameError> {
1421    match kind {
1422        UstarKind::Regular | UstarKind::Contiguous => Ok(()),
1423        UstarKind::HardLink
1424        | UstarKind::SymbolicLink
1425        | UstarKind::CharacterDevice
1426        | UstarKind::BlockDevice
1427        | UstarKind::Directory
1428        | UstarKind::Fifo => validate_payload_free_size(position, kind, size),
1429    }
1430}
1431
1432fn validate_payload_free_size(position: u64, kind: UstarKind, size: u64) -> Result<(), FrameError> {
1433    if size == 0 {
1434        Ok(())
1435    } else {
1436        Err(FrameError::at(
1437            position,
1438            FrameErrorInner::InvalidMemberSize { kind, size },
1439        ))
1440    }
1441}
1442
1443#[cfg(test)]
1444mod tests {
1445    use std::{
1446        cell::Cell,
1447        pin::Pin,
1448        rc::Rc,
1449        task::{Context, Poll},
1450    };
1451
1452    use tokio::io::ReadBuf;
1453
1454    use super::*;
1455    use crate::{
1456        ArchiveFormat, FrameError, FrameErrorInner, HdrCharset, PaxString, PaxValue,
1457        header::{DEVMAJOR_RANGE, DEVMINOR_RANGE},
1458        test_support::{
1459            ChunkedReader, append_block, append_gnu, append_pax, append_payload, append_terminator,
1460            collect_frames, gnu_base256_header, gnu_header, header, ready, record, set_checksum,
1461        },
1462    };
1463
1464    fn collect(bytes: Vec<u8>, max_chunk: usize) -> Vec<Result<Frame, FrameError>> {
1465        ready(collect_frames(TarStream::new(ChunkedReader::new(
1466            bytes, max_chunk,
1467        ))))
1468    }
1469
1470    fn collect_with_max_pax_extension_size(
1471        bytes: Vec<u8>,
1472        max_chunk: usize,
1473        max_pax_extension_size: u64,
1474    ) -> Vec<Result<Frame, FrameError>> {
1475        ready(collect_frames(
1476            TarStream::new(ChunkedReader::new(bytes, max_chunk)).with_policy(
1477                StreamPolicy::default().max_pax_extension_size(max_pax_extension_size),
1478            ),
1479        ))
1480    }
1481
1482    fn header_frame(frames: &[Result<Frame, FrameError>], index: usize) -> &HeaderFrame {
1483        let Ok(Frame::Header(frame)) = &frames[index] else {
1484            panic!("expected header frame");
1485        };
1486        frame
1487    }
1488
1489    fn data_frame(frames: &[Result<Frame, FrameError>], index: usize) -> &DataFrame {
1490        let Ok(Frame::Data(frame)) = &frames[index] else {
1491            panic!("expected data frame");
1492        };
1493        frame
1494    }
1495
1496    fn last_error(frames: &[Result<Frame, FrameError>]) -> &FrameError {
1497        frames
1498            .last()
1499            .expect("stream should emit an item")
1500            .as_ref()
1501            .expect_err("last item should be an error")
1502    }
1503
1504    fn last_error_inner(frames: &[Result<Frame, FrameError>]) -> &FrameErrorInner {
1505        &last_error(frames).inner
1506    }
1507
1508    struct CountingReader {
1509        bytes: Vec<u8>,
1510        position: usize,
1511        consumed: Rc<Cell<usize>>,
1512    }
1513
1514    impl AsyncRead for CountingReader {
1515        fn poll_read(
1516            mut self: Pin<&mut Self>,
1517            _context: &mut Context<'_>,
1518            buffer: &mut ReadBuf<'_>,
1519        ) -> Poll<std::io::Result<()>> {
1520            let len = buffer
1521                .remaining()
1522                .min(self.bytes.len().saturating_sub(self.position));
1523            let end = self.position + len;
1524            buffer.put_slice(&self.bytes[self.position..end]);
1525            self.position = end;
1526            self.consumed.set(self.consumed.get() + len);
1527            Poll::Ready(Ok(()))
1528        }
1529    }
1530
1531    #[derive(Clone, Copy)]
1532    enum ExpectedHeaderError {
1533        InvalidIdentity,
1534        InvalidChecksum,
1535        InvalidSize,
1536        InvalidNumericField(&'static str),
1537        UnterminatedUstarStringField(&'static str),
1538        UnsupportedTypeflag(u8),
1539    }
1540
1541    impl ExpectedHeaderError {
1542        fn matches(self, error: &FrameErrorInner) -> bool {
1543            match (self, error) {
1544                (Self::InvalidIdentity, FrameErrorInner::InvalidIdentity { .. })
1545                | (Self::InvalidChecksum, FrameErrorInner::InvalidChecksum { .. })
1546                | (Self::InvalidSize, FrameErrorInner::InvalidSize { .. }) => true,
1547                (
1548                    Self::InvalidNumericField(field),
1549                    FrameErrorInner::InvalidNumericField { field: found, .. },
1550                )
1551                | (
1552                    Self::UnterminatedUstarStringField(field),
1553                    FrameErrorInner::UnterminatedUstarStringField { field: found },
1554                ) => field == *found,
1555                (
1556                    Self::UnsupportedTypeflag(typeflag),
1557                    FrameErrorInner::UnsupportedTypeflag { typeflag: found },
1558                ) => typeflag == *found,
1559                _ => false,
1560            }
1561        }
1562    }
1563
1564    fn checksummed_header(mutate: impl FnOnce(&mut Block)) -> Block {
1565        let mut block = header(b'0', 0);
1566        mutate(&mut block);
1567        set_checksum(&mut block);
1568        block
1569    }
1570
1571    fn invalid_header_cases() -> Vec<(&'static str, Block, ExpectedHeaderError)> {
1572        let mut bad_magic = header(b'0', 0);
1573        bad_magic[IDENTITY_RANGE.start] = b'g';
1574        let mut bad_version = header(b'0', 0);
1575        bad_version[IDENTITY_RANGE.end - 2..IDENTITY_RANGE.end].copy_from_slice(b"  ");
1576        let mut bad_checksum = header(b'0', 0);
1577        bad_checksum[0] = b'X';
1578
1579        vec![
1580            ("magic", bad_magic, ExpectedHeaderError::InvalidIdentity),
1581            ("version", bad_version, ExpectedHeaderError::InvalidIdentity),
1582            (
1583                "checksum",
1584                bad_checksum,
1585                ExpectedHeaderError::InvalidChecksum,
1586            ),
1587            (
1588                "octal size",
1589                checksummed_header(|block| {
1590                    block[SIZE_RANGE].copy_from_slice(b"00000000008\0");
1591                }),
1592                ExpectedHeaderError::InvalidSize,
1593            ),
1594            (
1595                "base256 size",
1596                checksummed_header(|block| block[SIZE_RANGE.start] = 0x80),
1597                ExpectedHeaderError::InvalidSize,
1598            ),
1599            (
1600                "octal mode",
1601                checksummed_header(|block| {
1602                    block[MODE_RANGE].copy_from_slice(b"0000080\0");
1603                }),
1604                ExpectedHeaderError::InvalidNumericField("mode"),
1605            ),
1606            (
1607                "uid",
1608                checksummed_header(|block| {
1609                    block[UID_RANGE].copy_from_slice(b"invalid\0");
1610                }),
1611                ExpectedHeaderError::InvalidNumericField("uid"),
1612            ),
1613            (
1614                "gid",
1615                checksummed_header(|block| block[GID_RANGE.start] = b'8'),
1616                ExpectedHeaderError::InvalidNumericField("gid"),
1617            ),
1618            (
1619                "mtime",
1620                checksummed_header(|block| {
1621                    block[MTIME_RANGE].copy_from_slice(b"00000000008\0");
1622                }),
1623                ExpectedHeaderError::InvalidNumericField("mtime"),
1624            ),
1625            (
1626                "uname",
1627                checksummed_header(|block| block[UNAME_RANGE].fill(b'u')),
1628                ExpectedHeaderError::UnterminatedUstarStringField("uname"),
1629            ),
1630            (
1631                "gname",
1632                checksummed_header(|block| block[GNAME_RANGE].fill(b'g')),
1633                ExpectedHeaderError::UnterminatedUstarStringField("gname"),
1634            ),
1635            (
1636                "POSIX typeflag",
1637                header(b'X', 0),
1638                ExpectedHeaderError::UnsupportedTypeflag(b'X'),
1639            ),
1640            (
1641                "GNU typeflag",
1642                header(b'L', 0),
1643                ExpectedHeaderError::UnsupportedTypeflag(b'L'),
1644            ),
1645        ]
1646    }
1647
1648    #[test]
1649    fn frames_bare_member_across_fragmented_reads() {
1650        let mut bytes = Vec::new();
1651        append_block(&mut bytes, &header(b'0', 513));
1652        append_payload(&mut bytes, &[b'a'; BLOCK_SIZE]);
1653        append_payload(&mut bytes, b"b");
1654        append_terminator(&mut bytes);
1655
1656        let frames = collect(bytes, 7);
1657        assert_eq!(frames.len(), 3);
1658        let header = header_frame(&frames, 0);
1659        assert_eq!(header.kind, UstarKind::Regular);
1660        assert_eq!(header.declared_size, 513);
1661        assert_eq!(header.effective_size, 513);
1662        let first = data_frame(&frames, 1);
1663        let last = data_frame(&frames, 2);
1664        assert_eq!(first.len, BLOCK_SIZE);
1665        assert_eq!(last.len, 1);
1666        assert_eq!(last.owner, DataOwner::Member);
1667        assert!(first.completed_pax_records().is_none());
1668        assert!(last.completed_pax_records().is_none());
1669    }
1670
1671    #[test]
1672    fn frames_multiblock_pax_records_and_applies_size_override() {
1673        let mut payload = record("comment", &"x".repeat(BLOCK_SIZE));
1674        payload.extend_from_slice(&record("size", "513"));
1675        assert!(payload.len() > BLOCK_SIZE);
1676
1677        let mut bytes = Vec::new();
1678        append_pax(&mut bytes, b'x', &payload);
1679        append_block(&mut bytes, &header(b'0', 1));
1680        append_payload(&mut bytes, &[b'a'; BLOCK_SIZE]);
1681        append_payload(&mut bytes, b"b");
1682        append_terminator(&mut bytes);
1683
1684        let frames = collect(bytes, 19);
1685        assert_eq!(frames.len(), 6);
1686        let Frame::Pax(pax) = frames[0].as_ref().unwrap() else {
1687            panic!("expected pax header");
1688        };
1689        assert_eq!(pax.kind, PaxKind::Local);
1690        assert_eq!(pax.payload_size, payload.len() as u64);
1691        let first_pax_data = data_frame(&frames, 1);
1692        assert_eq!(first_pax_data.owner, DataOwner::Pax(PaxKind::Local));
1693        assert!(first_pax_data.completed_pax_records().is_none());
1694        let final_pax_data = data_frame(&frames, 2);
1695        assert_eq!(final_pax_data.owner, DataOwner::Pax(PaxKind::Local));
1696        assert_eq!(
1697            final_pax_data
1698                .completed_pax_records()
1699                .and_then(|records| records.last()),
1700            Some(&PaxRecord::Size(PaxValue::Value(513)))
1701        );
1702        let header = header_frame(&frames, 3);
1703        assert_eq!(header.declared_size, 1);
1704        assert_eq!(header.effective_size, 513);
1705        let last = data_frame(&frames, 5);
1706        assert_eq!(last.len, 1);
1707    }
1708
1709    #[test]
1710    fn rejects_oversized_pax_extensions_before_consuming_payload() {
1711        let mut payload = record("comment", "metadata");
1712        payload.extend_from_slice(&record("mtime", "1"));
1713        let declared_size = u64::try_from(payload.len()).expect("payload size should fit u64");
1714        for (case, typeflag) in [("local", b'x'), ("global", b'g')] {
1715            let mut bytes = Vec::new();
1716            append_pax(&mut bytes, typeflag, &payload);
1717            let frames = collect_with_max_pax_extension_size(bytes, BLOCK_SIZE, declared_size - 1);
1718            assert_eq!(frames.len(), 1, "{case}");
1719            assert!(matches!(
1720                last_error(&frames),
1721                FrameError {
1722                    position: 0,
1723                    inner: FrameErrorInner::ExtensionTooLarge {
1724                        format: ArchiveFormat::Pax,
1725                        size,
1726                        limit,
1727                    },
1728                } if *size == declared_size && *limit == declared_size - 1
1729            ));
1730        }
1731
1732        let frames = collect(
1733            header(b'x', DEFAULT_MAX_PAX_EXTENSION_SIZE + 1).to_vec(),
1734            BLOCK_SIZE,
1735        );
1736        assert_eq!(frames.len(), 1);
1737        assert!(matches!(
1738            last_error(&frames),
1739            FrameError {
1740                position: 0,
1741                inner: FrameErrorInner::ExtensionTooLarge {
1742                    format: ArchiveFormat::Pax,
1743                    size,
1744                    limit: DEFAULT_MAX_PAX_EXTENSION_SIZE,
1745                },
1746            } if *size == DEFAULT_MAX_PAX_EXTENSION_SIZE + 1
1747        ));
1748    }
1749
1750    #[test]
1751    fn oversized_pax_extension_does_not_read_its_payload_block() {
1752        let mut bytes = header(b'x', 1).to_vec();
1753        bytes.resize(BLOCK_SIZE * 2, 0);
1754        let consumed = Rc::new(Cell::new(0));
1755        let reader = CountingReader {
1756            bytes,
1757            position: 0,
1758            consumed: Rc::clone(&consumed),
1759        };
1760        let mut stream =
1761            TarStream::new(reader).with_policy(StreamPolicy::default().max_pax_extension_size(0));
1762
1763        assert!(matches!(
1764            ready(stream.next_frame()),
1765            Err(FrameError {
1766                position: 0,
1767                inner: FrameErrorInner::ExtensionTooLarge {
1768                    format: ArchiveFormat::Pax,
1769                    size: 1,
1770                    limit: 0,
1771                },
1772            })
1773        ));
1774        assert_eq!(consumed.get(), BLOCK_SIZE);
1775    }
1776
1777    #[test]
1778    fn accepts_pax_extensions_at_the_configured_limit() {
1779        let mut payload = record("comment", "metadata");
1780        payload.extend_from_slice(&record("ACME.attribute", "value"));
1781        for (case, typeflag) in [("local", b'x'), ("global", b'g')] {
1782            let mut bytes = Vec::new();
1783            append_pax(&mut bytes, typeflag, &payload);
1784            if typeflag == b'x' {
1785                append_block(&mut bytes, &header(b'0', 0));
1786            }
1787            append_terminator(&mut bytes);
1788
1789            let frames = collect_with_max_pax_extension_size(
1790                bytes,
1791                7,
1792                payload
1793                    .len()
1794                    .try_into()
1795                    .expect("payload size should fit u64"),
1796            );
1797            assert!(frames.iter().all(Result::is_ok), "{case}");
1798        }
1799    }
1800
1801    #[test]
1802    fn applies_global_pax_records_overrides_and_rejects_size_deletions() {
1803        let mut initial_global = record("comment", "old");
1804        initial_global.extend_from_slice(&record("size", "2"));
1805        let replacement_global = record("comment", "new");
1806        let mut local = record("comment", "local");
1807        local.extend_from_slice(&record("size", "3"));
1808        let mut deletion = record("comment", "");
1809        deletion.extend_from_slice(&record("size", ""));
1810
1811        let mut bytes = Vec::new();
1812        append_pax(&mut bytes, b'g', &initial_global);
1813        append_pax(&mut bytes, b'g', &replacement_global);
1814        append_block(&mut bytes, &header(b'0', 1));
1815        append_payload(&mut bytes, b"ab");
1816        append_pax(&mut bytes, b'x', &local);
1817        append_block(&mut bytes, &header(b'0', 1));
1818        append_payload(&mut bytes, b"abc");
1819        append_pax(&mut bytes, b'g', &deletion);
1820        append_block(&mut bytes, &header(b'5', 1));
1821        append_terminator(&mut bytes);
1822
1823        let frames = collect(bytes, 31);
1824        assert!(frames.iter().any(|frame| matches!(
1825            frame,
1826            Ok(Frame::Pax(PaxFrame {
1827                kind: PaxKind::Global,
1828                ..
1829            }))
1830        )));
1831        assert!(frames.iter().any(|frame| matches!(
1832            frame,
1833            Ok(Frame::Data(DataFrame {
1834                owner: DataOwner::Pax(PaxKind::Global),
1835                ..
1836            }))
1837        )));
1838        let completed_global_payloads: Vec<&[PaxRecord]> = frames
1839            .iter()
1840            .filter_map(|frame| match frame {
1841                Ok(Frame::Data(frame)) if frame.owner == DataOwner::Pax(PaxKind::Global) => {
1842                    frame.completed_pax_records()
1843                }
1844                _ => None,
1845            })
1846            .collect();
1847        assert_eq!(completed_global_payloads.len(), 3);
1848        assert_eq!(
1849            completed_global_payloads[2],
1850            [
1851                PaxRecord::Comment(PaxValue::Deleted),
1852                PaxRecord::Size(PaxValue::Deleted),
1853            ]
1854        );
1855        let headers: Vec<&HeaderFrame> = frames
1856            .iter()
1857            .filter_map(|frame| match frame {
1858                Ok(Frame::Header(header)) => Some(header),
1859                _ => None,
1860            })
1861            .collect();
1862        assert_eq!(headers.len(), 2);
1863        assert_eq!(headers[0].effective_size, 2);
1864        assert_eq!(headers[1].effective_size, 3);
1865        assert!(frames.iter().any(|frame| {
1866            matches!(
1867                frame,
1868                Ok(Frame::Data(frame))
1869                    if frame.owner == DataOwner::Pax(PaxKind::Local)
1870                        && frame.completed_pax_records() == Some(local_records("local", 3).as_slice())
1871            )
1872        }));
1873        assert!(matches!(
1874            last_error_inner(&frames),
1875            FrameErrorInner::DeletedPaxMetadata { keyword: "size" }
1876        ));
1877    }
1878
1879    fn local_records(comment: &str, size: u64) -> Vec<PaxRecord> {
1880        vec![
1881            PaxRecord::Comment(PaxValue::Value(comment.into())),
1882            PaxRecord::Size(PaxValue::Value(size)),
1883        ]
1884    }
1885
1886    #[test]
1887    fn allows_local_size_deletion_when_a_later_record_restores_size() {
1888        let mut local = record("size", "");
1889        local.extend_from_slice(&record("size", "2"));
1890        let mut bytes = Vec::new();
1891        append_pax(&mut bytes, b'x', &local);
1892        append_block(&mut bytes, &header(b'0', 1));
1893        append_payload(&mut bytes, b"ab");
1894        append_terminator(&mut bytes);
1895
1896        let frames = collect(bytes, BLOCK_SIZE);
1897        let header = header_frame(&frames, 2);
1898        assert_eq!(header.effective_size, 2);
1899        assert_eq!(
1900            data_frame(&frames, 1).completed_pax_records(),
1901            Some(
1902                [
1903                    PaxRecord::Size(PaxValue::Deleted),
1904                    PaxRecord::Size(PaxValue::Value(2)),
1905                ]
1906                .as_slice()
1907            )
1908        );
1909    }
1910
1911    #[test]
1912    fn pax_records_do_not_make_malformed_ordinary_header_fields_valid() {
1913        let cases = [
1914            (
1915                "local uid",
1916                b'x',
1917                record("uid", "1"),
1918                checksummed_header(|block| block[UID_RANGE].fill(b'u')),
1919                ExpectedHeaderError::InvalidNumericField("uid"),
1920            ),
1921            (
1922                "global gid",
1923                b'g',
1924                record("gid", "2"),
1925                checksummed_header(|block| block[GID_RANGE].fill(b'g')),
1926                ExpectedHeaderError::InvalidNumericField("gid"),
1927            ),
1928            (
1929                "local mtime",
1930                b'x',
1931                record("mtime", "3"),
1932                checksummed_header(|block| block[MTIME_RANGE].fill(b'm')),
1933                ExpectedHeaderError::InvalidNumericField("mtime"),
1934            ),
1935            (
1936                "global uname",
1937                b'g',
1938                record("uname", "user"),
1939                checksummed_header(|block| block[UNAME_RANGE].fill(b'u')),
1940                ExpectedHeaderError::UnterminatedUstarStringField("uname"),
1941            ),
1942            (
1943                "local gname",
1944                b'x',
1945                record("gname", "group"),
1946                checksummed_header(|block| block[GNAME_RANGE].fill(b'g')),
1947                ExpectedHeaderError::UnterminatedUstarStringField("gname"),
1948            ),
1949        ];
1950
1951        for (case, typeflag, records, malformed, expected) in cases {
1952            let mut bytes = Vec::new();
1953            append_pax(&mut bytes, typeflag, &records);
1954            append_block(&mut bytes, &malformed);
1955            append_terminator(&mut bytes);
1956
1957            let frames = collect(bytes, BLOCK_SIZE);
1958            assert!(
1959                expected.matches(last_error_inner(&frames)),
1960                "{case}: {frames:?}"
1961            );
1962        }
1963    }
1964
1965    #[test]
1966    fn accepts_all_nul_unused_device_fields() {
1967        let block = header(b'0', 0);
1968        assert_eq!(parse_octal(&block[DEVMAJOR_RANGE]), None);
1969        assert_eq!(parse_octal(&block[DEVMINOR_RANGE]), None);
1970
1971        let mut bytes = Vec::new();
1972        append_block(&mut bytes, &block);
1973        append_terminator(&mut bytes);
1974        assert!(collect(bytes, BLOCK_SIZE).iter().all(Result::is_ok));
1975    }
1976
1977    #[test]
1978    fn rejects_local_size_deletion_for_payload_free_members() {
1979        let global = record("size", "7");
1980        let local = record("size", "");
1981        let mut bytes = Vec::new();
1982        append_pax(&mut bytes, b'g', &global);
1983        append_pax(&mut bytes, b'x', &local);
1984        append_block(&mut bytes, &header(b'5', 3));
1985        append_terminator(&mut bytes);
1986
1987        assert!(matches!(
1988            last_error_inner(&collect(bytes, BLOCK_SIZE)),
1989            FrameErrorInner::DeletedPaxMetadata { keyword: "size" }
1990        ));
1991    }
1992
1993    #[test]
1994    fn rejects_deleted_size_when_member_payload_cannot_be_framed() {
1995        let records = record("size", "");
1996        for typeflag in *b"xg" {
1997            let mut bytes = Vec::new();
1998            append_pax(&mut bytes, typeflag, &records);
1999            append_block(&mut bytes, &header(b'0', 0));
2000
2001            assert!(
2002                matches!(
2003                    last_error_inner(&collect(bytes, BLOCK_SIZE)),
2004                    FrameErrorInner::DeletedPaxMetadata { keyword: "size" }
2005                ),
2006                "{typeflag:?}"
2007            );
2008        }
2009    }
2010
2011    #[test]
2012    fn allows_local_size_to_restore_an_active_global_deletion() {
2013        let global = record("size", "");
2014        let local = record("size", "2");
2015        let mut bytes = Vec::new();
2016        append_pax(&mut bytes, b'g', &global);
2017        append_pax(&mut bytes, b'x', &local);
2018        append_block(&mut bytes, &header(b'0', 1));
2019        append_payload(&mut bytes, b"ab");
2020        append_terminator(&mut bytes);
2021
2022        let frames = collect(bytes, BLOCK_SIZE);
2023        let header = header_frame(&frames, 4);
2024        assert_eq!(header.effective_size, 2);
2025        assert_eq!(
2026            data_frame(&frames, 1).completed_pax_records(),
2027            Some([PaxRecord::Size(PaxValue::Deleted)].as_slice())
2028        );
2029        assert_eq!(
2030            data_frame(&frames, 3).completed_pax_records(),
2031            Some([PaxRecord::Size(PaxValue::Value(2))].as_slice())
2032        );
2033    }
2034
2035    #[test]
2036    fn frames_pax_hard_link_bodies_from_header_or_size_override() {
2037        for (case, declared_size, override_size, header_index, data_index) in [
2038            ("physical size", 3, None, 0, 1),
2039            ("pax size", 0, Some("3"), 2, 3),
2040            ("pax size overrides physical size", 1, Some("3"), 2, 3),
2041        ] {
2042            let mut bytes = Vec::new();
2043            if let Some(override_size) = override_size {
2044                append_pax(&mut bytes, b'x', &record("size", override_size));
2045            }
2046            append_block(&mut bytes, &header(b'1', declared_size));
2047            append_payload(&mut bytes, b"abc");
2048            append_terminator(&mut bytes);
2049
2050            let frames = collect(bytes, BLOCK_SIZE);
2051            let header = header_frame(&frames, header_index);
2052            assert_eq!(header.format, ArchiveFormat::Pax, "{case}");
2053            assert_eq!(header.kind, UstarKind::HardLink, "{case}");
2054            assert_eq!(header.declared_size, declared_size, "{case}");
2055            assert_eq!(header.effective_size, 3, "{case}");
2056            assert_eq!(data_frame(&frames, data_index).len, 3, "{case}");
2057        }
2058    }
2059
2060    #[test]
2061    fn zero_data_block_is_not_a_terminator() {
2062        let mut bytes = Vec::new();
2063        append_block(&mut bytes, &header(b'0', BLOCK_SIZE as u64));
2064        append_block(&mut bytes, &[0; BLOCK_SIZE]);
2065        append_terminator(&mut bytes);
2066
2067        let frames = collect(bytes, BLOCK_SIZE);
2068        assert_eq!(frames.len(), 2);
2069        assert!(matches!(frames[1], Ok(Frame::Data(_))));
2070    }
2071
2072    #[test]
2073    fn zero_filled_block_inside_pax_payload_is_data() {
2074        let payload = record("comment", &"\0".repeat(BLOCK_SIZE * 3));
2075        let mut bytes = Vec::new();
2076        append_pax(&mut bytes, b'x', &payload);
2077        append_block(&mut bytes, &header(b'0', 0));
2078        append_terminator(&mut bytes);
2079
2080        let frames = collect(bytes, BLOCK_SIZE);
2081        assert!(frames.iter().any(|frame| matches!(
2082            frame,
2083            Ok(Frame::Data(DataFrame {
2084                block,
2085                owner: DataOwner::Pax(PaxKind::Local),
2086                ..
2087            })) if is_zero_block(block)
2088        )));
2089    }
2090
2091    #[test]
2092    fn frames_gnu_long_metadata_and_base256_payloads() {
2093        let mut bytes = Vec::new();
2094        append_block(&mut bytes, &gnu_base256_header(b'L', 513));
2095        append_payload(&mut bytes, &[b'n'; BLOCK_SIZE]);
2096        append_payload(&mut bytes, b"\0");
2097        append_gnu(&mut bytes, b'K', b"link\0");
2098        append_block(&mut bytes, &gnu_header(b'2', 0));
2099        append_terminator(&mut bytes);
2100
2101        let frames = collect(bytes, 13);
2102        assert_eq!(frames.len(), 6);
2103        assert!(matches!(
2104            frames[0].as_ref().unwrap(),
2105            Frame::Gnu(GnuFrame {
2106                kind: GnuKind::LongName,
2107                payload_size: 513,
2108                ..
2109            })
2110        ));
2111        let final_name = data_frame(&frames, 2);
2112        assert_eq!(final_name.owner, DataOwner::Gnu(GnuKind::LongName));
2113        assert_eq!(final_name.len, 1);
2114        assert!(final_name.completed_pax_records().is_none());
2115        assert!(matches!(
2116            frames[3].as_ref().unwrap(),
2117            Frame::Gnu(GnuFrame {
2118                kind: GnuKind::LongLink,
2119                ..
2120            })
2121        ));
2122        let header = header_frame(&frames, 5);
2123        assert_eq!(header.kind, UstarKind::SymbolicLink);
2124    }
2125
2126    #[test]
2127    fn rejects_header_format_type_and_field_errors() {
2128        for (case, block, expected) in invalid_header_cases() {
2129            let frames = collect(block.to_vec(), BLOCK_SIZE);
2130            let error = last_error_inner(&frames);
2131            assert!(expected.matches(error), "{case}: {error:?}");
2132        }
2133    }
2134
2135    #[test]
2136    fn rejects_malformed_gnu_numeric_fields() {
2137        for (field, range) in [
2138            ("mode", MODE_RANGE),
2139            ("uid", UID_RANGE),
2140            ("gid", GID_RANGE),
2141            ("mtime", MTIME_RANGE),
2142        ] {
2143            let mut block = gnu_header(b'0', 0);
2144            block[range].fill(b'x');
2145            set_checksum(&mut block);
2146
2147            assert!(matches!(
2148                last_error_inner(&collect(block.to_vec(), BLOCK_SIZE)),
2149                FrameErrorInner::InvalidNumericField { field: found, .. } if *found == field
2150            ));
2151        }
2152    }
2153
2154    #[test]
2155    fn rejects_nonzero_physical_sizes_for_payload_free_members() {
2156        for (format, block, kind) in [
2157            (ArchiveFormat::Pax, header(b'2', 1), UstarKind::SymbolicLink),
2158            (ArchiveFormat::Gnu, gnu_header(b'1', 1), UstarKind::HardLink),
2159            (
2160                ArchiveFormat::Gnu,
2161                gnu_header(b'2', 1),
2162                UstarKind::SymbolicLink,
2163            ),
2164            (
2165                ArchiveFormat::Pax,
2166                header(b'3', 1),
2167                UstarKind::CharacterDevice,
2168            ),
2169            (
2170                ArchiveFormat::Gnu,
2171                gnu_header(b'3', 1),
2172                UstarKind::CharacterDevice,
2173            ),
2174            (ArchiveFormat::Pax, header(b'4', 1), UstarKind::BlockDevice),
2175            (
2176                ArchiveFormat::Gnu,
2177                gnu_header(b'4', 1),
2178                UstarKind::BlockDevice,
2179            ),
2180            (ArchiveFormat::Pax, header(b'5', 1), UstarKind::Directory),
2181            (
2182                ArchiveFormat::Gnu,
2183                gnu_header(b'5', 1),
2184                UstarKind::Directory,
2185            ),
2186            (ArchiveFormat::Pax, header(b'6', 1), UstarKind::Fifo),
2187            (ArchiveFormat::Gnu, gnu_header(b'6', 1), UstarKind::Fifo),
2188        ] {
2189            let frames = collect(block.to_vec(), BLOCK_SIZE);
2190            assert!(
2191                matches!(
2192                    last_error_inner(&frames),
2193                    FrameErrorInner::InvalidMemberSize {
2194                        kind: found,
2195                        size: 1,
2196                    } if *found == kind
2197                ),
2198                "{format:?} {kind:?}"
2199            );
2200        }
2201    }
2202
2203    #[test]
2204    fn rejects_nonzero_declared_or_effective_pax_sizes_for_payload_free_members() {
2205        for (case, declared_size, override_size) in [("effective", 0, "1"), ("declared", 1, "0")] {
2206            for (typeflag, kind) in [
2207                (b'2', UstarKind::SymbolicLink),
2208                (b'3', UstarKind::CharacterDevice),
2209                (b'4', UstarKind::BlockDevice),
2210                (b'5', UstarKind::Directory),
2211                (b'6', UstarKind::Fifo),
2212            ] {
2213                let mut bytes = Vec::new();
2214                append_pax(&mut bytes, b'x', &record("size", override_size));
2215                append_block(&mut bytes, &header(typeflag, declared_size));
2216
2217                assert!(
2218                    matches!(
2219                        last_error_inner(&collect(bytes, BLOCK_SIZE)),
2220                        FrameErrorInner::InvalidMemberSize {
2221                            kind: found,
2222                            size: 1,
2223                        } if *found == kind
2224                    ),
2225                    "{case} {kind:?}"
2226                );
2227            }
2228        }
2229    }
2230
2231    #[test]
2232    fn header_errors_preserve_later_header_positions() {
2233        let position = BLOCK_SIZE as u64;
2234
2235        for (case, block, expected) in invalid_header_cases() {
2236            let mut bytes = Vec::new();
2237            append_block(&mut bytes, &header(b'0', 0));
2238            append_block(&mut bytes, &block);
2239            let frames = collect(bytes, BLOCK_SIZE);
2240            let error = last_error(&frames);
2241            assert_eq!(error.position, position, "{case}");
2242            assert!(expected.matches(&error.inner), "{case}: {error:?}");
2243        }
2244    }
2245
2246    #[test]
2247    fn rejects_invalid_pax_sequences() {
2248        assert!(matches!(
2249            last_error_inner(&collect(header(b'x', 0).to_vec(), BLOCK_SIZE)),
2250            FrameErrorInner::InvalidPaxRecord {
2251                source: PaxError::InvalidRecords { .. },
2252            }
2253        ));
2254
2255        let valid = record("path", "name");
2256        let mut consecutive = Vec::new();
2257        append_pax(&mut consecutive, b'x', &valid);
2258        append_block(&mut consecutive, &header(b'x', valid.len() as u64));
2259        assert!(matches!(
2260            last_error_inner(&collect(consecutive, BLOCK_SIZE)),
2261            FrameErrorInner::UnexpectedOrder { .. }
2262        ));
2263
2264        let mut missing_member = Vec::new();
2265        append_pax(&mut missing_member, b'x', &valid);
2266        assert!(matches!(
2267            last_error_inner(&collect(missing_member, BLOCK_SIZE)),
2268            FrameErrorInner::UnexpectedEof { .. }
2269        ));
2270    }
2271
2272    #[test]
2273    fn preserves_pax_parse_error_positions_in_stream() {
2274        let invalid = record("size", "bad");
2275        let mut bytes = Vec::new();
2276        append_block(&mut bytes, &header(b'0', 0));
2277        append_pax(&mut bytes, b'x', &invalid);
2278
2279        let frames = collect(bytes, BLOCK_SIZE);
2280        assert!(matches!(
2281            frames.last(),
2282            Some(Err(FrameError {
2283                position,
2284                inner: FrameErrorInner::InvalidPaxRecord {
2285                    source: PaxError::InvalidInteger { .. },
2286                },
2287            })) if *position == BLOCK_SIZE as u64
2288        ));
2289    }
2290
2291    #[test]
2292    fn accepts_binary_and_rejects_unknown_pax_charsets() {
2293        let mut global = record("hdrcharset", "BINARY");
2294        global.extend_from_slice(&record("path", "global"));
2295        let local = record("path", "local");
2296        let mut bytes = Vec::new();
2297        append_pax(&mut bytes, b'g', &global);
2298        append_pax(&mut bytes, b'x', &local);
2299        append_block(&mut bytes, &header(b'0', 0));
2300        append_terminator(&mut bytes);
2301        let frames = collect(bytes, BLOCK_SIZE);
2302        let member_header = header_frame(&frames, 4);
2303        assert_eq!(member_header.kind, UstarKind::Regular);
2304        assert_eq!(
2305            data_frame(&frames, 1).completed_pax_records(),
2306            Some(
2307                [
2308                    PaxRecord::HdrCharset(PaxValue::Value(HdrCharset::Binary)),
2309                    PaxRecord::Path(PaxValue::Value(PaxString::Binary(
2310                        b"global".to_vec().into(),
2311                    ))),
2312                ]
2313                .as_slice()
2314            )
2315        );
2316        assert_eq!(
2317            data_frame(&frames, 3).completed_pax_records(),
2318            Some(
2319                [PaxRecord::Path(PaxValue::Value(PaxString::Binary(
2320                    b"local".to_vec().into()
2321                )))]
2322                .as_slice()
2323            )
2324        );
2325
2326        let records = record("hdrcharset", "ISO-IR 8859 1 1998");
2327        let mut bytes = Vec::new();
2328        append_pax(&mut bytes, b'x', &records);
2329        assert!(matches!(
2330            last_error_inner(&collect(bytes, BLOCK_SIZE)),
2331            FrameErrorInner::InvalidPaxRecord {
2332                source: PaxError::UnsupportedCharset { value },
2333            } if value == "ISO-IR 8859 1 1998"
2334        ));
2335    }
2336
2337    #[test]
2338    fn rejects_invalid_gnu_sequences_and_sizes() {
2339        let mut duplicate = Vec::new();
2340        append_block(&mut duplicate, &gnu_header(b'L', 0));
2341        append_block(&mut duplicate, &gnu_header(b'L', 0));
2342        let mut long_link_for_regular = Vec::new();
2343        append_block(&mut long_link_for_regular, &gnu_header(b'K', 0));
2344        append_block(&mut long_link_for_regular, &gnu_header(b'0', 0));
2345        let mut dangling = Vec::new();
2346        append_block(&mut dangling, &gnu_header(b'L', 0));
2347        append_terminator(&mut dangling);
2348        for (case, bytes) in [
2349            ("duplicate", duplicate),
2350            ("long-link-for-regular", long_link_for_regular),
2351            ("dangling", dangling),
2352        ] {
2353            assert!(
2354                matches!(
2355                    last_error_inner(&collect(bytes, BLOCK_SIZE)),
2356                    FrameErrorInner::UnexpectedOrder { .. }
2357                ),
2358                "{case}"
2359            );
2360        }
2361
2362        assert!(matches!(
2363            last_error_inner(&collect(gnu_header(b'S', 0).to_vec(), BLOCK_SIZE)),
2364            FrameErrorInner::UnsupportedTypeflag { typeflag: b'S' }
2365        ));
2366
2367        let mut negative_size = gnu_header(b'0', 0);
2368        negative_size[SIZE_RANGE].fill(0xff);
2369        set_checksum(&mut negative_size);
2370        assert!(matches!(
2371            last_error_inner(&collect(negative_size.to_vec(), BLOCK_SIZE)),
2372            FrameErrorInner::InvalidSize { .. }
2373        ));
2374    }
2375
2376    #[test]
2377    fn detects_one_archive_family_and_rejects_mixing() {
2378        let mut posix_then_gnu = Vec::new();
2379        append_block(&mut posix_then_gnu, &header(b'0', 0));
2380        append_block(&mut posix_then_gnu, &gnu_header(b'0', 0));
2381        assert!(matches!(
2382            last_error_inner(&collect(posix_then_gnu, BLOCK_SIZE)),
2383            FrameErrorInner::FormatMismatch {
2384                expected: ArchiveFormat::Pax,
2385                found: ArchiveFormat::Gnu,
2386            }
2387        ));
2388
2389        // A family mismatch applies only to a successfully decoded header.
2390        let mut malformed_gnu = gnu_header(b'0', 0);
2391        malformed_gnu[0] = b'X';
2392        let mut posix_then_malformed_gnu = Vec::new();
2393        append_block(&mut posix_then_malformed_gnu, &header(b'0', 0));
2394        append_block(&mut posix_then_malformed_gnu, &malformed_gnu);
2395        assert!(matches!(
2396            last_error_inner(&collect(posix_then_malformed_gnu, BLOCK_SIZE)),
2397            FrameErrorInner::InvalidChecksum { .. }
2398        ));
2399
2400        let mut gnu_then_posix = Vec::new();
2401        append_block(&mut gnu_then_posix, &gnu_header(b'0', 0));
2402        append_block(&mut gnu_then_posix, &header(b'0', 0));
2403        assert!(matches!(
2404            last_error_inner(&collect(gnu_then_posix, BLOCK_SIZE)),
2405            FrameErrorInner::FormatMismatch {
2406                expected: ArchiveFormat::Gnu,
2407                found: ArchiveFormat::Pax,
2408            }
2409        ));
2410
2411        for typeflag in *b"xg" {
2412            assert!(
2413                matches!(
2414                    last_error_inner(&collect(gnu_header(typeflag, 0).to_vec(), BLOCK_SIZE)),
2415                    FrameErrorInner::UnsupportedTypeflag { typeflag: found } if *found == typeflag
2416                ),
2417                "{typeflag:?}"
2418            );
2419        }
2420
2421        let mut empty = Vec::new();
2422        append_terminator(&mut empty);
2423        let mut stream = TarStream::new(ChunkedReader::new(empty, BLOCK_SIZE));
2424        assert!(matches!(ready(stream.next_frame()), Ok(None)));
2425        assert_eq!(stream.format(), None);
2426    }
2427
2428    #[test]
2429    fn rejects_truncation_and_invalid_termination() {
2430        assert!(matches!(
2431            last_error_inner(&collect(vec![0; 3], 1)),
2432            FrameErrorInner::IncompleteBlock { read: 3 }
2433        ));
2434
2435        let mut payload_truncated = Vec::new();
2436        append_block(&mut payload_truncated, &header(b'0', 1));
2437        assert!(matches!(
2438            last_error_inner(&collect(payload_truncated, BLOCK_SIZE)),
2439            FrameErrorInner::TruncatedPayload {
2440                owner: DataOwner::Member,
2441                ..
2442            }
2443        ));
2444
2445        let mut pax_payload_truncated = Vec::new();
2446        append_block(&mut pax_payload_truncated, &header(b'x', 513));
2447        append_payload(&mut pax_payload_truncated, b"11 path=x\n");
2448        assert!(matches!(
2449            last_error_inner(&collect(pax_payload_truncated, BLOCK_SIZE)),
2450            FrameErrorInner::TruncatedPayload {
2451                owner: DataOwner::Pax(PaxKind::Local),
2452                ..
2453            }
2454        ));
2455
2456        let mut missing_second_zero = Vec::new();
2457        append_block(&mut missing_second_zero, &header(b'0', 0));
2458        append_block(&mut missing_second_zero, &[0; BLOCK_SIZE]);
2459        assert!(matches!(
2460            last_error_inner(&collect(missing_second_zero, BLOCK_SIZE)),
2461            FrameErrorInner::MissingEndMarker
2462        ));
2463
2464        let mut bad_second_zero = Vec::new();
2465        append_block(&mut bad_second_zero, &header(b'0', 0));
2466        append_block(&mut bad_second_zero, &[0; BLOCK_SIZE]);
2467        append_block(&mut bad_second_zero, &header(b'0', 0));
2468        assert!(matches!(
2469            last_error_inner(&collect(bad_second_zero, BLOCK_SIZE)),
2470            FrameErrorInner::InvalidEndMarker
2471        ));
2472    }
2473
2474    #[test]
2475    fn stream_is_fused_after_first_error() {
2476        let mut stream = TarStream::new(ChunkedReader::new(header(b'L', 0).to_vec(), BLOCK_SIZE));
2477        assert!(matches!(
2478            ready(stream.next_frame()),
2479            Err(FrameError {
2480                position: 0,
2481                inner: FrameErrorInner::UnsupportedTypeflag { typeflag: b'L' },
2482            })
2483        ));
2484        assert!(matches!(ready(stream.next_frame()), Ok(None)));
2485    }
2486}