Skip to main content

limnifs_core/codec/
mod.rs

1//! Codec registry — dispatches compression/decompression by codec id.
2//!
3//! Each drop record carries a `representation` triple `(codec, aead, ec)`.
4//! This module centralises codec dispatch behind a [`Codec`] trait and a
5//! [`CodecRegistry`], so adding a codec is a new file + one registration
6//! call (open/closed). The existing free functions [`compress`] and
7//! [`decompress`] remain as thin wrappers around the default registry.
8//!
9//! ## Supported codecs
10//!
11//! | Id  | Name   | Encode | Decode | Notes |
12//! |-----|--------|--------|--------|-------|
13//! | 0x00 | store | yes (identity) | yes | No compression |
14//! | 0x01 | lz4   | yes (`lz4_flex`) | yes | Fast baseline; pure Rust |
15//! | 0x02 | zstd  | yes (`ruzstd` `Fastest`) | yes (`ruzstd`) | Pure Rust; ZSTD level 1 |
16//! | 0x03 | xz    | yes (`omnizip-lzma`) | yes (`omnizip-lzma`) | LZMA2 in XZ container |
17//! | 0x04 | brotli | yes (`brotli` q11) | yes (`brotli`) | Best ratio; pure Rust |
18//! | 0x05 | deflate | yes (`miniz_oxide`) | yes (`miniz_oxide`) | RFC 1951; universal interop; pure Rust |
19//! | 0x06 | snappy | yes (`omnizip-snappy`) | yes (`omnizip-snappy`) | Google's high-speed codec; pure Rust |
20//!
21//! **100% pure Rust.** No C libraries. Air-gapped safe.
22
23mod bcj_composites;
24mod bitshuffle_lz4;
25mod brotli;
26mod bzip2;
27mod composite;
28mod deflate;
29mod deflate64;
30mod flac;
31pub mod fsst_brotli;
32mod glza;
33mod libdeflate;
34mod lz4;
35mod ppmd;
36mod ppmd8;
37mod ricepp;
38mod shuffle_lz4;
39mod shuffle_zstd;
40mod snappy;
41mod store;
42mod xz;
43mod zpaq;
44mod zstd;
45pub mod zstd_dict;
46
47use std::sync::OnceLock;
48
49use crate::error::CoreError;
50
51/// Codec id 0x00: store (no compression).
52pub const CODEC_STORE: u8 = 0x00;
53/// Codec id 0x01: LZ4 block format (`lz4_flex`, pure Rust).
54pub const CODEC_LZ4: u8 = 0x01;
55/// Codec id 0x02: Zstandard frame format (`ruzstd`, pure Rust).
56/// Encode uses `CompressionLevel::Fastest` (ZSTD level 1); decode supports
57/// any level the reference encoder can produce.
58pub const CODEC_ZSTD: u8 = 0x02;
59/// Codec id 0x03: XZ/LZMA2 format via `omnizip-lzma`.
60pub const CODEC_XZ: u8 = 0x03;
61/// Codec id 0x04: Brotli frame format (`brotli`, pure Rust). Encode at
62/// quality 11 (best ratio); decode at any quality.
63pub const CODEC_BROTLI: u8 = 0x04;
64/// Codec id 0x05: DEFLATE stream format (`miniz_oxide`, pure Rust).
65/// Raw RFC 1951 inside a zlib wrapper (RFC 1950).
66pub const CODEC_DEFLATE: u8 = 0x05;
67/// Codec id 0x06: Snappy format (`omnizip-snappy` → `snap`, pure Rust).
68/// No compression levels; ~500 MB/s encode and decode.
69pub const CODEC_SNAPPY: u8 = 0x06;
70/// Codec id 0x07: FLAC for PCM audio. **RESERVED** — pending
71/// `omnizip-flac` encoder port. The wrapper at `codec::flac::FlacCodec`
72/// returns `UnsupportedFeature` until the real codec lands.
73pub const CODEC_FLAC: u8 = 0x07;
74/// Codec id 0x08: Rice++ for FITS / scientific integer-pixel images.
75/// **RESERVED** — pending `omnizip-ricepp` encoder port.
76pub const CODEC_RICEPP: u8 = 0x08;
77/// Codec id 0x09: FSST + Brotli composite for CSV/JSON.
78pub const CODEC_FSST_BROTLI: u8 = 0x09;
79/// Codec id 0x0A: BLOSC shuffle + LZ4 for scientific float data.
80pub const CODEC_BLOSC2_SHUFFLE_LZ4: u8 = 0x0A;
81/// Codec id 0x0B: ZPAQ context-mixing archiver.
82pub const CODEC_ZPAQ: u8 = 0x0B;
83/// Codec id 0x0C: `PPMd` (dormant — raw fallback).
84pub const CODEC_PPMD: u8 = 0x0C;
85/// Codec id 0x0D: GLZA grammar-based LZ.
86pub const CODEC_GLZA: u8 = 0x0D;
87/// Codec id 0x0E: Shuffle+Zstd (BLOSC2 byte-shuffle + Zstd back-end).
88pub const CODEC_SHUFFLE_ZSTD: u8 = 0x0E;
89/// Codec id 0x0F: Bitshuffle+LZ4 (BLOSC2 bit-shuffle + LZ4 back-end).
90pub const CODEC_BITSHUFFLE_LZ4: u8 = 0x0F;
91/// Codec id 0x10: `BZip2`.
92pub const CODEC_BZIP2: u8 = 0x10;
93/// Codec id 0x11: Deflate64 (ZIP method 9, 64 KB window).
94pub const CODEC_DEFLATE64: u8 = 0x11;
95/// Codec id 0x12: PPMd8 (RESTART + RLE, user-tunable memory budget).
96pub const CODEC_PPMD8: u8 = 0x12;
97
98/// Codec id 0x13: LZ4 HC (hash-chain match finder + lazy parsing).
99/// Real encoder from omnizip-lz4 0.14.1; was a stub in 0.13.1.
100pub const CODEC_LZ4_HC: u8 = 0x13;
101
102/// Codec id 0x14: libdeflate-compatible DEFLATE (pure-Rust port).
103/// Wire-compatible with `CODEC_DEFLATE` (0x05) — both are RFC 1951
104/// DEFLATE wrapped in RFC 1950 zlib. Different implementation:
105/// `omnizip-libdeflate` is omnizip's in-house pure-Rust port
106/// (LZ77 + fixed-Huffman + canonical Huffman inflate), focused on
107/// decode speed; `omnizip-deflate` (0x05) wraps `miniz_oxide`.
108///
109/// LimniFS exposes both so users can pick the implementation that
110/// wins on their workload. Round-trip is byte-compatible: a writer
111/// using 0x14 produces output decodable by a reader using 0x05 and
112/// vice versa.
113pub const CODEC_LIBDEFLATE: u8 = 0x14;
114
115/// Codec id 0x20: BCJ-x86 filter + LZ4. For x86/x86_64 executables.
116pub const CODEC_BCJ_X86_LZ4: u8 = 0x20;
117/// Codec id 0x21: BCJ-x86 filter + ZSTD.
118pub const CODEC_BCJ_X86_ZSTD: u8 = 0x21;
119/// Codec id 0x23: BCJ-ARM64 filter + LZ4. For AArch64 executables.
120pub const CODEC_BCJ_ARM64_LZ4: u8 = 0x23;
121/// Codec id 0x24: BCJ-ARM64 filter + ZSTD.
122pub const CODEC_BCJ_ARM64_ZSTD: u8 = 0x24;
123
124/// Codec id 0xFE: REFERENCED. Sentinel codec id for drops that are
125/// not stored in this image's slabs — the bytes live in a base image
126/// and the reader resolves them via the overlay chain.
127///
128/// Never appears in a slab's drop records. Used as a marker in
129/// in-memory writer state (`PendingDrop::codec`) so that `pack_slabs`
130/// knows to skip the drop. Wire format is unchanged: drops absent
131/// from all slabs are simply absent from all slab index entries.
132pub const CODEC_REFERENCED: u8 = 0xFE;
133
134/// Codec-agnostic tunables. Every codec reads only the fields it
135/// understands; the rest are ignored. The struct is the
136/// single source of truth for "what knobs does the writer want to
137/// turn" — adding a new knob is one field here, not a new
138/// `compress_with_*` function per codec (OCP).
139#[derive(Clone, Debug)]
140pub struct CodecTunables {
141    /// Brotli quality (0..=11). Codecs without a quality
142    /// parameter ignore this. Historically this also served as a
143    /// ZSTD level proxy — decoupled below since the two scales
144    /// diverged (omnizip 0.21.12+ runs the optimal parser at every
145    /// zstd level >= L3, so brotli's default q11 was silently
146    /// pushing zstd into the slow band).
147    pub quality: u8,
148    /// ZSTD quality (0..=22 via `level_for_quality`). 0 = fast-tier
149    /// default. Independent of `quality` (brotli) by design.
150    pub zstd_quality: u8,
151    /// XZ preset (0..=9). 0 = preset 6 (xz's balanced default).
152    /// Independent of `quality` (brotli) — see zstd_quality.
153    pub xz_level: u8,
154    /// PPMd7 / PPMd8 context-model order (1..=16).
155    pub ppmd_order: u8,
156    /// PPMd7 context-tree memory budget in bytes. 0 = codec default.
157    pub ppmd7_budget: usize,
158    /// PPMd8 context-tree memory budget in bytes. 0 = codec default.
159    pub ppmd8_budget: usize,
160    /// BZip2 block size in KB (100..=900). Maps to level 1..=9.
161    pub bzip2_block_kb: u32,
162    /// LZMA dictionary size in MB. Reserved — no pure-Rust LZMA
163    /// encoder exists yet; field is here so profiles can declare
164    /// intent and we wire it when omnizip-lzma ships an encoder.
165    pub lzma_dict_mb: u32,
166}
167
168impl CodecTunables {
169    /// Build tunables carrying only `quality`. Codecs that don't
170    /// override `compress_with_tunables` see no difference from
171    /// `compress(plaintext)`.
172    #[must_use]
173    pub fn from_quality(quality: u8) -> Self {
174        Self {
175            quality,
176            zstd_quality: quality,
177            xz_level: 0,
178            ppmd_order: 0,
179            ppmd7_budget: 0,
180            ppmd8_budget: 0,
181            bzip2_block_kb: 0,
182            lzma_dict_mb: 0,
183        }
184    }
185}
186
187impl Default for CodecTunables {
188    fn default() -> Self {
189        Self {
190            quality: 0,
191            zstd_quality: 0,
192            xz_level: 0,
193            ppmd_order: 0,
194            ppmd7_budget: 0,
195            ppmd8_budget: 0,
196            bzip2_block_kb: 0,
197            lzma_dict_mb: 0,
198        }
199    }
200}
201
202/// The behaviour every compression codec implements. New codecs register
203/// a `Codec` impl with [`CodecRegistry::register`]; the dispatch code
204/// never changes.
205pub trait Codec: Send + Sync {
206    /// The wire-format codec id recorded in the drop record.
207    fn id(&self) -> u8;
208    /// Human-readable name for diagnostics.
209    fn name(&self) -> &'static str;
210    /// Compress `plaintext` into the codec's wire format.
211    ///
212    /// # Errors
213    ///
214    /// Returns [`CoreError::UnsupportedFeature`] if the codec is
215    /// decode-only in pure Rust (currently only XZ), or
216    /// [`CoreError::Corrupt`] if the encoder fails.
217    fn compress(&self, plaintext: &[u8]) -> Result<Vec<u8>, CoreError>;
218    /// Decompress `compressed`, verifying the output length matches
219    /// `expected_len` exactly.
220    ///
221    /// # Errors
222    ///
223    /// Returns [`CoreError::Corrupt`] if decompression fails or the
224    /// result length does not match `expected_len`.
225    fn decompress(&self, compressed: &[u8], expected_len: u32) -> Result<Vec<u8>, CoreError>;
226
227    /// Minimum input size for this codec to be tried in the compression
228    /// tournament. Chunks smaller than this skip the codec entirely.
229    /// Defaults to 0 (no threshold). Override in codec impls that have
230    /// significant per-call setup cost (context model initialization,
231    /// grammar construction, etc.).
232    fn min_compress_size(&self) -> usize {
233        0
234    }
235
236    /// Compress with a tunables hint. Codecs that have user-tunable
237    /// parameters (PPMd order/budget, Brotli quality, ZSTD level,
238    /// Bzip2 block size, …) override this; the default impl ignores
239    /// tunables and calls `compress`. Adding a tunable is therefore
240    /// backward-compatible — old callers keep working.
241    ///
242    /// # Errors
243    ///
244    /// Same as [`Codec::compress`].
245    fn compress_with_tunables(
246        &self,
247        plaintext: &[u8],
248        tunables: &CodecTunables,
249    ) -> Result<Vec<u8>, CoreError> {
250        let _ = tunables;
251        self.compress(plaintext)
252    }
253}
254
255/// Optional trait: codecs with strongly-typed per-codec tunables.
256///
257/// The flat [`CodecTunables`] struct works for today's six codec
258/// families but doesn't scale. Codecs that want clean OCP for their
259/// own knobs implement this trait alongside [`Codec`]; new codecs
260/// = one `impl PerCodecTunables` with a fresh `Tunables` type, no
261/// edits to existing code or to the flat struct.
262///
263/// The flat `CodecTunables` remains the dispatch entry point for
264/// callers that want a single uniform struct; codecs that implement
265/// `PerCodecTunables` can read from it inside their
266/// `compress_with_tunables` override.
267pub trait PerCodecTunables: Codec {
268    /// Per-codec tunables type. Should be `Clone + Send + Sync +
269    /// 'static` so it can live in a `Box<dyn Any>` registry if/when
270    /// we move to per-codec-keyed tunables dispatch.
271    type Tunables: Clone + Send + Sync + 'static;
272
273    /// Compress with this codec's strongly-typed tunables.
274    ///
275    /// # Errors
276    ///
277    /// Same as [`Codec::compress`].
278    fn compress_with_owned_tunables(
279        &self,
280        plaintext: &[u8],
281        tunables: &Self::Tunables,
282    ) -> Result<Vec<u8>, CoreError>;
283}
284
285/// Process-wide registry of codecs, keyed by codec id.
286pub struct CodecRegistry {
287    codecs: Vec<Box<dyn Codec>>,
288}
289
290impl CodecRegistry {
291    /// Construct an empty registry.
292    #[must_use]
293    pub fn new() -> Self {
294        Self { codecs: Vec::new() }
295    }
296
297    /// Register a codec. Id collisions are rejected at runtime — two codecs
298    /// claiming the same id is a programming error, not a recoverable
299    /// condition.
300    ///
301    /// # Panics
302    ///
303    /// Panics if a codec with the same id is already registered.
304    pub fn register(&mut self, codec: Box<dyn Codec>) {
305        let id = codec.id();
306        assert!(
307            !self.codecs.iter().any(|c| c.id() == id),
308            "codec id 0x{id:02X} already registered",
309        );
310        self.codecs.push(codec);
311    }
312
313    fn find(&self, id: u8) -> Option<&dyn Codec> {
314        self.codecs.iter().find(|c| c.id() == id).map(Box::as_ref)
315    }
316
317    fn registered_names(&self) -> String {
318        self.codecs
319            .iter()
320            .map(|c| format!("0x{:02X}={}", c.id(), c.name()))
321            .collect::<Vec<_>>()
322            .join(", ")
323    }
324
325    /// Dispatch compression to the codec identified by `id`.
326    ///
327    /// # Errors
328    ///
329    /// Returns [`CoreError::UnsupportedFeature`] if no codec with `id` is
330    /// registered.
331    pub fn compress(&self, id: u8, plaintext: &[u8]) -> Result<Vec<u8>, CoreError> {
332        match self.find(id) {
333            Some(codec) => codec_call(|| codec.compress(plaintext)),
334            None => Err(CoreError::UnsupportedFeature {
335                feature: format!(
336                    "compress codec 0x{id:02X} (registered: {registered})",
337                    registered = self.registered_names()
338                ),
339            }),
340        }
341    }
342
343    /// Dispatch decompression to the codec identified by `id`.
344    ///
345    /// # Errors
346    ///
347    /// Returns [`CoreError::UnsupportedFeature`] if no codec with `id` is
348    /// registered, or [`CoreError::Corrupt`] if decompression fails.
349    pub fn decompress(
350        &self,
351        id: u8,
352        compressed: &[u8],
353        expected_len: u32,
354    ) -> Result<Vec<u8>, CoreError> {
355        match self.find(id) {
356            Some(codec) => codec_call(|| codec.decompress(compressed, expected_len)),
357            None => Err(CoreError::UnsupportedFeature {
358                feature: format!(
359                    "decompress codec 0x{id:02X} (registered: {registered})",
360                    registered = self.registered_names()
361                ),
362            }),
363        }
364    }
365
366    /// Dispatch compression with a tunables hint. Codecs that don't
367    /// override the trait method fall through to plain `compress`.
368    ///
369    /// # Errors
370    ///
371    /// Same as [`CodecRegistry::compress`].
372    pub fn compress_with_tunables(
373        &self,
374        id: u8,
375        plaintext: &[u8],
376        tunables: &CodecTunables,
377    ) -> Result<Vec<u8>, CoreError> {
378        match self.find(id) {
379            Some(codec) => codec_call(|| codec.compress_with_tunables(plaintext, tunables)),
380            None => Err(CoreError::UnsupportedFeature {
381                feature: format!(
382                    "compress_with_tunables codec 0x{id:02X} (registered: {registered})",
383                    registered = self.registered_names()
384                ),
385            }),
386        }
387    }
388}
389
390/// Run a codec call, converting a panic into `Err(Corrupt)`.
391///
392/// A panicking codec (e.g. an omnizip encoder indexing past its
393/// internal window) must not kill the writer/reader process: the
394/// caller treats the panic as a failed candidate and moves on to
395/// the next codec or STORE. `AssertUnwindSafe` is sound here
396/// because a codec that panicked mid-flight is simply not used
397/// for that input again in the same tournament pass.
398pub(crate) fn codec_call<F>(f: F) -> Result<Vec<u8>, CoreError>
399where
400    F: FnOnce() -> Result<Vec<u8>, CoreError>,
401{
402    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
403        Ok(r) => r,
404        Err(payload) => {
405            let reason = if let Some(s) = payload.downcast_ref::<&str>() {
406                s.to_string()
407            } else if let Some(s) = payload.downcast_ref::<String>() {
408                s.clone()
409            } else {
410                "unknown panic payload".into()
411            };
412            Err(CoreError::Corrupt {
413                reason: format!("codec panicked: {reason}"),
414            })
415        }
416    }
417}
418
419impl CodecRegistry {
420    /// Human-readable name for a registered codec id.
421    pub fn codec_name(&self, codec_id: u8) -> Option<&'static str> {
422        self.codecs
423            .iter()
424            .find(|c| c.id() == codec_id)
425            .map(|c| c.name())
426    }
427}
428
429impl Default for CodecRegistry {
430    fn default() -> Self {
431        let mut registry = Self::new();
432        registry.register(Box::new(store::StoreCodec));
433        registry.register(Box::new(lz4::Lz4Codec));
434        registry.register(Box::new(lz4::Lz4HcCodec));
435        registry.register(Box::new(zstd::ZstdCodec));
436        registry.register(Box::new(xz::XzCodec));
437        registry.register(Box::new(brotli::BrotliCodec));
438        registry.register(Box::new(deflate::DeflateCodec));
439        registry.register(Box::new(libdeflate::LibdeflateCodec));
440        registry.register(Box::new(snappy::SnappyCodec));
441        // Reserved stubs — wire-format ids exist; codecs pending omnizip ports.
442        // Registered so `compress(CODEC_FLAC, ...)` surfaces a clear
443        // "codec 0x07 awaiting omnizip-flac" instead of "0x07 not
444        // registered". Categorizers can detect this and fall back
445        // gracefully.
446        registry.register(Box::new(flac::FlacCodec));
447        registry.register(Box::new(ricepp::RiceppCodec::fits_default()));
448        registry.register(Box::new(fsst_brotli::FsstBrotliCodec));
449        registry.register(Box::new(shuffle_lz4::float32()));
450        registry.register(Box::new(zpaq::ZpaqCodec));
451        registry.register(Box::new(ppmd::PpmdCodec::new()));
452        registry.register(Box::new(ppmd8::Ppmd8Codec::new()));
453        registry.register(Box::new(glza::GlzaCodec));
454        registry.register(Box::new(shuffle_zstd::shuffle_zstd()));
455        registry.register(Box::new(bitshuffle_lz4::bitshuffle_lz4()));
456        registry.register(Box::new(bzip2::Bzip2Codec::new()));
457        registry.register(Box::new(deflate64::Deflate64Codec::new()));
458        // BCJ composite codecs — filter executable code then compress.
459        // Categorizer picks the right one based on ELF/PE/Mach-O
460        // architecture (see TODO.impl/04-bcj-categorizer-routing.md).
461        registry.register(Box::new(bcj_composites::bcj_x86_lz4()));
462        registry.register(Box::new(bcj_composites::bcj_x86_zstd()));
463        registry.register(Box::new(bcj_composites::bcj_arm64_lz4()));
464        registry.register(Box::new(bcj_composites::bcj_arm64_zstd()));
465        registry
466    }
467}
468
469impl std::fmt::Debug for CodecRegistry {
470    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471        f.debug_struct("CodecRegistry")
472            .field("codecs", &self.registered_names())
473            .finish()
474    }
475}
476
477static DEFAULT_REGISTRY: OnceLock<CodecRegistry> = OnceLock::new();
478
479fn default_registry() -> &'static CodecRegistry {
480    DEFAULT_REGISTRY.get_or_init(CodecRegistry::default)
481}
482
483/// Returns the best available codec for compressible content classes
484/// (Text, Code). Brotli q5 is the current default — beats ZSTD L6
485/// (omnizip 0.7+) on real source code in our benchmarks. Try
486/// switching to `CODEC_ZSTD` if ZSTD's level differentiation
487/// improves enough to beat Brotli; the change is one line.
488#[must_use]
489pub fn best_compressible_codec() -> u8 {
490    CODEC_BROTLI
491}
492
493/// Returns the best available codec for binary content classes
494/// (structured binary — ELF, Mach-O, PE, object files, etc.).
495///
496/// LZ4 is the right choice in the current registry: ruzstd's encoder
497/// is level-1-only and produces output roughly the size of the input,
498/// so ZSTD effectively means "store with extra overhead". LZ4 gives
499/// 1.5–2× on structured binary at multiple-GB/s encode speed.
500///
501/// Will switch back to ZSTD once `omnizip-zstd` ships a real encoder
502/// (Phase C, tracked in `omnizip/omnizip-rs`).
503#[must_use]
504pub fn best_binary_codec() -> u8 {
505    CODEC_LZ4
506}
507
508/// Human-readable name for a registered codec id, e.g. for CLI
509/// inspection output.
510pub fn codec_name(codec_id: u8) -> Option<&'static str> {
511    default_registry().codec_name(codec_id)
512}
513
514/// Compress `plaintext` using the codec identified by `codec_id`, via
515/// the process-wide default [`CodecRegistry`].
516///
517/// # Errors
518///
519/// Returns [`CoreError::UnsupportedFeature`] for unknown codec ids.
520/// Returns [`CoreError::Corrupt`] if the encoder fails.
521pub fn compress(codec_id: u8, plaintext: &[u8]) -> Result<Vec<u8>, CoreError> {
522    default_registry().compress(codec_id, plaintext)
523}
524
525/// Compress with a quality/level hint. For codecs that support a
526/// quality parameter (Brotli, ZSTD), this overrides the default.
527/// For codecs without quality control (LZ4, Store, Snappy), the
528/// hint is silently ignored.
529///
530/// `quality` interpretation per codec:
531/// - Brotli (0x04): 0..=11 (higher = better ratio, slower)
532/// - ZSTD (0x02): 1..=22 (higher = better ratio, slower)
533/// - All others: ignored
534///
535/// For PPMd7 / PPMd8 / Bzip2 tunables, use
536/// [`compress_with_tunables`] with a fully-populated
537/// [`CodecTunables`].
538///
539/// # Errors
540/// Same as [`compress`].
541pub fn compress_with_options(
542    codec_id: u8,
543    plaintext: &[u8],
544    quality: u8,
545) -> Result<Vec<u8>, CoreError> {
546    let tunables = CodecTunables::from_quality(quality);
547    compress_with_tunables(codec_id, plaintext, &tunables)
548}
549
550/// Compress `plaintext` with the given codec and tunables. Codecs
551/// that don't override `compress_with_tunables` on the [`Codec`]
552/// trait fall back to plain `compress`.
553///
554/// # Errors
555///
556/// Same as [`compress`].
557pub fn compress_with_tunables(
558    codec_id: u8,
559    plaintext: &[u8],
560    tunables: &CodecTunables,
561) -> Result<Vec<u8>, CoreError> {
562    default_registry().compress_with_tunables(codec_id, plaintext, tunables)
563}
564
565/// Decompress `compressed` using the codec identified by `codec_id`, via
566/// the process-wide default [`CodecRegistry`]. The `expected_len` is the
567/// `plaintext_len` from the drop record; the decompressed output MUST
568/// match it exactly.
569///
570/// # Errors
571///
572/// Returns [`CoreError::UnsupportedFeature`] for unknown codec ids.
573/// Returns [`CoreError::Corrupt`] if decompression fails or the result
574/// length does not match `expected_len`.
575pub fn decompress(
576    codec_id: u8,
577    compressed: &[u8],
578    expected_len: u32,
579) -> Result<Vec<u8>, CoreError> {
580    default_registry().decompress(codec_id, compressed, expected_len)
581}
582
583/// Compress with LZ4, prepending the original size as a 4-byte LE
584/// header. Routes through `omnizip-lz4::Lz4FastCodec` so callers stay
585/// first-party (omnizip) for the codec implementation.
586#[must_use]
587pub fn compress_lz4_with_size(plaintext: &[u8]) -> Vec<u8> {
588    let codec = omnizip_lz4::Lz4FastCodec;
589    omnizip_codecs::Codec::compress(
590        &codec,
591        plaintext,
592        omnizip_codecs::CompressionLevel::default(),
593    )
594    .unwrap_or_else(|_| plaintext.to_vec())
595}
596
597/// Compress with Zstandard at `CompressionLevel::Fastest` (ZSTD level 1).
598/// The output is a standard ZSTD frame decodable by any conformant ZSTD
599/// decoder.
600///
601/// # Errors
602///
603/// Returns [`CoreError::Corrupt`] if the ZSTD encoder fails.
604pub fn compress_zstd(plaintext: &[u8]) -> Result<Vec<u8>, CoreError> {
605    zstd::compress(plaintext)
606}
607
608/// Compress with Brotli at quality 11 (best ratio).
609///
610/// # Errors
611///
612/// Returns [`CoreError::Corrupt`] if the Brotli encoder fails.
613pub fn compress_brotli(plaintext: &[u8]) -> Result<Vec<u8>, CoreError> {
614    brotli::compress(plaintext, brotli::DEFAULT_QUALITY)
615}
616
617/// Compress with Brotli at an explicit quality (0–11). Quality 0 is
618/// the fastest; quality 11 is the reference encoder's maximum.
619///
620/// This bypasses the codec registry's per-codec default and is the
621/// right call for callers that know they want Brotli at a specific
622/// quality — e.g. the writer's metadata-blob path, which often
623/// compresses multi-MiB blobs where the default q5 is the
624/// bottleneck.
625///
626/// # Errors
627///
628/// Returns [`CoreError::Corrupt`] if the Brotli encoder fails.
629pub fn compress_brotli_with_quality(plaintext: &[u8], quality: i32) -> Result<Vec<u8>, CoreError> {
630    let tunables = CodecTunables::from_quality(quality.clamp(0, 11) as u8);
631    default_registry().compress_with_tunables(CODEC_BROTLI, plaintext, &tunables)
632}
633
634/// Compress with DEFLATE at level 6 (default). Output is a zlib-framed
635/// DEFLATE stream (RFC 1950) decodable by any zlib decoder (`gzip -d`,
636/// `zlib.decompress`, etc.).
637///
638/// # Errors
639///
640/// Returns [`CoreError::Corrupt`] if the DEFLATE encoder fails (rare).
641pub fn compress_deflate(plaintext: &[u8]) -> Result<Vec<u8>, CoreError> {
642    deflate::compress(plaintext, deflate::DEFAULT_LEVEL)
643}
644
645#[cfg(test)]
646mod tests {
647    use super::*;
648
649    struct PanickingCodec;
650
651    impl Codec for PanickingCodec {
652        fn id(&self) -> u8 {
653            0xEE
654        }
655        fn name(&self) -> &'static str {
656            "panicking-test"
657        }
658        fn compress(&self, _plaintext: &[u8]) -> Result<Vec<u8>, CoreError> {
659            panic!("simulated encoder bug");
660        }
661        fn decompress(&self, _compressed: &[u8], _expected_len: u32) -> Result<Vec<u8>, CoreError> {
662            panic!("simulated decoder bug");
663        }
664    }
665
666    #[test]
667    fn panicking_codec_returns_err_not_unwind() {
668        let mut registry = CodecRegistry::new();
669        registry.register(Box::new(PanickingCodec));
670        let err = registry.compress(0xEE, b"data").expect_err("must be Err");
671        assert!(
672            matches!(err, CoreError::Corrupt { ref reason } if reason.contains("panicked")),
673            "got {err:?}"
674        );
675        let err = registry
676            .decompress(0xEE, b"data", 4)
677            .expect_err("must be Err");
678        assert!(
679            matches!(err, CoreError::Corrupt { ref reason } if reason.contains("panicked")),
680            "got {err:?}"
681        );
682    }
683
684    #[test]
685    fn tunables_ppmd7_bigger_budget_helps_ratio() {
686        // Synthetic but realistic: a 1 MB text fixture with mixed
687        // repetition. PPMd7 with 256 MB context budget should
688        // outperform the 8 MB default.
689        let mut input = Vec::with_capacity(1 * 1024 * 1024);
690        let paragraph = b"the quick brown fox jumps over the lazy dog. ";
691        while input.len() + paragraph.len() <= 1 * 1024 * 1024 {
692            input.extend_from_slice(paragraph);
693        }
694
695        let small = CodecTunables {
696            quality: 0,
697            zstd_quality: 0,
698            xz_level: 0,
699            ppmd_order: 4,
700            ppmd7_budget: 8 * 1024 * 1024,
701            ppmd8_budget: 0,
702            bzip2_block_kb: 0,
703            lzma_dict_mb: 0,
704        };
705        let big = CodecTunables {
706            ppmd7_budget: 256 * 1024 * 1024,
707            ..small.clone()
708        };
709
710        let small_c = compress_with_tunables(CODEC_PPMD, &input, &small).expect("ppmd7 small");
711        let big_c = compress_with_tunables(CODEC_PPMD, &input, &big).expect("ppmd7 big");
712        assert!(
713            big_c.len() <= small_c.len(),
714            "256MB budget should not be worse than 8MB ({} vs {})",
715            big_c.len(),
716            small_c.len()
717        );
718
719        // Round trip.
720        let recovered = decompress(CODEC_PPMD, &small_c, input.len() as u32).expect("d");
721        assert_eq!(recovered, input);
722    }
723
724    #[test]
725    fn tunables_brotli_quality_flows_through() {
726        // omnizip 0.14.40's from-spec encoder ignores quality (all
727        // levels dispatch to the same path). Assert both succeed and
728        // produce valid output; quality differentiation is TODO 173
729        // upstream.
730        let paragraph = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit, \
731                          sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
732        let mut input = Vec::with_capacity(10_000);
733        let mut i = 0;
734        while input.len() < 10_000 {
735            input.extend_from_slice(format!("{i:04}: {paragraph:?}\n").as_bytes());
736            i += 1;
737        }
738        let q0 = CodecTunables::from_quality(0);
739        let q11 = CodecTunables::from_quality(11);
740        let c0 = compress_with_tunables(CODEC_BROTLI, &input, &q0).expect("brotli q0");
741        let c11 = compress_with_tunables(CODEC_BROTLI, &input, &q11).expect("brotli q11");
742        // Both should produce output (may be identical until quality
743        // differentiation lands upstream).
744        assert!(!c0.is_empty() && !c11.is_empty());
745    }
746
747    #[test]
748    fn tunables_bzip2_block_size_maps_to_level() {
749        let input = b"the quick brown fox jumps over the lazy dog. ".repeat(2000);
750        let small = CodecTunables {
751            bzip2_block_kb: 100,
752            ..CodecTunables::default()
753        };
754        let big = CodecTunables {
755            bzip2_block_kb: 900,
756            ..CodecTunables::default()
757        };
758        let cs = compress_with_tunables(CODEC_BZIP2, &input, &small).expect("bzip2 100k");
759        let cb = compress_with_tunables(CODEC_BZIP2, &input, &big).expect("bzip2 900k");
760        assert!(
761            cb.len() <= cs.len(),
762            "900k ({}) <= 100k ({})",
763            cb.len(),
764            cs.len()
765        );
766    }
767
768    #[test]
769    fn store_compress_is_identity() {
770        let data = b"hello world";
771        let compressed = compress(CODEC_STORE, data).expect("store compress");
772        assert_eq!(compressed, data);
773    }
774
775    #[test]
776    fn store_decompress_validates_length() {
777        let data = b"hello world";
778        let result = decompress(CODEC_STORE, data, 11).expect("store decompress");
779        assert_eq!(result, data);
780    }
781
782    #[test]
783    fn zstd_higher_levels_compress_better_than_lower() {
784        // Regression for the omnizip 0.5→0.7 ZSTD level differentiation
785        // fix. omnizip 0.5 produced identical output for all 5 levels;
786        // 0.7 must differentiate.
787        //
788        // 0.14.8 had a regression where Default (L6) and higher produced
789        // pathological output on this input (50 KB+ and 14+ s). 0.14.10
790        // (omnizip-rs PR #90) fixes it; this test stays as a guard
791        // against future regressions. See
792        // `docs/omnizip-proposals/zstd-default-broken.md`.
793        //
794        // Since omnizip 0.21.14 the assertion guards GROSS inversions
795        // only: the reference itself inverts by a byte on tiny
796        // repetitive inputs (here L6 = 72 vs L1 = 71; upstream's own
797        // probe measured ref-L19 worse than ref-L1). Pathology looked
798        // like 50 KB; a byte is parser tuning.
799        let input: Vec<u8> = b"The quick brown fox jumps over the lazy dog. ".repeat(2000);
800        let l1 = omnizip_zstd::compress(&input, omnizip_zstd::ZstdLevel::Fastest).expect("zstd L1");
801        let l6 = omnizip_zstd::compress(&input, omnizip_zstd::ZstdLevel::Default).expect("zstd L6");
802        assert!(
803            l6.len() <= l1.len() + 64,
804            "ZSTD L6 ({}) grossly worse than L1 ({}); level differentiation broken",
805            l6.len(),
806            l1.len()
807        );
808    }
809
810    #[test]
811    fn xz_lzma_round_trips_via_lazy_parsing() {
812        // Regression for the omnizip 0.5→0.7 LZMA lazy-parsing rewrite.
813        // We don't assert LZMA beats ZSTD on synthetic-repetitive input
814        // (extreme inputs hit edge cases in the encoder), only that
815        // real-world text round-trips through the new encoder.
816        let input: Vec<u8> = b"The quick brown fox jumps over the lazy dog. \
817                               Lorem ipsum dolor sit amet. \
818                               SVG is a vector image format."
819            .repeat(500);
820        let xz = omnizip_lzma::xz_compress(&input).expect("xz encode");
821        let recovered = omnizip_lzma::xz_container::xz_decompress(&xz).expect("xz decode");
822        assert_eq!(recovered, input);
823        assert!(
824            xz.len() < input.len(),
825            "LZMA should compress real-world text; got {} vs {}",
826            xz.len(),
827            input.len()
828        );
829    }
830
831    #[test]
832    fn store_decompress_rejects_length_mismatch() {
833        let data = b"hello world";
834        match decompress(CODEC_STORE, data, 99) {
835            Err(CoreError::Corrupt { reason }) => {
836                assert!(reason.contains("does not match"), "got: {reason}");
837            }
838            other => panic!("expected Corrupt, got {other:?}"),
839        }
840    }
841
842    #[test]
843    fn lz4_round_trips() {
844        let data = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. \
845                    Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
846        let compressed = compress(CODEC_LZ4, data).expect("lz4 compress");
847        let decompressed = decompress(
848            CODEC_LZ4,
849            &compressed,
850            u32::try_from(data.len()).expect("fits u32"),
851        )
852        .expect("lz4 decompress");
853        assert_eq!(decompressed, data);
854    }
855
856    #[test]
857    fn lz4_compresses_repetitive_data() {
858        let data = vec![0x41u8; 10_000];
859        let compressed = compress(CODEC_LZ4, &data).expect("lz4 compress");
860        assert!(
861            compressed.len() < data.len(),
862            "lz4 should compress repetitive data: {} vs {}",
863            compressed.len(),
864            data.len()
865        );
866    }
867
868    #[test]
869    fn zstd_round_trips() {
870        let data = b"The quick brown fox jumps over the lazy dog. ".repeat(1000);
871        let compressed = compress_zstd(&data).expect("zstd compress");
872        let decompressed = decompress(
873            CODEC_ZSTD,
874            &compressed,
875            u32::try_from(data.len()).expect("fits u32"),
876        )
877        .expect("zstd decompress");
878        assert_eq!(decompressed, data);
879    }
880
881    #[test]
882    fn zstd_compresses_repetitive_data() {
883        let data = vec![0x41u8; 10_000];
884        let compressed = compress_zstd(&data).expect("zstd compress");
885        assert!(
886            compressed.len() < data.len(),
887            "zstd should compress repetitive data: {} vs {}",
888            compressed.len(),
889            data.len()
890        );
891    }
892
893    #[test]
894    fn zstd_compresses_better_than_lz4_on_text() {
895        let data = b"The quick brown fox. ".repeat(10_000);
896        let lz4 = compress(CODEC_LZ4, &data).expect("lz4");
897        let zstd = compress_zstd(&data).expect("zstd");
898        assert!(
899            zstd.len() < lz4.len(),
900            "zstd ({}) should be smaller than lz4 ({}) on text",
901            zstd.len(),
902            lz4.len()
903        );
904    }
905
906    #[test]
907    fn zstd_compresses_binary_data() {
908        let data: Vec<u8> = (0..100_000u32)
909            .map(|i| u8::try_from(i % 256).expect("fits u8"))
910            .collect();
911        let compressed = compress_zstd(&data).expect("zstd compress");
912        assert!(compressed.len() < data.len());
913        let decompressed = decompress(
914            CODEC_ZSTD,
915            &compressed,
916            u32::try_from(data.len()).expect("fits u32"),
917        )
918        .expect("zstd decompress");
919        assert_eq!(decompressed, data);
920    }
921
922    #[test]
923    fn xz_encode_round_trips() {
924        // omnizip-lzma's xz_compress is Phase B (literal-only) so the
925        // output is larger than the input, but it must round-trip
926        // through the LZMA2 decoder.
927        let plaintext = b"xz round-trip data";
928        let compressed = compress(CODEC_XZ, plaintext).expect("xz encode succeeds");
929        let decompressed =
930            decompress(CODEC_XZ, &compressed, plaintext.len() as u32).expect("xz decode succeeds");
931        assert_eq!(decompressed.as_slice(), plaintext);
932    }
933
934    #[test]
935    fn reject_unknown_codec() {
936        let result = compress(0xFF, b"data");
937        assert!(matches!(result, Err(CoreError::UnsupportedFeature { .. })));
938    }
939
940    #[test]
941    fn brotli_round_trips() {
942        let data = b"The quick brown fox jumps over the lazy dog. ".repeat(1000);
943        let compressed = compress_brotli(&data).expect("brotli compress");
944        let decompressed = decompress(
945            CODEC_BROTLI,
946            &compressed,
947            u32::try_from(data.len()).expect("fits u32"),
948        )
949        .expect("brotli decompress");
950        assert_eq!(decompressed, data);
951    }
952
953    #[test]
954    fn brotli_compresses_repetitive_data() {
955        let data = vec![0x41u8; 10_000];
956        let compressed = compress_brotli(&data).expect("brotli compress");
957        assert!(
958            compressed.len() < data.len(),
959            "brotli should compress repetitive data: {} vs {}",
960            compressed.len(),
961            data.len()
962        );
963    }
964
965    #[test]
966    fn brotli_and_zstd_both_compress_text() {
967        // ZSTD should compress text. Brotli's from-spec encoder may
968        // produce expansion on highly-repetitive inputs (store-mode
969        // metablocks); assert ZSTD compresses and Brotli succeeds
970        // without error. Round-trip is verified via brotli_round_trips.
971        let data = b"The quick brown fox. ".repeat(10_000);
972        let zstd = compress_zstd(&data).expect("zstd");
973        assert!(zstd.len() < data.len(), "zstd should compress text");
974        let _ = compress_brotli(&data).expect("brotli should not error");
975    }
976
977    #[test]
978    fn brotli_decompress_rejects_length_mismatch() {
979        let data = b"hello world";
980        let compressed = compress_brotli(data).expect("brotli compress");
981        match decompress(CODEC_BROTLI, &compressed, 99) {
982            Err(CoreError::Corrupt { reason }) => {
983                assert!(
984                    reason.contains("does not match") || reason.contains("mismatch"),
985                    "got: {reason}"
986                );
987            }
988            other => panic!("expected Corrupt, got {other:?}"),
989        }
990    }
991
992    #[test]
993    fn deflate_round_trips() {
994        let data = b"The quick brown fox jumps over the lazy dog. ".repeat(1000);
995        let compressed = compress_deflate(&data).expect("deflate compress");
996        let decompressed = decompress(
997            CODEC_DEFLATE,
998            &compressed,
999            u32::try_from(data.len()).expect("fits u32"),
1000        )
1001        .expect("deflate decompress");
1002        assert_eq!(decompressed, data);
1003    }
1004
1005    #[test]
1006    fn deflate_compresses_repetitive_data() {
1007        let data = vec![0x41u8; 10_000];
1008        let compressed = compress_deflate(&data).expect("deflate compress");
1009        assert!(
1010            compressed.len() < data.len(),
1011            "deflate should compress repetitive data: {} vs {}",
1012            compressed.len(),
1013            data.len()
1014        );
1015    }
1016
1017    #[test]
1018    fn deflate_decompress_rejects_length_mismatch() {
1019        let data = b"hello world";
1020        let compressed = compress_deflate(data).expect("deflate compress");
1021        match decompress(CODEC_DEFLATE, &compressed, 99) {
1022            Err(CoreError::Corrupt { reason }) => {
1023                assert!(
1024                    reason.contains("does not match") || reason.contains("mismatch"),
1025                    "got: {reason}"
1026                );
1027            }
1028            other => panic!("expected Corrupt, got {other:?}"),
1029        }
1030    }
1031
1032    #[test]
1033    fn snappy_round_trips() {
1034        let data = b"The quick brown fox jumps over the lazy dog. ".repeat(100);
1035        let compressed = compress(CODEC_SNAPPY, &data).expect("snappy compress");
1036        let decompressed = decompress(
1037            CODEC_SNAPPY,
1038            &compressed,
1039            u32::try_from(data.len()).expect("fits u32"),
1040        )
1041        .expect("snappy decompress");
1042        assert_eq!(decompressed, data);
1043    }
1044
1045    #[test]
1046    fn snappy_compresses_repetitive_data() {
1047        let data = vec![0x41u8; 10_000];
1048        let compressed = compress(CODEC_SNAPPY, &data).expect("snappy compress");
1049        assert!(
1050            compressed.len() < data.len(),
1051            "snappy should compress repetitive data: {} vs {}",
1052            compressed.len(),
1053            data.len()
1054        );
1055    }
1056
1057    #[test]
1058    fn snappy_decompress_rejects_length_mismatch() {
1059        let data = b"hello world";
1060        let compressed = compress(CODEC_SNAPPY, data).expect("snappy compress");
1061        match decompress(CODEC_SNAPPY, &compressed, 99) {
1062            Err(CoreError::Corrupt { reason }) => {
1063                assert!(
1064                    reason.contains("length mismatch") || reason.contains("does not match"),
1065                    "got: {reason}"
1066                );
1067            }
1068            other => panic!("expected Corrupt, got {other:?}"),
1069        }
1070    }
1071
1072    #[test]
1073    fn registry_registers_custom_codec_without_changing_dispatch() {
1074        struct NoopCodec;
1075        const NOOP_ID: u8 = 0xFE;
1076        impl Codec for NoopCodec {
1077            fn id(&self) -> u8 {
1078                NOOP_ID
1079            }
1080            fn name(&self) -> &'static str {
1081                "noop"
1082            }
1083            fn compress(&self, plaintext: &[u8]) -> Result<Vec<u8>, CoreError> {
1084                Ok(plaintext.to_vec())
1085            }
1086            fn decompress(
1087                &self,
1088                compressed: &[u8],
1089                expected_len: u32,
1090            ) -> Result<Vec<u8>, CoreError> {
1091                let expected = usize::try_from(expected_len).map_err(|_| CoreError::Corrupt {
1092                    reason: format!("noop: expected_len {expected_len} exceeds usize"),
1093                })?;
1094                if compressed.len() != expected {
1095                    return Err(CoreError::Corrupt {
1096                        reason: "noop: length mismatch".into(),
1097                    });
1098                }
1099                Ok(compressed.to_vec())
1100            }
1101        }
1102
1103        let mut registry = CodecRegistry::new();
1104        registry.register(Box::new(NoopCodec));
1105        assert_eq!(registry.compress(NOOP_ID, b"abc").expect("noop"), b"abc");
1106        assert_eq!(
1107            registry
1108                .decompress(NOOP_ID, b"abc", 3)
1109                .expect("noop decompress"),
1110            b"abc"
1111        );
1112    }
1113
1114    #[test]
1115    #[should_panic(expected = "codec id 0x00 already registered")]
1116    fn registry_rejects_duplicate_id() {
1117        let mut registry = CodecRegistry::new();
1118        registry.register(Box::new(store::StoreCodec));
1119        registry.register(Box::new(store::StoreCodec));
1120    }
1121
1122    #[test]
1123    fn default_registry_has_all_seven_codecs() {
1124        let registry = default_registry();
1125        assert!(registry.find(CODEC_STORE).is_some());
1126        assert!(registry.find(CODEC_LZ4).is_some());
1127        assert!(registry.find(CODEC_ZSTD).is_some());
1128        assert!(registry.find(CODEC_XZ).is_some());
1129        assert!(registry.find(CODEC_BROTLI).is_some());
1130        assert!(registry.find(CODEC_DEFLATE).is_some());
1131        assert!(registry.find(CODEC_SNAPPY).is_some());
1132        assert!(registry.find(0xFF).is_none());
1133    }
1134}
1135
1136#[cfg(test)]
1137mod per_codec_tunables_ocp_tests {
1138    //! IMPL-10 acceptance: the OCP proof. A brand-new codec — defined
1139    //! entirely in this test, with tunables this crate has never
1140    //! heard of — plugs into the registry and honors its own knobs
1141    //! through `PerCodecTunables`, with zero edits to the flat
1142    //! `CodecTunables` struct or any existing codec. If adding a
1143    //! tunable ever again requires touching shared code, this test
1144    //! is the place to catch the regression.
1145
1146    use super::*;
1147
1148    /// Hypothetical future codec: delta-encoding with a user-chosen
1149    /// stride. Its tunables type is unknown to `CodecTunables`.
1150    #[derive(Clone, Debug)]
1151    struct StrideTunables {
1152        stride: usize,
1153    }
1154
1155    struct DeltaStrideCodec;
1156
1157    impl Codec for DeltaStrideCodec {
1158        fn id(&self) -> u8 {
1159            0xFE // test-only id, never registered in default_registry
1160        }
1161        fn name(&self) -> &'static str {
1162            "delta-stride(test)"
1163        }
1164        fn compress(&self, plaintext: &[u8]) -> Result<Vec<u8>, CoreError> {
1165            // Default stride 1.
1166            Ok(self.delta(plaintext, 1))
1167        }
1168        fn decompress(&self, compressed: &[u8], _expected_len: u32) -> Result<Vec<u8>, CoreError> {
1169            let mut out = compressed.to_vec();
1170            for i in 1..out.len() {
1171                out[i] = out[i].wrapping_add(out[i - 1]);
1172            }
1173            Ok(out)
1174        }
1175    }
1176
1177    impl DeltaStrideCodec {
1178        fn delta(&self, data: &[u8], stride: usize) -> Vec<u8> {
1179            let mut out = data.to_vec();
1180            let stride = stride.max(1);
1181            for i in (stride..out.len()).rev() {
1182                out[i] = out[i].wrapping_sub(out[i - stride]);
1183            }
1184            out
1185        }
1186    }
1187
1188    impl PerCodecTunables for DeltaStrideCodec {
1189        type Tunables = StrideTunables;
1190
1191        fn compress_with_owned_tunables(
1192            &self,
1193            plaintext: &[u8],
1194            t: &Self::Tunables,
1195        ) -> Result<Vec<u8>, CoreError> {
1196            Ok(self.delta(plaintext, t.stride))
1197        }
1198    }
1199
1200    #[test]
1201    fn new_codec_tunables_require_no_edits_to_shared_struct() {
1202        let codec = DeltaStrideCodec;
1203        // 4-byte-periodic fixture: stride-4 delta collapses the
1204        // repeated pattern to zeros; stride-1 does not.
1205        let period: [u8; 4] = [0x11, 0x22, 0x33, 0x44];
1206        let payload: Vec<u8> = period.iter().cycle().copied().take(4096).collect();
1207
1208        // Stride 4 must produce different (smaller-on-this-fixture)
1209        // bytes than stride 1, proving the codec's OWN tunables flow
1210        // through `compress_with_owned_tunables`.
1211        let s1 = codec
1212            .compress_with_owned_tunables(&payload, &StrideTunables { stride: 1 })
1213            .expect("stride 1");
1214        let s4 = codec
1215            .compress_with_owned_tunables(&payload, &StrideTunables { stride: 4 })
1216            .expect("stride 4");
1217        assert_ne!(s1, s4, "different tunables must change the output");
1218        // On the 4-byte-periodic fixture, stride-4 delta collapses to
1219        // zeros — visibly different bytes from stride-1's ramp.
1220        assert!(
1221            s4.iter().filter(|&&b| b == 0).count() > s1.iter().filter(|&&b| b == 0).count(),
1222            "stride 4 zeroes the periodic pattern; stride 1 does not"
1223        );
1224
1225        // The stride-1 form round-trips through this codec's
1226        // (stride-1) decompress. s4 needs stride-aware inversion —
1227        // outside this proof's scope.
1228        let recovered = codec
1229            .decompress(&s1, payload.len() as u32)
1230            .expect("decompress");
1231        assert_eq!(recovered, payload);
1232
1233        // The OCP contract: `CodecTunables` (the flat struct) has no
1234        // stride field and needed no edit for this codec to exist.
1235        // (Compilation of this test with an unchanged struct IS the
1236        // proof; this assert documents the intent.)
1237        let flat = CodecTunables::from_quality(9);
1238        let via_default = codec.compress(&payload).expect("default path ignores flat");
1239        let _ = flat;
1240        assert_eq!(
1241            via_default, s1,
1242            "default compress == owned tunables stride 1"
1243        );
1244    }
1245}