Skip to main content

oci_zero/
tar.rs

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