Skip to main content

tar_framing/
logical.rs

1//! Member-oriented reading above the lossless physical frame stream.
2//!
3//! This API assembles PAX and GNU extension payloads with the ordinary members
4//! they describe. Each member carries a compact borrowed [`Header`], and each
5//! PAX member carries one unified [`PaxState`].
6
7use std::{borrow::Cow, mem, ops::Range};
8
9use tokio::io::AsyncRead;
10
11use crate::{
12    ArchiveFormat, Block, FrameError, FrameErrorInner, GnuKind, PaxKeyword, PaxKind, PaxRecord,
13    PaxString, PaxValue, StreamPolicy, UstarKind,
14    header::{GNAME_RANGE, LINK_NAME_RANGE, UNAME_RANGE},
15    pax::GlobalPaxRecords,
16    stream::{DataFrame, DataOwner, Frame, HeaderFrame, State, TarStream},
17};
18
19pub use crate::{PaxExtension, PaxState};
20
21const PAYLOAD_DRAIN_CHUNK_BYTES: usize = 1024 * 1024;
22
23/// A GNU long-name or long-link value needed to interpret a member.
24#[derive(Clone, Debug, Eq, PartialEq)]
25pub struct GnuMetadata {
26    /// The absolute byte position of the GNU extension header block.
27    pub position: u64,
28    /// The meaningful metadata payload bytes, excluding tar padding.
29    pub payload: Vec<u8>,
30}
31
32/// Extension metadata attached to one ordinary archive member.
33#[derive(Clone, Debug, Eq, PartialEq)]
34pub enum MemberExtensions<'a> {
35    /// Unified pax metadata applicable to an ordinary ustar member, borrowing
36    /// effective global values from the logical reader.
37    Pax(PaxState<'a>),
38    /// GNU metadata applying to an ordinary GNU member.
39    Gnu {
40        /// Optional GNU long-name metadata.
41        long_name: Option<GnuMetadata>,
42        /// Optional GNU long-link metadata.
43        long_link: Option<GnuMetadata>,
44    },
45}
46
47/// Extracted ordinary-header metadata for one logical archive member.
48///
49/// Unlike [`HeaderFrame`], this type does not retain the lossless physical
50/// header block. Its ordinary path and link-path fallbacks borrow reusable
51/// storage owned by [`TarReader`].
52#[derive(Clone, Copy, Debug, Eq, PartialEq)]
53pub struct Header<'a> {
54    /// The absolute byte position of the ordinary member header block.
55    pub position: u64,
56    /// The selected archive family of this member header.
57    pub format: ArchiveFormat,
58    /// The member type identified by the header.
59    pub kind: UstarKind,
60    /// The size encoded directly in the member header field.
61    pub declared_size: u64,
62    /// The size after applying applicable pax `size` records.
63    ///
64    /// This is also the number of payload bytes exposed through
65    /// [`MemberPayload`]. Member kinds that cannot carry payload are rejected
66    /// when either their declared or effective size is nonzero.
67    pub effective_size: u64,
68    /// Permission and mode bits decoded from the ordinary header, if present.
69    ///
70    /// Note that pax only defines the semantics of the lower 12 bits of this
71    /// field. Higher bits may or may not be set, and have no assigned semantics.
72    ///
73    /// This is [`None`] only when the field is wholly NUL and the framing policy
74    /// permits missing numeric metadata.
75    pub mode: Option<u64>,
76    /// Numeric user identifier from the ordinary header, if present.
77    ///
78    /// This is [`None`] only when the field is wholly NUL and the framing policy
79    /// permits missing numeric metadata.
80    ///
81    /// Applicable pax metadata may override or delete this fallback.
82    pub uid: Option<u64>,
83    /// Numeric group identifier from the ordinary header, if present.
84    ///
85    /// This is [`None`] only when the field is wholly NUL and the framing policy
86    /// permits missing numeric metadata.
87    ///
88    /// Applicable pax metadata may override or delete this fallback.
89    pub gid: Option<u64>,
90    /// Modification time in seconds from the ordinary header, if present.
91    ///
92    /// This is [`None`] only when the field is wholly NUL and the framing policy
93    /// permits missing numeric metadata.
94    ///
95    /// Applicable pax metadata may override or delete this fallback.
96    pub mtime: Option<u64>,
97    /// User name bytes from the ordinary header, empty if absent or unusable.
98    ///
99    /// Applicable pax metadata may override or delete this fallback.
100    pub uname: &'a [u8],
101    /// Group name bytes from the ordinary header, empty if absent or unusable.
102    ///
103    /// Applicable pax metadata may override or delete this fallback.
104    pub gname: &'a [u8],
105    header_path: &'a [u8],
106    link_name: &'a [u8],
107}
108
109/// One meaningful payload block belonging to an ordinary archive member.
110#[derive(Clone, Debug, Eq, PartialEq)]
111pub struct PayloadBlock {
112    /// The absolute byte position of this payload block.
113    pub position: u64,
114    /// The lossless payload block bytes, including any final padding.
115    pub block: Block,
116    /// The number of meaningful bytes in this block.
117    pub len: usize,
118}
119
120/// An ordinary archive member and its streaming payload cursor.
121pub struct MemberFrame<'a, R> {
122    /// The ordinary member header.
123    pub header: Header<'a>,
124    /// Extension metadata applying to this member.
125    pub extensions: MemberExtensions<'a>,
126    /// A cursor over the member payload bytes.
127    pub payload: MemberPayload<'a, R>,
128}
129
130impl<R> MemberFrame<'_, R> {
131    /// Returns the effective member path after applying pax or GNU metadata.
132    ///
133    /// An explicit pax deletion is an error because it also removes the
134    /// ordinary-header fallback required to identify this member. Empty paths
135    /// and paths containing embedded NUL bytes are also rejected.
136    pub fn effective_path(&self) -> Result<Cow<'_, [u8]>, FrameError> {
137        let path = effective_member_path(&self.header, &self.extensions)?;
138        if path.is_empty() {
139            return Err(FrameError::at(
140                self.header.position,
141                FrameErrorInner::EmptyMemberPath,
142            ));
143        }
144        reject_nul(self.header.position, "path", path.as_ref())?;
145        Ok(path)
146    }
147
148    /// Returns the effective member link target after applying pax or GNU metadata.
149    ///
150    /// An explicit pax deletion is an error because it also removes the
151    /// ordinary-header fallback required to identify a link target. Link
152    /// targets containing embedded NUL bytes are also rejected.
153    pub fn effective_link_path(&self) -> Result<Cow<'_, [u8]>, FrameError> {
154        let path = match &self.extensions {
155            MemberExtensions::Pax(state) => resolve_pax_text(
156                self.header.position,
157                state,
158                &PaxKeyword::LinkPath,
159                "linkpath",
160                Cow::Borrowed(self.header.link_name),
161                |record| match record {
162                    PaxRecord::LinkPath(value) => Some(value),
163                    _ => None,
164                },
165            ),
166            MemberExtensions::Gnu { long_link, .. } => match long_link {
167                Some(metadata) => Ok(Cow::Borrowed(parse_gnu_metadata(
168                    metadata,
169                    GnuKind::LongLink,
170                )?)),
171                None => Ok(Cow::Borrowed(self.header.link_name)),
172            },
173        }?;
174        reject_nul(self.header.position, "link path", path.as_ref())?;
175        Ok(path)
176    }
177}
178
179/// A streaming, typed cursor over one member's payload blocks.
180pub struct MemberPayload<'a, R> {
181    reader: &'a mut PayloadReader<R>,
182}
183
184/// A logical reader that assembles physical frames into archive-level items.
185///
186/// Unlike [`TarStream`], this API attaches PAX or GNU extension metadata to the
187/// ordinary member it describes. Each PAX member carries one [`PaxState`] with
188/// effective metadata and newly encountered positioned extensions. Ordinary
189/// header path and link-path fallbacks are copied into reusable storage and
190/// borrowed by the returned [`Header`].
191pub struct TarReader<R> {
192    // Keep the logical effective state outside `payload` so a returned
193    // `PaxState` can borrow it while `MemberPayload` mutably borrows only the
194    // independent payload machinery. `TarStream` maintains its own physical
195    // copy for framing decisions.
196    global_pax_records: Option<GlobalPaxRecords>,
197    payload: PayloadReader<R>,
198    header_storage: HeaderStorage,
199    pending_extensions: PendingExtensions,
200    extension_payload: Option<ExtensionPayload>,
201}
202
203/// Payload state kept separate so [`MemberPayload`] can borrow it mutably while
204/// the logical [`Header`] borrows reusable header storage.
205struct PayloadReader<R> {
206    stream: TarStream<R>,
207    remaining: u64,
208    drain_buffer: Vec<u8>,
209}
210
211/// Logical member metadata retained across cancellation of [`TarReader::next_frame`].
212#[derive(Default)]
213struct PendingExtensions {
214    global_pax: Vec<PaxExtension>,
215    local_pax: Option<PaxExtension>,
216    gnu_long_name: Option<GnuMetadata>,
217    gnu_long_link: Option<GnuMetadata>,
218}
219
220impl PendingExtensions {
221    fn set_gnu(&mut self, kind: GnuKind, metadata: GnuMetadata) {
222        *match kind {
223            GnuKind::LongName => &mut self.gnu_long_name,
224            GnuKind::LongLink => &mut self.gnu_long_link,
225        } = Some(metadata);
226    }
227}
228
229/// An extension payload being assembled across physical frames.
230enum ExtensionPayload {
231    Pax {
232        position: u64,
233        kind: PaxKind,
234    },
235    Gnu {
236        position: u64,
237        kind: GnuKind,
238        remaining: u64,
239        payload: Vec<u8>,
240    },
241}
242
243#[derive(Default)]
244struct HeaderStorage {
245    path: Vec<u8>,
246    link_name: Vec<u8>,
247    uname: Vec<u8>,
248    gname: Vec<u8>,
249}
250
251impl HeaderStorage {
252    fn update<'a>(&'a mut self, frame: &HeaderFrame) -> Header<'a> {
253        frame.copy_header_path_into(&mut self.path);
254        copy_string_field_into(&frame.block, LINK_NAME_RANGE, &mut self.link_name);
255        copy_string_field_into(&frame.block, UNAME_RANGE, &mut self.uname);
256        copy_string_field_into(&frame.block, GNAME_RANGE, &mut self.gname);
257        Header {
258            position: frame.position,
259            format: frame.format,
260            kind: frame.kind,
261            declared_size: frame.declared_size,
262            effective_size: frame.effective_size,
263            mode: frame.mode,
264            uid: frame.uid,
265            gid: frame.gid,
266            mtime: frame.mtime,
267            uname: &self.uname,
268            gname: &self.gname,
269            header_path: &self.path,
270            link_name: &self.link_name,
271        }
272    }
273}
274
275fn copy_string_field_into(block: &Block, range: Range<usize>, destination: &mut Vec<u8>) {
276    let field = &block[range];
277    let len = field
278        .iter()
279        .position(|byte| *byte == 0)
280        .unwrap_or(field.len());
281    destination.clear();
282    destination.extend_from_slice(&field[..len]);
283}
284
285impl<R> TarReader<R> {
286    /// Creates a new logical reader from an uncompressed tar reader.
287    pub fn new(reader: R) -> Self {
288        Self {
289            global_pax_records: None,
290            payload: PayloadReader {
291                stream: TarStream::new(reader),
292                remaining: 0,
293                drain_buffer: Vec::new(),
294            },
295            header_storage: HeaderStorage::default(),
296            pending_extensions: PendingExtensions::default(),
297            extension_payload: None,
298        }
299    }
300
301    /// Configures the framing policy used by this reader.
302    ///
303    /// Call before reading any members.
304    pub fn with_policy(mut self, policy: StreamPolicy) -> Self {
305        self.payload.stream = self.payload.stream.with_policy(policy);
306        self
307    }
308}
309
310impl<R: AsyncRead + Unpin> TarReader<R> {
311    /// Returns the next ordinary archive member.
312    ///
313    /// If the preceding member payload was not fully consumed, it is first
314    /// drained and validated. Extension metadata is then consumed and attached
315    /// before the next member is returned. Global pax updates not followed by
316    /// an ordinary member are consumed and ignored. A returned pax state is a
317    /// view borrowing this reader; it must be dropped before requesting another
318    /// member.
319    pub async fn next_frame(&mut self) -> Result<Option<MemberFrame<'_, R>>, FrameError> {
320        if matches!(self.payload.stream.state, State::Failed) {
321            self.clear_extension_state();
322            return Ok(None);
323        }
324
325        if let Err(error) = self.payload.drain_payload().await {
326            self.clear_extension_state();
327            return Err(error);
328        }
329
330        loop {
331            let frame = match self.payload.stream.next_frame().await {
332                Ok(Some(frame)) => frame,
333                Err(error) => {
334                    self.clear_extension_state();
335                    return Err(error);
336                }
337                Ok(None) => {
338                    self.clear_extension_state();
339                    return Ok(None);
340                }
341            };
342            match frame {
343                Frame::Pax(frame) => {
344                    self.extension_payload = Some(ExtensionPayload::Pax {
345                        position: frame.position,
346                        kind: frame.kind,
347                    });
348                }
349                Frame::Gnu(frame) => {
350                    if frame.payload_size == 0 {
351                        let metadata = GnuMetadata {
352                            position: frame.position,
353                            payload: Vec::new(),
354                        };
355                        self.pending_extensions.set_gnu(frame.kind, metadata);
356                    } else {
357                        self.extension_payload = Some(ExtensionPayload::Gnu {
358                            position: frame.position,
359                            kind: frame.kind,
360                            remaining: frame.payload_size,
361                            payload: Vec::new(),
362                        });
363                    }
364                }
365                Frame::Header(header) => {
366                    let pending_extensions = mem::take(&mut self.pending_extensions);
367                    let extensions = match header.format {
368                        ArchiveFormat::Pax => MemberExtensions::Pax(PaxState::new(
369                            self.global_pax_records.as_ref(),
370                            pending_extensions.global_pax,
371                            pending_extensions.local_pax,
372                        )),
373                        ArchiveFormat::Gnu => MemberExtensions::Gnu {
374                            long_name: pending_extensions.gnu_long_name,
375                            long_link: pending_extensions.gnu_long_link,
376                        },
377                    };
378                    self.payload.remaining = header.effective_size;
379                    let header = self.header_storage.update(&header);
380                    return Ok(Some(MemberFrame {
381                        header,
382                        extensions,
383                        payload: MemberPayload {
384                            reader: &mut self.payload,
385                        },
386                    }));
387                }
388                Frame::Data(frame) => {
389                    if let Err(error) = self.process_extension_data(frame) {
390                        self.clear_extension_state();
391                        return Err(error);
392                    }
393                }
394            }
395        }
396    }
397
398    fn clear_extension_state(&mut self) {
399        self.pending_extensions = PendingExtensions::default();
400        self.extension_payload = None;
401    }
402
403    fn process_extension_data(&mut self, frame: DataFrame) -> Result<(), FrameError> {
404        let Some(payload) = self.extension_payload.take() else {
405            return Err(FrameError::unexpected_order(
406                frame.position,
407                "extension header or ordinary member header",
408                "unattached payload data",
409            ));
410        };
411        match payload {
412            ExtensionPayload::Pax { position, kind } => {
413                if frame.owner != DataOwner::Pax(kind) {
414                    return Err(FrameError::unexpected_order(
415                        frame.position,
416                        "pax extension payload",
417                        "different payload data",
418                    ));
419                }
420                if let Some(records) = frame.into_completed_pax_records() {
421                    match kind {
422                        PaxKind::Global => {
423                            records.apply_global(&mut self.global_pax_records);
424                            self.pending_extensions
425                                .global_pax
426                                .push(PaxExtension::new(position, kind, records));
427                        }
428                        PaxKind::Local => {
429                            self.pending_extensions.local_pax =
430                                Some(PaxExtension::new(position, kind, records));
431                        }
432                    }
433                } else {
434                    self.extension_payload = Some(ExtensionPayload::Pax { position, kind });
435                }
436            }
437            ExtensionPayload::Gnu {
438                position,
439                kind,
440                mut remaining,
441                mut payload,
442            } => {
443                if frame.owner != DataOwner::Gnu(kind) {
444                    return Err(FrameError::unexpected_order(
445                        frame.position,
446                        "GNU metadata payload",
447                        "different payload data",
448                    ));
449                }
450                let len = u64::try_from(frame.len).map_err(|_| {
451                    FrameError::arithmetic_overflow(frame.position, "GNU metadata payload length")
452                })?;
453                remaining = remaining.checked_sub(len).ok_or_else(|| {
454                    FrameError::unexpected_order(
455                        frame.position,
456                        "bounded GNU metadata payload",
457                        "oversized GNU metadata payload",
458                    )
459                })?;
460                payload.extend_from_slice(&frame.block[..frame.len]);
461                if remaining == 0 {
462                    let metadata = GnuMetadata { position, payload };
463                    self.pending_extensions.set_gnu(kind, metadata);
464                } else {
465                    self.extension_payload = Some(ExtensionPayload::Gnu {
466                        position,
467                        kind,
468                        remaining,
469                        payload,
470                    });
471                }
472            }
473        }
474        Ok(())
475    }
476}
477
478impl<R: AsyncRead + Unpin> PayloadReader<R> {
479    async fn next_payload_block(&mut self) -> Result<Option<PayloadBlock>, FrameError> {
480        if self.remaining == 0 {
481            return Ok(None);
482        }
483        let (position, block, len) = self.stream.read_member_block().await?;
484        let payload_len = u64::try_from(len)
485            .map_err(|_| FrameError::arithmetic_overflow(position, "member payload length"))?;
486        self.remaining = self.remaining.checked_sub(payload_len).ok_or_else(|| {
487            FrameError::unexpected_order(
488                position,
489                "bounded member payload",
490                "oversized member payload",
491            )
492        })?;
493        Ok(Some(PayloadBlock {
494            position,
495            block,
496            len,
497        }))
498    }
499
500    async fn next_payload_chunk(
501        &mut self,
502        buffer: &mut Vec<u8>,
503        target_len: usize,
504    ) -> Result<bool, FrameError> {
505        if self.remaining == 0 {
506            return Ok(false);
507        }
508        let len = self.stream.read_member_chunk(buffer, target_len).await?;
509        let len = u64::try_from(len).map_err(|_| {
510            FrameError::arithmetic_overflow(self.stream.position, "member payload chunk length")
511        })?;
512        self.remaining = self.remaining.checked_sub(len).ok_or_else(|| {
513            FrameError::unexpected_order(
514                self.stream.position,
515                "bounded member payload",
516                "oversized member payload chunk",
517            )
518        })?;
519        Ok(true)
520    }
521
522    async fn drain_payload(&mut self) -> Result<(), FrameError> {
523        let mut buffer = mem::take(&mut self.drain_buffer);
524        let result = loop {
525            match self
526                .next_payload_chunk(&mut buffer, PAYLOAD_DRAIN_CHUNK_BYTES)
527                .await
528            {
529                Ok(true) => {}
530                Ok(false) => break Ok(()),
531                Err(error) => break Err(error),
532            }
533        };
534        self.drain_buffer = buffer;
535        result
536    }
537}
538
539impl<R: AsyncRead + Unpin> MemberPayload<'_, R> {
540    /// Returns the next meaningful payload block, excluding final padding in `len`.
541    pub async fn next_block(&mut self) -> Result<Option<PayloadBlock>, FrameError> {
542        self.reader.next_payload_block().await
543    }
544
545    /// Reads validated payload bytes into a reusable chunk buffer.
546    ///
547    /// When this returns `true`, the buffer's existing contents are replaced.
548    /// When the payload is exhausted, it returns `false` without changing the
549    /// buffer so its initialized storage can be reused. Complete physical blocks
550    /// are read directly into it until the chunk contains at least `target_len`
551    /// bytes or the payload ends. The target is raised to one physical block
552    /// when it is smaller, and final-block padding is removed before this
553    /// returns. This preserves [`Self::next_block`] as the lossless interface
554    /// while allowing higher-level consumers to amortize per-block bookkeeping
555    /// and copies.
556    pub async fn next_chunk(
557        &mut self,
558        buffer: &mut Vec<u8>,
559        target_len: usize,
560    ) -> Result<bool, FrameError> {
561        self.reader.next_payload_chunk(buffer, target_len).await
562    }
563
564    /// Discards and validates all remaining payload bytes using reusable storage.
565    pub async fn skip(self) -> Result<(), FrameError> {
566        self.reader.drain_payload().await
567    }
568}
569
570fn effective_member_path<'a>(
571    header: &Header<'a>,
572    extensions: &'a MemberExtensions<'_>,
573) -> Result<Cow<'a, [u8]>, FrameError> {
574    match extensions {
575        MemberExtensions::Pax(state) => resolve_pax_text(
576            header.position,
577            state,
578            &PaxKeyword::Path,
579            "path",
580            Cow::Borrowed(header.header_path),
581            |record| match record {
582                PaxRecord::Path(value) => Some(value),
583                _ => None,
584            },
585        ),
586        MemberExtensions::Gnu { long_name, .. } => match long_name {
587            Some(metadata) => Ok(Cow::Borrowed(parse_gnu_metadata(
588                metadata,
589                GnuKind::LongName,
590            )?)),
591            None => Ok(Cow::Borrowed(header.header_path)),
592        },
593    }
594}
595
596fn reject_nul(position: u64, field: &'static str, value: &[u8]) -> Result<(), FrameError> {
597    if value.contains(&0) {
598        return Err(FrameError::at(
599            position,
600            FrameErrorInner::NulInMemberName { field },
601        ));
602    }
603    Ok(())
604}
605
606fn resolve_pax_text<'a>(
607    position: u64,
608    state: &'a PaxState<'_>,
609    keyword: &PaxKeyword,
610    field: &'static str,
611    header_value: Cow<'a, [u8]>,
612    select: fn(&PaxRecord) -> Option<&PaxValue<PaxString>>,
613) -> Result<Cow<'a, [u8]>, FrameError> {
614    if let Some(value) = state.effective_record(keyword).and_then(select) {
615        return pax_value(position, field, value);
616    }
617    Ok(header_value)
618}
619
620/// Return the raw bytes of a pax record, erroring if the record is a tombstone
621/// (i.e.) explicitly deleted.
622fn pax_value<'a>(
623    position: u64,
624    keyword: &'static str,
625    value: &'a PaxValue<PaxString>,
626) -> Result<Cow<'a, [u8]>, FrameError> {
627    match value {
628        PaxValue::Value(PaxString::Utf8(value)) => Ok(Cow::Borrowed(value.as_bytes())),
629        PaxValue::Value(PaxString::Binary(value)) => Ok(Cow::Borrowed(value.as_ref())),
630        // A pax value that has been explicitly deleted does *not*
631        // result in a fallthrough to the corresponding ustar header value:
632        //
633        // "If a keyword in an extended header record (or in a -o option-
634        // argument) overrides or deletes a corresponding field in the ustar
635        // header block, pax shall ignore the contents of that header block
636        // field."
637        //
638        // See: pax spec, "pax Extended Header"
639        PaxValue::Deleted => Err(FrameError::deleted_pax_metadata(position, keyword)),
640    }
641}
642
643fn parse_gnu_metadata(metadata: &GnuMetadata, kind: GnuKind) -> Result<&[u8], FrameError> {
644    let terminator = metadata
645        .payload
646        .iter()
647        .position(|byte| *byte == 0)
648        .ok_or_else(|| {
649            FrameError::invalid_gnu_metadata(metadata.position, kind, "value is not NUL-terminated")
650        })?;
651
652    // TODO: Make this configurable through some kind of policy?
653    // Might be overly strict in practice.
654    if metadata.payload[terminator..].iter().any(|byte| *byte != 0) {
655        return Err(FrameError::invalid_gnu_metadata(
656            metadata.position,
657            kind,
658            "non-NUL bytes follow the terminator",
659        ));
660    }
661    Ok(&metadata.payload[..terminator])
662}
663
664#[cfg(test)]
665mod tests {
666    use tokio::io::AsyncRead;
667
668    use super::*;
669    use crate::{
670        BLOCK_SIZE, DEFAULT_MAX_GNU_EXTENSION_SIZE, FrameError, FrameErrorInner, PaxRecord,
671        PaxValue,
672        header::{
673            GID_RANGE, GNAME_RANGE, LINK_NAME_RANGE, MODE_RANGE, MTIME_RANGE, NAME_RANGE,
674            PREFIX_RANGE, TYPEFLAG_OFFSET, UID_RANGE, UNAME_RANGE,
675        },
676        stream::DataOwner,
677        test_support::{
678            ChunkedReader, append_block, append_gnu, append_pax, append_payload, append_terminator,
679            cancel_pending, gnu_header, header, ready, ready_ok, record, set_checksum,
680        },
681    };
682
683    fn set_field(block: &mut Block, range: std::ops::Range<usize>, value: &[u8]) {
684        block[range.clone()].fill(0);
685        block[range.start..range.start + value.len()].copy_from_slice(value);
686    }
687
688    async fn next_member<R: AsyncRead + Unpin>(
689        reader: &mut TarReader<R>,
690    ) -> Result<MemberFrame<'_, R>, FrameError> {
691        let Some(member) = reader.next_frame().await? else {
692            panic!("expected logical member");
693        };
694        Ok(member)
695    }
696
697    fn pax_state<'a, R>(member: &'a MemberFrame<'_, R>) -> Option<&'a PaxState<'a>> {
698        if let MemberExtensions::Pax(state) = &member.extensions {
699            Some(state)
700        } else {
701            None
702        }
703    }
704
705    fn member_followed_by_empty_member(payload: &[u8]) -> (Vec<u8>, u64) {
706        let mut bytes = Vec::new();
707        append_pax(&mut bytes, b'0', payload);
708        let next_position = u64::try_from(bytes.len()).expect("test position should fit u64");
709        append_block(&mut bytes, &header(b'0', 0));
710        append_terminator(&mut bytes);
711        (bytes, next_position)
712    }
713
714    #[test]
715    fn exposes_ordinary_header_metadata_and_decodes_modes() {
716        let mut ustar_header = header(b'2', 0);
717        set_field(&mut ustar_header, NAME_RANGE, b"file");
718        set_field(&mut ustar_header, PREFIX_RANGE, b"dir");
719        set_field(&mut ustar_header, LINK_NAME_RANGE, b"target");
720        ustar_header[MODE_RANGE].copy_from_slice(b"0100644\0");
721        ustar_header[UID_RANGE].copy_from_slice(b"0000001\0");
722        ustar_header[GID_RANGE].copy_from_slice(b"0000002\0");
723        ustar_header[MTIME_RANGE].copy_from_slice(b"00000000003\0");
724        set_field(&mut ustar_header, UNAME_RANGE, b"user");
725        set_field(&mut ustar_header, GNAME_RANGE, b"group");
726        set_checksum(&mut ustar_header);
727
728        let mut empty_header = header(b'0', 0);
729        for range in [
730            MODE_RANGE,
731            UID_RANGE,
732            GID_RANGE,
733            MTIME_RANGE,
734            UNAME_RANGE,
735            GNAME_RANGE,
736        ] {
737            empty_header[range].fill(0);
738        }
739        set_checksum(&mut empty_header);
740
741        ready_ok(async {
742            let mut bytes = Vec::new();
743            append_block(&mut bytes, &ustar_header);
744            append_block(&mut bytes, &empty_header);
745            append_terminator(&mut bytes);
746            let mut reader = TarReader::new(ChunkedReader::new(bytes, BLOCK_SIZE));
747            {
748                let member = next_member(&mut reader).await?;
749                assert_eq!(member.header.format, ArchiveFormat::Pax);
750                assert_eq!(member.header.header_path, b"dir/file");
751                assert_eq!(member.header.link_name, b"target");
752                assert_eq!(member.header.mode, Some(0o100644));
753                assert_eq!(member.header.uid, Some(1));
754                assert_eq!(member.header.gid, Some(2));
755                assert_eq!(member.header.mtime, Some(3));
756                assert_eq!(member.header.uname, b"user");
757                assert_eq!(member.header.gname, b"group");
758                assert_eq!(member.effective_path()?.as_ref(), b"dir/file");
759                assert_eq!(member.effective_link_path()?.as_ref(), b"target");
760            }
761            let member = next_member(&mut reader).await?;
762            assert_eq!(member.header.mode, None);
763            assert_eq!(member.header.uid, None);
764            assert_eq!(member.header.gid, None);
765            assert_eq!(member.header.mtime, None);
766            assert!(member.header.uname.is_empty());
767            assert!(member.header.gname.is_empty());
768            Ok(())
769        });
770
771        let mut gnu_member_header = gnu_header(b'0', 0);
772        set_field(&mut gnu_member_header, NAME_RANGE, b"name");
773        set_field(&mut gnu_member_header, PREFIX_RANGE, b"ignored");
774        gnu_member_header[MODE_RANGE].fill(0);
775        gnu_member_header[MODE_RANGE.start] = 0x80;
776        gnu_member_header[MODE_RANGE.end - 2..MODE_RANGE.end].copy_from_slice(&[0x81, 0xa4]);
777        set_checksum(&mut gnu_member_header);
778
779        let mut empty_gnu_header = gnu_header(b'0', 0);
780        for range in [MODE_RANGE, UID_RANGE, GID_RANGE, MTIME_RANGE] {
781            empty_gnu_header[range].fill(0);
782        }
783        set_checksum(&mut empty_gnu_header);
784
785        ready_ok(async {
786            let mut bytes = Vec::new();
787            append_block(&mut bytes, &gnu_member_header);
788            append_block(&mut bytes, &empty_gnu_header);
789            append_terminator(&mut bytes);
790            let mut reader = TarReader::new(ChunkedReader::new(bytes, BLOCK_SIZE));
791            {
792                let member = next_member(&mut reader).await?;
793                assert_eq!(member.header.format, ArchiveFormat::Gnu);
794                assert_eq!(member.header.header_path, b"name");
795                assert_eq!(member.header.mode, Some(0o100644));
796                assert_eq!(member.header.uid, Some(0));
797                assert_eq!(member.header.gid, Some(0));
798                assert_eq!(member.header.mtime, Some(0));
799            }
800            let member = next_member(&mut reader).await?;
801            assert_eq!(member.header.mode, None);
802            assert_eq!(member.header.uid, None);
803            assert_eq!(member.header.gid, None);
804            assert_eq!(member.header.mtime, None);
805            Ok(())
806        });
807    }
808
809    #[test]
810    fn preserves_ustar_separator_when_name_is_empty() {
811        let mut ustar_header = header(b'5', 0);
812        set_field(&mut ustar_header, NAME_RANGE, b"");
813        set_field(&mut ustar_header, PREFIX_RANGE, b"victim");
814        set_checksum(&mut ustar_header);
815
816        ready_ok(async {
817            let mut bytes = Vec::new();
818            append_block(&mut bytes, &ustar_header);
819            append_terminator(&mut bytes);
820            let mut reader = TarReader::new(ChunkedReader::new(bytes, BLOCK_SIZE));
821            let member = next_member(&mut reader).await?;
822            assert_eq!(member.header.header_path, b"victim/");
823            assert_eq!(member.effective_path()?.as_ref(), b"victim/");
824            Ok(())
825        });
826    }
827
828    #[test]
829    fn keeps_borrowed_header_metadata_available_while_streaming_payload() {
830        let mut member_header = header(b'0', 1);
831        set_field(&mut member_header, NAME_RANGE, b"file");
832        set_field(&mut member_header, PREFIX_RANGE, b"dir");
833        set_field(&mut member_header, LINK_NAME_RANGE, b"target");
834        member_header[MODE_RANGE].copy_from_slice(b"0000755\0");
835        set_checksum(&mut member_header);
836
837        ready_ok(async {
838            let mut bytes = Vec::new();
839            append_block(&mut bytes, &member_header);
840            append_payload(&mut bytes, b"x");
841            append_terminator(&mut bytes);
842            let mut reader = TarReader::new(ChunkedReader::new(bytes, BLOCK_SIZE));
843            let mut member = next_member(&mut reader).await?;
844
845            assert!(member.payload.next_block().await?.is_some());
846            assert_eq!(member.header.header_path, b"dir/file");
847            assert_eq!(member.header.link_name, b"target");
848            assert_eq!(member.header.mode, Some(0o755));
849            assert_eq!(member.effective_path()?.as_ref(), b"dir/file");
850            assert_eq!(member.effective_link_path()?.as_ref(), b"target");
851            Ok(())
852        });
853    }
854
855    #[test]
856    fn resolves_pax_path_precedence_and_deletions() {
857        let mut global = record("path", "global");
858        global.extend_from_slice(&record("linkpath", "global-link"));
859        let mut local = record("path", "local");
860        local.extend_from_slice(&record("linkpath", ""));
861        let mut bytes = Vec::new();
862        append_pax(&mut bytes, b'g', &global);
863        append_pax(&mut bytes, b'x', &local);
864        append_block(&mut bytes, &header(b'2', 0));
865        append_block(&mut bytes, &header(b'2', 0));
866        append_terminator(&mut bytes);
867
868        ready_ok(async {
869            let mut reader = TarReader::new(ChunkedReader::new(bytes, BLOCK_SIZE));
870            {
871                let member = next_member(&mut reader).await?;
872                assert_eq!(member.effective_path()?.as_ref(), b"local");
873                assert!(matches!(
874                    member.effective_link_path(),
875                    Err(FrameError {
876                        position: 2048,
877                        inner: FrameErrorInner::DeletedPaxMetadata {
878                            keyword: "linkpath"
879                        },
880                    })
881                ));
882            }
883            let member = next_member(&mut reader).await?;
884            assert_eq!(member.effective_path()?.as_ref(), b"global");
885            assert_eq!(member.effective_link_path()?.as_ref(), b"global-link");
886            Ok(())
887        });
888    }
889
890    #[test]
891    fn rejects_empty_effective_member_paths() {
892        for (case, mut bytes) in [
893            ("pax-header", {
894                let mut bytes = Vec::new();
895                let mut member = header(b'0', 0);
896                set_field(&mut member, NAME_RANGE, b"");
897                set_field(&mut member, PREFIX_RANGE, b"");
898                set_checksum(&mut member);
899                append_block(&mut bytes, &member);
900                bytes
901            }),
902            ("gnu-header", {
903                let mut bytes = Vec::new();
904                let mut member = gnu_header(b'0', 0);
905                set_field(&mut member, NAME_RANGE, b"");
906                set_checksum(&mut member);
907                append_block(&mut bytes, &member);
908                bytes
909            }),
910            ("gnu-long-name", {
911                let mut bytes = Vec::new();
912                append_gnu(&mut bytes, b'L', b"\0");
913                let mut member = gnu_header(b'0', 0);
914                set_field(&mut member, NAME_RANGE, b"physical");
915                set_checksum(&mut member);
916                append_block(&mut bytes, &member);
917                bytes
918            }),
919        ] {
920            append_terminator(&mut bytes);
921            let result: Result<(), FrameError> = ready(async {
922                let mut reader = TarReader::new(ChunkedReader::new(bytes, BLOCK_SIZE));
923                let member = next_member(&mut reader).await?;
924                member.effective_path().map(|_| ())
925            });
926            assert!(
927                matches!(
928                    result,
929                    Err(FrameError {
930                        inner: FrameErrorInner::EmptyMemberPath,
931                        ..
932                    })
933                ),
934                "{case}: {result:?}"
935            );
936        }
937    }
938
939    #[test]
940    fn rejects_nul_in_effective_member_names() {
941        for (field, mut bytes) in [
942            ("path", {
943                let mut bytes = Vec::new();
944                append_pax(&mut bytes, b'x', &record("path", "bad\0name"));
945                append_block(&mut bytes, &header(b'0', 0));
946                bytes
947            }),
948            ("link path", {
949                let mut bytes = Vec::new();
950                append_pax(&mut bytes, b'x', &record("linkpath", "bad\0target"));
951                append_block(&mut bytes, &header(b'2', 0));
952                bytes
953            }),
954        ] {
955            append_terminator(&mut bytes);
956            let result: Result<(), FrameError> = ready(async {
957                let mut reader = TarReader::new(ChunkedReader::new(bytes, BLOCK_SIZE));
958                let member = next_member(&mut reader).await?;
959                if field == "path" {
960                    member.effective_path().map(|_| ())
961                } else {
962                    member.effective_link_path().map(|_| ())
963                }
964            });
965            assert!(
966                matches!(
967                    result,
968                    Err(FrameError {
969                        inner: FrameErrorInner::NulInMemberName { field: found },
970                        ..
971                    }) if found == field
972                ),
973                "{field}: {result:?}"
974            );
975        }
976    }
977
978    #[test]
979    fn ignores_nul_in_overridden_pax_member_names() {
980        let mut global = record("path", "bad\0name");
981        global.extend_from_slice(&record("linkpath", "bad\0target"));
982        let mut local = record("path", "good-name");
983        local.extend_from_slice(&record("linkpath", "good-target"));
984        let mut bytes = Vec::new();
985        append_pax(&mut bytes, b'g', &global);
986        append_pax(&mut bytes, b'x', &local);
987        append_block(&mut bytes, &header(b'2', 0));
988        append_terminator(&mut bytes);
989
990        ready_ok(async {
991            let mut reader = TarReader::new(ChunkedReader::new(bytes, BLOCK_SIZE));
992            let member = next_member(&mut reader).await?;
993            assert_eq!(member.effective_path()?.as_ref(), b"good-name");
994            assert_eq!(member.effective_link_path()?.as_ref(), b"good-target");
995            Ok(())
996        });
997    }
998
999    #[test]
1000    fn accepts_nonempty_extension_paths_over_empty_header_names() {
1001        for (case, mut bytes, expected) in [
1002            (
1003                "pax",
1004                {
1005                    let mut bytes = Vec::new();
1006                    append_pax(&mut bytes, b'x', &record("path", "pax-name"));
1007                    let mut member = header(b'0', 0);
1008                    set_field(&mut member, NAME_RANGE, b"");
1009                    set_field(&mut member, PREFIX_RANGE, b"");
1010                    set_checksum(&mut member);
1011                    append_block(&mut bytes, &member);
1012                    bytes
1013                },
1014                b"pax-name".as_slice(),
1015            ),
1016            (
1017                "gnu",
1018                {
1019                    let mut bytes = Vec::new();
1020                    append_gnu(&mut bytes, b'L', b"gnu-name\0");
1021                    let mut member = gnu_header(b'0', 0);
1022                    set_field(&mut member, NAME_RANGE, b"");
1023                    set_checksum(&mut member);
1024                    append_block(&mut bytes, &member);
1025                    bytes
1026                },
1027                b"gnu-name".as_slice(),
1028            ),
1029        ] {
1030            append_terminator(&mut bytes);
1031            ready_ok(async {
1032                let mut reader = TarReader::new(ChunkedReader::new(bytes, BLOCK_SIZE));
1033                let member = next_member(&mut reader).await?;
1034                assert_eq!(member.effective_path()?.as_ref(), expected, "{case}");
1035                Ok(())
1036            });
1037        }
1038    }
1039
1040    #[test]
1041    fn global_path_deletion_suppresses_the_physical_header_path() {
1042        let mut physical_header = header(b'0', 0);
1043        set_field(&mut physical_header, NAME_RANGE, b"physical");
1044        set_checksum(&mut physical_header);
1045
1046        let mut bytes = Vec::new();
1047        append_pax(&mut bytes, b'g', &record("path", "global"));
1048        append_block(&mut bytes, &header(b'0', 0));
1049        append_pax(&mut bytes, b'g', &record("path", ""));
1050        append_block(&mut bytes, &physical_header);
1051        append_terminator(&mut bytes);
1052
1053        ready_ok(async {
1054            let mut reader = TarReader::new(ChunkedReader::new(bytes, BLOCK_SIZE));
1055            {
1056                let member = next_member(&mut reader).await?;
1057                assert_eq!(member.effective_path()?.as_ref(), b"global");
1058            }
1059
1060            let member = next_member(&mut reader).await?;
1061            assert!(matches!(
1062                member.effective_path(),
1063                Err(FrameError {
1064                    inner: FrameErrorInner::DeletedPaxMetadata { keyword: "path" },
1065                    ..
1066                })
1067            ));
1068            let state = pax_state(&member).expect("expected pax member metadata");
1069            assert_eq!(
1070                state.effective_record(&PaxKeyword::Path),
1071                Some(&PaxRecord::Path(PaxValue::Deleted))
1072            );
1073            let extensions = state.extensions().collect::<Vec<_>>();
1074            assert_eq!(extensions.len(), 1);
1075            assert!(matches!(
1076                extensions[0].records(),
1077                [PaxRecord::Path(PaxValue::Deleted)]
1078            ));
1079            Ok(())
1080        });
1081    }
1082
1083    #[test]
1084    fn resolves_and_validates_gnu_metadata_lazily() {
1085        let mut bytes = Vec::new();
1086        append_block(&mut bytes, &gnu_header(b'L', 5));
1087        append_payload(&mut bytes, b"name\0");
1088        append_block(&mut bytes, &gnu_header(b'K', 5));
1089        append_payload(&mut bytes, b"link\0");
1090        append_block(&mut bytes, &gnu_header(b'2', 0));
1091        append_terminator(&mut bytes);
1092
1093        ready_ok(async {
1094            let mut reader = TarReader::new(ChunkedReader::new(bytes, BLOCK_SIZE));
1095            let member = next_member(&mut reader).await?;
1096            assert_eq!(member.effective_path()?.as_ref(), b"name");
1097            assert_eq!(member.effective_link_path()?.as_ref(), b"link");
1098            Ok(())
1099        });
1100
1101        for (typeflag, payload, kind) in [
1102            (b'L', b"no-nul".as_slice(), GnuKind::LongName),
1103            (b'K', b"link\0bad".as_slice(), GnuKind::LongLink),
1104        ] {
1105            let mut bytes = Vec::new();
1106            append_gnu(&mut bytes, typeflag, payload);
1107            append_block(&mut bytes, &gnu_header(b'2', 0));
1108            append_terminator(&mut bytes);
1109            let result: Result<(), FrameError> = ready(async {
1110                let mut reader = TarReader::new(ChunkedReader::new(bytes, BLOCK_SIZE));
1111                let member = next_member(&mut reader).await?;
1112                match kind {
1113                    GnuKind::LongName => member.effective_path().map(|_| ()),
1114                    GnuKind::LongLink => member.effective_link_path().map(|_| ()),
1115                }
1116            });
1117            assert!(matches!(
1118                result,
1119                Err(FrameError {
1120                    position: 0,
1121                    inner: FrameErrorInner::InvalidGnuMetadata { kind: found, .. },
1122                }) if found == kind
1123            ));
1124        }
1125    }
1126
1127    #[test]
1128    fn groups_pax_metadata_and_streams_member_payload() {
1129        let mut global = record("comment", "first");
1130        global.extend_from_slice(&record("comment", "last"));
1131        let mut local = record("path", "renamed");
1132        local.extend_from_slice(&record("size", "513"));
1133        let mut bytes = Vec::new();
1134        append_pax(&mut bytes, b'g', &global);
1135        append_pax(&mut bytes, b'x', &local);
1136        append_block(&mut bytes, &header(b'0', 1));
1137        append_payload(&mut bytes, &[b'a'; BLOCK_SIZE]);
1138        append_payload(&mut bytes, b"b");
1139        append_terminator(&mut bytes);
1140
1141        ready_ok(async {
1142            let mut reader = TarReader::new(ChunkedReader::new(bytes, 17));
1143            {
1144                let mut member = next_member(&mut reader).await?;
1145                assert_eq!(member.header.effective_size, 513);
1146                let state = pax_state(&member).expect("expected pax member metadata");
1147                let extensions = state.extensions().collect::<Vec<_>>();
1148                assert_eq!(extensions.len(), 2);
1149                assert_eq!(extensions[0].position, 0);
1150                assert_eq!(extensions[0].kind, PaxKind::Global);
1151                assert_eq!(
1152                    extensions[0].records(),
1153                    [
1154                        PaxRecord::Comment(PaxValue::Value("first".into())),
1155                        PaxRecord::Comment(PaxValue::Value("last".into())),
1156                    ]
1157                );
1158                assert_eq!(extensions[1].position, (BLOCK_SIZE * 2) as u64);
1159                assert_eq!(extensions[1].kind, PaxKind::Local);
1160                assert_eq!(
1161                    state.effective_record(&PaxKeyword::Size),
1162                    Some(&PaxRecord::Size(PaxValue::Value(513)))
1163                );
1164                assert_eq!(
1165                    state.effective_record(&PaxKeyword::Comment),
1166                    Some(&PaxRecord::Comment(PaxValue::Value("last".into())))
1167                );
1168                let Some(first) = member.payload.next_block().await? else {
1169                    panic!("expected first member payload block");
1170                };
1171                let Some(last) = member.payload.next_block().await? else {
1172                    panic!("expected last member payload block");
1173                };
1174                assert_eq!(first.len, BLOCK_SIZE);
1175                assert_eq!(last.len, 1);
1176                assert!(member.payload.next_block().await?.is_none());
1177            }
1178            assert!(reader.next_frame().await?.is_none());
1179            Ok(())
1180        });
1181    }
1182
1183    #[test]
1184    fn bounds_cumulative_global_pax_extension_payloads() {
1185        let payload = record("comment", "metadata");
1186        let payload_size = u64::try_from(payload.len()).expect("payload size should fit u64");
1187        let limit = payload_size
1188            .checked_mul(2)
1189            .expect("test payload total should fit u64");
1190
1191        let mut rejected = Vec::new();
1192        append_pax(&mut rejected, b'g', &payload);
1193        append_pax(&mut rejected, b'g', &payload);
1194        let rejected_position =
1195            u64::try_from(rejected.len()).expect("test position should fit u64");
1196        append_block(&mut rejected, &header(b'g', payload_size));
1197        let error: Result<(), FrameError> = ready(async {
1198            let mut reader = TarReader::new(ChunkedReader::new(rejected, BLOCK_SIZE))
1199                .with_policy(StreamPolicy::default().max_global_pax_extensions_size(limit));
1200            reader.next_frame().await.map(|_| ())
1201        });
1202        assert!(matches!(
1203            error,
1204            Err(FrameError {
1205                position,
1206                inner: FrameErrorInner::GlobalPaxExtensionsTooLarge {
1207                    size,
1208                    limit: found_limit,
1209                },
1210            }) if position == rejected_position
1211                && size == payload_size * 3
1212                && found_limit == limit
1213        ));
1214
1215        let mut accepted = Vec::new();
1216        for _ in 0..2 {
1217            for _ in 0..3 {
1218                append_pax(&mut accepted, b'g', &payload);
1219            }
1220            append_block(&mut accepted, &header(b'0', 0));
1221        }
1222        append_terminator(&mut accepted);
1223        ready_ok(async {
1224            let mut reader = TarReader::new(ChunkedReader::new(accepted, BLOCK_SIZE)).with_policy(
1225                StreamPolicy::default().max_global_pax_extensions_size(payload_size * 3),
1226            );
1227            for _ in 0..2 {
1228                let member = next_member(&mut reader).await?;
1229                assert_eq!(
1230                    pax_state(&member)
1231                        .expect("expected pax member metadata")
1232                        .extensions()
1233                        .count(),
1234                    3
1235                );
1236            }
1237            Ok(())
1238        });
1239    }
1240
1241    #[test]
1242    fn retains_global_pax_extension_across_cancelled_reads() {
1243        let mut bytes = Vec::new();
1244        append_pax(&mut bytes, b'g', &record("comment", "metadata"));
1245        let after_extension_header = BLOCK_SIZE;
1246        let after_extension_payload = bytes.len();
1247        append_block(&mut bytes, &header(b'0', 0));
1248        append_terminator(&mut bytes);
1249
1250        for pending_at in [after_extension_header, after_extension_payload] {
1251            let mut reader = TarReader::new(ChunkedReader::pending_once(bytes.clone(), pending_at));
1252            cancel_pending(reader.next_frame());
1253
1254            ready_ok(async {
1255                let member = next_member(&mut reader).await?;
1256                let state = pax_state(&member).expect("expected pax member metadata");
1257                let extensions = state.extensions().collect::<Vec<_>>();
1258                assert_eq!(extensions.len(), 1);
1259                assert_eq!(extensions[0].position, 0);
1260                assert_eq!(extensions[0].kind, PaxKind::Global);
1261                assert_eq!(
1262                    extensions[0].records(),
1263                    &[PaxRecord::Comment(PaxValue::Value("metadata".into()))]
1264                );
1265                Ok(())
1266            });
1267        }
1268    }
1269
1270    #[test]
1271    fn retains_gnu_metadata_across_cancelled_reads() {
1272        let expected_name = vec![b'n'; BLOCK_SIZE + 10];
1273        let mut long_name = expected_name.clone();
1274        long_name.push(0);
1275
1276        let mut bytes = Vec::new();
1277        append_gnu(&mut bytes, b'L', &long_name);
1278        let after_first_payload_block = BLOCK_SIZE * 2;
1279        append_block(&mut bytes, &gnu_header(b'0', 0));
1280        append_terminator(&mut bytes);
1281
1282        let mut reader = TarReader::new(ChunkedReader::pending_once(
1283            bytes,
1284            after_first_payload_block,
1285        ));
1286        cancel_pending(reader.next_frame());
1287
1288        ready_ok(async {
1289            let member = next_member(&mut reader).await?;
1290            assert_eq!(member.effective_path()?.as_ref(), expected_name);
1291            Ok(())
1292        });
1293
1294        let mut bytes = Vec::new();
1295        append_gnu(&mut bytes, b'L', &[]);
1296        let after_extension_header = bytes.len();
1297        append_block(&mut bytes, &gnu_header(b'0', 0));
1298        append_terminator(&mut bytes);
1299        let mut reader = TarReader::new(ChunkedReader::pending_once(bytes, after_extension_header));
1300        cancel_pending(reader.next_frame());
1301
1302        ready_ok(async {
1303            let member = next_member(&mut reader).await?;
1304            assert!(matches!(
1305                &member.extensions,
1306                MemberExtensions::Gnu {
1307                    long_name: Some(GnuMetadata { payload, .. }),
1308                    ..
1309                } if payload.is_empty()
1310            ));
1311            Ok(())
1312        });
1313    }
1314
1315    #[test]
1316    fn applies_global_pax_updates_to_each_borrowed_state() {
1317        let first = record("comment", "first");
1318        let second = record("gname", "second");
1319        let replacement = record("comment", "replacement");
1320        let mut bytes = Vec::new();
1321        append_pax(&mut bytes, b'g', &first);
1322        append_pax(&mut bytes, b'g', &second);
1323        append_block(&mut bytes, &header(b'0', 0));
1324        append_block(&mut bytes, &header(b'0', 0));
1325        append_pax(&mut bytes, b'g', &replacement);
1326        append_block(&mut bytes, &header(b'0', 0));
1327        append_terminator(&mut bytes);
1328
1329        ready_ok(async {
1330            let mut reader = TarReader::new(ChunkedReader::new(bytes, BLOCK_SIZE));
1331            {
1332                let member = next_member(&mut reader).await?;
1333                let state = pax_state(&member).expect("expected pax member metadata");
1334                let extensions = state.extensions().collect::<Vec<_>>();
1335                assert_eq!(extensions.len(), 2);
1336                assert_eq!(extensions[0].position, 0);
1337                assert_eq!(extensions[1].position, (BLOCK_SIZE * 2) as u64);
1338                assert_eq!(
1339                    state.effective_record(&PaxKeyword::Comment),
1340                    Some(&PaxRecord::Comment(PaxValue::Value("first".into())))
1341                );
1342            }
1343            {
1344                let member = next_member(&mut reader).await?;
1345                let state = pax_state(&member).expect("expected pax member metadata");
1346                assert_eq!(state.extensions().count(), 0);
1347                assert_eq!(
1348                    state.effective_record(&PaxKeyword::Comment),
1349                    Some(&PaxRecord::Comment(PaxValue::Value("first".into())))
1350                );
1351            }
1352
1353            let member = next_member(&mut reader).await?;
1354            let state = pax_state(&member).expect("expected pax member metadata");
1355            let extensions = state.extensions().collect::<Vec<_>>();
1356            assert_eq!(extensions.len(), 1);
1357            assert_eq!(extensions[0].kind, PaxKind::Global);
1358            assert_eq!(
1359                state.effective_record(&PaxKeyword::Comment),
1360                Some(&PaxRecord::Comment(PaxValue::Value("replacement".into())))
1361            );
1362            Ok(())
1363        });
1364    }
1365
1366    #[test]
1367    fn streams_member_payload_in_reusable_chunks() {
1368        let payload = (0..BLOCK_SIZE * 3 + 7)
1369            .map(|index| u8::try_from(index % 251).unwrap())
1370            .collect::<Vec<_>>();
1371        let mut bytes = Vec::new();
1372        append_pax(&mut bytes, b'0', &payload);
1373        append_terminator(&mut bytes);
1374
1375        ready_ok(async {
1376            let mut reader = TarReader::new(ChunkedReader::new(bytes, 17));
1377            let mut member = next_member(&mut reader).await?;
1378            let mut chunk = vec![b'x'; BLOCK_SIZE * 2];
1379            assert!(
1380                member
1381                    .payload
1382                    .next_chunk(&mut chunk, BLOCK_SIZE + 1)
1383                    .await?
1384            );
1385            let allocation = chunk.as_ptr();
1386            assert_eq!(chunk, payload[..BLOCK_SIZE * 2]);
1387            assert!(
1388                member
1389                    .payload
1390                    .next_chunk(&mut chunk, BLOCK_SIZE + 1)
1391                    .await?
1392            );
1393            assert_eq!(chunk.as_ptr(), allocation);
1394            assert_eq!(chunk, payload[BLOCK_SIZE * 2..]);
1395            assert!(
1396                !member
1397                    .payload
1398                    .next_chunk(&mut chunk, BLOCK_SIZE + 1)
1399                    .await?
1400            );
1401            assert_eq!(chunk, payload[BLOCK_SIZE * 2..]);
1402            assert!(reader.next_frame().await?.is_none());
1403            Ok(())
1404        });
1405    }
1406
1407    #[test]
1408    fn resumes_cancelled_member_payload_chunk_with_either_read_api() {
1409        let payload = (0..BLOCK_SIZE * 2 + 17)
1410            .map(|index| u8::try_from(index % 251).expect("test byte should fit"))
1411            .collect::<Vec<_>>();
1412        let (bytes, next_member_position) = member_followed_by_empty_member(&payload);
1413
1414        ready_ok(async {
1415            let mut reader = TarReader::new(ChunkedReader::pending_once(bytes, BLOCK_SIZE + 73));
1416            {
1417                let mut member = next_member(&mut reader).await?;
1418                let mut cancelled_buffer = vec![b'x'; 17];
1419                cancel_pending(
1420                    member
1421                        .payload
1422                        .next_chunk(&mut cancelled_buffer, payload.len()),
1423                );
1424                assert!(cancelled_buffer.is_empty());
1425
1426                let first = member
1427                    .payload
1428                    .next_block()
1429                    .await?
1430                    .expect("cancelled chunk should resume as a payload block");
1431                let mut resumed_buffer = vec![b'y'; 23];
1432                assert!(member.payload.next_chunk(&mut resumed_buffer, 1).await?);
1433                let mut observed = first.block[..first.len].to_vec();
1434                observed.extend_from_slice(&resumed_buffer);
1435                assert_eq!(observed, payload);
1436                assert!(!member.payload.next_chunk(&mut resumed_buffer, 1).await?);
1437            }
1438
1439            let member = next_member(&mut reader).await?;
1440            assert_eq!(member.header.position, next_member_position);
1441            Ok(())
1442        });
1443    }
1444
1445    #[test]
1446    fn resumes_cancelled_member_payload_block_during_automatic_drain() {
1447        let payload = vec![b'x'; BLOCK_SIZE * 2 + 17];
1448        let (bytes, next_member_position) = member_followed_by_empty_member(&payload);
1449
1450        ready_ok(async {
1451            let mut reader = TarReader::new(ChunkedReader::pending_once(bytes, BLOCK_SIZE + 73));
1452            {
1453                let mut member = next_member(&mut reader).await?;
1454                cancel_pending(member.payload.next_block());
1455            }
1456
1457            let member = next_member(&mut reader).await?;
1458            assert_eq!(member.header.position, next_member_position);
1459            drop(member);
1460            assert!(reader.next_frame().await?.is_none());
1461            Ok(())
1462        });
1463    }
1464
1465    #[test]
1466    fn resumes_cancelled_automatic_payload_drain() {
1467        let payload = vec![b'x'; BLOCK_SIZE * 2 + 17];
1468        let (bytes, next_member_position) = member_followed_by_empty_member(&payload);
1469
1470        ready_ok(async {
1471            let mut reader = TarReader::new(ChunkedReader::pending_once(bytes, BLOCK_SIZE + 73));
1472            drop(next_member(&mut reader).await?);
1473            cancel_pending(reader.next_frame());
1474
1475            let member = next_member(&mut reader).await?;
1476            assert_eq!(member.header.position, next_member_position);
1477            drop(member);
1478            assert!(reader.next_frame().await?.is_none());
1479            Ok(())
1480        });
1481    }
1482
1483    #[test]
1484    fn reports_cancelled_chunk_errors_at_physical_block_boundaries() {
1485        #[derive(Clone, Copy, Debug)]
1486        enum ExpectedError {
1487            TruncatedPayload,
1488            IncompleteBlock,
1489        }
1490
1491        for (expected, trailing_byte) in [
1492            (ExpectedError::TruncatedPayload, None),
1493            (ExpectedError::IncompleteBlock, Some(b'x')),
1494        ] {
1495            let mut bytes = Vec::new();
1496            append_block(&mut bytes, &header(b'0', (BLOCK_SIZE + 1) as u64));
1497            append_payload(&mut bytes, b"payload");
1498            if let Some(trailing_byte) = trailing_byte {
1499                bytes.push(trailing_byte);
1500            }
1501            let error = ready(async {
1502                let mut reader =
1503                    TarReader::new(ChunkedReader::pending_once(bytes, BLOCK_SIZE + 73));
1504                let Ok(Some(mut member)) = reader.next_frame().await else {
1505                    panic!("expected member");
1506                };
1507                let mut buffer = Vec::new();
1508                cancel_pending(member.payload.next_chunk(&mut buffer, BLOCK_SIZE * 2));
1509                member.payload.next_chunk(&mut buffer, BLOCK_SIZE * 2).await
1510            });
1511            let Err(FrameError { position, inner }) = &error else {
1512                panic!("{expected:?}: expected error, got {error:?}");
1513            };
1514            assert_eq!(*position, (BLOCK_SIZE * 2) as u64, "{expected:?}");
1515            assert!(
1516                matches!(
1517                    (expected, inner),
1518                    (
1519                        ExpectedError::TruncatedPayload,
1520                        FrameErrorInner::TruncatedPayload {
1521                            owner: DataOwner::Member,
1522                            remaining: 1,
1523                        },
1524                    ) | (
1525                        ExpectedError::IncompleteBlock,
1526                        FrameErrorInner::IncompleteBlock { read: 1 },
1527                    )
1528                ),
1529                "{expected:?}: {error:?}"
1530            );
1531        }
1532    }
1533
1534    #[test]
1535    fn groups_gnu_metadata_with_its_member() {
1536        let mut bytes = Vec::new();
1537        append_block(&mut bytes, &gnu_header(b'L', 5));
1538        append_payload(&mut bytes, b"name\0");
1539        append_block(&mut bytes, &gnu_header(b'K', 5));
1540        append_payload(&mut bytes, b"link\0");
1541        append_block(&mut bytes, &gnu_header(b'2', 0));
1542        append_terminator(&mut bytes);
1543
1544        ready_ok(async {
1545            let mut reader = TarReader::new(ChunkedReader::new(bytes, BLOCK_SIZE));
1546            let mut member = next_member(&mut reader).await?;
1547            let MemberExtensions::Gnu {
1548                long_name: Some(long_name),
1549                long_link: Some(long_link),
1550            } = &member.extensions
1551            else {
1552                panic!("expected GNU extensions");
1553            };
1554            assert_eq!(long_name.payload, b"name\0");
1555            assert_eq!(long_link.payload, b"link\0");
1556            assert!(member.payload.next_block().await?.is_none());
1557            Ok(())
1558        });
1559    }
1560
1561    #[test]
1562    fn rejects_oversized_gnu_extensions_before_consuming_payload() {
1563        let declared_size = 9;
1564        for (case, typeflag) in [("long-name", b'L'), ("long-link", b'K')] {
1565            let mut reader = TarReader::new(ChunkedReader::new(
1566                gnu_header(typeflag, declared_size).to_vec(),
1567                BLOCK_SIZE,
1568            ))
1569            .with_policy(StreamPolicy::default().max_gnu_extension_size(declared_size - 1));
1570            assert!(
1571                matches!(
1572                    ready(reader.next_frame()),
1573                    Err(FrameError {
1574                        position: 0,
1575                        inner: FrameErrorInner::ExtensionTooLarge {
1576                            format: ArchiveFormat::Gnu,
1577                            size,
1578                            limit,
1579                        },
1580                    }) if size == declared_size && limit == declared_size - 1
1581                ),
1582                "{case}"
1583            );
1584        }
1585
1586        let mut reader = TarReader::new(ChunkedReader::new(
1587            gnu_header(b'L', DEFAULT_MAX_GNU_EXTENSION_SIZE + 1).to_vec(),
1588            BLOCK_SIZE,
1589        ));
1590        assert!(matches!(
1591            ready(reader.next_frame()),
1592            Err(FrameError {
1593                position: 0,
1594                inner: FrameErrorInner::ExtensionTooLarge {
1595                    format: ArchiveFormat::Gnu,
1596                    size,
1597                    limit: DEFAULT_MAX_GNU_EXTENSION_SIZE,
1598                },
1599            }) if size == DEFAULT_MAX_GNU_EXTENSION_SIZE + 1
1600        ));
1601    }
1602
1603    #[test]
1604    fn logical_reader_is_fused_after_oversized_gnu_extension() {
1605        let payload = b"renamed\0";
1606        let payload_size = u64::try_from(payload.len()).expect("payload size should fit u64");
1607        let mut bytes = Vec::new();
1608        append_gnu(&mut bytes, b'L', payload);
1609        append_block(&mut bytes, &gnu_header(b'0', 0));
1610        append_terminator(&mut bytes);
1611
1612        ready_ok(async {
1613            let mut reader = TarReader::new(ChunkedReader::new(bytes, BLOCK_SIZE))
1614                .with_policy(StreamPolicy::default().max_gnu_extension_size(payload_size - 1));
1615            assert!(matches!(
1616                reader.next_frame().await,
1617                Err(FrameError {
1618                    position: 0,
1619                    inner: FrameErrorInner::ExtensionTooLarge {
1620                        format: ArchiveFormat::Gnu,
1621                        size,
1622                        limit,
1623                    },
1624                }) if size == payload_size && limit == payload_size - 1
1625            ));
1626            assert!(reader.next_frame().await?.is_none());
1627            Ok(())
1628        });
1629    }
1630
1631    #[test]
1632    fn preserves_multiblock_gnu_metadata_payloads() {
1633        let mut long_name = vec![b'n'; BLOCK_SIZE * 2 + 37];
1634        long_name.push(0);
1635        let mut long_link = vec![b'l'; BLOCK_SIZE + 19];
1636        long_link.push(0);
1637
1638        let mut bytes = Vec::new();
1639        append_gnu(&mut bytes, b'L', &long_name);
1640        append_gnu(&mut bytes, b'K', &long_link);
1641        append_block(&mut bytes, &gnu_header(b'2', 0));
1642        append_terminator(&mut bytes);
1643
1644        ready_ok(async {
1645            let mut reader = TarReader::new(ChunkedReader::new(bytes, 19));
1646            let member = next_member(&mut reader).await?;
1647            let MemberExtensions::Gnu {
1648                long_name: Some(name_metadata),
1649                long_link: Some(link_metadata),
1650            } = &member.extensions
1651            else {
1652                panic!("expected GNU extensions");
1653            };
1654            assert_eq!(name_metadata.position, 0);
1655            assert_eq!(name_metadata.payload, long_name);
1656            assert_eq!(link_metadata.position, (BLOCK_SIZE * 4) as u64);
1657            assert_eq!(link_metadata.payload, long_link);
1658            member.payload.skip().await?;
1659            assert!(reader.next_frame().await?.is_none());
1660            Ok(())
1661        });
1662    }
1663
1664    #[test]
1665    fn handles_empty_archives_and_trailing_global_pax() {
1666        let mut empty = Vec::new();
1667        append_terminator(&mut empty);
1668        ready_ok(async {
1669            let mut reader = TarReader::new(ChunkedReader::new(empty, BLOCK_SIZE));
1670            assert!(reader.next_frame().await?.is_none());
1671            Ok(())
1672        });
1673
1674        for header in [
1675            header(b'x', record("path", "name").len() as u64),
1676            gnu_header(b'L', 0),
1677        ] {
1678            let mut bytes = Vec::new();
1679            append_block(&mut bytes, &header);
1680            if header[TYPEFLAG_OFFSET] == b'x' {
1681                append_payload(&mut bytes, &record("path", "name"));
1682            }
1683            let error: Result<(), FrameError> = ready(async {
1684                let mut reader = TarReader::new(ChunkedReader::new(bytes, BLOCK_SIZE));
1685                reader.next_frame().await.map(|_| ())
1686            });
1687            assert!(matches!(
1688                error,
1689                Err(FrameError {
1690                    inner: FrameErrorInner::UnexpectedEof { .. },
1691                    ..
1692                })
1693            ));
1694        }
1695
1696        let mut global = Vec::new();
1697        append_pax(&mut global, b'g', &record("comment", "metadata"));
1698        append_pax(&mut global, b'g', &record("gname", "group"));
1699        append_terminator(&mut global);
1700        ready_ok(async {
1701            let mut reader = TarReader::new(ChunkedReader::new(global, BLOCK_SIZE));
1702            assert!(reader.next_frame().await?.is_none());
1703            Ok(())
1704        });
1705
1706        let mut malformed_global = Vec::new();
1707        append_pax(&mut malformed_global, b'g', b"invalid");
1708        append_terminator(&mut malformed_global);
1709        let error: Result<(), FrameError> = ready(async {
1710            let mut reader = TarReader::new(ChunkedReader::new(malformed_global, BLOCK_SIZE));
1711            reader.next_frame().await.map(|_| ())
1712        });
1713        assert!(matches!(
1714            error,
1715            Err(FrameError {
1716                position: 0,
1717                inner: FrameErrorInner::InvalidPaxRecord { .. },
1718            })
1719        ));
1720    }
1721
1722    #[test]
1723    fn skips_unread_payload_before_advancing() {
1724        for payload_len in [BLOCK_SIZE + 1, PAYLOAD_DRAIN_CHUNK_BYTES + 7] {
1725            let payload = vec![b'a'; payload_len];
1726            let mut bytes = Vec::new();
1727            append_pax(&mut bytes, b'0', &payload);
1728            append_block(&mut bytes, &header(b'0', 0));
1729            append_terminator(&mut bytes);
1730
1731            ready_ok(async {
1732                let mut reader = TarReader::new(ChunkedReader::new(bytes, BLOCK_SIZE));
1733                {
1734                    let member = next_member(&mut reader).await?;
1735                    member.payload.skip().await?;
1736                }
1737                let member = next_member(&mut reader).await?;
1738                assert_eq!(member.header.effective_size, 0);
1739                drop(member);
1740                assert!(reader.next_frame().await?.is_none());
1741                Ok(())
1742            });
1743        }
1744
1745        let mut auto_bytes = Vec::new();
1746        append_block(&mut auto_bytes, &header(b'0', 1));
1747        append_payload(&mut auto_bytes, b"a");
1748        append_block(&mut auto_bytes, &header(b'0', 0));
1749        append_terminator(&mut auto_bytes);
1750        ready_ok(async {
1751            let mut reader = TarReader::new(ChunkedReader::new(auto_bytes, BLOCK_SIZE));
1752            let first = next_member(&mut reader).await?;
1753            drop(first);
1754            assert!(reader.next_frame().await?.is_some());
1755            Ok(())
1756        });
1757    }
1758
1759    #[test]
1760    fn reports_truncated_payload_when_read_or_skipped() {
1761        #[derive(Clone, Copy, Debug)]
1762        enum Operation {
1763            ReadBlock,
1764            ReadChunk,
1765            ExplicitSkip,
1766            AutomaticSkip,
1767        }
1768
1769        for operation in [
1770            Operation::ReadBlock,
1771            Operation::ReadChunk,
1772            Operation::ExplicitSkip,
1773            Operation::AutomaticSkip,
1774        ] {
1775            ready_ok(async {
1776                let mut reader =
1777                    TarReader::new(ChunkedReader::new(header(b'0', 1).to_vec(), BLOCK_SIZE));
1778                let result = {
1779                    let mut member = next_member(&mut reader).await?;
1780                    match operation {
1781                        Operation::ReadBlock => member.payload.next_block().await.map(|_| ()),
1782                        Operation::ReadChunk => {
1783                            let mut chunk = Vec::new();
1784                            member.payload.next_chunk(&mut chunk, 1).await.map(|_| ())
1785                        }
1786                        Operation::ExplicitSkip => member.payload.skip().await,
1787                        Operation::AutomaticSkip => {
1788                            drop(member);
1789                            reader.next_frame().await.map(|_| ())
1790                        }
1791                    }
1792                };
1793
1794                assert!(
1795                    matches!(
1796                        result,
1797                        Err(FrameError {
1798                            inner: FrameErrorInner::TruncatedPayload {
1799                                owner: DataOwner::Member,
1800                                ..
1801                            },
1802                            ..
1803                        })
1804                    ),
1805                    "{operation:?}"
1806                );
1807
1808                for attempt in 1..=2 {
1809                    assert!(
1810                        reader.next_frame().await?.is_none(),
1811                        "{operation:?}, iteration {attempt}"
1812                    );
1813                }
1814
1815                Ok(())
1816            });
1817        }
1818    }
1819}