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[0] == b'/' || target.contains(&0) {
1066        return Err(ArchiveError::InvalidLink);
1067    }
1068    let mut depth = path.split(|byte| *byte == b'/').count().saturating_sub(1);
1069    for component in target.split(|byte| *byte == b'/') {
1070        match component {
1071            b"" | b"." => {}
1072            b".." => {
1073                depth = depth.checked_sub(1).ok_or(ArchiveError::InvalidLink)?;
1074            }
1075            _ => depth += 1,
1076        }
1077    }
1078    Ok(())
1079}
1080
1081fn whiteout(path: &mut [u8], length: &mut usize) -> Option<Whiteout> {
1082    let basename = path[..*length]
1083        .iter()
1084        .rposition(|byte| *byte == b'/')
1085        .map_or(0, |position| position + 1);
1086    let name = &path[basename..*length];
1087    if name == b".wh..wh..opq" {
1088        if basename == 0 {
1089            path[0] = b'.';
1090            *length = 1;
1091        } else {
1092            *length = basename - 1;
1093        }
1094        return Some(Whiteout::Opaque);
1095    }
1096    let target = name.strip_prefix(b".wh.")?;
1097    if target.is_empty() {
1098        return None;
1099    }
1100    path.copy_within(basename + 4..*length, basename);
1101    *length -= 4;
1102    Some(Whiteout::Remove)
1103}
1104
1105fn validate_pax<E>(bytes: &[u8]) -> Result<(), ArchiveError<E>> {
1106    for record in PaxRecords::new(bytes) {
1107        record?;
1108    }
1109    Ok(())
1110}
1111
1112fn pax_value<'a, E>(bytes: &'a [u8], key: &[u8]) -> Result<Option<&'a [u8]>, ArchiveError<E>> {
1113    let mut found = None;
1114    for record in PaxRecords::new(bytes) {
1115        let record = record?;
1116        if record.key == key {
1117            if found.is_some() {
1118                return Err(ArchiveError::InvalidPax(PaxError::InvalidRecord));
1119            }
1120            found = Some(record.value);
1121        }
1122    }
1123    Ok(found)
1124}
1125
1126fn parse_pax_record(bytes: &[u8], position: usize) -> Result<(PaxRecord<'_>, usize), PaxError> {
1127    let space = bytes[position..]
1128        .iter()
1129        .position(|byte| *byte == b' ')
1130        .map(|offset| position + offset)
1131        .ok_or(PaxError::InvalidLength)?;
1132    let length = decimal(&bytes[position..space]).ok_or(PaxError::InvalidLength)?;
1133    let length = usize::try_from(length).map_err(|_| PaxError::InvalidLength)?;
1134    let end = position
1135        .checked_add(length)
1136        .ok_or(PaxError::InvalidLength)?;
1137    let record = bytes.get(space + 1..end).ok_or(PaxError::InvalidLength)?;
1138    let record = record.strip_suffix(b"\n").ok_or(PaxError::InvalidRecord)?;
1139    let equals = record
1140        .iter()
1141        .position(|byte| *byte == b'=')
1142        .ok_or(PaxError::InvalidRecord)?;
1143    if equals == 0 {
1144        return Err(PaxError::InvalidRecord);
1145    }
1146    Ok((
1147        PaxRecord {
1148            key: &record[..equals],
1149            value: &record[equals + 1..],
1150        },
1151        end,
1152    ))
1153}
1154
1155fn decimal(bytes: &[u8]) -> Option<u64> {
1156    if bytes.is_empty() {
1157        return None;
1158    }
1159    bytes.iter().try_fold(0u64, |value, byte| {
1160        value.checked_mul(10)?.checked_add(u64::from(
1161            byte.checked_sub(b'0').filter(|digit| *digit <= 9)?,
1162        ))
1163    })
1164}
1165
1166#[cfg(test)]
1167mod tests {
1168    use super::{
1169        header_path, normalize_path, validate_pax, validate_symbolic_link, Archive, ArchiveBuffers,
1170        ArchiveError, Entry, EntryExtractor, EntryKind, ExtractError, FinishError, LayerEventSink,
1171        TarWriteError, TarWriter, BLOCK_SIZE,
1172    };
1173    use std::{format, string::ToString, vec::Vec};
1174
1175    type OwnedPaxRecord = (Vec<u8>, Vec<u8>);
1176    type OwnedEntry = (Vec<u8>, EntryKind, Vec<OwnedPaxRecord>);
1177
1178    #[derive(Default)]
1179    struct Events {
1180        entries: Vec<OwnedEntry>,
1181        contents: Vec<u8>,
1182        whiteouts: Vec<Vec<u8>>,
1183        opaque: Vec<Vec<u8>>,
1184    }
1185
1186    #[test]
1187    fn writes_deterministic_ustar_files() {
1188        let mut bytes = Vec::new();
1189        let mut writer = TarWriter::new();
1190        writer
1191            .begin_file(b"blobs/sha256/abc", 5, 0o644, |chunk| {
1192                bytes.extend_from_slice(chunk);
1193                Ok::<_, ()>(())
1194            })
1195            .unwrap();
1196        writer
1197            .write_file_data(b"hello", |chunk| {
1198                bytes.extend_from_slice(chunk);
1199                Ok::<_, ()>(())
1200            })
1201            .unwrap();
1202        writer
1203            .end_file(|chunk| {
1204                bytes.extend_from_slice(chunk);
1205                Ok::<_, ()>(())
1206            })
1207            .unwrap();
1208        writer
1209            .finish(|chunk| {
1210                bytes.extend_from_slice(chunk);
1211                Ok::<_, ()>(())
1212            })
1213            .unwrap();
1214
1215        assert_eq!(&bytes[..18], b"blobs/sha256/abc\0\0");
1216        assert_eq!(&bytes[BLOCK_SIZE..BLOCK_SIZE + 5], b"hello");
1217        assert_eq!(bytes.len(), BLOCK_SIZE * 4);
1218
1219        let mut invalid = TarWriter::new();
1220        assert_eq!(
1221            invalid.begin_file(b"../escape", 0, 0o644, |_| Ok::<_, ()>(())),
1222            Err(TarWriteError::InvalidPath)
1223        );
1224    }
1225
1226    impl LayerEventSink for Events {
1227        type Error = ();
1228
1229        fn begin_entry(&mut self, entry: Entry<'_>) -> Result<(), Self::Error> {
1230            let pax = entry
1231                .pax_records()
1232                .map(|record| {
1233                    let record = record.unwrap();
1234                    (record.key.to_vec(), record.value.to_vec())
1235                })
1236                .collect();
1237            self.entries.push((entry.path.to_vec(), entry.kind, pax));
1238            Ok(())
1239        }
1240
1241        fn entry_data(&mut self, bytes: &[u8]) -> Result<(), Self::Error> {
1242            self.contents.extend_from_slice(bytes);
1243            Ok(())
1244        }
1245
1246        fn end_entry(&mut self) -> Result<(), Self::Error> {
1247            Ok(())
1248        }
1249
1250        fn whiteout(&mut self, path: &[u8]) -> Result<(), Self::Error> {
1251            self.whiteouts.push(path.to_vec());
1252            Ok(())
1253        }
1254
1255        fn opaque_directory(&mut self, path: &[u8]) -> Result<(), Self::Error> {
1256            self.opaque.push(path.to_vec());
1257            Ok(())
1258        }
1259    }
1260
1261    #[test]
1262    fn streams_layer_events_and_whiteouts_at_every_fragment_size() {
1263        let mut bytes = Vec::new();
1264        append_entry(&mut bytes, b"./etc//config", b"value", b'0', b"");
1265        append_entry(&mut bytes, b"etc/.wh.deleted", b"", b'0', b"");
1266        append_entry(&mut bytes, b"var/lib/.wh..wh..opq", b"", b'0', b"");
1267        finish_archive(&mut bytes);
1268
1269        for fragment in 1..=BLOCK_SIZE + 1 {
1270            let mut path = [0; 256];
1271            let mut link = [0; 256];
1272            let mut pax = [0; 512];
1273            let mut archive = Archive::new(ArchiveBuffers {
1274                path: &mut path,
1275                link: &mut link,
1276                pax: &mut pax,
1277            });
1278            let mut events = Events::default();
1279            for chunk in bytes.chunks(fragment) {
1280                archive.push(chunk, &mut events).unwrap();
1281            }
1282            archive.finish().unwrap();
1283            assert_eq!(events.entries[0].0, b"etc/config");
1284            assert_eq!(events.contents, b"value");
1285            assert_eq!(events.whiteouts, [b"etc/deleted".to_vec()]);
1286            assert_eq!(events.opaque, [b"var/lib".to_vec()]);
1287        }
1288    }
1289
1290    #[test]
1291    fn applies_pax_and_gnu_long_path_overrides_without_allocating() {
1292        let mut bytes = Vec::new();
1293        let record = pax_record("path", "pax/overridden/file");
1294        append_entry(&mut bytes, b"PaxHeaders.0", &record, b'x', b"");
1295        append_entry(&mut bytes, b"ignored", b"pax", b'0', b"");
1296
1297        let long = [b'a'; 140];
1298        let mut long_nul = long.to_vec();
1299        long_nul.push(0);
1300        append_entry(&mut bytes, b"LongLink", &long_nul, b'L', b"");
1301        append_entry(&mut bytes, b"ignored", b"gnu", b'0', b"");
1302        finish_archive(&mut bytes);
1303
1304        let mut path = [0; 256];
1305        let mut link = [0; 256];
1306        let mut pax = [0; 512];
1307        let mut archive = Archive::new(ArchiveBuffers {
1308            path: &mut path,
1309            link: &mut link,
1310            pax: &mut pax,
1311        });
1312        let mut events = Events::default();
1313        archive.push(&bytes, &mut events).unwrap();
1314        archive.finish().unwrap();
1315        assert_eq!(events.entries[0].0, b"pax/overridden/file");
1316        assert_eq!(events.entries[0].2[0].0, b"path");
1317        assert_eq!(events.entries[1].0, long);
1318        assert_eq!(events.contents, b"paxgnu");
1319    }
1320
1321    #[test]
1322    fn rejects_paths_that_escape_the_root() {
1323        let mut bytes = Vec::new();
1324        append_entry(&mut bytes, b"../escape", b"", b'0', b"");
1325        let mut path = [0; 64];
1326        let mut link = [0; 64];
1327        let mut pax = [0; 64];
1328        let mut archive = Archive::new(ArchiveBuffers {
1329            path: &mut path,
1330            link: &mut link,
1331            pax: &mut pax,
1332        });
1333        assert!(archive.push(&bytes, &mut Events::default()).is_err());
1334    }
1335
1336    #[test]
1337    fn rejects_absolute_symbolic_link_targets() {
1338        let mut bytes = Vec::new();
1339        append_link(&mut bytes, b"link", b"/etc/passwd", b'2');
1340        let mut path = [0; 64];
1341        let mut link = [0; 64];
1342        let mut pax = [0; 64];
1343        let mut archive = Archive::new(ArchiveBuffers {
1344            path: &mut path,
1345            link: &mut link,
1346            pax: &mut pax,
1347        });
1348        assert_eq!(
1349            archive.push(&bytes, &mut Events::default()),
1350            Err(ArchiveError::InvalidLink)
1351        );
1352    }
1353
1354    #[test]
1355    fn rejects_symbolic_link_targets_that_escape_the_root() {
1356        let mut bytes = Vec::new();
1357        append_link(&mut bytes, b"dir/link", b"../../escape", b'2');
1358        let mut path = [0; 64];
1359        let mut link = [0; 64];
1360        let mut pax = [0; 64];
1361        let mut archive = Archive::new(ArchiveBuffers {
1362            path: &mut path,
1363            link: &mut link,
1364            pax: &mut pax,
1365        });
1366        assert_eq!(
1367            archive.push(&bytes, &mut Events::default()),
1368            Err(ArchiveError::InvalidLink)
1369        );
1370    }
1371
1372    #[test]
1373    fn resolves_symbolic_link_target_components_before_checking_depth() {
1374        assert_eq!(
1375            validate_symbolic_link::<()>(b"link", b"dir/../target"),
1376            Ok(())
1377        );
1378        assert_eq!(
1379            validate_symbolic_link::<()>(b"dir/link", b"./../../escape"),
1380            Err(ArchiveError::InvalidLink)
1381        );
1382        assert_eq!(
1383            validate_symbolic_link::<()>(b"dir/link", b"nested//../../../escape"),
1384            Err(ArchiveError::InvalidLink)
1385        );
1386    }
1387
1388    #[test]
1389    fn joins_ustar_name_and_prefix_with_exact_buffer_bounds() {
1390        let mut header = [0; BLOCK_SIZE];
1391        header[..4].copy_from_slice(b"name");
1392        header[345..351].copy_from_slice(b"prefix");
1393
1394        let mut exact = [0; 11];
1395        assert_eq!(header_path::<()>(&header, &mut exact), Ok(11));
1396        assert_eq!(&exact, b"prefix/name");
1397
1398        let mut too_small = [0; 10];
1399        assert!(header_path::<()>(&header, &mut too_small).is_err());
1400
1401        header[345..351].fill(0);
1402        let mut name_only = [0; 4];
1403        assert_eq!(header_path::<()>(&header, &mut name_only), Ok(4));
1404        assert_eq!(&name_only, b"name");
1405    }
1406
1407    #[test]
1408    fn normalizes_paths_and_rejects_invalid_inputs() {
1409        let mut path = *b"./a//b/./";
1410        let length = normalize_path::<()>(&mut path, 9).unwrap();
1411        assert_eq!(&path[..length], b"a/b");
1412
1413        let mut dot = *b".";
1414        let length = normalize_path::<()>(&mut dot, 1).unwrap();
1415        assert_eq!(&dot[..length], b".");
1416
1417        for invalid in [b"".as_slice(), b"/root", b"a\0b", b"../root"] {
1418            let mut path = [0; 16];
1419            path[..invalid.len()].copy_from_slice(invalid);
1420            assert_eq!(
1421                normalize_path::<()>(&mut path, invalid.len()),
1422                Err(ArchiveError::InvalidPath)
1423            );
1424        }
1425    }
1426
1427    #[test]
1428    fn rejects_malformed_pax_records() {
1429        assert!(validate_pax::<()>(b"garbage").is_err());
1430    }
1431
1432    #[test]
1433    fn extracts_a_regular_file_from_arbitrary_fragments() {
1434        let mut archive = Vec::new();
1435        append_entry(&mut archive, b"first", b"skip", b'0', b"");
1436        append_entry(&mut archive, b"wanted", b"contents", b'0', b"");
1437        append_entry(&mut archive, b"wanted", b"later", b'0', b"");
1438        finish_archive(&mut archive);
1439
1440        for fragment_size in 1..=BLOCK_SIZE + 1 {
1441            let mut extractor = EntryExtractor::new(b"wanted");
1442            let mut output = Vec::new();
1443            for fragment in archive.chunks(fragment_size) {
1444                extractor
1445                    .push(fragment, |bytes| {
1446                        output.extend_from_slice(bytes);
1447                        Ok::<_, ()>(())
1448                    })
1449                    .unwrap();
1450            }
1451            extractor.finish().unwrap();
1452            assert_eq!(output, b"contents");
1453        }
1454    }
1455
1456    #[test]
1457    fn matches_a_ustar_prefix_without_allocating_a_path() {
1458        let mut archive = Vec::new();
1459        append_entry(
1460            &mut archive,
1461            b"settings.yaml",
1462            b"value",
1463            b'0',
1464            b"etc/datadog-agent",
1465        );
1466        finish_archive(&mut archive);
1467        let mut extractor = EntryExtractor::new(b"etc/datadog-agent/settings.yaml");
1468        let mut output = Vec::new();
1469        extractor
1470            .push(&archive, |bytes| {
1471                output.extend_from_slice(bytes);
1472                Ok::<_, ()>(())
1473            })
1474            .unwrap();
1475        extractor.finish().unwrap();
1476        assert_eq!(output, b"value");
1477    }
1478
1479    #[test]
1480    fn recognizes_an_empty_regular_file() {
1481        let mut archive = Vec::new();
1482        append_entry(&mut archive, b"empty", b"", b'0', b"");
1483        finish_archive(&mut archive);
1484        let mut extractor = EntryExtractor::new(b"empty");
1485        extractor.push(&archive, |_| Ok::<_, ()>(())).unwrap();
1486        assert!(extractor.found());
1487        extractor.finish().unwrap();
1488    }
1489
1490    #[test]
1491    fn does_not_extract_a_directory_with_the_requested_name() {
1492        let mut archive = Vec::new();
1493        append_entry(&mut archive, b"wanted", b"", b'5', b"");
1494        finish_archive(&mut archive);
1495        let mut extractor = EntryExtractor::new(b"wanted");
1496        extractor.push(&archive, |_| Ok::<_, ()>(())).unwrap();
1497        assert_eq!(extractor.finish(), Err(FinishError::NotFound));
1498    }
1499
1500    #[test]
1501    fn rejects_an_invalid_checksum() {
1502        let mut archive = Vec::new();
1503        append_entry(&mut archive, b"wanted", b"contents", b'0', b"");
1504        archive[0] ^= 1;
1505        let mut extractor = EntryExtractor::new(b"wanted");
1506        assert_eq!(
1507            extractor.push(&archive, |_| Ok::<_, ()>(())),
1508            Err(ExtractError::InvalidChecksum)
1509        );
1510    }
1511
1512    #[test]
1513    fn reports_a_truncated_stream() {
1514        let mut archive = Vec::new();
1515        append_entry(&mut archive, b"wanted", b"contents", b'0', b"");
1516        let mut extractor = EntryExtractor::new(b"wanted");
1517        extractor
1518            .push(&archive[..BLOCK_SIZE + 2], |_| Ok::<_, ()>(()))
1519            .unwrap();
1520        assert_eq!(extractor.finish(), Err(FinishError::UnexpectedEof));
1521    }
1522
1523    fn append_entry(archive: &mut Vec<u8>, name: &[u8], contents: &[u8], kind: u8, prefix: &[u8]) {
1524        let mut header = [0u8; BLOCK_SIZE];
1525        header[..name.len()].copy_from_slice(name);
1526        write_octal(&mut header[100..108], 0o644);
1527        write_octal(&mut header[108..116], 0);
1528        write_octal(&mut header[116..124], 0);
1529        write_octal(&mut header[124..136], contents.len() as u64);
1530        write_octal(&mut header[136..148], 0);
1531        header[148..156].fill(b' ');
1532        header[156] = kind;
1533        header[257..263].copy_from_slice(b"ustar\0");
1534        header[263..265].copy_from_slice(b"00");
1535        header[345..345 + prefix.len()].copy_from_slice(prefix);
1536        let checksum = header.iter().map(|byte| u64::from(*byte)).sum();
1537        write_octal(&mut header[148..156], checksum);
1538
1539        archive.extend_from_slice(&header);
1540        archive.extend_from_slice(contents);
1541        archive.resize(archive.len() + super::padding_for(contents.len() as u64), 0);
1542    }
1543
1544    fn append_link(archive: &mut Vec<u8>, name: &[u8], target: &[u8], kind: u8) {
1545        let start = archive.len();
1546        append_entry(archive, name, b"", kind, b"");
1547        let header = &mut archive[start..start + BLOCK_SIZE];
1548        header[157..157 + target.len()].copy_from_slice(target);
1549        header[148..156].fill(b' ');
1550        let checksum = header.iter().map(|byte| u64::from(*byte)).sum();
1551        write_octal(&mut header[148..156], checksum);
1552    }
1553
1554    fn finish_archive(archive: &mut Vec<u8>) {
1555        archive.resize(archive.len() + 2 * BLOCK_SIZE, 0);
1556    }
1557
1558    fn pax_record(key: &str, value: &str) -> Vec<u8> {
1559        let payload = key.len() + 1 + value.len() + 1;
1560        let mut digits = 1;
1561        loop {
1562            let length = digits + 1 + payload;
1563            let actual_digits = length.to_string().len();
1564            if actual_digits == digits {
1565                return format!("{length} {key}={value}\n").into_bytes();
1566            }
1567            digits = actual_digits;
1568        }
1569    }
1570
1571    fn write_octal(field: &mut [u8], mut value: u64) {
1572        field.fill(b'0');
1573        *field.last_mut().unwrap() = 0;
1574        let digits = field.len() - 1;
1575        for byte in field[..digits].iter_mut().rev() {
1576            *byte = b'0' + (value % 8) as u8;
1577            value /= 8;
1578        }
1579        assert_eq!(value, 0);
1580    }
1581}