Skip to main content

stuffr_core/
archive.rs

1//! The two orthogonal traits, and the entry model they share.
2
3use std::io::{Read, Write};
4use std::time::SystemTime;
5
6use crate::error::Result;
7use crate::fidelity::FidelityReport;
8use crate::format::{CodecCaps, ContainerCaps, FormatId};
9use crate::source::Source;
10
11/// What a codec's decoder is opened with.
12///
13/// **Clones share one [`FidelitySink`](crate::FidelitySink).** `Clone`
14/// copies the [`fidelity`](Self::fidelity) handle, not the warnings behind
15/// it, so every clone of one value — and every decoder opened with any of
16/// them — records into the same sink. Reusing one `DecodeOpts` (or clones of
17/// it) across independent decodes therefore accumulates their warnings
18/// together until the sink is drained; give each decode whose fidelity is
19/// reported on its own a fresh value (`DecodeOpts::default()`, or a new
20/// sink in `fidelity`).
21#[derive(Clone, Default)]
22pub struct DecodeOpts {
23    /// Worker hint. The governor has final say; this is only a request.
24    pub threads: Option<usize>,
25    /// The shared worker/memory budget. `None` means single-threaded — a codec
26    /// parallelises only if handed a governor, never from an ambient global.
27    pub governor: Option<std::sync::Arc<crate::Governor>>,
28    /// Cap on memory a decoder may ask the allocator for. `None` means
29    /// unbounded, which is the library default; the CLI always sets a value.
30    ///
31    /// This exists because three pure codecs size a dictionary buffer from a
32    /// value the *file declares in its own header*, before producing any
33    /// output — measured at 69.35 MB peak RSS for a 60-byte `.xz`, and 538 MB
34    /// for a crafted 114-byte `.lz` that decoded correctly, so the cost came
35    /// purely from the declaration. `--max-ratio` cannot see it: that guard
36    /// counts decoded output bytes and the allocation precedes any output.
37    ///
38    /// Exceeding it is [`crate::Error::ResourceLimit`] (exit 6), never
39    /// `Corrupt` (exit 5) — the file is not damaged, this build simply will
40    /// not allocate that much.
41    pub memory_limit: Option<u64>,
42    /// Where the decoder records what it approximated — today, bytes after a
43    /// gzip or bzip2 file's last verified member that are neither another
44    /// member nor padding ([`crate::Fidelity::TrailingDataIgnored`]). The
45    /// default is a fresh, empty sink; a driver that reports fidelity keeps a
46    /// clone and drains it into its report once the stream has been read.
47    /// Shared by every clone of this value (see the type's doc). See
48    /// [`crate::FidelitySink`].
49    pub fidelity: crate::fidelity::FidelitySink,
50}
51
52impl std::fmt::Debug for DecodeOpts {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        f.debug_struct("DecodeOpts")
55            .field("threads", &self.threads)
56            .field("governor", &self.governor.as_ref().map(|_| "Governor"))
57            .field("memory_limit", &self.memory_limit)
58            .field("fidelity", &self.fidelity)
59            .finish()
60    }
61}
62
63#[derive(Clone, Default)]
64pub struct EncodeOpts {
65    /// Format-relative compression level. `None` means the format's default.
66    pub level: Option<i32>,
67    /// Worker hint. The governor has final say; this is only a request.
68    pub threads: Option<usize>,
69    /// The shared worker/memory budget. `None` means single-threaded — a codec
70    /// parallelises only if handed a governor, never from an ambient global.
71    pub governor: Option<std::sync::Arc<crate::Governor>>,
72}
73
74impl std::fmt::Debug for EncodeOpts {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        f.debug_struct("EncodeOpts")
77            .field("level", &self.level)
78            .field("threads", &self.threads)
79            .field("governor", &self.governor.as_ref().map(|_| "Governor"))
80            .finish()
81    }
82}
83
84#[derive(Clone, Default)]
85pub struct OpenOpts {
86    pub password: Option<String>,
87    /// The shared worker/memory budget. `None` means single-threaded — a
88    /// container parallelises only if handed a governor, never from an
89    /// ambient global. Needed so a container with `per_entry_codec` can build
90    /// a `DecodeOpts` per entry, carrying the same governor forward.
91    pub governor: Option<std::sync::Arc<crate::Governor>>,
92}
93
94impl std::fmt::Debug for OpenOpts {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        f.debug_struct("OpenOpts")
97            .field("password", &self.password)
98            .field("governor", &self.governor.as_ref().map(|_| "Governor"))
99            .finish()
100    }
101}
102
103#[derive(Clone, Default)]
104pub struct CreateOpts {
105    pub level: Option<i32>,
106    /// Codec for entry payloads, for containers with `per_entry_codec`.
107    pub entry_codec: Option<FormatId>,
108    /// The shared worker/memory budget. `None` means single-threaded — a
109    /// container parallelises only if handed a governor, never from an
110    /// ambient global. Needed so a container with `per_entry_codec` can build
111    /// an `EncodeOpts` per entry, carrying the same governor forward.
112    pub governor: Option<std::sync::Arc<crate::Governor>>,
113}
114
115impl std::fmt::Debug for CreateOpts {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        f.debug_struct("CreateOpts")
118            .field("level", &self.level)
119            .field("entry_codec", &self.entry_codec)
120            .field("governor", &self.governor.as_ref().map(|_| "Governor"))
121            .finish()
122    }
123}
124
125/// What an entry is. `Hardlink` is the same content as an EARLIER entry; a
126/// later phase adds `CharDevice`, `BlockDevice`, `Fifo` and `Socket` for tar
127/// and cpio, and `#[non_exhaustive]` keeps that from breaking downstream
128/// matches. Struct types deliberately do NOT carry this attribute — see the
129/// `#[non_exhaustive]` section of CONTRIBUTING.md.
130#[non_exhaustive]
131#[derive(Clone, PartialEq, Eq, Debug, Default)]
132pub enum EntryKind {
133    #[default]
134    File,
135    Dir,
136    Symlink {
137        target: String,
138    },
139    /// The same content as the EARLIER entry named `target`, in the
140    /// archive's own name space (exactly the `EntryMeta::name` that entry
141    /// carried, before any destination join).
142    ///
143    /// A `Hardlink` entry's own reader yields no bytes, and `meta.size` is
144    /// the shared content's size when the reader knows it, else `None`.
145    /// Consumers never read a link's payload; they take it from the target.
146    Hardlink {
147        target: String,
148    },
149    Other,
150}
151
152#[derive(Clone, PartialEq, Eq, Debug, Default)]
153pub struct EntryMeta {
154    pub name: String,
155    pub size: Option<u64>,
156    pub compressed_size: Option<u64>,
157    pub mtime: Option<SystemTime>,
158    pub mode: Option<u32>,
159    pub uid: Option<u32>,
160    pub gid: Option<u32>,
161    pub kind: EntryKind,
162    /// Codec of this entry's payload, for `per_entry_codec` containers.
163    pub codec: Option<FormatId>,
164}
165
166impl EntryMeta {
167    pub fn file(name: impl Into<String>) -> Self {
168        Self {
169            name: name.into(),
170            ..Default::default()
171        }
172    }
173}
174
175/// One entry, borrowed from the archive it came from.
176///
177/// The reader is deliberately **not** `Send`. It was until Phase 2's first
178/// real container, and the bound was unsound to require: `tar::Entry` holds
179/// `&Archive<dyn Read>` over a `RefCell`, so it is `!Send` for any reader,
180/// and every container built on the `tar` crate would have had to assert
181/// `unsafe impl Send` for a property that is false — a lie that becomes UB
182/// the day anything actually sends one.
183///
184/// Nothing needs it. An entry borrows the archive it came from
185/// (`ArchiveRead::next_entry(&mut self) -> Entry<'_>`), so sending an entry
186/// to another thread would mean sending the archive with it, and no caller in
187/// this tree does either: entries are read on the thread that asked for
188/// them. Dropping the bound is strictly more permissive, so a container whose
189/// reader IS `Send` is unaffected.
190///
191/// Since 0.23.0 a codec decodes straight from it: `Source` is no longer
192/// `Send` and [`Codec::decoder`] takes a borrowed `Box<dyn Source + 'a>`, so
193/// `decode_reader(&codec, entry.reader(), &opts)` decodes a `.Z` or `.gz`
194/// member in place, without copying it out first.
195pub struct Entry<'a> {
196    meta: EntryMeta,
197    reader: Box<dyn Read + 'a>,
198    announces_links: Option<u32>,
199}
200
201impl<'a> Entry<'a> {
202    pub fn new(meta: EntryMeta, reader: Box<dyn Read + 'a>) -> Self {
203        Self {
204            meta,
205            reader,
206            announces_links: None,
207        }
208    }
209
210    /// Marks this entry as one the reader KNOWS later entries in the same
211    /// archive are hard links to, and how many: `links_to_come` names besides
212    /// this one share it (cpio reads `nlink`; tar cannot know, so it never
213    /// sets this). A hint for consumers that keep payloads for later links,
214    /// so they can drop a payload once its last link has passed; never a
215    /// promise that a link follows, nor that no more will (an archive can
216    /// hold fewer names than `nlink` says, or a crafted larger count).
217    #[must_use]
218    pub fn announce_links(mut self, links_to_come: u32) -> Self {
219        self.announces_links = Some(links_to_come);
220        self
221    }
222
223    /// How many later hard links the reader announced to this entry; see
224    /// [`Self::announce_links`]. `None` unless a reader set it.
225    pub fn announces_links(&self) -> Option<u32> {
226        self.announces_links
227    }
228
229    pub fn meta(&self) -> &EntryMeta {
230        &self.meta
231    }
232
233    pub fn reader(&mut self) -> &mut (dyn Read + 'a) {
234        &mut *self.reader
235    }
236
237    /// Takes the payload reader out, leaving the metadata behind — the one
238    /// way to rebuild an `Entry` with different metadata over the SAME,
239    /// still-streaming reader.
240    ///
241    /// `#[cfg(test)]` and `pub(crate)` on purpose: no container needs it,
242    /// and nothing outside this crate should be rewriting an entry's
243    /// metadata between the container that read it and the caller that acts
244    /// on it. It exists for `container_conformance`'s property 11 doubles,
245    /// which have to report a mode the framed mock's wire format does not
246    /// carry while still satisfying property 8 — buffering the payload to
247    /// fake one would trip the incrementality property first and the double
248    /// would then prove the wrong thing. Not `pub`: widening the visibility
249    /// to silence the dead-code lint would ship a metadata-rewriting hook to
250    /// every caller for a test's convenience.
251    #[cfg(test)]
252    pub(crate) fn into_reader(self) -> Box<dyn Read + 'a> {
253        self.reader
254    }
255}
256
257/// Manual impl: `reader` is `Box<dyn Read>`, which cannot derive `Debug`.
258/// Needed so `Result<Entry<'_>, Error>::unwrap_err()` type-checks in tests
259/// (`unwrap_err` requires the `Ok` side to be `Debug`).
260impl std::fmt::Debug for Entry<'_> {
261    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262        f.debug_struct("Entry")
263            .field("meta", &self.meta)
264            .finish_non_exhaustive()
265    }
266}
267
268/// A writer that needs an explicit, fallible completion step (trailers, CRCs).
269///
270/// `finish` must flush the underlying writer before returning, once every
271/// trailer byte has been written to it. `Codec::encoder` takes the
272/// destination `Box<dyn Write + Send>` by value, so once a caller has handed
273/// it over, `finish` is the only code left holding it — the caller has no
274/// remaining handle to flush afterwards. Skipping this is silent on an
275/// unbuffered destination (a plain `File`) but loses the tail of the stream
276/// on a buffered one: notably stdout's `LineWriter`, which only auto-flushes
277/// on `\n` and may see none in a long run of binary output.
278///
279/// `#[must_use]` on the trait, so a `Box<dyn Sink>` that is produced and then
280/// dropped without `finish` is a warning rather than a silent truncation.
281/// `ArchiveWrite::finish` returns one precisely so the layer beneath can be
282/// finished next; `archive.finish()?;` on its own compiles perfectly well and
283/// loses every trailer below the container — the exact defect Phase 2c
284/// exists to fix, reintroduced by a line that looks finished.
285#[must_use = "a Sink must be completed with `finish`; dropping it skips the \
286              trailer and the flush, truncating the output"]
287pub trait Sink: Write + Send {
288    fn finish(self: Box<Self>) -> Result<()>;
289}
290
291/// A [`Sink`] over a destination that needs no trailer — an uncompressed file.
292///
293/// Exists so `Container::create` can take a `Box<dyn Sink>` uniformly. A
294/// composed write never constructs one: there the codec's own `Sink` IS the
295/// container's destination. `finish` flushes, which is exactly what all four
296/// containers used to do by hand at the end of their own `finish`.
297pub struct PlainSink(Box<dyn Write + Send>);
298
299impl PlainSink {
300    /// Returns the boxed trait object rather than `Self` on purpose: every
301    /// call site wants a `Box<dyn Sink>`, and handing back `Self` would make
302    /// each of them re-box it.
303    #[allow(clippy::new_ret_no_self)]
304    pub fn new(w: Box<dyn Write + Send>) -> Box<dyn Sink> {
305        Box::new(Self(w))
306    }
307}
308
309impl Write for PlainSink {
310    fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
311        self.0.write(b)
312    }
313    fn flush(&mut self) -> std::io::Result<()> {
314        self.0.flush()
315    }
316}
317
318impl Sink for PlainSink {
319    fn finish(mut self: Box<Self>) -> Result<()> {
320        self.0.flush()?;
321        Ok(())
322    }
323}
324
325/// A byte-stream transform. Knows nothing about files, names, or entries.
326pub trait Codec: Send + Sync {
327    fn id(&self) -> FormatId;
328    fn caps(&self) -> CodecCaps;
329    /// Validates encode options without touching the filesystem.
330    ///
331    /// `ops` calls this before opening any destination, so a rejected option
332    /// costs nothing — no temp file created, no existing file disturbed. The
333    /// default accepts everything, for codecs with no options to reject.
334    ///
335    /// With one codec this looks like ceremony. With nine, each carrying its
336    /// own level range, "validate before touching the filesystem" has to be a
337    /// contract rather than a habit: in Phase 1b an invalid level created and
338    /// then deleted a file for nothing.
339    fn check_encode_opts(&self, _o: &EncodeOpts) -> Result<()> {
340        Ok(())
341    }
342    /// Decodes `src`. Returns a [`Source`] rather than a bare reader so a codec
343    /// carrying a frame index can advertise seekability to the layer above;
344    /// forward-only codecs wrap their reader in [`crate::StreamOnly`].
345    ///
346    /// `src` may borrow (`'a`) and need not be `Send`: an archive member's
347    /// reader is both, and the decoder over it lives no longer than it. A
348    /// caller holding a plain reader uses [`crate::decode_reader`].
349    fn decoder<'a>(
350        &self,
351        src: Box<dyn Source + 'a>,
352        o: &DecodeOpts,
353    ) -> Result<Box<dyn Source + 'a>>;
354    fn encoder(&self, dst: Box<dyn Write + Send>, o: &EncodeOpts) -> Result<Box<dyn Sink>>;
355}
356
357/// A structure of entries. May invoke codecs internally.
358pub trait Container: Send + Sync {
359    fn id(&self) -> FormatId;
360    fn caps(&self) -> ContainerCaps;
361    /// Opens an archive from a ladder-resolved source.
362    ///
363    /// Takes the whole [`crate::Resolved`] rather than just its source so the
364    /// container knows which rung it was given — which is what makes
365    /// `Rung::Degraded` implementable — and so it can seed its fidelity report
366    /// from the ladder's rather than re-deriving the same facts.
367    fn open(&self, resolved: crate::ladder::Resolved, o: &OpenOpts)
368    -> Result<Box<dyn ArchiveRead>>;
369    /// Creates an archive writing into `dst`.
370    ///
371    /// Takes a [`Sink`] rather than a bare `Write` so a container over a codec
372    /// composes: the codec's own `Sink` is handed straight in, and
373    /// [`ArchiveWrite::finish`] gives it back so the caller can finish it.
374    /// Passing a bare file means wrapping it in [`PlainSink`].
375    fn create(&self, dst: Box<dyn Sink>, o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>>;
376}
377
378pub trait ArchiveRead {
379    /// Forward iteration. Available for every container on every input — this
380    /// is the streaming path, and the reason `Entry` borrows.
381    fn next_entry(&mut self) -> Result<Option<Entry<'_>>>;
382
383    /// Random access. `Err(NotSeekable)` when the ladder could not supply seek.
384    fn by_index(&mut self, index: usize) -> Result<Entry<'_>>;
385
386    fn fidelity(&self) -> &FidelityReport;
387}
388
389pub trait ArchiveWrite {
390    fn add(&mut self, meta: &EntryMeta, data: &mut dyn Read) -> Result<()>;
391    /// Writes the container's trailer and returns the destination.
392    ///
393    /// Returning it is what makes container-over-codec possible: the caller
394    /// still holds a `Sink` afterwards and can finish the codec layer, which
395    /// writes ITS trailer. Dropping the destination here — what this used to
396    /// do — silently truncated every composed archive.
397    ///
398    /// Implementors must NOT call `finish` on the returned sink. Finishing is
399    /// the caller's, once, at the outermost layer.
400    fn finish(self: Box<Self>) -> Result<Box<dyn Sink>>;
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406
407    #[test]
408    fn an_entry_announces_links_only_when_its_reader_says_so() {
409        let plain = Entry::new(EntryMeta::default(), Box::new(std::io::empty()));
410        assert_eq!(plain.announces_links(), None);
411        let marked = Entry::new(EntryMeta::default(), Box::new(std::io::empty())).announce_links(2);
412        assert_eq!(marked.announces_links(), Some(2));
413    }
414
415    #[test]
416    fn a_plain_sink_flushes_its_destination_on_finish() {
417        use std::sync::{Arc, Mutex};
418
419        #[derive(Default)]
420        struct Rec {
421            flushed: bool,
422            bytes: Vec<u8>,
423        }
424        struct W(Arc<Mutex<Rec>>);
425        impl Write for W {
426            fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
427                self.0.lock().unwrap().bytes.extend_from_slice(b);
428                Ok(b.len())
429            }
430            fn flush(&mut self) -> std::io::Result<()> {
431                self.0.lock().unwrap().flushed = true;
432                Ok(())
433            }
434        }
435
436        let rec = Arc::new(Mutex::new(Rec::default()));
437        let mut sink = PlainSink::new(Box::new(W(Arc::clone(&rec))));
438        sink.write_all(b"payload").unwrap();
439        assert!(
440            !rec.lock().unwrap().flushed,
441            "finish has not been called yet"
442        );
443        sink.finish().unwrap();
444
445        let r = rec.lock().unwrap();
446        assert!(
447            r.flushed,
448            "PlainSink::finish must flush — a buffered destination loses its tail otherwise"
449        );
450        assert_eq!(r.bytes, b"payload");
451    }
452}