Skip to main content

oci_zero/
tar.rs

1//! Allocation-free extraction of one regular file from a tar stream.
2
3const BLOCK_SIZE: usize = 512;
4const NAME_RANGE: core::ops::Range<usize> = 0..100;
5const SIZE_RANGE: core::ops::Range<usize> = 124..136;
6const CHECKSUM_RANGE: core::ops::Range<usize> = 148..156;
7const TYPE_OFFSET: usize = 156;
8const PREFIX_RANGE: core::ops::Range<usize> = 345..500;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11enum WriteState {
12    Ready,
13    File { remaining: u64, size: u64 },
14    Finished,
15}
16
17/// Allocation-free writer for deterministic ustar archives.
18pub struct TarWriter {
19    state: WriteState,
20}
21
22impl TarWriter {
23    pub const fn new() -> Self {
24        Self {
25            state: WriteState::Ready,
26        }
27    }
28
29    pub fn begin_file<E>(
30        &mut self,
31        path: &[u8],
32        size: u64,
33        mode: u32,
34        mut output: impl FnMut(&[u8]) -> Result<(), E>,
35    ) -> Result<(), TarWriteError<E>> {
36        if self.state != WriteState::Ready {
37            return Err(TarWriteError::InvalidState);
38        }
39        if path.is_empty()
40            || path.len() > NAME_RANGE.len()
41            || path[0] == b'/'
42            || path.contains(&0)
43            || path
44                .split(|byte| *byte == b'/')
45                .any(|component| component == b"..")
46        {
47            return Err(TarWriteError::InvalidPath);
48        }
49
50        let mut header = [0u8; BLOCK_SIZE];
51        header[..path.len()].copy_from_slice(path);
52        write_octal(&mut header[100..108], mode as u64)
53            .map_err(|_| TarWriteError::ValueTooLarge)?;
54        write_octal(&mut header[SIZE_RANGE], size).map_err(|_| TarWriteError::ValueTooLarge)?;
55        // UID, GID and mtime are deterministic zeroes. The initialized header
56        // already supplies each field's terminating NUL.
57        for digits in [108..115, 116..123, 136..147] {
58            header[digits].fill(b'0');
59        }
60        header[CHECKSUM_RANGE].fill(b' ');
61        header[TYPE_OFFSET] = b'0';
62        header[257..263].copy_from_slice(b"ustar\0");
63        header[263..265].copy_from_slice(b"00");
64        let checksum = header.iter().map(|byte| *byte as u64).sum();
65        let checksum_field = &mut header[CHECKSUM_RANGE];
66        write_octal(&mut checksum_field[..7], checksum)
67            .map_err(|_| TarWriteError::ValueTooLarge)?;
68        checksum_field[7] = b' ';
69        output(&header).map_err(TarWriteError::Output)?;
70        self.state = WriteState::File {
71            remaining: size,
72            size,
73        };
74        Ok(())
75    }
76
77    pub fn write_file_data<E>(
78        &mut self,
79        bytes: &[u8],
80        mut output: impl FnMut(&[u8]) -> Result<(), E>,
81    ) -> Result<(), TarWriteError<E>> {
82        let WriteState::File { remaining, size } = self.state else {
83            return Err(TarWriteError::InvalidState);
84        };
85        if bytes.len() as u64 > remaining {
86            return Err(TarWriteError::TooMuchData);
87        }
88        output(bytes).map_err(TarWriteError::Output)?;
89        self.state = WriteState::File {
90            remaining: remaining - bytes.len() as u64,
91            size,
92        };
93        Ok(())
94    }
95
96    pub fn end_file<E>(
97        &mut self,
98        mut output: impl FnMut(&[u8]) -> Result<(), E>,
99    ) -> Result<(), TarWriteError<E>> {
100        let WriteState::File { remaining, size } = self.state else {
101            return Err(TarWriteError::InvalidState);
102        };
103        if remaining != 0 {
104            return Err(TarWriteError::SizeMismatch { remaining });
105        }
106        let padding = padding_for(size);
107        if padding != 0 {
108            output(&[0; BLOCK_SIZE][..padding]).map_err(TarWriteError::Output)?;
109        }
110        self.state = WriteState::Ready;
111        Ok(())
112    }
113
114    pub fn finish<E>(
115        &mut self,
116        mut output: impl FnMut(&[u8]) -> Result<(), E>,
117    ) -> Result<(), TarWriteError<E>> {
118        if self.state != WriteState::Ready {
119            return Err(TarWriteError::InvalidState);
120        }
121        output(&[0; BLOCK_SIZE * 2]).map_err(TarWriteError::Output)?;
122        self.state = WriteState::Finished;
123        Ok(())
124    }
125}
126
127impl Default for TarWriter {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133#[derive(Clone, Copy, Debug, Eq, PartialEq, derive_more::Display)]
134pub enum TarWriteError<E> {
135    #[display("invalid tar writer state")]
136    InvalidState,
137    #[display("invalid tar member path")]
138    InvalidPath,
139    #[display("tar header value is too large")]
140    ValueTooLarge,
141    #[display("tar member received too much data")]
142    TooMuchData,
143    #[display("tar member is missing {remaining} bytes")]
144    SizeMismatch { remaining: u64 },
145    #[display("tar writer output failed: {_0}")]
146    Output(E),
147}
148
149fn write_octal(field: &mut [u8], mut value: u64) -> Result<(), ()> {
150    field.fill(b'0');
151    let digits = field.len().checked_sub(1).ok_or(())?;
152    field[digits] = 0;
153    for index in (0..digits).rev() {
154        field[index] = b'0' + (value & 7) as u8;
155        value >>= 3;
156    }
157    if value != 0 {
158        return Err(());
159    }
160    Ok(())
161}
162
163/// A failure encountered while consuming a tar stream.
164#[derive(Clone, Copy, Debug, Eq, PartialEq, derive_more::Display)]
165pub enum ExtractError<E> {
166    /// A header checksum did not match its contents.
167    #[display("invalid tar header checksum")]
168    InvalidChecksum,
169    /// A header contained an invalid or unsupported size field.
170    #[display("invalid tar entry size")]
171    InvalidSize,
172    /// A non-zero header followed the first end-of-archive block.
173    #[display("invalid tar end marker")]
174    InvalidEndMarker,
175    /// The output callback failed.
176    #[display("tar output failed: {_0}")]
177    Output(E),
178}
179
180/// A failure detected when the end of the input stream is reported.
181#[derive(Clone, Copy, Debug, Eq, PartialEq, derive_more::Display)]
182pub enum FinishError {
183    /// The stream ended inside an entry, a header, or the end marker.
184    #[display("unexpected end of tar stream")]
185    UnexpectedEof,
186    /// The archive ended without a matching regular file.
187    #[display("tar entry not found")]
188    NotFound,
189}
190
191/// Extracts the first regular file whose path exactly matches `target`.
192///
193/// The extractor does not allocate. It buffers one 512-byte tar header and
194/// passes matching file contents directly to the callback supplied to
195/// [`push`](Self::push). Input can be split at arbitrary byte boundaries.
196///
197/// Paths are compared as raw bytes. POSIX ustar `prefix/name` paths are
198/// supported; extension records such as GNU long names and PAX path overrides
199/// are intentionally not interpreted.
200pub struct EntryExtractor<'target> {
201    target: &'target [u8],
202    header: [u8; BLOCK_SIZE],
203    header_len: usize,
204    entry_remaining: u64,
205    padding_remaining: usize,
206    selected: bool,
207    found: bool,
208    zero_blocks: u8,
209}
210
211impl<'target> EntryExtractor<'target> {
212    /// Creates an extractor for an exact archive path.
213    pub const fn new(target: &'target [u8]) -> Self {
214        Self {
215            target,
216            header: [0; BLOCK_SIZE],
217            header_len: 0,
218            entry_remaining: 0,
219            padding_remaining: 0,
220            selected: false,
221            found: false,
222            zero_blocks: 0,
223        }
224    }
225
226    /// Consumes another fragment of the tar byte stream.
227    ///
228    /// `output` is called only with bytes belonging to the selected regular
229    /// file. Once a match has been seen, later entries with the same path are
230    /// skipped.
231    pub fn push<E>(
232        &mut self,
233        mut input: &[u8],
234        mut output: impl FnMut(&[u8]) -> Result<(), E>,
235    ) -> Result<(), ExtractError<E>> {
236        while !input.is_empty() {
237            if self.zero_blocks == 2 {
238                return Ok(());
239            }
240
241            if self.entry_remaining != 0 {
242                let length = input.len().min(u64_to_usize(self.entry_remaining));
243                let bytes = &input[..length];
244                if self.selected {
245                    output(bytes).map_err(ExtractError::Output)?;
246                }
247                self.entry_remaining -= length as u64;
248                input = &input[length..];
249                continue;
250            }
251
252            if self.padding_remaining != 0 {
253                let length = input.len().min(self.padding_remaining);
254                self.padding_remaining -= length;
255                input = &input[length..];
256                continue;
257            }
258
259            let length = input.len().min(BLOCK_SIZE - self.header_len);
260            self.header[self.header_len..self.header_len + length]
261                .copy_from_slice(&input[..length]);
262            self.header_len += length;
263            input = &input[length..];
264
265            if self.header_len == BLOCK_SIZE {
266                self.consume_header()?;
267                self.header_len = 0;
268            }
269        }
270
271        Ok(())
272    }
273
274    /// Reports whether the requested entry has already been encountered.
275    pub const fn found(&self) -> bool {
276        self.found
277    }
278
279    /// Validates that the complete archive and requested entry were observed.
280    pub fn finish(&self) -> Result<(), FinishError> {
281        if self.zero_blocks != 2
282            || self.header_len != 0
283            || self.entry_remaining != 0
284            || self.padding_remaining != 0
285        {
286            return Err(FinishError::UnexpectedEof);
287        }
288        if !self.found {
289            return Err(FinishError::NotFound);
290        }
291        Ok(())
292    }
293
294    fn consume_header<E>(&mut self) -> Result<(), ExtractError<E>> {
295        if self.header.iter().all(|byte| *byte == 0) {
296            self.zero_blocks += 1;
297            return Ok(());
298        }
299        if self.zero_blocks != 0 {
300            return Err(ExtractError::InvalidEndMarker);
301        }
302
303        verify_checksum(&self.header)?;
304        let size = parse_number(&self.header[SIZE_RANGE]).ok_or(ExtractError::InvalidSize)?;
305        let is_regular = matches!(self.header[TYPE_OFFSET], 0 | b'0');
306        self.selected = !self.found && is_regular && path_matches(&self.header, self.target);
307        self.found |= self.selected;
308        self.entry_remaining = size;
309        self.padding_remaining = padding_for(size);
310        Ok(())
311    }
312}
313
314fn verify_checksum<E>(header: &[u8; BLOCK_SIZE]) -> Result<(), ExtractError<E>> {
315    let expected = parse_octal(&header[CHECKSUM_RANGE]).ok_or(ExtractError::InvalidChecksum)?;
316    let actual = header
317        .iter()
318        .enumerate()
319        .map(|(index, byte)| {
320            if CHECKSUM_RANGE.contains(&index) {
321                u64::from(b' ')
322            } else {
323                u64::from(*byte)
324            }
325        })
326        .sum::<u64>();
327    if actual == expected {
328        Ok(())
329    } else {
330        Err(ExtractError::InvalidChecksum)
331    }
332}
333
334fn parse_number(field: &[u8]) -> Option<u64> {
335    if field.first().copied()? & 0x80 == 0 {
336        return parse_octal(field);
337    }
338
339    let mut value = u64::from(field[0] & 0x7f);
340    for byte in &field[1..] {
341        value = value.checked_mul(256)?.checked_add(u64::from(*byte))?;
342    }
343    Some(value)
344}
345
346fn parse_octal(field: &[u8]) -> Option<u64> {
347    let mut value = 0u64;
348    let mut saw_digit = false;
349    let mut saw_terminator = false;
350
351    for byte in field {
352        match *byte {
353            b'0'..=b'7' if !saw_terminator => {
354                saw_digit = true;
355                value = value.checked_mul(8)?.checked_add(u64::from(*byte - b'0'))?;
356            }
357            b' ' if !saw_digit => {}
358            0 | b' ' if saw_digit => saw_terminator = true,
359            _ => return None,
360        }
361    }
362
363    saw_digit.then_some(value)
364}
365
366fn path_matches(header: &[u8; BLOCK_SIZE], target: &[u8]) -> bool {
367    let name = nul_terminated(&header[NAME_RANGE]);
368    let prefix = nul_terminated(&header[PREFIX_RANGE]);
369    if prefix.is_empty() {
370        return target == name;
371    }
372
373    target.len() == prefix.len() + 1 + name.len()
374        && target.starts_with(prefix)
375        && target[prefix.len()] == b'/'
376        && &target[prefix.len() + 1..] == name
377}
378
379fn nul_terminated(field: &[u8]) -> &[u8] {
380    let length = field
381        .iter()
382        .position(|byte| *byte == 0)
383        .unwrap_or(field.len());
384    &field[..length]
385}
386
387fn padding_for(size: u64) -> usize {
388    ((BLOCK_SIZE as u64 - size % BLOCK_SIZE as u64) % BLOCK_SIZE as u64) as usize
389}
390
391fn u64_to_usize(value: u64) -> usize {
392    usize::try_from(value).unwrap_or(usize::MAX)
393}
394
395/// Caller-owned scratch used by [`Archive`].
396pub struct ArchiveBuffers<'a> {
397    /// Normalized entry path and GNU/PAX path override storage.
398    pub path: &'a mut [u8],
399    /// Link target and GNU/PAX link override storage.
400    pub link: &'a mut [u8],
401    /// Raw per-entry PAX records.
402    pub pax: &'a mut [u8],
403}
404
405/// Type of a tar entry delivered to a [`LayerEventSink`].
406#[derive(Clone, Copy, Debug, Eq, PartialEq)]
407pub enum EntryKind {
408    Regular,
409    HardLink,
410    SymbolicLink,
411    CharacterDevice,
412    BlockDevice,
413    Directory,
414    Fifo,
415    Contiguous,
416    Other(u8),
417}
418
419/// Metadata for one validated archive entry.
420#[derive(Clone, Copy)]
421pub struct Entry<'a> {
422    pub path: &'a [u8],
423    pub kind: EntryKind,
424    pub size: u64,
425    pub mode: u64,
426    pub uid: u64,
427    pub gid: u64,
428    pub mtime: u64,
429    pub link_target: Option<&'a [u8]>,
430    pub device_major: Option<u64>,
431    pub device_minor: Option<u64>,
432    pax: &'a [u8],
433}
434
435impl<'a> Entry<'a> {
436    /// Iterates raw PAX key/value records associated with this entry.
437    pub fn pax_records(&self) -> PaxRecords<'a> {
438        PaxRecords::new(self.pax)
439    }
440}
441
442/// One PAX extended-header record.
443#[derive(Clone, Copy, Debug, Eq, PartialEq)]
444pub struct PaxRecord<'a> {
445    pub key: &'a [u8],
446    pub value: &'a [u8],
447}
448
449/// Iterator over validated PAX records.
450pub struct PaxRecords<'a> {
451    bytes: &'a [u8],
452}
453
454impl<'a> PaxRecords<'a> {
455    const fn new(bytes: &'a [u8]) -> Self {
456        Self { bytes }
457    }
458}
459
460impl<'a> Iterator for PaxRecords<'a> {
461    type Item = Result<PaxRecord<'a>, PaxError>;
462
463    fn next(&mut self) -> Option<Self::Item> {
464        if self.bytes.is_empty() {
465            return None;
466        }
467        let result = parse_pax_record(self.bytes);
468        self.bytes = match &result {
469            Ok((_, length)) => &self.bytes[*length..],
470            Err(_) => &[],
471        };
472        Some(result.map(|(record, _)| record))
473    }
474}
475
476/// Receives validated, ordered layer archive events.
477pub trait LayerEventSink {
478    type Error;
479
480    fn begin_entry(&mut self, entry: Entry<'_>) -> Result<(), Self::Error>;
481    fn entry_data(&mut self, bytes: &[u8]) -> Result<(), Self::Error>;
482    fn end_entry(&mut self) -> Result<(), Self::Error>;
483    fn whiteout(&mut self, path: &[u8]) -> Result<(), Self::Error>;
484    fn opaque_directory(&mut self, path: &[u8]) -> Result<(), Self::Error>;
485}
486
487/// A sink capable of staging a complete layer until integrity checks succeed.
488pub trait TransactionalLayerSink: LayerEventSink {
489    fn begin_layer(&mut self) -> Result<(), Self::Error>;
490    fn commit_layer(&mut self) -> Result<(), Self::Error>;
491    fn abort_layer(&mut self);
492}
493
494#[derive(Clone, Copy, Debug, Eq, PartialEq)]
495enum ArchiveEntry {
496    None,
497    Normal,
498    LongPath,
499    LongLink,
500    Pax,
501}
502
503/// Allocation-free streaming tar/PAX layer parser.
504pub struct Archive<'a> {
505    header: [u8; BLOCK_SIZE],
506    header_len: usize,
507    entry_remaining: u64,
508    padding_remaining: usize,
509    current: ArchiveEntry,
510    entry_open: bool,
511    zero_blocks: u8,
512    path: &'a mut [u8],
513    path_len: usize,
514    link: &'a mut [u8],
515    link_len: usize,
516    pax: &'a mut [u8],
517    pax_len: usize,
518    pending_long_path: bool,
519    pending_long_link: bool,
520    pending_pax: bool,
521}
522
523impl<'a> Archive<'a> {
524    pub fn new(buffers: ArchiveBuffers<'a>) -> Self {
525        Self {
526            header: [0; BLOCK_SIZE],
527            header_len: 0,
528            entry_remaining: 0,
529            padding_remaining: 0,
530            current: ArchiveEntry::None,
531            entry_open: false,
532            zero_blocks: 0,
533            path: buffers.path,
534            path_len: 0,
535            link: buffers.link,
536            link_len: 0,
537            pax: buffers.pax,
538            pax_len: 0,
539            pending_long_path: false,
540            pending_long_link: false,
541            pending_pax: false,
542        }
543    }
544
545    pub fn push<S: LayerEventSink>(
546        &mut self,
547        mut input: &[u8],
548        sink: &mut S,
549    ) -> Result<(), ArchiveError<S::Error>> {
550        while !input.is_empty() {
551            if self.zero_blocks == 2 {
552                return Ok(());
553            }
554            if self.entry_remaining != 0 {
555                let length = input.len().min(u64_to_usize(self.entry_remaining));
556                let bytes = &input[..length];
557                match self.current {
558                    ArchiveEntry::Normal => sink.entry_data(bytes).map_err(ArchiveError::Sink)?,
559                    ArchiveEntry::LongPath => {
560                        append_scratch(self.path, &mut self.path_len, bytes, Scratch::Path)?
561                    }
562                    ArchiveEntry::LongLink => {
563                        append_scratch(self.link, &mut self.link_len, bytes, Scratch::Link)?
564                    }
565                    ArchiveEntry::Pax => {
566                        append_scratch(self.pax, &mut self.pax_len, bytes, Scratch::Pax)?
567                    }
568                    ArchiveEntry::None => return Err(ArchiveError::InvalidState),
569                }
570                self.entry_remaining -= length as u64;
571                input = &input[length..];
572                if self.entry_remaining == 0 {
573                    self.finish_content(sink)?;
574                }
575                continue;
576            }
577            if self.padding_remaining != 0 {
578                let length = input.len().min(self.padding_remaining);
579                self.padding_remaining -= length;
580                input = &input[length..];
581                if self.padding_remaining == 0 {
582                    self.current = ArchiveEntry::None;
583                }
584                continue;
585            }
586
587            let length = input.len().min(BLOCK_SIZE - self.header_len);
588            self.header[self.header_len..self.header_len + length]
589                .copy_from_slice(&input[..length]);
590            self.header_len += length;
591            input = &input[length..];
592            if self.header_len == BLOCK_SIZE {
593                self.consume_header(sink)?;
594                self.header_len = 0;
595            }
596        }
597        Ok(())
598    }
599
600    pub fn finish(&self) -> Result<(), ArchiveFinishError> {
601        if self.zero_blocks != 2
602            || self.header_len != 0
603            || self.entry_remaining != 0
604            || self.padding_remaining != 0
605            || self.entry_open
606            || self.current != ArchiveEntry::None
607        {
608            return Err(ArchiveFinishError::UnexpectedEof);
609        }
610        if self.pending_long_path || self.pending_long_link || self.pending_pax {
611            return Err(ArchiveFinishError::DanglingExtension);
612        }
613        Ok(())
614    }
615
616    fn consume_header<S: LayerEventSink>(
617        &mut self,
618        sink: &mut S,
619    ) -> Result<(), ArchiveError<S::Error>> {
620        if self.header.iter().all(|byte| *byte == 0) {
621            if self.current != ArchiveEntry::None {
622                return Err(ArchiveError::InvalidState);
623            }
624            self.zero_blocks += 1;
625            return Ok(());
626        }
627        if self.zero_blocks != 0 {
628            return Err(ArchiveError::InvalidEndMarker);
629        }
630        verify_checksum::<()>(&self.header).map_err(|_| ArchiveError::InvalidChecksum)?;
631
632        let mut size = parse_number(&self.header[SIZE_RANGE]).ok_or(ArchiveError::InvalidSize)?;
633        let type_flag = self.header[TYPE_OFFSET];
634        if self.start_extension(type_flag, size, sink)? {
635            return Ok(());
636        }
637
638        let pax = if self.pending_pax {
639            &self.pax[..self.pax_len]
640        } else {
641            &[]
642        };
643        let [pax_size, pax_path, pax_link] = pax_overrides(pax)?;
644        if let Some(value) = pax_size {
645            size = decimal(value).ok_or(ArchiveError::InvalidSize)?;
646        }
647
648        if let Some(value) = pax_path {
649            self.path_len = copy_scratch(self.path, value, Scratch::Path)?;
650        } else if !self.pending_long_path {
651            self.path_len = header_path(&self.header, self.path)?;
652        }
653        trim_nul(self.path, &mut self.path_len);
654        self.path_len = normalize_path(self.path, self.path_len)?;
655
656        if let Some(value) = pax_link {
657            self.link_len = copy_scratch(self.link, value, Scratch::Link)?;
658        } else if !self.pending_long_link {
659            self.link_len = copy_scratch(
660                self.link,
661                nul_terminated(&self.header[157..257]),
662                Scratch::Link,
663            )?;
664        }
665        trim_nul(self.link, &mut self.link_len);
666
667        let kind = entry_kind(type_flag);
668        match kind {
669            EntryKind::HardLink if self.link_len != 0 => {
670                self.link_len = normalize_path(self.link, self.link_len)?;
671            }
672            EntryKind::SymbolicLink if self.link_len != 0 => {
673                validate_symbolic_link(&self.path[..self.path_len], &self.link[..self.link_len])?;
674            }
675            _ => {}
676        }
677
678        self.pending_long_path = false;
679        self.pending_long_link = false;
680        self.current = ArchiveEntry::Normal;
681        self.entry_remaining = size;
682        self.padding_remaining = padding_for(size);
683
684        if let Some(whiteout) = whiteout(self.path, &mut self.path_len) {
685            if kind != EntryKind::Regular || size != 0 {
686                return Err(ArchiveError::InvalidWhiteout);
687            }
688            match whiteout {
689                Whiteout::Remove => sink
690                    .whiteout(&self.path[..self.path_len])
691                    .map_err(ArchiveError::Sink)?,
692                Whiteout::Opaque => sink
693                    .opaque_directory(&self.path[..self.path_len])
694                    .map_err(ArchiveError::Sink)?,
695            }
696            self.pending_pax = false;
697            self.pax_len = 0;
698            self.current = ArchiveEntry::None;
699            return Ok(());
700        }
701
702        let number = |range| parse_number(&self.header[range]).ok_or(ArchiveError::InvalidMetadata);
703        let (device_major, device_minor) = match kind {
704            EntryKind::CharacterDevice | EntryKind::BlockDevice => {
705                (Some(number(329..337)?), Some(number(337..345)?))
706            }
707            _ => (None, None),
708        };
709        let link_target = matches!(kind, EntryKind::HardLink | EntryKind::SymbolicLink)
710            .then_some(&self.link[..self.link_len]);
711        sink.begin_entry(Entry {
712            path: &self.path[..self.path_len],
713            kind,
714            size,
715            mode: number(100..108)?,
716            uid: number(108..116)?,
717            gid: number(116..124)?,
718            mtime: number(136..148)?,
719            link_target,
720            device_major,
721            device_minor,
722            pax,
723        })
724        .map_err(ArchiveError::Sink)?;
725        self.entry_open = true;
726        self.pending_pax = false;
727        self.pax_len = 0;
728        if size == 0 {
729            self.finish_content(sink)?;
730        }
731        Ok(())
732    }
733
734    fn start_extension<S: LayerEventSink>(
735        &mut self,
736        type_flag: u8,
737        size: u64,
738        sink: &mut S,
739    ) -> Result<bool, ArchiveError<S::Error>> {
740        self.current = match type_flag {
741            b'L' => {
742                self.path_len = 0;
743                ArchiveEntry::LongPath
744            }
745            b'K' => {
746                self.link_len = 0;
747                ArchiveEntry::LongLink
748            }
749            b'x' => {
750                self.pax_len = 0;
751                ArchiveEntry::Pax
752            }
753            b'g' => return Err(ArchiveError::UnsupportedGlobalPax),
754            _ => return Ok(false),
755        };
756        self.entry_remaining = size;
757        self.padding_remaining = padding_for(size);
758        if size == 0 {
759            self.finish_content(sink)?;
760        }
761        Ok(true)
762    }
763
764    fn finish_content<S: LayerEventSink>(
765        &mut self,
766        sink: &mut S,
767    ) -> Result<(), ArchiveError<S::Error>> {
768        match self.current {
769            ArchiveEntry::Normal => {
770                if self.entry_open {
771                    sink.end_entry().map_err(ArchiveError::Sink)?;
772                    self.entry_open = false;
773                }
774            }
775            ArchiveEntry::LongPath => {
776                trim_nul(self.path, &mut self.path_len);
777                self.pending_long_path = true;
778            }
779            ArchiveEntry::LongLink => {
780                trim_nul(self.link, &mut self.link_len);
781                self.pending_long_link = true;
782            }
783            ArchiveEntry::Pax => {
784                validate_pax(&self.pax[..self.pax_len])?;
785                self.pending_pax = true;
786            }
787            ArchiveEntry::None => return Err(ArchiveError::InvalidState),
788        }
789        if self.padding_remaining == 0 {
790            self.current = ArchiveEntry::None;
791        }
792        Ok(())
793    }
794}
795
796#[derive(Clone, Copy, Debug, Eq, PartialEq)]
797pub enum Scratch {
798    Path,
799    Link,
800    Pax,
801}
802
803#[derive(Debug, Eq, PartialEq, derive_more::Display, derive_more::From)]
804pub enum ArchiveError<E> {
805    #[display("invalid tar header checksum")]
806    InvalidChecksum,
807    #[display("invalid tar entry size")]
808    InvalidSize,
809    #[display("invalid tar entry metadata")]
810    InvalidMetadata,
811    #[display("invalid tar end marker")]
812    InvalidEndMarker,
813    #[display("unsafe or invalid tar path")]
814    InvalidPath,
815    #[display("unsafe tar link target")]
816    InvalidLink,
817    #[display("invalid OCI whiteout entry")]
818    InvalidWhiteout,
819    #[display("invalid PAX header: {_0}")]
820    #[from]
821    InvalidPax(PaxError),
822    #[display("global PAX headers are not supported")]
823    UnsupportedGlobalPax,
824    #[display("{_0:?} buffer is too small")]
825    BufferTooSmall(Scratch),
826    #[display("invalid tar parser state")]
827    InvalidState,
828    #[display("layer sink failed: {_0}")]
829    Sink(E),
830}
831
832#[derive(Clone, Copy, Debug, Eq, PartialEq, derive_more::Display)]
833pub enum ArchiveFinishError {
834    #[display("unexpected end of tar archive")]
835    UnexpectedEof,
836    #[display("tar archive ended after an extension header")]
837    DanglingExtension,
838}
839
840#[derive(Clone, Copy, Debug, Eq, PartialEq, derive_more::Display)]
841pub enum PaxError {
842    #[display("invalid record length")]
843    InvalidLength,
844    #[display("invalid key/value record")]
845    InvalidRecord,
846}
847
848#[derive(Clone, Copy)]
849enum Whiteout {
850    Remove,
851    Opaque,
852}
853
854fn entry_kind(type_flag: u8) -> EntryKind {
855    match type_flag {
856        0 | b'0' => EntryKind::Regular,
857        b'1' => EntryKind::HardLink,
858        b'2' => EntryKind::SymbolicLink,
859        b'3' => EntryKind::CharacterDevice,
860        b'4' => EntryKind::BlockDevice,
861        b'5' => EntryKind::Directory,
862        b'6' => EntryKind::Fifo,
863        b'7' => EntryKind::Contiguous,
864        other => EntryKind::Other(other),
865    }
866}
867
868fn header_path<E>(
869    header: &[u8; BLOCK_SIZE],
870    destination: &mut [u8],
871) -> Result<usize, ArchiveError<E>> {
872    let name = nul_terminated(&header[NAME_RANGE]);
873    let prefix = nul_terminated(&header[PREFIX_RANGE]);
874    let needed = name.len() + usize::from(!prefix.is_empty()) + prefix.len();
875    if needed > destination.len() {
876        return Err(ArchiveError::BufferTooSmall(Scratch::Path));
877    }
878    let mut length = 0;
879    if !prefix.is_empty() {
880        destination[..prefix.len()].copy_from_slice(prefix);
881        length += prefix.len();
882        destination[length] = b'/';
883        length += 1;
884    }
885    destination[length..length + name.len()].copy_from_slice(name);
886    Ok(length + name.len())
887}
888
889fn append_scratch<E>(
890    destination: &mut [u8],
891    length: &mut usize,
892    bytes: &[u8],
893    kind: Scratch,
894) -> Result<(), ArchiveError<E>> {
895    crate::buffer::append(destination, length, bytes)
896        .map_err(|_| ArchiveError::BufferTooSmall(kind))
897}
898
899fn copy_scratch<E>(
900    destination: &mut [u8],
901    bytes: &[u8],
902    kind: Scratch,
903) -> Result<usize, ArchiveError<E>> {
904    if bytes.len() > destination.len() {
905        return Err(ArchiveError::BufferTooSmall(kind));
906    }
907    destination[..bytes.len()].copy_from_slice(bytes);
908    Ok(bytes.len())
909}
910
911fn trim_nul(bytes: &[u8], length: &mut usize) {
912    while *length != 0 && bytes[*length - 1] == 0 {
913        *length -= 1;
914    }
915}
916
917fn normalize_path<E>(bytes: &mut [u8], length: usize) -> Result<usize, ArchiveError<E>> {
918    if length == 0 || bytes[0] == b'/' || bytes[..length].contains(&0) {
919        return Err(ArchiveError::InvalidPath);
920    }
921    let mut read = 0;
922    let mut write = 0;
923    while read < length {
924        let start = read;
925        let end = bytes[start..length]
926            .iter()
927            .position(|byte| *byte == b'/')
928            .map_or(length, |offset| start + offset);
929        read = end + 1;
930        match &bytes[start..end] {
931            b"" | b"." => continue,
932            b".." => return Err(ArchiveError::InvalidPath),
933            _ => {}
934        }
935        if write != 0 {
936            bytes[write] = b'/';
937            write += 1;
938        }
939        bytes.copy_within(start..end, write);
940        write += end - start;
941    }
942    if write == 0 {
943        bytes[0] = b'.';
944        write = 1;
945    }
946    Ok(write)
947}
948
949fn validate_symbolic_link<E>(path: &[u8], target: &[u8]) -> Result<(), ArchiveError<E>> {
950    if target.is_empty() || target[0] == b'/' || target.contains(&0) {
951        return Err(ArchiveError::InvalidLink);
952    }
953    let mut depth = path.split(|byte| *byte == b'/').count().saturating_sub(1);
954    for component in target.split(|byte| *byte == b'/') {
955        match component {
956            b"" | b"." => {}
957            b".." => {
958                depth = depth.checked_sub(1).ok_or(ArchiveError::InvalidLink)?;
959            }
960            _ => depth += 1,
961        }
962    }
963    Ok(())
964}
965
966fn whiteout(path: &mut [u8], length: &mut usize) -> Option<Whiteout> {
967    let basename = path[..*length]
968        .iter()
969        .rposition(|byte| *byte == b'/')
970        .map_or(0, |position| position + 1);
971    let name = &path[basename..*length];
972    if name == b".wh..wh..opq" {
973        if basename == 0 {
974            path[0] = b'.';
975            *length = 1;
976        } else {
977            *length = basename - 1;
978        }
979        return Some(Whiteout::Opaque);
980    }
981    let target = name.strip_prefix(b".wh.")?;
982    if target.is_empty() {
983        return None;
984    }
985    path.copy_within(basename + 4..*length, basename);
986    *length -= 4;
987    Some(Whiteout::Remove)
988}
989
990fn validate_pax<E>(bytes: &[u8]) -> Result<(), ArchiveError<E>> {
991    for record in PaxRecords::new(bytes) {
992        record?;
993    }
994    Ok(())
995}
996
997fn pax_overrides<E>(bytes: &[u8]) -> Result<[Option<&[u8]>; 3], ArchiveError<E>> {
998    let mut values = [None; 3];
999    for record in PaxRecords::new(bytes) {
1000        let record = record?;
1001        let index = match record.key {
1002            b"size" => 0,
1003            b"path" => 1,
1004            b"linkpath" => 2,
1005            _ => continue,
1006        };
1007        if values[index].replace(record.value).is_some() {
1008            return Err(ArchiveError::InvalidPax(PaxError::InvalidRecord));
1009        }
1010    }
1011    Ok(values)
1012}
1013
1014fn parse_pax_record(bytes: &[u8]) -> Result<(PaxRecord<'_>, usize), PaxError> {
1015    let space = bytes
1016        .iter()
1017        .position(|byte| *byte == b' ')
1018        .ok_or(PaxError::InvalidLength)?;
1019    let length = decimal(&bytes[..space]).ok_or(PaxError::InvalidLength)?;
1020    let length = usize::try_from(length).map_err(|_| PaxError::InvalidLength)?;
1021    let end = length;
1022    let record = bytes.get(space + 1..end).ok_or(PaxError::InvalidLength)?;
1023    let record = record.strip_suffix(b"\n").ok_or(PaxError::InvalidRecord)?;
1024    let equals = record
1025        .iter()
1026        .position(|byte| *byte == b'=')
1027        .ok_or(PaxError::InvalidRecord)?;
1028    if equals == 0 {
1029        return Err(PaxError::InvalidRecord);
1030    }
1031    Ok((
1032        PaxRecord {
1033            key: &record[..equals],
1034            value: &record[equals + 1..],
1035        },
1036        end,
1037    ))
1038}
1039
1040fn decimal(bytes: &[u8]) -> Option<u64> {
1041    if !bytes.iter().all(u8::is_ascii_digit) {
1042        return None;
1043    }
1044    core::str::from_utf8(bytes).ok()?.parse().ok()
1045}
1046
1047#[cfg(test)]
1048mod tests {
1049    use super::{
1050        header_path, normalize_path, pax_overrides, validate_pax, validate_symbolic_link, Archive,
1051        ArchiveBuffers, ArchiveError, Entry, EntryExtractor, EntryKind, ExtractError, FinishError,
1052        LayerEventSink, PaxError, TarWriteError, TarWriter, BLOCK_SIZE,
1053    };
1054    use std::{format, string::ToString, vec::Vec};
1055
1056    type OwnedPaxRecord = (Vec<u8>, Vec<u8>);
1057    type OwnedEntry = (Vec<u8>, EntryKind, Vec<OwnedPaxRecord>);
1058
1059    #[derive(Default)]
1060    struct Events {
1061        entries: Vec<OwnedEntry>,
1062        contents: Vec<u8>,
1063        whiteouts: Vec<Vec<u8>>,
1064        opaque: Vec<Vec<u8>>,
1065    }
1066
1067    #[test]
1068    fn writes_deterministic_ustar_files() {
1069        let mut bytes = Vec::new();
1070        let mut writer = TarWriter::new();
1071        writer
1072            .begin_file(b"blobs/sha256/abc", 5, 0o644, |chunk| {
1073                bytes.extend_from_slice(chunk);
1074                Ok::<_, ()>(())
1075            })
1076            .unwrap();
1077        writer
1078            .write_file_data(b"hello", |chunk| {
1079                bytes.extend_from_slice(chunk);
1080                Ok::<_, ()>(())
1081            })
1082            .unwrap();
1083        writer
1084            .end_file(|chunk| {
1085                bytes.extend_from_slice(chunk);
1086                Ok::<_, ()>(())
1087            })
1088            .unwrap();
1089        writer
1090            .finish(|chunk| {
1091                bytes.extend_from_slice(chunk);
1092                Ok::<_, ()>(())
1093            })
1094            .unwrap();
1095
1096        assert_eq!(&bytes[..18], b"blobs/sha256/abc\0\0");
1097        assert_eq!(&bytes[BLOCK_SIZE..BLOCK_SIZE + 5], b"hello");
1098        assert_eq!(bytes.len(), BLOCK_SIZE * 4);
1099
1100        let mut invalid = TarWriter::new();
1101        assert_eq!(
1102            invalid.begin_file(b"../escape", 0, 0o644, |_| Ok::<_, ()>(())),
1103            Err(TarWriteError::InvalidPath)
1104        );
1105    }
1106
1107    impl LayerEventSink for Events {
1108        type Error = ();
1109
1110        fn begin_entry(&mut self, entry: Entry<'_>) -> Result<(), Self::Error> {
1111            let pax = entry
1112                .pax_records()
1113                .map(|record| {
1114                    let record = record.unwrap();
1115                    (record.key.to_vec(), record.value.to_vec())
1116                })
1117                .collect();
1118            self.entries.push((entry.path.to_vec(), entry.kind, pax));
1119            Ok(())
1120        }
1121
1122        fn entry_data(&mut self, bytes: &[u8]) -> Result<(), Self::Error> {
1123            self.contents.extend_from_slice(bytes);
1124            Ok(())
1125        }
1126
1127        fn end_entry(&mut self) -> Result<(), Self::Error> {
1128            Ok(())
1129        }
1130
1131        fn whiteout(&mut self, path: &[u8]) -> Result<(), Self::Error> {
1132            self.whiteouts.push(path.to_vec());
1133            Ok(())
1134        }
1135
1136        fn opaque_directory(&mut self, path: &[u8]) -> Result<(), Self::Error> {
1137            self.opaque.push(path.to_vec());
1138            Ok(())
1139        }
1140    }
1141
1142    #[test]
1143    fn streams_layer_events_and_whiteouts_at_every_fragment_size() {
1144        let mut bytes = Vec::new();
1145        append_entry(&mut bytes, b"./etc//config", b"value", b'0', b"");
1146        append_entry(&mut bytes, b"etc/.wh.deleted", b"", b'0', b"");
1147        append_entry(&mut bytes, b"var/lib/.wh..wh..opq", b"", b'0', b"");
1148        finish_archive(&mut bytes);
1149
1150        for fragment in 1..=BLOCK_SIZE + 1 {
1151            let mut path = [0; 256];
1152            let mut link = [0; 256];
1153            let mut pax = [0; 512];
1154            let mut archive = Archive::new(ArchiveBuffers {
1155                path: &mut path,
1156                link: &mut link,
1157                pax: &mut pax,
1158            });
1159            let mut events = Events::default();
1160            for chunk in bytes.chunks(fragment) {
1161                archive.push(chunk, &mut events).unwrap();
1162            }
1163            archive.finish().unwrap();
1164            assert_eq!(events.entries[0].0, b"etc/config");
1165            assert_eq!(events.contents, b"value");
1166            assert_eq!(events.whiteouts, [b"etc/deleted".to_vec()]);
1167            assert_eq!(events.opaque, [b"var/lib".to_vec()]);
1168        }
1169    }
1170
1171    #[test]
1172    fn applies_pax_and_gnu_long_path_overrides_without_allocating() {
1173        let mut bytes = Vec::new();
1174        let record = pax_record("path", "pax/overridden/file");
1175        append_entry(&mut bytes, b"PaxHeaders.0", &record, b'x', b"");
1176        append_entry(&mut bytes, b"ignored", b"pax", b'0', b"");
1177
1178        let long = [b'a'; 140];
1179        let mut long_nul = long.to_vec();
1180        long_nul.push(0);
1181        append_entry(&mut bytes, b"LongLink", &long_nul, b'L', b"");
1182        append_entry(&mut bytes, b"ignored", b"gnu", b'0', b"");
1183        finish_archive(&mut bytes);
1184
1185        let mut path = [0; 256];
1186        let mut link = [0; 256];
1187        let mut pax = [0; 512];
1188        let mut archive = Archive::new(ArchiveBuffers {
1189            path: &mut path,
1190            link: &mut link,
1191            pax: &mut pax,
1192        });
1193        let mut events = Events::default();
1194        archive.push(&bytes, &mut events).unwrap();
1195        archive.finish().unwrap();
1196        assert_eq!(events.entries[0].0, b"pax/overridden/file");
1197        assert_eq!(events.entries[0].2[0].0, b"path");
1198        assert_eq!(events.entries[1].0, long);
1199        assert_eq!(events.contents, b"paxgnu");
1200    }
1201
1202    #[test]
1203    fn rejects_paths_that_escape_the_root() {
1204        let mut bytes = Vec::new();
1205        append_entry(&mut bytes, b"../escape", b"", b'0', b"");
1206        let mut path = [0; 64];
1207        let mut link = [0; 64];
1208        let mut pax = [0; 64];
1209        let mut archive = Archive::new(ArchiveBuffers {
1210            path: &mut path,
1211            link: &mut link,
1212            pax: &mut pax,
1213        });
1214        assert!(archive.push(&bytes, &mut Events::default()).is_err());
1215    }
1216
1217    #[test]
1218    fn rejects_absolute_symbolic_link_targets() {
1219        let mut bytes = Vec::new();
1220        append_link(&mut bytes, b"link", b"/etc/passwd", b'2');
1221        let mut path = [0; 64];
1222        let mut link = [0; 64];
1223        let mut pax = [0; 64];
1224        let mut archive = Archive::new(ArchiveBuffers {
1225            path: &mut path,
1226            link: &mut link,
1227            pax: &mut pax,
1228        });
1229        assert_eq!(
1230            archive.push(&bytes, &mut Events::default()),
1231            Err(ArchiveError::InvalidLink)
1232        );
1233    }
1234
1235    #[test]
1236    fn rejects_symbolic_link_targets_that_escape_the_root() {
1237        let mut bytes = Vec::new();
1238        append_link(&mut bytes, b"dir/link", b"../../escape", b'2');
1239        let mut path = [0; 64];
1240        let mut link = [0; 64];
1241        let mut pax = [0; 64];
1242        let mut archive = Archive::new(ArchiveBuffers {
1243            path: &mut path,
1244            link: &mut link,
1245            pax: &mut pax,
1246        });
1247        assert_eq!(
1248            archive.push(&bytes, &mut Events::default()),
1249            Err(ArchiveError::InvalidLink)
1250        );
1251    }
1252
1253    #[test]
1254    fn resolves_symbolic_link_target_components_before_checking_depth() {
1255        assert_eq!(
1256            validate_symbolic_link::<()>(b"link", b"dir/../target"),
1257            Ok(())
1258        );
1259        assert_eq!(
1260            validate_symbolic_link::<()>(b"dir/link", b"./../../escape"),
1261            Err(ArchiveError::InvalidLink)
1262        );
1263        assert_eq!(
1264            validate_symbolic_link::<()>(b"dir/link", b"nested//../../../escape"),
1265            Err(ArchiveError::InvalidLink)
1266        );
1267    }
1268
1269    #[test]
1270    fn joins_ustar_name_and_prefix_with_exact_buffer_bounds() {
1271        let mut header = [0; BLOCK_SIZE];
1272        header[..4].copy_from_slice(b"name");
1273        header[345..351].copy_from_slice(b"prefix");
1274
1275        let mut exact = [0; 11];
1276        assert_eq!(header_path::<()>(&header, &mut exact), Ok(11));
1277        assert_eq!(&exact, b"prefix/name");
1278
1279        let mut too_small = [0; 10];
1280        assert!(header_path::<()>(&header, &mut too_small).is_err());
1281
1282        header[345..351].fill(0);
1283        let mut name_only = [0; 4];
1284        assert_eq!(header_path::<()>(&header, &mut name_only), Ok(4));
1285        assert_eq!(&name_only, b"name");
1286    }
1287
1288    #[test]
1289    fn normalizes_paths_and_rejects_invalid_inputs() {
1290        let mut path = *b"./a//b/./";
1291        let length = normalize_path::<()>(&mut path, 9).unwrap();
1292        assert_eq!(&path[..length], b"a/b");
1293
1294        let mut dot = *b".";
1295        let length = normalize_path::<()>(&mut dot, 1).unwrap();
1296        assert_eq!(&dot[..length], b".");
1297
1298        for invalid in [b"".as_slice(), b"/root", b"a\0b", b"../root"] {
1299            let mut path = [0; 16];
1300            path[..invalid.len()].copy_from_slice(invalid);
1301            assert_eq!(
1302                normalize_path::<()>(&mut path, invalid.len()),
1303                Err(ArchiveError::InvalidPath)
1304            );
1305        }
1306    }
1307
1308    #[test]
1309    fn rejects_malformed_pax_records() {
1310        assert!(validate_pax::<()>(b"garbage").is_err());
1311        for key in ["size", "path", "linkpath"] {
1312            let duplicate = pax_record(key, "first").repeat(2);
1313            assert_eq!(
1314                pax_overrides::<()>(&duplicate),
1315                Err(ArchiveError::InvalidPax(PaxError::InvalidRecord))
1316            );
1317        }
1318        assert_eq!(
1319            pax_overrides::<()>(&pax_record("vendor.key", "value").repeat(2)),
1320            Ok([None; 3])
1321        );
1322    }
1323
1324    #[test]
1325    fn extracts_a_regular_file_from_arbitrary_fragments() {
1326        let mut archive = Vec::new();
1327        append_entry(&mut archive, b"first", b"skip", b'0', b"");
1328        append_entry(&mut archive, b"wanted", b"contents", b'0', b"");
1329        append_entry(&mut archive, b"wanted", b"later", b'0', b"");
1330        finish_archive(&mut archive);
1331
1332        for fragment_size in 1..=BLOCK_SIZE + 1 {
1333            let mut extractor = EntryExtractor::new(b"wanted");
1334            let mut output = Vec::new();
1335            for fragment in archive.chunks(fragment_size) {
1336                extractor
1337                    .push(fragment, |bytes| {
1338                        output.extend_from_slice(bytes);
1339                        Ok::<_, ()>(())
1340                    })
1341                    .unwrap();
1342            }
1343            extractor.finish().unwrap();
1344            assert_eq!(output, b"contents");
1345        }
1346    }
1347
1348    #[test]
1349    fn matches_a_ustar_prefix_without_allocating_a_path() {
1350        let mut archive = Vec::new();
1351        append_entry(
1352            &mut archive,
1353            b"settings.yaml",
1354            b"value",
1355            b'0',
1356            b"etc/datadog-agent",
1357        );
1358        finish_archive(&mut archive);
1359        let mut extractor = EntryExtractor::new(b"etc/datadog-agent/settings.yaml");
1360        let mut output = Vec::new();
1361        extractor
1362            .push(&archive, |bytes| {
1363                output.extend_from_slice(bytes);
1364                Ok::<_, ()>(())
1365            })
1366            .unwrap();
1367        extractor.finish().unwrap();
1368        assert_eq!(output, b"value");
1369    }
1370
1371    #[test]
1372    fn recognizes_an_empty_regular_file() {
1373        let mut archive = Vec::new();
1374        append_entry(&mut archive, b"empty", b"", b'0', b"");
1375        finish_archive(&mut archive);
1376        let mut extractor = EntryExtractor::new(b"empty");
1377        extractor.push(&archive, |_| Ok::<_, ()>(())).unwrap();
1378        assert!(extractor.found());
1379        extractor.finish().unwrap();
1380    }
1381
1382    #[test]
1383    fn does_not_extract_a_directory_with_the_requested_name() {
1384        let mut archive = Vec::new();
1385        append_entry(&mut archive, b"wanted", b"", b'5', b"");
1386        finish_archive(&mut archive);
1387        let mut extractor = EntryExtractor::new(b"wanted");
1388        extractor.push(&archive, |_| Ok::<_, ()>(())).unwrap();
1389        assert_eq!(extractor.finish(), Err(FinishError::NotFound));
1390    }
1391
1392    #[test]
1393    fn rejects_an_invalid_checksum() {
1394        let mut archive = Vec::new();
1395        append_entry(&mut archive, b"wanted", b"contents", b'0', b"");
1396        archive[0] ^= 1;
1397        let mut extractor = EntryExtractor::new(b"wanted");
1398        assert_eq!(
1399            extractor.push(&archive, |_| Ok::<_, ()>(())),
1400            Err(ExtractError::InvalidChecksum)
1401        );
1402    }
1403
1404    #[test]
1405    fn reports_a_truncated_stream() {
1406        let mut archive = Vec::new();
1407        append_entry(&mut archive, b"wanted", b"contents", b'0', b"");
1408        let mut extractor = EntryExtractor::new(b"wanted");
1409        extractor
1410            .push(&archive[..BLOCK_SIZE + 2], |_| Ok::<_, ()>(()))
1411            .unwrap();
1412        assert_eq!(extractor.finish(), Err(FinishError::UnexpectedEof));
1413    }
1414
1415    fn append_entry(archive: &mut Vec<u8>, name: &[u8], contents: &[u8], kind: u8, prefix: &[u8]) {
1416        let mut header = [0u8; BLOCK_SIZE];
1417        header[..name.len()].copy_from_slice(name);
1418        write_octal(&mut header[100..108], 0o644);
1419        write_octal(&mut header[108..116], 0);
1420        write_octal(&mut header[116..124], 0);
1421        write_octal(&mut header[124..136], contents.len() as u64);
1422        write_octal(&mut header[136..148], 0);
1423        header[148..156].fill(b' ');
1424        header[156] = kind;
1425        header[257..263].copy_from_slice(b"ustar\0");
1426        header[263..265].copy_from_slice(b"00");
1427        header[345..345 + prefix.len()].copy_from_slice(prefix);
1428        let checksum = header.iter().map(|byte| u64::from(*byte)).sum();
1429        write_octal(&mut header[148..156], checksum);
1430
1431        archive.extend_from_slice(&header);
1432        archive.extend_from_slice(contents);
1433        archive.resize(archive.len() + super::padding_for(contents.len() as u64), 0);
1434    }
1435
1436    fn append_link(archive: &mut Vec<u8>, name: &[u8], target: &[u8], kind: u8) {
1437        let start = archive.len();
1438        append_entry(archive, name, b"", kind, b"");
1439        let header = &mut archive[start..start + BLOCK_SIZE];
1440        header[157..157 + target.len()].copy_from_slice(target);
1441        header[148..156].fill(b' ');
1442        let checksum = header.iter().map(|byte| u64::from(*byte)).sum();
1443        write_octal(&mut header[148..156], checksum);
1444    }
1445
1446    fn finish_archive(archive: &mut Vec<u8>) {
1447        archive.resize(archive.len() + 2 * BLOCK_SIZE, 0);
1448    }
1449
1450    fn pax_record(key: &str, value: &str) -> Vec<u8> {
1451        let payload = key.len() + 1 + value.len() + 1;
1452        let mut digits = 1;
1453        loop {
1454            let length = digits + 1 + payload;
1455            let actual_digits = length.to_string().len();
1456            if actual_digits == digits {
1457                return format!("{length} {key}={value}\n").into_bytes();
1458            }
1459            digits = actual_digits;
1460        }
1461    }
1462
1463    fn write_octal(field: &mut [u8], mut value: u64) {
1464        field.fill(b'0');
1465        *field.last_mut().unwrap() = 0;
1466        let digits = field.len() - 1;
1467        for byte in field[..digits].iter_mut().rev() {
1468            *byte = b'0' + (value % 8) as u8;
1469            value /= 8;
1470        }
1471        assert_eq!(value, 0);
1472    }
1473}