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