Skip to main content

znippy_common/
precompressed.rs

1//! Deciding whether a payload is **already compressed**, and so should be stored
2//! raw instead of handed to the codec.
3//!
4//! ## Where the probe lives
5//!
6//! In [`ldeflate::precompressed`], not here. This module is a **re-export** plus
7//! the one leg that cannot live there: the path/extension fallback.
8//!
9//! The direction is forced, not chosen. `znippy-common` already depends on
10//! `znippy-zoomies` (`Cargo.toml:96`, plus `ljar`/`lbzip2`/`lgz`), so an
11//! `ldeflate -> znippy-common` edge would close a cycle; and `znippy-common`
12//! drags `arrow`, `blake3` and an `openzl-sys` whose `build.rs` fetches a tarball
13//! over the network — for 88 lines of `std`-only byte matching. The dependency
14//! arrow already pointed at zoomies, which makes `ldeflate` the home: it is the
15//! leaf, and it is the *compressor*, the only thing that ever needs to ask the
16//! question.
17//!
18//! What is left here is genuinely znippy's: [`SkipPolicy::skip_by_path`] consults
19//! [`crate::index::is_probably_compressed`], an extension table that `ldeflate`
20//! has no business knowing about — it is handed `&[u8]`, never a `Path`.
21//! [`SkipPolicy`] is therefore a thin wrapper that adds the path leg and
22//! **delegates** the byte leg to `ldeflate::precompressed::SkipPolicy::skip`.
23//! There is one magic table, one `looks_compressed`, one `is_zlib_stream`, and
24//! one hint-to-verdict mapping for bytes — in `ldeflate`.
25//!
26//! ## The resolution order
27//!
28//! | # | mechanism | cost | how it can be wrong |
29//! |---|---|---|---|
30//! | 1 | an explicit caller [`ContentHint`] | free | only if the caller lies |
31//! | 2 | the extension table ([`crate::index::is_probably_compressed`]) | one `str` compare | the name can lie |
32//! | 3 | a [magic-byte probe](looks_compressed) over the first [`SNIFF_PREFIX_LEN`] bytes | ≤ 16 byte compares | a container whose magic is not at offset 0 |
33//! | 4 | default | — | — |
34//!
35//! **The hint is the primary mechanism, not the fallback.** A caller sealing a
36//! git packfile *knows* what it is holding; no amount of inspection beats being
37//! told. It is deterministic by construction, costs nothing, and carries
38//! knowledge the bytes themselves do not.
39//!
40//! ## Why there is no entropy estimate
41//!
42//! An entropy estimate was considered and **rejected**. It is a heuristic with a
43//! tunable threshold, it costs a scan proportional to the data, and it misjudges
44//! in both directions — high-entropy-but-compressible (a table of hashes with a
45//! compressible envelope) and low-entropy-but-already-compressed (a short zstd
46//! frame) both exist. A wrong guess wastes CPU or wastes bytes and reports
47//! neither. Every probe in `ldeflate` is exact: a fixed byte pattern at a fixed
48//! offset, no threshold, no sample size to tune.
49//!
50//! ## What a probe does *not* claim
51//!
52//! [`looks_compressed`] reads only the head of a buffer. For a file sliced into
53//! chunks, only chunk 0 carries the magic. The big-file pass in `znippy-compress`
54//! therefore takes chunk 0's verdict and carries it over the whole file — but
55//! **excluding** [`is_zlib_stream`], which is a two-byte test accepting 1 in 2048
56//! two-byte heads (measured exhaustively over all 65 536, not sampled) and so is
57//! not a claim to make about 8 GiB. See `slot_packer::carries_file_wide`. The
58//! extension table covers the whole file at once and so has no such gap, which is
59//! why `.pack` / `.idx` belong there too.
60//!
61//! ## The git case, precisely
62//!
63//! Two different things are called "a git object" and they need opposite
64//! answers:
65//!
66//! * a **loose object file** in `.git/objects/ab/cdef…` is a zlib stream, has no
67//!   extension at all, and must be stored raw. [`is_zlib_stream`] catches it.
68//! * a `znippy-plugin-git` archive entry is the object's **canonical form**
69//!   (`"<type> <size>\0<content>"`) — inflated bytes that compress perfectly
70//!   well and *must* be compressed. It begins `commit `/`tree `/`blob `/`tag `,
71//!   matches no probe there, and is correctly left alone.
72//!
73//! A rule of the shape "an oid-keyed entry is already compressed" would get the
74//! second case backwards and silently store compressible data raw. Probing the
75//! bytes at offset 0 gets both right. Both halves are asserted against a real
76//! encoder in `ldeflate::precompressed`'s own tests.
77
78use std::path::Path;
79
80/// The probe itself, re-exported so `znippy_common::precompressed::*` keeps
81/// naming exactly what it always named. These are `ldeflate`'s items, not copies
82/// of them — breaking one there breaks every znippy caller, which is the point
83/// of the move.
84pub use ldeflate::precompressed::{ContentHint, SNIFF_PREFIX_LEN, is_zlib_stream, looks_compressed};
85
86/// A batch-level compression policy: one [`ContentHint`] covering everything in
87/// a run, plus the fallbacks for when the hint says nothing.
88///
89/// Batch-level because that is how the knowledge actually arrives — a caller
90/// sealing a pack knows it for the whole pack, not entry by entry.
91///
92/// The two decision points are deliberately split, because the compress path has
93/// two moments with different information available:
94///
95/// * [`skip_by_path`](Self::skip_by_path) runs during enumeration, before a
96///   single byte is read;
97/// * [`skip_by_bytes`](Self::skip_by_bytes) refines it inside the compressor,
98///   once a chunk's real bytes are in hand, and is
99///   `ldeflate::precompressed::SkipPolicy::skip` under a name that says which of
100///   the two moments it belongs to.
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
102pub struct SkipPolicy(ldeflate::precompressed::SkipPolicy);
103
104impl SkipPolicy {
105    /// No claim: decide per entry from its name, then from its bytes.
106    pub const fn resolve() -> Self {
107        Self(ldeflate::precompressed::SkipPolicy::resolve())
108    }
109
110    /// Everything in this batch is already compressed — store it all raw.
111    pub const fn already_compressed() -> Self {
112        Self(ldeflate::precompressed::SkipPolicy::already_compressed())
113    }
114
115    /// Compress everything, whatever it looks like. The meaning of `--no-skip`.
116    pub const fn compress_everything() -> Self {
117        Self(ldeflate::precompressed::SkipPolicy::compress_everything())
118    }
119
120    /// Bridge from the historical `no_skip: bool`, which is precisely the
121    /// two-valued subset of [`ContentHint`] that the CLI already exposed.
122    pub const fn from_no_skip(no_skip: bool) -> Self {
123        if no_skip { Self::compress_everything() } else { Self::resolve() }
124    }
125
126    pub const fn from_hint(hint: ContentHint) -> Self {
127        Self(ldeflate::precompressed::SkipPolicy::from_hint(hint))
128    }
129
130    pub const fn hint(&self) -> ContentHint {
131        self.0.hint()
132    }
133
134    /// The same policy as `ldeflate` sees it — for a caller handing bytes
135    /// straight to `ldeflate::compress_objects` rather than to znippy's packer,
136    /// so the two cannot disagree about what a hint means.
137    pub const fn as_ldeflate(&self) -> ldeflate::precompressed::SkipPolicy {
138        self.0
139    }
140
141    /// The decision available before any bytes are read: hint, else the
142    /// extension table.
143    ///
144    /// This is the leg `ldeflate` cannot have — it is handed `&[u8]`, never a
145    /// `Path`.
146    ///
147    /// `true` means "store raw". A `false` here is **not** final — the caller
148    /// should still offer the bytes to [`skip_by_bytes`](Self::skip_by_bytes)
149    /// once it has them.
150    pub fn skip_by_path(&self, path: &Path) -> bool {
151        match self.0.hint() {
152            ContentHint::AlreadyCompressed => true,
153            ContentHint::Compressible => false,
154            ContentHint::Unknown => crate::index::is_probably_compressed(path),
155        }
156    }
157
158    /// The refinement once real bytes are in hand: a magic-byte probe.
159    ///
160    /// Only meaningful when [`skip_by_path`](Self::skip_by_path) returned
161    /// `false`; calling it otherwise is harmless but pointless. Honours the hint
162    /// in both directions, so a `Compressible` batch is never vetoed by a probe.
163    pub fn skip_by_bytes(&self, prefix: &[u8]) -> bool {
164        self.0.skip(prefix)
165    }
166}
167
168impl From<ldeflate::precompressed::SkipPolicy> for SkipPolicy {
169    fn from(p: ldeflate::precompressed::SkipPolicy) -> Self {
170        Self(p)
171    }
172}
173
174impl From<SkipPolicy> for ldeflate::precompressed::SkipPolicy {
175    fn from(p: SkipPolicy) -> Self {
176        p.0
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    // The probe's own behaviour — the magic table, the bzip2/WebP special cases,
185    // the zlib header against streams from a real encoder, canonical git object
186    // bytes — is asserted in `ldeflate::precompressed`, which is where the code
187    // now is. Repeating it here would be the twin this change removed.
188    //
189    // What is asserted here is what this crate adds: the path leg, the bridge
190    // from `no_skip`, and — first, because everything else assumes it — that the
191    // re-export really does reach `ldeflate` rather than a leftover copy.
192
193    /// The dedup itself. If `znippy-common` ever grows a second magic table
194    /// again, these calls stop agreeing.
195    #[test]
196    fn the_probe_is_ldeflates_and_not_a_second_copy() {
197        for bytes in [
198            b"PACK\x00\x00\x00\x02".as_slice(),
199            b"\x1f\x8b\x08\x00".as_slice(),
200            b"\x28\xb5\x2f\xfd\x00\x58".as_slice(),
201            b"RIFF\x24\x00\x00\x00WEBPVP8 ".as_slice(),
202            b"\x78\x9c".as_slice(),
203            b"SELECT * FROM njord WHERE id = 42;".as_slice(),
204            b"x = 1;\n".as_slice(),
205            b"".as_slice(),
206        ] {
207            assert_eq!(
208                looks_compressed(bytes),
209                ldeflate::precompressed::looks_compressed(bytes),
210                "the re-export must BE ldeflate's probe: {bytes:?}"
211            );
212            assert_eq!(
213                is_zlib_stream(bytes),
214                ldeflate::precompressed::is_zlib_stream(bytes),
215                "the re-export must BE ldeflate's zlib probe: {bytes:?}"
216            );
217            // And the policy's byte leg must be the same decision, not a
218            // parallel one.
219            assert_eq!(
220                SkipPolicy::resolve().skip_by_bytes(bytes),
221                ldeflate::precompressed::SkipPolicy::resolve().skip(bytes),
222                "skip_by_bytes must delegate: {bytes:?}"
223            );
224        }
225        assert_eq!(SNIFF_PREFIX_LEN, ldeflate::precompressed::SNIFF_PREFIX_LEN);
226    }
227
228    /// The wrapper must not lose the hint on the way in or out — the failure
229    /// that would make `--no-skip` silently stop meaning anything.
230    #[test]
231    fn the_hint_survives_the_wrapper_in_both_directions() {
232        for (p, want) in [
233            (SkipPolicy::resolve(), ContentHint::Unknown),
234            (SkipPolicy::already_compressed(), ContentHint::AlreadyCompressed),
235            (SkipPolicy::compress_everything(), ContentHint::Compressible),
236        ] {
237            assert_eq!(p.hint(), want);
238            assert_eq!(p.as_ldeflate().hint(), want, "the ldeflate view must carry the same hint");
239            assert_eq!(SkipPolicy::from(p.as_ldeflate()), p, "round-trip through ldeflate");
240            assert_eq!(ldeflate::precompressed::SkipPolicy::from(p), p.as_ldeflate());
241            assert_eq!(SkipPolicy::from_hint(want), p, "from_hint must agree with the constructor");
242        }
243        assert_eq!(SkipPolicy::default(), SkipPolicy::resolve(), "the probe is the default");
244    }
245
246    // ── the hint, both directions ────────────────────────────────────────────
247
248    #[test]
249    fn an_already_compressed_hint_is_honoured_without_inspection() {
250        let p = SkipPolicy::already_compressed();
251        // Plain compressible bytes under a plain name: only the hint can decide
252        // this, and it must.
253        assert!(p.skip_by_path(Path::new("objects/00ff")), "hint must win on the path");
254        assert!(p.skip_by_bytes(b"SELECT * FROM njord;"), "hint must win on the bytes");
255    }
256
257    #[test]
258    fn an_unhinted_batch_of_compressible_data_is_still_compressed() {
259        // The other direction: a hint that swallows everything is as broken as
260        // one that is ignored.
261        let p = SkipPolicy::resolve();
262        assert!(!p.skip_by_path(Path::new("dbdump/njord.dump")));
263        assert!(!p.skip_by_bytes(b"SELECT * FROM njord WHERE id = 42;"));
264    }
265
266    #[test]
267    fn a_compressible_hint_overrules_both_fallbacks() {
268        let p = SkipPolicy::compress_everything();
269        assert!(!p.skip_by_path(Path::new("photo.png")), "--no-skip must beat the extension table");
270        assert!(!p.skip_by_bytes(b"\x89PNG\r\n\x1a\n"), "--no-skip must beat the probe");
271    }
272
273    #[test]
274    fn no_skip_maps_onto_the_hint() {
275        assert_eq!(SkipPolicy::from_no_skip(true), SkipPolicy::compress_everything());
276        assert_eq!(SkipPolicy::from_no_skip(false), SkipPolicy::resolve());
277    }
278
279    /// The whole point of a sniffer: a name that lies.
280    #[test]
281    fn an_extension_that_lies_is_caught_by_the_bytes() {
282        let p = SkipPolicy::resolve();
283        let notes = Path::new("release-notes.txt");
284        let zstd_bytes = b"\x28\xb5\x2f\xfd\x00\x58\x2d\x00\x00";
285
286        assert!(!p.skip_by_path(notes), "the name says text, so the fast path lets it through");
287        assert!(p.skip_by_bytes(zstd_bytes), "the bytes say zstd, and the bytes are the authority");
288    }
289
290    /// And the same trap in reverse: a `.pack` name over compressible bytes is
291    /// still skipped by the fast path. Documented so the asymmetry is a decision
292    /// rather than a surprise — the extension table is trusted, by design.
293    #[test]
294    fn the_extension_fast_path_covers_a_whole_file_not_just_its_head() {
295        let p = SkipPolicy::resolve();
296        assert!(p.skip_by_path(Path::new("objects/pack/pack-9f2c.pack")));
297        assert!(p.skip_by_path(Path::new("objects/pack/pack-9f2c.idx")));
298    }
299
300    /// A git loose object is the case with **no extension at all**, so only the
301    /// byte leg can reach it — and the path leg must say "not from the name",
302    /// which is what makes the byte leg necessary rather than decorative.
303    #[test]
304    fn an_oid_named_loose_object_is_reached_only_by_the_bytes() {
305        let p = SkipPolicy::resolve();
306        let oid = Path::new(".git/objects/9f/2c1e0b7a4d3f8c6e5a2b9d0c7f4e1a8b3d6c9e2f");
307        assert!(!p.skip_by_path(oid), "an oid has no extension for the table to match");
308        assert!(p.skip_by_bytes(b"\x78\x01\x4b\xca\x49"), "so the zlib header must catch it");
309    }
310}