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