Skip to main content

structured_zstd/encoding/
mod.rs

1//! Zstandard encoder — frame compression, streaming, dictionary support.
2//!
3//! Four entry points cover the common use cases:
4//!
5//! * [`compress`] — one-shot helper that builds a self-contained
6//!   Zstandard frame from a `Read` source to a `Write` sink. The
7//!   input is consumed incrementally from `Read`, so input buffering
8//!   stays bounded; however, the compressed output is buffered in
9//!   memory until the frame is complete so the Frame Content Size
10//!   field can be filled in the header — peak memory is
11//!   `O(compressed_size)` (worst-case `O(input_size)` for
12//!   incompressible payloads, plus a small frame overhead). The
13//!   savings vs [`compress_to_vec`] come from not materialising the
14//!   input alongside the output.
15//! * [`compress_to_vec`] — same one-shot path as [`compress`] but
16//!   the input is eagerly drained into an internal `Vec` first
17//!   (`read_to_end`) so the encoder can be handed a `&[u8]` and a
18//!   precise source-size hint. Peak memory is therefore ≈
19//!   `input_size + output_size`; prefer [`compress`] or
20//!   [`StreamingEncoder`] when the input is large or unbounded.
21//! * [`StreamingEncoder`] — implements [`crate::io::Write`], which
22//!   re-exports [`std::io::Write`] under the `std` feature and falls
23//!   back to a `no_std`-friendly trait otherwise. Accepts bytes
24//!   incrementally and flushes compressed output as blocks fill.
25//!   Requires `set_pledged_content_size` before the first write if
26//!   the Frame Content Size field is to be populated.
27//! * [`FrameCompressor`] — lower-level builder that owns the matcher and
28//!   the per-frame configuration; the streaming and one-shot helpers are
29//!   thin wrappers over it. Reach for it when you need to swap in a custom
30//!   [`Matcher`] implementation or share the matcher across frames.
31//!
32//! Compression intensity is selected via [`CompressionLevel`], which
33//! provides both named presets (`Fastest`, `Default`, `Better`, `Best`) and
34//! numeric levels (`from_level(n)`) that mirror C zstd's level numbering
35//! (negative for ultra-fast, `0` = default, `1..=22` for the standard
36//! range).
37//!
38//! All produced frames are valid RFC 8878 Zstandard streams and decode
39//! through both this crate's [`crate::decoding`] module and upstream C zstd.
40
41pub(crate) mod block_header;
42pub(crate) mod blocks;
43pub(crate) mod fastpath;
44pub(crate) mod frame_header;
45pub(crate) mod incompressible;
46pub(crate) mod match_generator;
47pub(crate) mod util;
48
49// `#111` encoder architecture rewrite. `cost_model`, `opt`,
50// `strategy`, `dfast`, `row`, and `simple` host the relocated
51// cost-model types, the optimal-parser plain-data types, the
52// const-generic [`strategy::Strategy`] trait + per-level [`strategy::
53// StrategyTag`] dispatcher, and the Dfast / Row / Simple matchers
54// respectively. `match_table::helpers` hosts the shared match-finder
55// primitives. The rewrite plan is tracked in
56// <https://github.com/structured-world/structured-zstd/issues/111>;
57// per-phase boundaries are `perf/post-pr-110-baseline` (start),
58// `perf/post-pr-121-baseline` (post-Phase-2).
59pub(crate) mod bt;
60pub(crate) mod cost_model;
61pub(crate) mod dfast;
62pub(crate) mod hc;
63// LDM uses `twox_hash::XxHash64` (per-window XXH64 over the
64// `min_match_length` byte slice, donor `zstd_ldm.c:315`). The
65// `twox-hash` dependency is gated behind the `hash` feature so
66// `default-features = false` builds (no_std, embedded) don't pull
67// it in. `BtMatcher::ldm_producer` and the `cfg(feature = "hash")`
68// blocks inside `BtMatcher::prepare_ldm_candidates` /
69// `BtMatcher::reset` carry the same gate; the call site in
70// `match_generator.rs::start_matching_optimal` invokes
71// `prepare_ldm_candidates` unconditionally because the
72// gating is internal to the method body (under
73// `not(feature = "hash")` the method shrinks to the legacy
74// `ldm_sequences.clear()` stub).
75#[cfg(feature = "hash")]
76pub(crate) mod ldm;
77pub(crate) mod match_table;
78pub(crate) mod opt;
79pub(crate) mod row;
80pub(crate) mod simple;
81pub(crate) mod strategy;
82
83mod frame_compressor;
84mod levels;
85mod streaming_encoder;
86pub use frame_compressor::FrameCompressor;
87pub use match_generator::MatchGeneratorDriver;
88pub use streaming_encoder::StreamingEncoder;
89
90use crate::io::{Read, Write};
91use alloc::vec::Vec;
92
93pub(crate) const BETTER_WINDOW_LOG: u8 = 23;
94
95/// Worst-case compressed-size bound for `src_size` uncompressed
96/// bytes. Mirrors the upstream `ZSTD_COMPRESSBOUND(srcSize)` macro
97/// from `lib/zstd.h` (zstd 1.5.7) — the C *macro*, NOT the public
98/// `ZSTD_compressBound()` function. The function adds frame/block-
99/// header headroom in some code paths; the macro is a tighter
100/// expression suitable for sizing an internal output Vec, which is
101/// the only call site here. Callers that need a hard upper bound on
102/// the wire-format compressed size should NOT reuse this — use the
103/// `zstd-sys` function call directly. If the macro changes in a
104/// future upstream revision, treat the upstream header as
105/// authoritative and re-derive this function — do not trust the
106/// inline copy without cross-checking.
107///
108/// Inline approximation (this revision):
109///
110/// ```text
111/// srcSize
112///   + (srcSize >> 8)
113///   + (srcSize < 128 KiB ? ((128 KiB - srcSize) >> 11) : 0)
114/// ```
115///
116/// Consulted by [`compress_slice_to_vec`] for the small-input branch
117/// of its output-`Vec` seed: the seed is
118/// `min(compress_bound(src), OUTPUT_BLOCK_CAP = 128 KiB)`, so for
119/// inputs that fit within one donor block (`≤ ~128 KiB`) the
120/// destination never reallocates inside the measured window. Without
121/// the bound, `Vec::push` growth doubles in powers of two and pins
122/// `~2 × final_compressed_size` resident at the last realloc, which
123/// shows up as ~1 MiB of peak-RSS noise on 1 MiB inputs and prevents
124/// FFI-parity on the memory bench. For inputs larger than the cap the
125/// `Vec` still grows by amortized doubling — see
126/// [`compress_slice_to_vec`] for the trade-off rationale.
127#[inline]
128pub(crate) const fn compress_bound(src_size: usize) -> usize {
129    const SMALL_INPUT_THRESHOLD: usize = 128 * 1024;
130    let tail = if src_size < SMALL_INPUT_THRESHOLD {
131        (SMALL_INPUT_THRESHOLD - src_size) >> 11
132    } else {
133        0
134    };
135    // `saturating_add` guards against the corner case where `src_size`
136    // is close to `usize::MAX`. The current sole caller
137    // (`compress_slice_to_vec`) clamps the result with `.min(OUTPUT_BLOCK_CAP)`
138    // so an overflow there would still produce a sensible cap, but
139    // future `pub(crate)` callers may use the raw bound — keep the
140    // arithmetic itself overflow-safe.
141    src_size.saturating_add(src_size >> 8).saturating_add(tail)
142}
143
144/// Convenience function to compress some source into a target without reusing any resources of the compressor
145/// ```rust
146/// use structured_zstd::encoding::{compress, CompressionLevel};
147/// let data: &[u8] = &[0,0,0,0,0,0,0,0,0,0,0,0];
148/// let mut target = Vec::new();
149/// compress(data, &mut target, CompressionLevel::Fastest);
150/// ```
151pub fn compress<R: Read, W: Write>(source: R, target: W, level: CompressionLevel) {
152    let mut frame_enc = FrameCompressor::new(level);
153    frame_enc.set_source(source);
154    frame_enc.set_drain(target);
155    frame_enc.compress();
156}
157
158/// Convenience function to compress some source into a Vec without reusing any resources of the compressor.
159///
160/// This helper eagerly buffers the full input (`Read`) before compression so it
161/// can provide a source-size hint to the one-shot encoder path. Peak memory can
162/// therefore be roughly `input_size + output_size`. For very large payloads or
163/// tighter memory budgets, prefer streaming APIs such as [`StreamingEncoder`].
164///
165/// **Peak-memory shape change in this revision.** The implementation
166/// delegates to [`compress_slice_to_vec`], which seeds the output
167/// `Vec` with `min(compress_bound(input.len()), OUTPUT_BLOCK_CAP =
168/// 128 KiB)` instead of the previous `Vec::new()` (zero-capacity +
169/// power-of-two growth). For inputs in the few-KiB to ~128 KiB range
170/// this is a strict improvement (no doubling spikes inside the
171/// measured window). For inputs significantly larger than 128 KiB the
172/// allocation curve still grows by amortized doubling but starts from
173/// a 128 KiB floor rather than 0. Downstream consumers that measure
174/// peak RSS on this entry point will see a different curve than
175/// pre-revision; bench shape, not steady-state, is what changed.
176///
177/// **This is NOT a streaming API.** The source is fully buffered
178/// into a `Vec<u8>` before any compression work begins, so peak input
179/// memory is bounded by `source.len()` (not "constant regardless of
180/// payload size" as a stream-shaped encoder would offer). The RSS
181/// notes below apply to the materialization-then-compress shape; if
182/// the source is large enough that holding it in memory is not
183/// acceptable, use [`StreamingEncoder`] which consumes chunks
184/// incrementally without the up-front Vec build.
185///
186/// The other side of the peak shape is the input buffering: this
187/// helper drives `read_to_end` to materialize the full source into a
188/// `Vec<u8>` before forwarding the slice to [`compress_slice_to_vec`].
189/// For a `Read` whose size is unknown ahead of time, `read_to_end`
190/// grows that input `Vec` via power-of-two doubling — peak input
191/// allocation can be up to 2× the final source length transiently.
192/// At the moment that input buffer crosses ~128 KiB the output Vec
193/// seed kicks in concurrently. The total live working set on this
194/// entry point is approximately
195/// `input.capacity() + output_vec_seed + internal_accumulators`,
196/// where `output_vec_seed` is `min(compress_bound(input.len()), 128
197/// KiB)` and `internal_accumulators` covers
198/// `FrameCompressor::all_blocks` (pre-reserved at frame start, up to
199/// ~130 KiB at default block cap) plus per-block scratch (hash tables,
200/// literal/sequence staging). Round the helper's RSS peak to
201/// `input.capacity() + output_vec_seed + ~130 KiB internal +
202/// per-block scratch` rather than the bare `input.capacity() + 128
203/// KiB` figure quoted in earlier revisions, which only accounted for
204/// the output seed. [`StreamingEncoder`] avoids the input
205/// materialization step entirely and is the right entry point when
206/// the source is large or unbounded.
207///
208/// ```rust
209/// use structured_zstd::encoding::{compress_to_vec, CompressionLevel};
210/// let data: &[u8] = &[0,0,0,0,0,0,0,0,0,0,0,0];
211/// let compressed = compress_to_vec(data, CompressionLevel::Fastest);
212/// ```
213pub fn compress_to_vec<R: Read>(source: R, level: CompressionLevel) -> Vec<u8> {
214    let mut source = source;
215    let mut input = Vec::new();
216    source.read_to_end(&mut input).unwrap();
217    compress_slice_to_vec(input.as_slice(), level)
218}
219
220/// Compress a contiguous byte slice into a fresh `Vec<u8>` without
221/// the input-buffering step that [`compress_to_vec`] performs to
222/// adapt a `Read` source. Donor-parity peak-memory shape: the input
223/// is read by reference, and the output `Vec` is seeded with
224/// `min(compress_bound(src), OUTPUT_BLOCK_CAP = 128 KiB)`.
225///
226/// The seed is intentionally capped at one donor block
227/// (`OUTPUT_BLOCK_CAP = 128 KiB`). Three regimes:
228///
229/// * Inputs up to ~127 KiB where `compress_bound(src) <
230///   OUTPUT_BLOCK_CAP` — the `min` picks the tighter bound and the
231///   `Vec` never reallocates inside the measured window (cheap, no
232///   doubling spikes, no over-allocation).
233/// * Inputs around 128 KiB where `compress_bound(src) >=
234///   OUTPUT_BLOCK_CAP` — the `min` clamps to the cap, so the "no
235///   reallocation" property holds only as long as the actual
236///   compressed output also fits within `OUTPUT_BLOCK_CAP`. For
237///   high-entropy inputs near the cap boundary the `Vec` may grow
238///   by one doubling step before the next block lands.
239/// * Larger inputs — the `Vec` grows via amortized doubling, with
240///   peak transient memory ≈ 2× current compressed size at the last
241///   realloc. Deliberate trade-off against pinning
242///   `compress_bound(src)` upfront, which on the 100 MiB
243///   `large-log-stream` scenario pinned 100.4 MiB even though the
244///   actual compressed output was ≪ 1 MiB.
245///
246/// # Panics
247///
248/// Panics on encoder error (matches the failure surface of
249/// [`compress_to_vec`], which this function backs). The internal
250/// [`FrameCompressor::compress`] call propagates `io::Error` from the
251/// output [`Vec`] writer; under the in-tree default writer that
252/// channel is infallible and any error becomes a `panic`. Out-of-
253/// memory during `Vec::with_capacity` or the encoder's per-block
254/// scratch allocations is handled by the global allocator's abort
255/// policy. Migrating to a fallible variant requires plumbing
256/// `Result` through `FrameCompressor::compress` — out of scope for
257/// the slice/Vec entry points which mirror the donor `ZSTD_compress`
258/// shape (no error return on the bulk path).
259///
260/// ```rust
261/// use structured_zstd::encoding::{compress_slice_to_vec, CompressionLevel};
262/// let data: &[u8] = &[0,0,0,0,0,0,0,0,0,0,0,0];
263/// let compressed = compress_slice_to_vec(data, CompressionLevel::Fastest);
264/// ```
265pub fn compress_slice_to_vec(source: &[u8], level: CompressionLevel) -> Vec<u8> {
266    // Initial capacity sized to a single output block, matching donor
267    // `ZSTD_CStreamOutSize()` (≈ `ZSTD_BLOCKSIZE_MAX` + headroom). For
268    // small inputs `compress_bound(src)` is smaller than this block —
269    // use the tighter bound to avoid over-allocation. For larger inputs
270    // the `Vec` grows via amortized doubling, with peak ≈ 2×current
271    // compressed size at the last realloc (donor streaming shape: the
272    // caller buffer is one block, not `ZSTD_compressBound(srcSize)`).
273    // Pre-allocating `compress_bound(src)` here would charge worst-case
274    // expansion against peak even on highly compressible inputs — on
275    // the 100 MiB `large-log-stream` scenario that single allocation
276    // dominated the encoder's measured footprint (`compress_bound` ≈
277    // 100.4 MiB pinned, actual compressed output ≪ 1 MiB).
278    const OUTPUT_BLOCK_CAP: usize = 128 * 1024;
279    let cap = compress_bound(source.len()).min(OUTPUT_BLOCK_CAP);
280    let mut vec = Vec::with_capacity(cap);
281    let mut frame_enc = FrameCompressor::new(level);
282    frame_enc.set_source_size_hint(source.len() as u64);
283    frame_enc.set_source(source);
284    frame_enc.set_drain(&mut vec);
285    frame_enc.compress();
286    vec
287}
288
289/// The compression mode used impacts the speed of compression,
290/// and resulting compression ratios. Faster compression will result
291/// in worse compression ratios, and vice versa.
292#[derive(Copy, Clone, Debug)]
293pub enum CompressionLevel {
294    /// This level does not compress the data at all, and simply wraps
295    /// it in a Zstandard frame.
296    Uncompressed,
297    /// This level is roughly equivalent to Zstd compression level 1
298    Fastest,
299    /// This level uses the crate's dedicated `dfast`-style matcher to
300    /// target a better speed/ratio tradeoff than [`CompressionLevel::Fastest`].
301    ///
302    /// It represents this crate's "default" compression setting and may
303    /// evolve in future versions as the implementation moves closer to
304    /// reference zstd level 3 behavior.
305    Default,
306    /// This level is roughly equivalent to Zstd level 7.
307    ///
308    /// Uses the hash-chain matcher with a lazy2 matching strategy: the encoder
309    /// evaluates up to two positions ahead before committing to a match,
310    /// trading speed for a better compression ratio than [`CompressionLevel::Default`].
311    Better,
312    /// This level is roughly equivalent to Zstd level 11.
313    ///
314    /// Uses the hash-chain matcher with a deep lazy2 matching strategy and
315    /// a 16 MiB window. Compared to [`CompressionLevel::Better`], this level
316    /// uses larger hash and chain tables (2 M / 1 M entries vs 1 M / 512 K),
317    /// a deeper search (32 candidates vs 16), and a higher target match
318    /// length (128 vs 48), trading speed for the best compression ratio
319    /// available in this crate.
320    Best,
321    /// Numeric compression level.
322    ///
323    /// Levels 1–22 correspond to the C zstd level numbering.  Higher values
324    /// produce smaller output at the cost of more CPU time.  Negative values
325    /// select ultra-fast modes that trade ratio for speed.  Level 0 is
326    /// treated as [`DEFAULT_LEVEL`](Self::DEFAULT_LEVEL), matching C zstd
327    /// semantics.
328    ///
329    /// Named variants map to specific numeric levels:
330    /// [`Fastest`](Self::Fastest) = 1, [`Default`](Self::Default) = 3,
331    /// [`Better`](Self::Better) = 7, [`Best`](Self::Best) = 11.
332    /// [`Best`](Self::Best) remains the highest-ratio named preset, but
333    /// [`Level`](Self::Level) values above 11 can target stronger (slower)
334    /// tuning than the named hierarchy.
335    ///
336    /// Levels above 11 use progressively larger windows and deeper search.
337    /// Levels 16–17 use a `btopt`-style price parser, 18–19 use `btultra`,
338    /// and 20–22 use a `btultra2`-style two-pass selection profile.
339    ///
340    /// Semver note: this variant was added after the initial enum shape and
341    /// is a breaking API change for downstream crates that exhaustively
342    /// `match` on [`CompressionLevel`] without a wildcard arm.
343    Level(i32),
344}
345
346impl CompressionLevel {
347    /// The minimum supported numeric compression level (ultra-fast mode).
348    pub const MIN_LEVEL: i32 = -131072;
349    /// The maximum supported numeric compression level.
350    pub const MAX_LEVEL: i32 = 22;
351    /// The default numeric compression level (equivalent to [`Default`](Self::Default)).
352    pub const DEFAULT_LEVEL: i32 = 3;
353
354    /// Create a compression level from a numeric value.
355    ///
356    /// Returns named variants for canonical levels (`0`/`3`, `1`, `7`, `11`)
357    /// and [`Level`](Self::Level) for all other values.
358    ///
359    /// With the default matcher backend (`MatchGeneratorDriver`), values
360    /// outside [`MIN_LEVEL`](Self::MIN_LEVEL)..=[`MAX_LEVEL`](Self::MAX_LEVEL)
361    /// are silently clamped during built-in level parameter resolution.
362    pub const fn from_level(level: i32) -> Self {
363        match level {
364            0 | Self::DEFAULT_LEVEL => Self::Default,
365            1 => Self::Fastest,
366            7 => Self::Better,
367            11 => Self::Best,
368            _ => Self::Level(level),
369        }
370    }
371}
372
373/// Trait used by the encoder that users can use to extend the matching facilities with their own algorithm
374/// making their own tradeoffs between runtime, memory usage and compression ratio
375///
376/// This trait operates on buffers that represent the chunks of data the matching algorithm wants to work on.
377/// Each one of these buffers is referred to as a *space*. One or more of these buffers represent the window
378/// the decoder will need to decode the data again.
379///
380/// This library asks the Matcher for a new buffer using `get_next_space` to allow reusing of allocated buffers when they are no longer part of the
381/// window of data that is being used for matching.
382///
383/// The library fills the buffer with data that is to be compressed and commits them back to the matcher using `commit_space`.
384///
385/// Then it will either call `start_matching` or, if the space is deemed not worth compressing, `skip_matching` is called.
386///
387/// This is repeated until no more data is left to be compressed.
388pub trait Matcher {
389    /// Get a space where we can put data to be matched on. Will be encoded as one block. The maximum allowed size is 128 kB.
390    fn get_next_space(&mut self) -> alloc::vec::Vec<u8>;
391    /// Get a reference to the last committed space
392    fn get_last_space(&mut self) -> &[u8];
393    /// Commit a space to the matcher so it can be matched against
394    fn commit_space(&mut self, space: alloc::vec::Vec<u8>);
395    /// Just process the data in the last committed space for future matching.
396    fn skip_matching(&mut self);
397    /// Hint-aware skip path used internally to thread a precomputed block
398    /// incompressibility verdict to matcher backends.
399    ///
400    /// Default implementation preserves backwards compatibility for external
401    /// custom matchers by delegating to [`skip_matching`](Self::skip_matching).
402    fn skip_matching_with_hint(&mut self, _incompressible_hint: Option<bool>) {
403        self.skip_matching();
404    }
405    /// Process the data in the last committed space for future matching AND generate matches for the data
406    fn start_matching(&mut self, handle_sequence: impl for<'a> FnMut(Sequence<'a>));
407    /// Reset this matcher so it can be used for the next new frame
408    fn reset(&mut self, level: CompressionLevel);
409    /// Provide a hint about the total uncompressed size for the next frame.
410    ///
411    /// Implementations may use this to select smaller hash tables and windows
412    /// for small inputs, matching the C zstd source-size-class behavior.
413    /// Called before [`reset`](Self::reset) when the caller knows the input
414    /// size (e.g. from pledged content size or file metadata).
415    ///
416    /// The default implementation is a no-op for custom matchers and
417    /// test stubs. The built-in runtime matcher (`MatchGeneratorDriver`)
418    /// overrides this hook and applies the hint during level resolution.
419    fn set_source_size_hint(&mut self, _size: u64) {}
420    /// Prime matcher state with dictionary history before compressing the next frame.
421    /// Default implementation is a no-op for custom matchers that do not support this.
422    fn prime_with_dictionary(&mut self, _dict_content: &[u8], _offset_hist: [u32; 3]) {}
423    /// Seed matcher cost model with dictionary entropy tables before the next frame.
424    /// Default implementation is a no-op for custom matchers.
425    fn seed_dictionary_entropy(
426        &mut self,
427        _huff: Option<&crate::huff0::huff0_encoder::HuffmanTable>,
428        _ll: Option<&crate::fse::fse_encoder::FSETable>,
429        _ml: Option<&crate::fse::fse_encoder::FSETable>,
430        _of: Option<&crate::fse::fse_encoder::FSETable>,
431    ) {
432    }
433    /// Returns whether this matcher can consume dictionary priming state and produce
434    /// dictionary-dependent sequences. Defaults to `false` for custom matchers.
435    fn supports_dictionary_priming(&self) -> bool {
436        false
437    }
438    /// The size of the window the decoder will need to execute all sequences produced by this matcher.
439    ///
440    /// Must return a positive (non-zero) value; returning 0 causes
441    /// [`StreamingEncoder`] to reject the first write with an invalid-input error
442    /// (`InvalidInput` with `std`, `Other` with `no_std`).
443    ///
444    /// Must remain stable for the lifetime of a frame.
445    /// It may change only after `reset()` is called for the next frame
446    /// (for example because the compression level changed).
447    fn window_size(&self) -> u64;
448}
449
450#[derive(PartialEq, Eq, Debug)]
451/// Sequences that a [`Matcher`] can produce
452pub enum Sequence<'data> {
453    /// Is encoded as a sequence for the decoder sequence execution.
454    ///
455    /// First the literals will be copied to the decoded data,
456    /// then `match_len` bytes are copied from `offset` bytes back in the decoded data
457    Triple {
458        literals: &'data [u8],
459        offset: usize,
460        match_len: usize,
461    },
462    /// This is returned as the last sequence in a block
463    ///
464    /// These literals will just be copied at the end of the sequence execution by the decoder
465    Literals { literals: &'data [u8] },
466}