Skip to main content

jdx_tar/
lib.rs

1//! A secure, synchronous, streaming tar reader, writer, and extractor.
2//!
3//! This crate reads already-decompressed tar streams and supports all GNU
4//! sparse formats commonly found in release archives.
5
6mod builder;
7mod format;
8mod unpack;
9
10use std::borrow::Cow;
11use std::cell::RefCell;
12use std::collections::{BTreeMap, BTreeSet};
13use std::fs::File;
14use std::io::{self, ErrorKind, Read, Seek, SeekFrom, Write};
15use std::marker::PhantomData;
16use std::path::Path;
17use std::rc::Rc;
18
19pub use builder::Builder;
20use format::{
21    apply_pax_header, bytes_to_path, parse_decimal, parse_header, parse_number, parse_pax,
22    parse_sparse_csv, parse_sparse_pairs, pax_text_checked, pax_u64_checked, pax_value,
23    push_sparse_pair, trim_metadata, validate_sparse, verify_checksum,
24};
25pub use unpack::{
26    EntryCallback, EntryInfo, EntryUnpacker, Progress, ProgressCallback, SkipReason, SkippedEntry,
27    UnpackOptions, UnpackSummary,
28};
29use unpack::{ProgressReporter, unpack_archive};
30
31const BLOCK: u64 = 512;
32const MAX_METADATA_SIZE: u64 = 1024 * 1024;
33const MAX_SPARSE_SEGMENTS: usize = 1_000_000;
34const PAX_HEADER_KEYS: [&str; 7] = ["path", "linkpath", "size", "mode", "uid", "gid", "mtime"];
35type PaxRecords = Vec<(String, Vec<u8>)>;
36
37struct PaxLayer {
38    parent: Option<Rc<Self>>,
39    records: BTreeMap<String, Vec<u8>>,
40}
41
42impl Drop for PaxLayer {
43    fn drop(&mut self) {
44        // Retained entries can keep a long update chain alive. Release uniquely
45        // owned ancestors iteratively rather than recursively dropping it.
46        let mut parent = self.parent.take();
47        while let Some(mut layer) = parent {
48            parent = Rc::get_mut(&mut layer).and_then(|layer| layer.parent.take());
49        }
50    }
51}
52
53/// The crate's result type.
54pub type Result<T> = io::Result<T>;
55
56/// One contiguous data region in a sparse file. Gaps are logical holes.
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub struct SparseSegment {
59    /// Logical byte offset at which data begins.
60    pub offset: u64,
61    /// Number of data bytes stored at this offset.
62    pub len: u64,
63}
64
65/// The kind of an archive entry.
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
67#[non_exhaustive]
68pub enum EntryType {
69    /// A regular file.
70    File,
71    /// A directory.
72    Directory,
73    /// A symbolic link.
74    Symlink,
75    /// A hard link.
76    Hardlink,
77    /// A character device.
78    CharDevice,
79    /// A block device.
80    BlockDevice,
81    /// A FIFO.
82    Fifo,
83    /// An unrecognized type flag.
84    Other(u8),
85}
86
87impl EntryType {
88    fn from_flag(flag: u8) -> Self {
89        match flag {
90            0 | b'0' | b'7' | b'S' => Self::File,
91            b'5' => Self::Directory,
92            b'2' => Self::Symlink,
93            b'1' => Self::Hardlink,
94            b'3' => Self::CharDevice,
95            b'4' => Self::BlockDevice,
96            b'6' => Self::Fifo,
97            other => Self::Other(other),
98        }
99    }
100
101    const fn type_flag(self) -> u8 {
102        match self {
103            Self::File => b'0',
104            Self::Directory => b'5',
105            Self::Symlink => b'2',
106            Self::Hardlink => b'1',
107            Self::CharDevice => b'3',
108            Self::BlockDevice => b'4',
109            Self::Fifo => b'6',
110            Self::Other(flag) => flag,
111        }
112    }
113}
114
115/// Parsed metadata for a tar header.
116#[derive(Clone, Debug)]
117pub struct Header {
118    path: Vec<u8>,
119    link_name: Option<Vec<u8>>,
120    mode: u32,
121    uid: u64,
122    gid: u64,
123    stored_size: u64,
124    mtime: i64,
125    type_flag: u8,
126}
127
128impl Header {
129    /// Creates a deterministic GNU-format header for `entry_type`.
130    ///
131    /// Numeric fields default to zero. Callers must set the entry size before
132    /// appending regular-file data. Paths and checksums are populated by
133    /// [`Builder`].
134    #[must_use]
135    pub fn new_gnu(entry_type: EntryType) -> Self {
136        Self {
137            path: Vec::new(),
138            link_name: None,
139            mode: 0,
140            uid: 0,
141            gid: 0,
142            stored_size: 0,
143            mtime: 0,
144            type_flag: entry_type.type_flag(),
145        }
146    }
147
148    /// File mode from the header.
149    #[must_use]
150    pub const fn mode(&self) -> u32 {
151        self.mode
152    }
153    /// User id from the header.
154    #[must_use]
155    pub const fn uid(&self) -> u64 {
156        self.uid
157    }
158    /// Group id from the header.
159    #[must_use]
160    pub const fn gid(&self) -> u64 {
161        self.gid
162    }
163    /// Raw stored size before sparse expansion.
164    #[must_use]
165    pub const fn stored_size(&self) -> u64 {
166        self.stored_size
167    }
168    /// Modification time as seconds since the Unix epoch.
169    #[must_use]
170    pub const fn mtime(&self) -> i64 {
171        self.mtime
172    }
173    /// Raw tar type flag.
174    #[must_use]
175    pub const fn type_flag(&self) -> u8 {
176        self.type_flag
177    }
178
179    /// Sets the file mode stored in the header.
180    pub const fn set_mode(&mut self, mode: u32) {
181        self.mode = mode;
182    }
183
184    /// Sets the numeric user id stored in the header.
185    pub const fn set_uid(&mut self, uid: u64) {
186        self.uid = uid;
187    }
188
189    /// Sets the numeric group id stored in the header.
190    pub const fn set_gid(&mut self, gid: u64) {
191        self.gid = gid;
192    }
193
194    /// Sets the number of data bytes that follow the header.
195    pub const fn set_size(&mut self, size: u64) {
196        self.stored_size = size;
197    }
198
199    /// Sets the modification time as seconds since the Unix epoch.
200    pub const fn set_mtime(&mut self, mtime: i64) {
201        self.mtime = mtime;
202    }
203
204    /// Link target, when present.
205    pub fn link_name(&self) -> Option<Cow<'_, Path>> {
206        self.link_name.as_deref().map(bytes_to_path)
207    }
208}
209
210struct ReaderState<R> {
211    reader: R,
212    raw_bytes: u64,
213    pending: u64,
214    padding: u64,
215    generation: u64,
216}
217
218impl<R: Read> ReaderState<R> {
219    fn read_counted(&mut self, buf: &mut [u8]) -> io::Result<usize> {
220        let n = self.reader.read(buf)?;
221        self.raw_bytes = self.raw_bytes.saturating_add(n as u64);
222        Ok(n)
223    }
224
225    fn read_exact_counted(&mut self, mut buf: &mut [u8]) -> io::Result<()> {
226        while !buf.is_empty() {
227            let n = self.read_counted(buf)?;
228            if n == 0 {
229                return Err(error(ErrorKind::UnexpectedEof, "truncated tar archive"));
230            }
231            buf = &mut buf[n..];
232        }
233        Ok(())
234    }
235
236    fn skip(&mut self, mut amount: u64) -> io::Result<()> {
237        let mut buf = [0_u8; 8192];
238        while amount != 0 {
239            let want = usize::try_from(amount.min(buf.len() as u64)).unwrap_or(buf.len());
240            self.read_exact_counted(&mut buf[..want])?;
241            amount -= want as u64;
242        }
243        Ok(())
244    }
245
246    fn finish_entry(&mut self) -> io::Result<()> {
247        self.skip(self.pending)?;
248        self.pending = 0;
249        self.skip(self.padding)?;
250        self.padding = 0;
251        Ok(())
252    }
253
254    fn read_entry_data(&mut self, generation: u64, buf: &mut [u8]) -> io::Result<usize> {
255        if generation != self.generation {
256            return Err(error(
257                ErrorKind::InvalidInput,
258                "entry is stale because iteration advanced",
259            ));
260        }
261        let want = usize::try_from(self.pending.min(buf.len() as u64)).unwrap_or(buf.len());
262        if want == 0 {
263            return Ok(0);
264        }
265        let n = self.read_counted(&mut buf[..want])?;
266        if n == 0 {
267            return Err(error(ErrorKind::UnexpectedEof, "truncated entry data"));
268        }
269        self.pending -= n as u64;
270        Ok(n)
271    }
272}
273
274/// A tar archive over a non-seekable input stream.
275pub struct Archive<R: Read> {
276    state: Rc<RefCell<ReaderState<R>>>,
277}
278
279impl<R: Read> Archive<R> {
280    /// Creates an archive reader. Compression must already have been removed.
281    pub fn new(reader: R) -> Self {
282        Self {
283            state: Rc::new(RefCell::new(ReaderState {
284                reader,
285                raw_bytes: 0,
286                pending: 0,
287                padding: 0,
288                generation: 0,
289            })),
290        }
291    }
292
293    /// Returns a streaming iterator over logical archive entries.
294    ///
295    /// # Errors
296    ///
297    /// Returns an error if a previous iterator has consumed archive bytes.
298    pub fn entries(&mut self) -> Result<Entries<'_, R>> {
299        if self.state.borrow().raw_bytes != 0 {
300            return Err(error(
301                ErrorKind::InvalidInput,
302                "cannot restart entry iteration after consuming archive bytes",
303            ));
304        }
305        Ok(Entries {
306            state: Rc::clone(&self.state),
307            done: false,
308            zero_blocks: 0,
309            global_pax: BTreeMap::new(),
310            pending_global_pax_bytes: 0,
311            global_pax_snapshot: None,
312            local_pax: None,
313            long_name: None,
314            long_link: None,
315            marker: PhantomData,
316        })
317    }
318
319    /// Securely extracts this archive into `dest`.
320    ///
321    /// # Errors
322    ///
323    /// Returns an error for malformed archives, unsafe paths, truncated input,
324    /// callback-independent I/O failures, or filesystem extraction failures.
325    pub fn unpack<P: AsRef<Path>>(
326        mut self,
327        dest: P,
328        opts: &mut UnpackOptions,
329    ) -> Result<UnpackSummary> {
330        unpack_archive(&mut self, dest.as_ref(), opts)
331    }
332}
333
334/// Streaming iterator returned by [`Archive::entries`].
335pub struct Entries<'a, R: Read> {
336    state: Rc<RefCell<ReaderState<R>>>,
337    done: bool,
338    zero_blocks: u8,
339    global_pax: BTreeMap<String, Vec<u8>>,
340    pending_global_pax_bytes: u64,
341    global_pax_snapshot: Option<Rc<PaxLayer>>,
342    local_pax: Option<Vec<(String, Vec<u8>)>>,
343    long_name: Option<Vec<u8>>,
344    long_link: Option<Vec<u8>>,
345    marker: PhantomData<&'a mut Archive<R>>,
346}
347
348impl<R: Read> Iterator for Entries<'_, R> {
349    type Item = Result<Entry<R>>;
350
351    fn next(&mut self) -> Option<Self::Item> {
352        if self.done {
353            return None;
354        }
355        match self.next_entry() {
356            Ok(Some(entry)) => Some(Ok(entry)),
357            Ok(None) => {
358                self.done = true;
359                None
360            }
361            Err(err) => {
362                self.done = true;
363                Some(Err(err))
364            }
365        }
366    }
367}
368
369impl<R: Read> Entries<'_, R> {
370    #[allow(clippy::too_many_lines)]
371    fn next_entry(&mut self) -> Result<Option<Entry<R>>> {
372        loop {
373            let mut block = [0_u8; 512];
374            {
375                let mut state = self.state.borrow_mut();
376                state.finish_entry()?;
377                state.generation = state.generation.wrapping_add(1);
378                let mut first = [0_u8; 1];
379                let n = state.read_counted(&mut first)?;
380                if n == 0 {
381                    return self.finish_stream();
382                }
383                block[0] = first[0];
384                state.read_exact_counted(&mut block[1..])?;
385            }
386            if block.iter().all(|byte| *byte == 0) {
387                self.zero_blocks += 1;
388                if self.zero_blocks == 2 {
389                    return self.finish_stream();
390                }
391                continue;
392            }
393            if self.zero_blocks != 0 {
394                return Err(invalid(
395                    "tar zero block was not followed by a second zero block",
396                ));
397            }
398            self.zero_blocks = 0;
399            verify_checksum(&block)?;
400            let mut header = parse_header(&block)?;
401            let flag = header.type_flag;
402            let size = header.stored_size;
403
404            let identity = &block[257..265];
405            let is_ustar = identity == b"ustar\x0000";
406            let is_gnu = identity == b"ustar  \0";
407            if matches!(flag, b'x' | b'g') && !is_ustar {
408                return Err(invalid("PAX extension requires a USTAR carrier"));
409            }
410            if matches!(flag, b'L' | b'K') && !(is_ustar || is_gnu) {
411                return Err(invalid(
412                    "GNU long-name/link extension requires a USTAR or GNU carrier",
413                ));
414            }
415
416            if matches!(flag, b'x' | b'g' | b'L' | b'K') {
417                if size > MAX_METADATA_SIZE {
418                    return Err(error(
419                        ErrorKind::InvalidData,
420                        "tar metadata exceeds 1 MiB limit",
421                    ));
422                }
423                if flag == b'g' {
424                    let total = self
425                        .pending_global_pax_bytes
426                        .checked_add(size)
427                        .ok_or_else(|| invalid("global PAX metadata size overflow"))?;
428                    if total > MAX_METADATA_SIZE {
429                        return Err(invalid("pending global PAX metadata exceeds 1 MiB limit"));
430                    }
431                    self.pending_global_pax_bytes = total;
432                }
433                let payload = self.read_metadata(size)?;
434                match flag {
435                    b'x' => {
436                        if self.local_pax.is_some() {
437                            return Err(invalid("two local PAX headers describe one entry"));
438                        }
439                        self.local_pax = Some(parse_pax(&payload)?);
440                    }
441                    b'g' => {
442                        let records = parse_pax(&payload)?;
443                        // Sparse metadata describes one member, not an inherited default.
444                        if records
445                            .iter()
446                            .any(|(key, _)| key.starts_with("GNU.sparse."))
447                        {
448                            return Err(invalid("GNU sparse PAX metadata is not valid globally"));
449                        }
450                        for (key, value) in &records {
451                            self.global_pax.insert(key.clone(), value.clone());
452                        }
453                        if let Some(layer) = self.global_pax_snapshot.as_mut().and_then(Rc::get_mut)
454                        {
455                            layer.records.extend(records);
456                        } else {
457                            self.global_pax_snapshot = Some(Rc::new(PaxLayer {
458                                parent: self.global_pax_snapshot.take(),
459                                records: records.into_iter().collect(),
460                            }));
461                        }
462                    }
463                    b'L' => {
464                        if self.long_name.is_some() {
465                            return Err(invalid("two GNU long-name headers describe one entry"));
466                        }
467                        self.long_name = Some(trim_metadata(payload));
468                    }
469                    b'K' => {
470                        if self.long_link.is_some() {
471                            return Err(invalid("two GNU long-link headers describe one entry"));
472                        }
473                        self.long_link = Some(trim_metadata(payload));
474                    }
475                    _ => unreachable!(),
476                }
477                continue;
478            }
479
480            let pax_global = self.global_pax_snapshot.clone();
481            let pax_local = self.local_pax.take().unwrap_or_default();
482            let pax_local_keys = pax_local
483                .iter()
484                .map(|(key, _)| key.clone())
485                .collect::<BTreeSet<_>>();
486
487            // Resolve only fields used by header/sparse interpretation. Other
488            // global records remain shared until a caller requests them.
489            let mut pax = Vec::new();
490            for key in PAX_HEADER_KEYS {
491                if let Some(value) = self
492                    .global_pax
493                    .get(key)
494                    .filter(|_| !pax_local_keys.contains(key))
495                {
496                    pax.push((key.to_owned(), value.clone()));
497                }
498            }
499            for (key, value) in self.global_pax.range("GNU.sparse.".to_owned()..) {
500                if !key.starts_with("GNU.sparse.") {
501                    break;
502                }
503                if !pax_local_keys.contains(key) {
504                    pax.push((key.clone(), value.clone()));
505                }
506            }
507            pax.extend(
508                pax_local
509                    .iter()
510                    .filter(|(key, _)| {
511                        PAX_HEADER_KEYS.contains(&key.as_str()) || key.starts_with("GNU.sparse.")
512                    })
513                    .cloned(),
514            );
515            if self.long_name.is_some()
516                && pax_value(&pax, "path").is_some_and(|path| !path.is_empty())
517            {
518                return Err(invalid(
519                    "PAX path and GNU long-name describe the same entry",
520                ));
521            }
522            if self.long_link.is_some()
523                && pax_value(&pax, "linkpath").is_some_and(|link| !link.is_empty())
524            {
525                return Err(invalid(
526                    "PAX linkpath and GNU long-link describe the same entry",
527                ));
528            }
529            apply_pax_header(&mut header, &pax)?;
530            if let Some(name) = self.long_name.take() {
531                header.path = name;
532            }
533            if let Some(link) = self.long_link.take() {
534                header.link_name = Some(link);
535            }
536
537            // GNU sparse PAX archives use the tar header's size for the packed
538            // body. Some writers (notably Go's archive/tar) also emit a PAX
539            // `size` record containing the logical sparse size.
540            let pax_size = header.stored_size;
541            let has_pax_sparse = pax.iter().any(|(key, _)| key.starts_with("GNU.sparse."));
542            if has_pax_sparse && !matches!(flag, 0 | b'0' | b'7') {
543                return Err(invalid("GNU sparse PAX metadata requires a regular file"));
544            }
545            let physical_size = if has_pax_sparse { size } else { pax_size };
546            if matches!(flag, b'1'..=b'6') && (size != 0 || physical_size != 0) {
547                return Err(invalid("nonregular tar entry cannot carry payload"));
548            }
549            header.stored_size = physical_size;
550            let generation;
551            {
552                let mut state = self.state.borrow_mut();
553                state.pending = physical_size;
554                state.padding = padding(physical_size);
555                generation = state.generation;
556            }
557
558            let (sparse, logical_size) = if flag == b'S' {
559                self.parse_old_gnu_sparse(&block, physical_size)?
560            } else {
561                self.parse_pax_sparse(&pax, physical_size)?
562            };
563            if has_pax_sparse && pax_size != physical_size && pax_size != logical_size {
564                return Err(invalid(
565                    "GNU sparse PAX size conflicts with physical and logical sizes",
566                ));
567            }
568            if let Some(name) = pax_value(&pax, "GNU.sparse.name") {
569                header.path = name.to_vec();
570            }
571            if header.path.is_empty() && pax_value(&pax, "path").is_some_and(<[u8]>::is_empty) {
572                return Err(invalid("PAX path deletion leaves entry without a path"));
573            }
574            if matches!(flag, b'1' | b'2')
575                && pax_value(&pax, "linkpath").is_some_and(<[u8]>::is_empty)
576                && header.link_name.as_ref().is_none_or(Vec::is_empty)
577            {
578                return Err(invalid(
579                    "PAX linkpath deletion leaves link without a target",
580                ));
581            }
582            let kind = EntryType::from_flag(flag);
583            if header.path.is_empty() {
584                return Err(invalid("tar entry has an empty effective path"));
585            }
586            if header.path.contains(&0) {
587                return Err(invalid("tar entry path contains a NUL byte"));
588            }
589            if matches!(kind, EntryType::Symlink | EntryType::Hardlink)
590                && header
591                    .link_name
592                    .as_ref()
593                    .is_none_or(|target| target.is_empty() || target.contains(&0))
594            {
595                return Err(invalid("tar link entry has an empty or NUL-bearing target"));
596            }
597            self.pending_global_pax_bytes = 0;
598            return Ok(Some(Entry {
599                state: Rc::clone(&self.state),
600                header,
601                kind,
602                pax_global,
603                pax_local,
604                pax_local_keys,
605                sparse,
606                logical_size,
607                logical_pos: 0,
608                sparse_index: 0,
609                generation,
610                extraction_started: false,
611            }));
612        }
613    }
614
615    fn finish_stream(&self) -> Result<Option<Entry<R>>> {
616        if self.local_pax.is_some() || self.long_name.is_some() || self.long_link.is_some() {
617            return Err(invalid("extension entry was not followed by a member"));
618        }
619        Ok(None)
620    }
621
622    fn read_metadata(&self, size: u64) -> Result<Vec<u8>> {
623        let alloc =
624            usize::try_from(size).map_err(|_| invalid("metadata size cannot fit in memory"))?;
625        let mut payload = vec![0_u8; alloc];
626        let mut state = self.state.borrow_mut();
627        state.read_exact_counted(&mut payload)?;
628        state.skip(padding(size))?;
629        Ok(payload)
630    }
631
632    fn parse_old_gnu_sparse(
633        &self,
634        block: &[u8; 512],
635        physical: u64,
636    ) -> Result<(Option<Vec<SparseSegment>>, u64)> {
637        if &block[257..265] != b"ustar  \0" {
638            return Err(invalid("old GNU sparse entry lacks a GNU header"));
639        }
640        let mut map = Vec::new();
641        for chunk in block[386..482].chunks_exact(24) {
642            if !push_sparse_pair(&mut map, &chunk[..12], &chunk[12..])? {
643                break;
644            }
645        }
646        let mut extended = block[482] != 0;
647        // Include the inline descriptors and each full continuation block.
648        let mut metadata_bytes = 96_u64;
649        while extended {
650            metadata_bytes = metadata_bytes
651                .checked_add(BLOCK)
652                .ok_or_else(|| invalid("old GNU sparse metadata size overflow"))?;
653            if metadata_bytes > MAX_METADATA_SIZE {
654                return Err(invalid("old GNU sparse metadata exceeds 1 MiB limit"));
655            }
656            let mut ext = [0_u8; 512];
657            self.state.borrow_mut().read_exact_counted(&mut ext)?;
658            for chunk in ext[..504].chunks_exact(24) {
659                if !push_sparse_pair(&mut map, &chunk[..12], &chunk[12..])? {
660                    break;
661                }
662            }
663            extended = ext[504] != 0;
664            if map.len() > MAX_SPARSE_SEGMENTS {
665                return Err(invalid("sparse map has too many segments"));
666            }
667        }
668        let logical = parse_number(&block[483..495])?;
669        validate_sparse(&map, logical, physical, true)?;
670        Ok((Some(map), logical))
671    }
672
673    fn parse_pax_sparse(
674        &self,
675        pax: &[(String, Vec<u8>)],
676        physical: u64,
677    ) -> Result<(Option<Vec<SparseSegment>>, u64)> {
678        let has_sparse = pax.iter().any(|(key, _)| key.starts_with("GNU.sparse."));
679        if !has_sparse {
680            return Ok((None, physical));
681        }
682        let major = pax_text_checked(pax, "GNU.sparse.major")?;
683        let minor = pax_text_checked(pax, "GNU.sparse.minor")?;
684        if major == Some("1") && minor == Some("0") {
685            if pax.iter().any(|(key, _)| {
686                matches!(
687                    key.as_str(),
688                    "GNU.sparse.map"
689                        | "GNU.sparse.numblocks"
690                        | "GNU.sparse.offset"
691                        | "GNU.sparse.numbytes"
692                        | "GNU.sparse.size"
693                )
694            }) {
695                return Err(invalid(
696                    "GNU sparse 1.0 metadata mixes sparse representations",
697                ));
698            }
699            let logical = pax_u64_checked(pax, "GNU.sparse.realsize")?
700                .ok_or_else(|| invalid("PAX sparse 1.0 lacks GNU.sparse.realsize"))?;
701            let (map, map_bytes) = self.read_sparse_1_0_map()?;
702            let packed = physical
703                .checked_sub(map_bytes)
704                .ok_or_else(|| invalid("sparse map exceeds entry size"))?;
705            validate_sparse(&map, logical, packed, true)?;
706            return Ok((Some(map), logical));
707        }
708        let is_version_zero = major == Some("0") && matches!(minor, Some("0" | "1"));
709        if (major.is_some() || minor.is_some()) && !is_version_zero {
710            return Err(invalid(
711                "unsupported or contradictory GNU sparse PAX version",
712            ));
713        }
714        let size = pax_u64_checked(pax, "GNU.sparse.size")?;
715        let realsize = pax_u64_checked(pax, "GNU.sparse.realsize")?;
716        if size
717            .zip(realsize)
718            .is_some_and(|(size, realsize)| size != realsize)
719        {
720            return Err(invalid(
721                "GNU sparse PAX metadata has conflicting logical sizes",
722            ));
723        }
724        let logical = size
725            .or(realsize)
726            .ok_or_else(|| invalid("GNU sparse PAX metadata lacks logical size"))?;
727        let map = if let Some(value) = pax_text_checked(pax, "GNU.sparse.map")? {
728            if pax
729                .iter()
730                .any(|(key, _)| matches!(key.as_str(), "GNU.sparse.offset" | "GNU.sparse.numbytes"))
731            {
732                return Err(invalid("GNU sparse PAX metadata mixes maps and pairs"));
733            }
734            let map = parse_sparse_csv(value)?;
735            if pax_u64_checked(pax, "GNU.sparse.numblocks")?
736                .is_some_and(|count| usize::try_from(count).ok() != Some(map.len()))
737            {
738                return Err(invalid("GNU sparse PAX map count does not match"));
739            }
740            map
741        } else if let Some(count) = pax_u64_checked(pax, "GNU.sparse.numblocks")? {
742            parse_sparse_pairs(pax, count)?
743        } else {
744            return Err(invalid("orphaned GNU sparse PAX metadata"));
745        };
746        validate_sparse(&map, logical, physical, true)?;
747        Ok((Some(map), logical))
748    }
749
750    fn read_sparse_1_0_map(&self) -> Result<(Vec<SparseSegment>, u64)> {
751        let mut consumed = 0_u64;
752        let count = self.read_sparse_line(&mut consumed)?;
753        let count =
754            usize::try_from(count).map_err(|_| invalid("sparse segment count is too large"))?;
755        if count > MAX_SPARSE_SEGMENTS {
756            return Err(invalid("sparse map has too many segments"));
757        }
758        let mut map = Vec::new();
759        for _ in 0..count {
760            let offset = self.read_sparse_line(&mut consumed)?;
761            let len = self.read_sparse_line(&mut consumed)?;
762            map.push(SparseSegment { offset, len });
763        }
764        let map_bytes = consumed
765            .checked_add(padding(consumed))
766            .ok_or_else(|| invalid("sparse map overflow"))?;
767        let extra = map_bytes - consumed;
768        let mut state = self.state.borrow_mut();
769        if extra > state.pending {
770            return Err(invalid("sparse map exceeds entry size"));
771        }
772        state.skip(extra)?;
773        state.pending -= extra;
774        Ok((map, map_bytes))
775    }
776
777    fn read_sparse_line(&self, consumed: &mut u64) -> Result<u64> {
778        let mut digits = Vec::with_capacity(20);
779        loop {
780            if *consumed >= MAX_METADATA_SIZE {
781                return Err(invalid("GNU sparse 1.0 metadata exceeds 1 MiB limit"));
782            }
783            let mut byte = [0_u8; 1];
784            let mut state = self.state.borrow_mut();
785            if state.pending == 0 {
786                return Err(invalid("truncated GNU sparse 1.0 map"));
787            }
788            state.read_exact_counted(&mut byte)?;
789            state.pending -= 1;
790            drop(state);
791            *consumed += 1;
792            if byte[0] == b'\n' {
793                break;
794            }
795            if !byte[0].is_ascii_digit() || digits.len() == 20 {
796                return Err(invalid("invalid decimal in GNU sparse 1.0 map"));
797            }
798            digits.push(byte[0]);
799        }
800        if digits.is_empty() {
801            return Err(invalid("empty decimal in GNU sparse 1.0 map"));
802        }
803        parse_decimal(&digits)
804    }
805}
806
807/// A logical archive entry. Reading a sparse entry materializes holes as zeroes.
808pub struct Entry<R: Read> {
809    state: Rc<RefCell<ReaderState<R>>>,
810    header: Header,
811    kind: EntryType,
812    pax_global: Option<Rc<PaxLayer>>,
813    pax_local: PaxRecords,
814    pax_local_keys: BTreeSet<String>,
815    sparse: Option<Vec<SparseSegment>>,
816    logical_size: u64,
817    logical_pos: u64,
818    sparse_index: usize,
819    generation: u64,
820    extraction_started: bool,
821}
822
823impl<R: Read> Entry<R> {
824    /// Returns the parsed header.
825    #[must_use]
826    pub const fn header(&self) -> &Header {
827        &self.header
828    }
829    /// Returns the final path after long-name, PAX, and sparse-name resolution.
830    ///
831    /// # Errors
832    ///
833    /// Reserved for platform-specific path conversion failures.
834    pub fn path(&self) -> Result<Cow<'_, Path>> {
835        Ok(bytes_to_path(&self.header.path))
836    }
837    /// Returns this entry's type.
838    #[must_use]
839    pub const fn entry_type(&self) -> EntryType {
840        self.kind
841    }
842    /// Returns the logical size, including sparse holes.
843    #[must_use]
844    pub const fn size(&self) -> u64 {
845        self.logical_size
846    }
847    /// Returns data extents for a sparse entry, or `None` for a dense entry.
848    #[must_use]
849    pub fn sparse_map(&self) -> Option<&[SparseSegment]> {
850        self.sparse.as_deref()
851    }
852    /// Iterates over effective PAX key/value records.
853    pub fn pax_extensions(&self) -> impl Iterator<Item = (&str, &[u8])> {
854        let mut layers = Vec::new();
855        let mut layer = self.pax_global.as_deref();
856        while let Some(current) = layer {
857            layers.push(current);
858            layer = current.parent.as_deref();
859        }
860        let mut global = BTreeMap::new();
861        for layer in layers.into_iter().rev() {
862            for (key, value) in &layer.records {
863                if value.is_empty() {
864                    global.remove(key.as_str());
865                } else {
866                    global.insert(key.as_str(), value.as_slice());
867                }
868            }
869        }
870        global.retain(|key, _| !self.pax_local_keys.contains(*key));
871        global.into_iter().chain(
872            self.pax_local
873                .iter()
874                .map(|(key, value)| (key.as_str(), value.as_slice())),
875        )
876    }
877    /// Returns cumulative raw bytes consumed from the underlying stream.
878    #[must_use]
879    pub fn bytes_read(&self) -> u64 {
880        self.state.borrow().raw_bytes
881    }
882
883    fn read_physical(&mut self, buf: &mut [u8]) -> Result<usize> {
884        self.state
885            .borrow_mut()
886            .read_entry_data(self.generation, buf)
887    }
888
889    fn copy_sparse_to(
890        &mut self,
891        file: &mut File,
892        progress: &mut ProgressReporter<'_>,
893    ) -> Result<()> {
894        file.set_len(self.logical_size)?;
895        let map = self.sparse.clone().unwrap_or_default();
896        let mut buf = vec![0_u8; 64 * 1024];
897        for segment in map {
898            file.seek(SeekFrom::Start(segment.offset))?;
899            let mut remaining = segment.len;
900            while remaining != 0 {
901                let want = usize::try_from(remaining.min(buf.len() as u64)).unwrap_or(buf.len());
902                let n = self.read_physical(&mut buf[..want])?;
903                if n == 0 {
904                    return Err(error(ErrorKind::UnexpectedEof, "truncated sparse data"));
905                }
906                file.write_all(&buf[..n])?;
907                remaining -= n as u64;
908                progress.update(self.bytes_read())?;
909            }
910        }
911        Ok(())
912    }
913}
914
915impl<R: Read> Read for Entry<R> {
916    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
917        if buf.is_empty() || self.logical_pos >= self.logical_size {
918            return Ok(0);
919        }
920        if self.generation != self.state.borrow().generation {
921            return Err(error(
922                ErrorKind::InvalidInput,
923                "entry is stale because iteration advanced",
924            ));
925        }
926        let remaining = self.logical_size - self.logical_pos;
927        let limit = usize::try_from(remaining.min(buf.len() as u64)).unwrap_or(buf.len());
928        let Some(map) = self.sparse.as_ref() else {
929            let n = self.read_physical(&mut buf[..limit])?;
930            self.logical_pos += n as u64;
931            return Ok(n);
932        };
933        while self.sparse_index < map.len()
934            && self.logical_pos
935                >= map[self.sparse_index]
936                    .offset
937                    .saturating_add(map[self.sparse_index].len)
938        {
939            self.sparse_index += 1;
940        }
941        if self.sparse_index == map.len() || self.logical_pos < map[self.sparse_index].offset {
942            let hole_end = map
943                .get(self.sparse_index)
944                .map_or(self.logical_size, |segment| segment.offset);
945            let n =
946                usize::try_from((hole_end - self.logical_pos).min(limit as u64)).unwrap_or(limit);
947            buf[..n].fill(0);
948            self.logical_pos += n as u64;
949            return Ok(n);
950        }
951        let segment_end = map[self.sparse_index].offset + map[self.sparse_index].len;
952        let nmax =
953            usize::try_from((segment_end - self.logical_pos).min(limit as u64)).unwrap_or(limit);
954        let n = self.read_physical(&mut buf[..nmax])?;
955        self.logical_pos += n as u64;
956        Ok(n)
957    }
958}
959
960fn padding(size: u64) -> u64 {
961    (BLOCK - size % BLOCK) % BLOCK
962}
963fn invalid(message: &'static str) -> io::Error {
964    error(ErrorKind::InvalidData, message)
965}
966fn error(kind: ErrorKind, message: &'static str) -> io::Error {
967    io::Error::new(kind, message)
968}