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