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