structured_zstd/encoding/frame_compressor.rs
1//! Utilities and interfaces for encoding an entire frame. Allows reusing resources
2
3use alloc::vec::Vec;
4#[cfg(feature = "hash")]
5use twox_hash::XxHash64;
6
7#[cfg(feature = "hash")]
8use core::hash::Hasher;
9
10use super::{
11 CompressionLevel, LiteralCompressionMode, Matcher, block_header::BlockHeader,
12 frame_header::FrameHeader, levels::*, match_generator::MatchGeneratorDriver,
13 workspace::IngestBuffer,
14};
15use crate::common::MAX_BLOCK_SIZE;
16use crate::fse::fse_encoder::{FSETable, default_ll_table, default_ml_table, default_of_table};
17
18use crate::io::{Read, Write};
19
20/// A dictionary prepared for the ENCODER side, analogous to zstd's `CDict`
21/// (vs the decoder's [`Dictionary`](crate::decoding::Dictionary) / `DDict`).
22///
23/// It carries the entropy tables, content, and repeat-offset history the
24/// compressor needs, but is a distinct type with **no decode path**: there is
25/// no way to turn it into a [`DictionaryHandle`](crate::decoding::DictionaryHandle)
26/// or feed it to a [`FrameDecoder`](crate::decoding::FrameDecoder). That keeps
27/// the compress-only state (which may have been parsed without building the
28/// decode lookup tables, see
29/// [`set_dictionary_from_bytes`](FrameCompressor::set_dictionary_from_bytes))
30/// from ever reaching the decode side — the encoder/decoder dictionary split
31/// mirrors C zstd's `CDict` / `DDict`.
32/// Cloning one is a handle, not a copy: it is attached to a compressor by
33/// value, so a dictionary serving many frames would otherwise have its parsed
34/// tables and content duplicated for each of them — on exactly the path where
35/// one dictionary is prepared once precisely to be used again and again.
36///
37/// The encoder entropy tables a dictionary seeds into each frame's first block
38/// (upstream zstd `cdict->cBlockState`) are built once, here, and shared by
39/// every compressor the dictionary is attached to.
40#[derive(Clone)]
41pub struct EncoderDictionary {
42 pub(crate) inner: SharedEncoderDictionary,
43 /// Size of the serialized dictionary this was built from (header, entropy
44 /// tables, repeat offsets and content); the CDict cParams tier key
45 /// (upstream `ZSTD_createCDict(dictBuffer, dictSize, level)`). Falls back
46 /// to the content length when the wrapped
47 /// [`Dictionary`](crate::decoding::Dictionary) was handed over
48 /// already parsed ([`Self::from_dictionary`]) — exact for raw-content
49 /// dictionaries, a close lower bound otherwise.
50 serialized_len: usize,
51}
52
53/// What an [`EncoderDictionary`] shares between its clones: the parsed
54/// dictionary and the encoder entropy tables derived from it, in one
55/// allocation so a clone is one reference count, not two. Reads through to the
56/// [`Dictionary`](crate::decoding::Dictionary) for its content, offsets and id.
57pub(crate) struct EncoderDictionaryParts {
58 dictionary: crate::decoding::Dictionary,
59 pub(crate) entropy: CachedDictionaryEntropy,
60}
61
62impl core::ops::Deref for EncoderDictionaryParts {
63 type Target = crate::decoding::Dictionary;
64
65 fn deref(&self) -> &Self::Target {
66 &self.dictionary
67 }
68}
69
70/// Shared owner of [`EncoderDictionaryParts`]: `Arc` on atomic-pointer
71/// targets, `Rc` otherwise, as [`SharedFseTable`] is.
72#[cfg(target_has_atomic = "ptr")]
73pub(crate) type SharedEncoderDictionary = alloc::sync::Arc<EncoderDictionaryParts>;
74#[cfg(not(target_has_atomic = "ptr"))]
75pub(crate) type SharedEncoderDictionary = alloc::rc::Rc<EncoderDictionaryParts>;
76
77impl EncoderDictionary {
78 /// Share `dictionary` with the entropy tables it seeds, built once here.
79 fn prepared(dictionary: crate::decoding::Dictionary, serialized_len: usize) -> Self {
80 let entropy = CachedDictionaryEntropy::from_dictionary(&dictionary);
81 Self {
82 inner: SharedEncoderDictionary::new(EncoderDictionaryParts {
83 dictionary,
84 entropy,
85 }),
86 serialized_len,
87 }
88 }
89
90 /// Wrap an already-parsed [`Dictionary`](crate::decoding::Dictionary) for
91 /// encoder use. A fully-decoded dictionary is valid here; only the encoder
92 /// entropy tables, content, and offset history are read. The CDict cParams
93 /// tier is keyed by the content length here; prefer [`Self::from_bytes`]
94 /// when the serialized blob is at hand — it keys the tier by the exact
95 /// serialized size as upstream `ZSTD_createCDict` does.
96 pub fn from_dictionary(dictionary: crate::decoding::Dictionary) -> Self {
97 let serialized_len = dictionary.dict_content.len();
98 Self::prepared(dictionary, serialized_len)
99 }
100
101 /// Parse a serialized dictionary blob for encoder use, skipping the decode
102 /// lookup-table build the encoder never reads (see
103 /// `Dictionary::decode_dict_for_encoding`). The encoder entropy tables — and
104 /// thus the emitted frame — are identical to a full parse.
105 pub fn from_bytes(
106 raw_dictionary: &[u8],
107 ) -> Result<Self, crate::decoding::errors::DictionaryDecodeError> {
108 Ok(Self::prepared(
109 crate::decoding::Dictionary::decode_dict_for_encoding(raw_dictionary)?,
110 raw_dictionary.len(),
111 ))
112 }
113
114 /// Load whichever kind of dictionary `raw_dictionary` holds, the way
115 /// `zstd -D` does: a serialized blob is parsed, anything else is taken as
116 /// raw content (see
117 /// [`Dictionary::from_serialized_or_raw_content`](crate::decoding::Dictionary::from_serialized_or_raw_content)).
118 ///
119 /// Either way the blob's own length is what the compression-parameter tier
120 /// is chosen by, which is why this exists rather than parsing and calling
121 /// [`Self::from_dictionary`]: that keys the tier on the content length, and
122 /// for a serialized dictionary the entropy tables in between can put the
123 /// two on opposite sides of a boundary.
124 pub fn from_serialized_or_raw_content(
125 raw_dictionary: &[u8],
126 ) -> Result<Self, crate::decoding::errors::DictionaryDecodeError> {
127 // Parsed for the encoder, which reads the entropy probabilities, the
128 // content and the offsets and never the decode lookup tables: routing a
129 // serialized blob through the full parser builds those tables for
130 // nothing. The emitted frame is identical either way — only the wasted
131 // build is dropped (see `Dictionary::decode_dict_for_encoding`).
132 if crate::decoding::Dictionary::is_serialized(raw_dictionary) {
133 return Self::from_bytes(raw_dictionary);
134 }
135 Ok(Self::prepared(
136 crate::decoding::Dictionary::from_raw_content(0, raw_dictionary.to_vec())?,
137 raw_dictionary.len(),
138 ))
139 }
140
141 /// Heap bytes the prepared dictionary holds: the shared allocation itself,
142 /// the parsed dictionary's content and tables, and the entropy tables it
143 /// seeds. Clones share all of it, so this counts once however many
144 /// compressors the dictionary is attached to.
145 ///
146 /// # Examples
147 /// ```
148 /// use structured_zstd::encoding::EncoderDictionary;
149 ///
150 /// let dictionary =
151 /// EncoderDictionary::from_serialized_or_raw_content(b"raw content, no header").unwrap();
152 /// assert!(dictionary.heap_size() >= b"raw content, no header".len());
153 /// ```
154 pub fn heap_size(&self) -> usize {
155 Self::shared_allocation_size()
156 + self.inner.dictionary.heap_bytes()
157 + self.inner.entropy.heap_size()
158 }
159
160 /// Upper bound on [`Self::heap_size`] for a dictionary prepared from a
161 /// `serialized_len`-byte blob, before preparing it: for budgeting memory
162 /// ahead of time, as the C `ZSTD_estimateCDictSize` does.
163 ///
164 /// # Examples
165 /// ```
166 /// use structured_zstd::encoding::EncoderDictionary;
167 ///
168 /// let blob = b"raw content, no header";
169 /// let dictionary = EncoderDictionary::from_serialized_or_raw_content(blob).unwrap();
170 /// assert!(dictionary.heap_size() <= EncoderDictionary::estimated_heap_size(blob.len()));
171 /// ```
172 pub fn estimated_heap_size(serialized_len: usize) -> usize {
173 // The content is at most the blob. The tables are bounded by their
174 // largest form: every FSE table and the Huffman table at their maximum
175 // accuracy, as a dictionary's entropy section can declare them.
176 const PREPARED_TABLES_HEAP: usize = 64 * 1024;
177 // Saturating on purpose: `usize::MAX` still bounds what no allocation
178 // can exceed, where a wrapped sum would under-report.
179 Self::shared_allocation_size()
180 .saturating_add(serialized_len)
181 .saturating_add(PREPARED_TABLES_HEAP)
182 }
183
184 /// Bytes of the shared allocation itself: `Arc` and `Rc` both place two
185 /// word-sized counts ahead of the value, padded to its alignment.
186 fn shared_allocation_size() -> usize {
187 core::alloc::Layout::new::<[usize; 2]>()
188 .extend(core::alloc::Layout::new::<EncoderDictionaryParts>())
189 .expect("a fixed-size layout extends")
190 .0
191 .pad_to_align()
192 .size()
193 }
194
195 /// Heap bytes that `handles` alone keep alive: each distinct dictionary
196 /// among them once, and only when no handle outside them shares it.
197 ///
198 /// For an owner holding several handles, such as a context whose
199 /// compressors each keep the dictionary they last ran with: what it reports
200 /// should count one dictionary once, and none that someone else also holds.
201 ///
202 /// # Examples
203 /// ```
204 /// use structured_zstd::encoding::EncoderDictionary;
205 ///
206 /// let dictionary = EncoderDictionary::from_serialized_or_raw_content(b"some shared history").unwrap();
207 /// let copy = dictionary.clone();
208 /// // Both handles are in the set: counted once.
209 /// assert_eq!(
210 /// EncoderDictionary::exclusive_heap_size([&dictionary, ©]),
211 /// dictionary.heap_size()
212 /// );
213 /// // `copy` is held elsewhere: nothing is this set's alone.
214 /// assert_eq!(EncoderDictionary::exclusive_heap_size([&dictionary]), 0);
215 /// ```
216 pub fn exclusive_heap_size<'a, I>(handles: I) -> usize
217 where
218 I: IntoIterator<Item = &'a EncoderDictionary>,
219 I::IntoIter: Clone,
220 {
221 let handles = handles.into_iter();
222 let mut total = 0;
223 for (position, handle) in handles.clone().enumerate() {
224 let same = |other: &&EncoderDictionary| {
225 SharedEncoderDictionary::ptr_eq(&handle.inner, &other.inner)
226 };
227 // The first handle to an allocation speaks for all of them.
228 if handles.clone().take(position).any(|other| same(&other)) {
229 continue;
230 }
231 // Holders, not references: one handle listed twice is one count.
232 let held_here = handles
233 .clone()
234 .enumerate()
235 .filter(|(at, other)| {
236 same(other)
237 && !handles
238 .clone()
239 .take(*at)
240 .any(|earlier| core::ptr::eq(earlier, *other))
241 })
242 .count();
243 if SharedEncoderDictionary::strong_count(&handle.inner) == held_here {
244 total += handle.heap_size();
245 }
246 }
247 total
248 }
249
250 /// The content and serialized sizes the encoder's matcher is hinted with.
251 pub(crate) fn sizes(&self) -> crate::encoding::DictionarySizes {
252 crate::encoding::DictionarySizes {
253 content: self.inner.dict_content.len(),
254 serialized: self.serialized_len,
255 }
256 }
257
258 /// Whether frames compress against this dictionary: one prepared from a
259 /// blob under 8 bytes takes no part, as zstd.h states for every dictionary
260 /// entry point and upstream zstd applies when it inserts one
261 /// (`zstd_compress.c:5206`). Attaching such a dictionary attaches none. A
262 /// decoder still reads that blob as content, as upstream's does.
263 ///
264 /// ```
265 /// use structured_zstd::encoding::EncoderDictionary;
266 ///
267 /// let tiny = EncoderDictionary::from_serialized_or_raw_content(b"tiny").unwrap();
268 /// assert!(!tiny.is_used_for_compression());
269 /// let history = EncoderDictionary::from_serialized_or_raw_content(b"some history").unwrap();
270 /// assert!(history.is_used_for_compression());
271 /// ```
272 pub fn is_used_for_compression(&self) -> bool {
273 self.serialized_len >= 8
274 }
275
276 /// Bytes of dictionary content, the part a frame can reference.
277 ///
278 /// This is the extent a caller weighs a source against when deciding
279 /// whether a frame is still about the dictionary or about its own content.
280 ///
281 /// # Examples
282 ///
283 /// ```
284 /// use structured_zstd::encoding::EncoderDictionary;
285 ///
286 /// let dictionary = EncoderDictionary::from_serialized_or_raw_content(&[7u8; 4096])
287 /// .expect("raw content is always usable");
288 /// assert_eq!(dictionary.content_size(), 4096);
289 /// ```
290 pub fn content_size(&self) -> usize {
291 self.inner.dict_content.len()
292 }
293
294 /// The dictionary content a frame's matches can reach back into.
295 ///
296 /// ```
297 /// use structured_zstd::encoding::EncoderDictionary;
298 ///
299 /// let dictionary = EncoderDictionary::from_serialized_or_raw_content(b"history").unwrap();
300 /// assert_eq!(dictionary.content(), b"history");
301 /// ```
302 pub fn content(&self) -> &[u8] {
303 &self.inner.dict_content
304 }
305
306 /// The dictionary id.
307 ///
308 /// Zero is a raw-content dictionary, which has no header to carry an id.
309 /// Such a dictionary attaches like any other; what changes is the frame,
310 /// which omits the `Dictionary_ID` field rather than storing a zero, so a
311 /// decoder has to be handed the same bytes explicitly.
312 pub fn id(&self) -> u32 {
313 self.inner.id
314 }
315}
316
317/// How a [`FrameCompressor`] holds its dictionary: owned, as
318/// [`FrameCompressor::set_encoder_dictionary`] attaches one, or borrowed for
319/// the compressor's lifetime through
320/// [`FrameCompressor::new_with_matcher_and_dictionary`], so a short-lived
321/// compressor reads a prepared dictionary without taking a reference on it.
322/// Implemented for [`EncoderDictionary`] and `&EncoderDictionary` only.
323pub trait DictionaryHold: core::borrow::Borrow<EncoderDictionary> + hold::Sealed {
324 /// Whether the compressor owns what it holds, so its footprint counts it.
325 const OWNED: bool;
326}
327
328impl DictionaryHold for EncoderDictionary {
329 const OWNED: bool = true;
330}
331
332impl DictionaryHold for &EncoderDictionary {
333 const OWNED: bool = false;
334}
335
336mod hold {
337 pub trait Sealed {}
338 impl Sealed for super::EncoderDictionary {}
339 impl Sealed for &super::EncoderDictionary {}
340}
341
342/// The dictionary `held` holds, however it holds it.
343fn attached<D: DictionaryHold>(held: &Option<D>) -> Option<&EncoderDictionary> {
344 held.as_ref().map(core::borrow::Borrow::borrow)
345}
346
347/// An interface for compressing arbitrary data with the ZStandard compression algorithm.
348///
349/// `FrameCompressor` will generally be used by:
350/// 1. Initializing a compressor by providing a buffer of data using `FrameCompressor::new()`
351/// 2. Starting compression and writing that compression into a vec using `FrameCompressor::begin`
352///
353/// # Examples
354/// ```
355/// use structured_zstd::encoding::{FrameCompressor, CompressionLevel};
356/// let mock_data: &[_] = &[0x1, 0x2, 0x3, 0x4];
357/// let mut output = std::vec::Vec::new();
358/// // Initialize a compressor.
359/// let mut compressor = FrameCompressor::new(CompressionLevel::Uncompressed);
360/// compressor.set_source(mock_data);
361/// compressor.set_drain(&mut output);
362///
363/// // `compress` writes the compressed output into the provided buffer.
364/// compressor.compress();
365/// ```
366pub struct FrameCompressor<
367 R: Read = &'static [u8],
368 W: Write = Vec<u8>,
369 M: Matcher = MatchGeneratorDriver,
370 D: DictionaryHold = EncoderDictionary,
371> {
372 uncompressed_data: Option<R>,
373 compressed_data: Option<W>,
374 compression_level: CompressionLevel,
375 dictionary: Option<D>,
376 source_size_hint: Option<u64>,
377 state: CompressState<M>,
378 /// When true, emitted frames omit the 4-byte magic number prefix
379 /// (`ZSTD_f_zstd1_magicless`). Default false. The caller is
380 /// responsible for ensuring the decoder is configured for the
381 /// matching format — wire-format only round-trips with a
382 /// magicless-aware decoder.
383 magicless: bool,
384 /// Whether to emit a trailing XXH64 content checksum and set the frame
385 /// header's `Content_Checksum_flag` (semantics of upstream
386 /// `ZSTD_c_checksumFlag`). Default `false`, matching the upstream
387 /// library default; combined with the `hash` feature at frame-build
388 /// time, so without `hash` no checksum is emitted regardless. Set via
389 /// [`Self::set_content_checksum`].
390 content_checksum: bool,
391 /// Diagnostic: skip the block pre-splitter and cut full blocks only
392 /// (upstream's block structure under `ZSTD_generateSequences`, whose
393 /// sequence-collecting mode never accrues the savings the splitter
394 /// requires). Set via `set_pre_split_disabled` (a `bench-internals`
395 /// diagnostic); default `false`.
396 pre_split_disabled: bool,
397 /// Whether to record `Frame_Content_Size` in the frame header when the
398 /// total size is known (semantics of upstream `ZSTD_c_contentSizeFlag`).
399 /// Default `true`, matching upstream. With the flag off the header
400 /// carries a window descriptor instead (single-segment requires an FCS,
401 /// so it is disabled too). Set via [`Self::set_content_size_flag`].
402 content_size_flag: bool,
403 /// Whether to record the dictionary ID in the frame header when a
404 /// dictionary is attached (semantics of upstream `ZSTD_c_dictIDFlag`).
405 /// Default `true`, matching upstream. Decoders can still decode the
406 /// frame by being handed the right dictionary explicitly. Set via
407 /// [`Self::set_dictionary_id_flag`].
408 dict_id_flag: bool,
409 /// Upper bound on emitted block sizes (semantics of upstream
410 /// `ZSTD_c_targetCBlockSize`): capping the RAW block length at the
411 /// target bounds every physical block's compressed payload at the
412 /// target too (a compressed block never exceeds its raw input — the
413 /// raw-block fallback fires otherwise), so blocks land at or under
414 /// `target + 3` header bytes on the wire. `None` = no target (full
415 /// 128 KiB blocks). Set via [`Self::set_target_block_size`].
416 target_block_size: Option<u32>,
417 /// Largest block the frame may carry (upstream `ZSTD_c_maxBlockSize`),
418 /// below the format's 128 KiB. Set via [`Self::set_max_block_size`].
419 max_block_size: Option<u32>,
420 #[cfg(feature = "hash")]
421 hasher: XxHash64,
422 /// Block-layout introspection populated at the end of every
423 /// successful `compress()`. `None` until the first call.
424 /// Behind the `lsm` feature gate.
425 #[cfg(feature = "lsm")]
426 frame_emit_info: Option<crate::encoding::frame_emit_info::FrameEmitInfo>,
427 /// When `true`, `compress()` XXH64-hashes each block's
428 /// uncompressed bytes and appends the low-32-bit digest to
429 /// `block_checksums`. Default `false` (zero cost). Gated on
430 /// `all(lsm, hash)` because XXH64 lives behind the `hash`
431 /// feature; an `lsm`-only build has no way to compute digests.
432 #[cfg(all(feature = "lsm", feature = "hash"))]
433 per_block_checksums_enabled: bool,
434 /// Per-block XXH64 (low 32 bits) digests captured during
435 /// `compress()` when `per_block_checksums_enabled` is set. Ordered
436 /// by block-emit order. `None` until the first call after enabling.
437 /// Gated on `all(lsm, hash)` (see `per_block_checksums_enabled`).
438 #[cfg(all(feature = "lsm", feature = "hash"))]
439 block_checksums: Option<alloc::vec::Vec<u32>>,
440 /// Per-physical-block decompressed (regenerated) sizes captured
441 /// during `compress()`, in block-emit order (1:1 with
442 /// `frame_emit_info.blocks`). Always captured under `lsm` (no
443 /// opt-in, unlike `block_checksums`) because `FrameEmitInfo` is
444 /// always built under `lsm` and `decompressed_byte_range` needs
445 /// the per-block sizes. Cleared and refilled per frame.
446 #[cfg(feature = "lsm")]
447 block_decompressed_sizes: alloc::vec::Vec<u32>,
448 tuning: FrameTuning,
449}
450
451/// What a compressor keeps of the fine-grained parameters (#27) beside the
452/// level: the parts the literal-compression gates and the block splitter read
453/// at frame start, where the matcher holds the rest. Shared by
454/// [`FrameCompressor`] and [`CompressionContext`](crate::encoding::CompressionContext).
455#[derive(Clone, Copy, Default)]
456pub(crate) struct FrameTuning {
457 /// A public-parameter [`Strategy`](crate::encoding::Strategy) override:
458 /// its tag and lazy depth (the collapsed `Lazy` tag needs the depth for
459 /// the pre-split tier). `Some` wins over the level-derived
460 /// `state.strategy_tag` so the gates and the dict-attach cutoff see the
461 /// strategy the matcher runs; `None` keeps the level's.
462 pub(crate) strategy: Option<(crate::encoding::strategy::StrategyTag, u8)>,
463 /// Public `target_length` override, kept so the raw-literals gate can be
464 /// recomputed per frame: a dictionary attached or cleared after
465 /// `set_parameters` can move the strategy the gate reads.
466 pub(crate) target_length: Option<u32>,
467 /// Public literal-compression mode (upstream
468 /// `ZSTD_c_literalCompressionMode`), kept for the same per-frame
469 /// recomputation of the raw-literals gate.
470 pub(crate) literal_compression: LiteralCompressionMode,
471}
472
473impl FrameTuning {
474 /// The tuning `overrides` asks for.
475 pub(crate) fn from_overrides(overrides: &crate::encoding::parameters::ParamOverrides) -> Self {
476 Self {
477 strategy: overrides.strategy.map(|s| (s.tag(), s.lazy_depth())),
478 target_length: overrides.target_length,
479 literal_compression: overrides.literal_compression,
480 }
481 }
482}
483
484#[derive(Clone, Default)]
485pub(crate) struct CachedDictionaryEntropy {
486 pub(crate) huff: Option<crate::huff0::huff0_encoder::HuffmanTable>,
487 pub(crate) ll_previous: Option<PreviousFseTable>,
488 pub(crate) ml_previous: Option<PreviousFseTable>,
489 pub(crate) of_previous: Option<PreviousFseTable>,
490}
491
492impl CachedDictionaryEntropy {
493 /// Heap bytes the cached dictionary entropy holds: the literals Huffman
494 /// table plus any `Custom` LL/ML/OF FSE tables (the `Arc`-boxed `FSETable`
495 /// payload and its flat state array). `Default` / `Rle` variants own no heap.
496 pub(crate) fn heap_size(&self) -> usize {
497 let mut total = self.huff.as_ref().map_or(0, |h| h.heap_size());
498 for prev in [&self.ll_previous, &self.ml_previous, &self.of_previous] {
499 if let Some(PreviousFseTable::Custom(table)) = prev {
500 total +=
501 core::mem::size_of::<crate::fse::fse_encoder::FSETable>() + table.heap_size();
502 }
503 }
504 total
505 }
506
507 /// Derive the encoder-side entropy tables a dictionary seeds for the first
508 /// block of each frame (the upstream zstd `cdict->cBlockState`): the literals
509 /// Huffman table plus the literal-length / match-length / offset FSE
510 /// "previous" tables. Shared by [`FrameCompressor`] and
511 /// [`crate::encoding::StreamingEncoder`] so both seed identically.
512 pub(crate) fn from_dictionary(dictionary: &crate::decoding::Dictionary) -> Self {
513 Self {
514 huff: dictionary.huf.table.to_encoder_table(),
515 ll_previous: dictionary
516 .fse
517 .literal_lengths
518 .to_encoder_table()
519 .map(|table| PreviousFseTable::Custom(SharedFseTable::new(table))),
520 ml_previous: dictionary
521 .fse
522 .match_lengths
523 .to_encoder_table()
524 .map(|table| PreviousFseTable::Custom(SharedFseTable::new(table))),
525 of_previous: dictionary
526 .fse
527 .offsets
528 .to_encoder_table()
529 .map(|table| PreviousFseTable::Custom(SharedFseTable::new(table))),
530 }
531 }
532}
533
534/// Shared owner for a custom "previous" FSE encoder table. `Arc` on
535/// atomic-pointer targets, `Rc` otherwise (keeps `no_std` no-atomics
536/// builds compiling, single-thread there anyway), mirroring
537/// `decoding::dictionary::SharedDictionary`. Cloning the cached
538/// dictionary entropy into the per-frame state is then a refcount bump,
539/// not a full `FSETable` copy — the upstream zstd references `cdict->cBlockState`
540/// instead of rebuilding it per frame.
541#[cfg(target_has_atomic = "ptr")]
542pub(crate) type SharedFseTable = alloc::sync::Arc<FSETable>;
543#[cfg(not(target_has_atomic = "ptr"))]
544pub(crate) type SharedFseTable = alloc::rc::Rc<FSETable>;
545
546/// Bytes a [`SharedFseTable`] allocation carries in FRONT of the table: the two
547/// reference counts, then whatever padding the table's alignment adds. One
548/// allocation holds both, so a caller sizing a context is told about the whole
549/// of it rather than the payload alone.
550pub(crate) const fn shared_table_overhead() -> usize {
551 let counts = 2 * core::mem::size_of::<usize>();
552 let align = core::mem::align_of::<FSETable>();
553 counts.div_ceil(align) * align
554}
555
556#[derive(Clone)]
557pub(crate) enum PreviousFseTable {
558 // Default tables are immutable and already stored alongside the state, so
559 // repeating them only needs a lightweight marker instead of cloning FSETable.
560 Default,
561 // Shared handle: cloning (per-frame dictionary entropy seed) is a refcount
562 // bump. The table is only ever read or REPLACED wholesale (a block that
563 // builds a new table swaps in a fresh `SharedFseTable`), never mutated in
564 // place, so sharing is sound.
565 Custom(SharedFseTable),
566 Rle(u8),
567}
568
569impl PreviousFseTable {
570 pub(crate) fn as_table<'a>(&'a self, default: &'a FSETable) -> Option<&'a FSETable> {
571 match self {
572 Self::Default => Some(default),
573 Self::Custom(table) => Some(table),
574 Self::Rle(_) => None,
575 }
576 }
577}
578
579pub(crate) struct FseTables {
580 /// The three predefined LL/ML/OF tables are functions of
581 /// compile-time-constant distributions. The
582 /// [`FseDefaultTable`](crate::fse::fse_encoder::FseDefaultTable) type alias resolves to
583 /// `&'static FSETable` when a process-wide cache is available
584 /// (atomic-pointer targets, or no-atomic targets with the
585 /// `critical-section` feature) and to `Box<FSETable>` on the
586 /// cache-less no-atomic path (one per-frame allocation, dropped
587 /// with the compressor — no `Box::leak`, no unbounded growth).
588 /// Both arms `Deref` to `FSETable`, so consumers in
589 /// `encoding/blocks/compressed.rs` borrow through `&` uniformly
590 /// without seeing the per-target divergence.
591 pub(crate) ll_default: crate::fse::fse_encoder::FseDefaultTable,
592 pub(crate) ll_previous: Option<PreviousFseTable>,
593 pub(crate) ml_default: crate::fse::fse_encoder::FseDefaultTable,
594 pub(crate) ml_previous: Option<PreviousFseTable>,
595 pub(crate) of_default: crate::fse::fse_encoder::FseDefaultTable,
596 pub(crate) of_previous: Option<PreviousFseTable>,
597 /// Where a block builds the table it is about to emit, before that table
598 /// becomes the axis's `*_previous`.
599 ///
600 /// Upstream's `ZSTD_blockState_t` keeps `prevCBlock` and `nextCBlock` for
601 /// exactly this: the entropy build reads the previous tables and writes the
602 /// next ones, so the two never alias and committing a block is a pointer
603 /// swap (`ZSTD_blockState_confirmRepcodesAndEntropyTables`). A block that
604 /// ends up raw simply does not swap. Holding the slot here is what lets a
605 /// table be built in place instead of on the stack.
606 /// `None` until an axis first builds a table. Lazy because a state that
607 /// never emits a custom table must not pay for a slot: the block splitter
608 /// makes one of these per probe, and eagerly giving each three tables
609 /// faulted in pages for buffers most probes never wrote to.
610 pub(crate) ll_next: Option<SharedFseTable>,
611 pub(crate) ml_next: Option<SharedFseTable>,
612 pub(crate) of_next: Option<SharedFseTable>,
613}
614
615impl FseTables {
616 /// Keep the frame's tables as buffers before the next frame overwrites
617 /// them.
618 ///
619 /// A frame start replaces every previous slot — with the dictionary's
620 /// seed, or with nothing — and dropping the table that was there costs a
621 /// reused compressor an eleven-kilobyte allocation per axis on the next
622 /// frame that builds one. The slot the axis builds into is exactly where
623 /// it belongs. Only an unshared table is worth keeping: one still held by
624 /// the dictionary entropy cache cannot be built into anyway.
625 pub(crate) fn park_previous_before_frame(&mut self) {
626 for (previous, next) in [
627 (&mut self.ll_previous, &mut self.ll_next),
628 (&mut self.ml_previous, &mut self.ml_next),
629 (&mut self.of_previous, &mut self.of_next),
630 ] {
631 // Decided before taking: a handle the cache still holds stays
632 // where it is rather than being pulled out and dropped.
633 let worth_keeping = matches!(
634 previous.as_ref(),
635 Some(PreviousFseTable::Custom(handle))
636 if SharedFseTable::strong_count(handle) == 1
637 );
638 // An occupied slot is not the same as a usable one. A frame that
639 // settled on a predefined table parks the dictionary cache's handle
640 // here, and a shared handle cannot be built into — so leaving it
641 // there while the uniquely owned table is dropped costs the
642 // allocation this exists to avoid, on the first custom build of
643 // every frame.
644 let next_is_a_buffer = next
645 .as_ref()
646 .is_some_and(|handle| SharedFseTable::strong_count(handle) == 1);
647 if !next_is_a_buffer
648 && worth_keeping
649 && let Some(PreviousFseTable::Custom(handle)) = previous.take()
650 {
651 *next = Some(handle);
652 }
653 }
654 }
655
656 /// Heap bytes the retained encoder tables hold.
657 ///
658 /// Both slots of an axis count: the previous table is what the next block
659 /// reads, the next slot is the buffer it builds into, and each lives as
660 /// long as the compressor. A handle shared with the dictionary entropy
661 /// cache is left out, because that cache reports its own tables and the
662 /// caller would otherwise be told about the same allocation twice.
663 pub(crate) fn heap_size(&self) -> usize {
664 // The table AND the reference counts in front of it: a shared handle is
665 // one allocation holding both, and reporting only the payload
666 // understates every retained table by the control block and whatever
667 // padding its alignment adds.
668 let per_table = core::mem::size_of::<FSETable>() + shared_table_overhead();
669 let mut total = 0;
670 for previous in [&self.ll_previous, &self.ml_previous, &self.of_previous] {
671 if let Some(PreviousFseTable::Custom(handle)) = previous
672 && SharedFseTable::strong_count(handle) == 1
673 {
674 total += per_table;
675 }
676 }
677 for next in [&self.ll_next, &self.ml_next, &self.of_next] {
678 // Same ownership test as the previous slots, and for the same
679 // reason: a dictionary-seeded axis that settles on a predefined
680 // table parks the CACHE's handle here, and that cache reports the
681 // table itself.
682 if let Some(handle) = next
683 && SharedFseTable::strong_count(handle) == 1
684 {
685 total += per_table;
686 }
687 }
688 // Where there is neither a pointer atomic nor `critical-section` to
689 // guard a process-wide cache, each default table is an owned box built
690 // per compressor, so it is this struct's allocation to report. With the
691 // cache it is a `&'static` shared by every compressor and counts as
692 // nothing.
693 #[cfg(not(any(target_has_atomic = "ptr", feature = "critical-section")))]
694 {
695 total += 3 * core::mem::size_of::<FSETable>();
696 }
697 total
698 }
699
700 /// The most [`Self::heap_size`] reports: both slots of all three axes
701 /// owned, and the defaults where they are boxed per compressor.
702 pub(crate) const fn max_heap_size() -> usize {
703 let owned = 6 * (core::mem::size_of::<FSETable>() + shared_table_overhead());
704 if cfg!(any(target_has_atomic = "ptr", feature = "critical-section")) {
705 owned
706 } else {
707 owned + 3 * core::mem::size_of::<FSETable>()
708 }
709 }
710
711 pub fn new() -> Self {
712 Self {
713 ll_next: None,
714 ml_next: None,
715 of_next: None,
716 ll_default: default_ll_table(),
717 ll_previous: None,
718 ml_default: default_ml_table(),
719 ml_previous: None,
720 of_default: default_of_table(),
721 of_previous: None,
722 }
723 }
724
725 /// Borrow the LL default table as `&FSETable`.
726 ///
727 /// Test-only now: the encoder and the estimator both destructure
728 /// `FseTables` so they can hold a `*_next` slot mutably, and a method
729 /// borrowing the whole struct cannot coexist with that. Tests keep it
730 /// because they touch one table at a time.
731 ///
732 /// Abstracts the cfg
733 /// split in [`crate::fse::fse_encoder::FseDefaultTable`] —
734 /// `&'static FSETable` (atomic / `critical-section`) auto-derefs
735 /// directly; `Box<FSETable>` (cache-less no-atomic) derefs
736 /// through `Box`. Both arms yield `&FSETable` uniformly so
737 /// downstream consumers can stay cfg-agnostic.
738 #[inline]
739 #[allow(clippy::borrow_deref_ref)]
740 #[cfg(test)]
741 pub(crate) fn ll_default_ref(&self) -> &FSETable {
742 &*self.ll_default
743 }
744
745 /// Borrow the ML default table as `&FSETable`. See [`Self::ll_default_ref`].
746 #[inline]
747 #[allow(clippy::borrow_deref_ref)]
748 #[cfg(test)]
749 pub(crate) fn ml_default_ref(&self) -> &FSETable {
750 &*self.ml_default
751 }
752
753 /// Borrow the OF default table as `&FSETable`. See [`Self::ll_default_ref`].
754 #[inline]
755 #[allow(clippy::borrow_deref_ref)]
756 #[cfg(test)]
757 pub(crate) fn of_default_ref(&self) -> &FSETable {
758 &*self.of_default
759 }
760}
761
762const PRESPLIT_BLOCK_MIN: usize = 3500;
763const PRESPLIT_THRESHOLD_PENALTY_RATE: u64 = 16;
764const PRESPLIT_THRESHOLD_BASE: u64 = PRESPLIT_THRESHOLD_PENALTY_RATE - 2;
765const PRESPLIT_THRESHOLD_PENALTY: i32 = 3;
766const PRESPLIT_CHUNK_SIZE: usize = 8 << 10;
767const PRESPLIT_HASH_LOG_MAX: usize = 10;
768const PRESPLIT_HASH_TABLE_SIZE: usize = 1 << PRESPLIT_HASH_LOG_MAX;
769const PRESPLIT_KNUTH: u32 = 0x9E37_79B9;
770/// Upstream zstd `SEGMENT_SIZE` in `ZSTD_splitBlock_fromBorders` (`zstd_preSplit.c:201`).
771/// Two `SEGMENT_SIZE`-byte fingerprints — one from the start, one from the end —
772/// drive the cheap border heuristic; a third one from the middle disambiguates
773/// where in the block the transition sits.
774const PRESPLIT_BORDERS_SEGMENT: usize = 512;
775
776#[derive(Clone)]
777struct PreSplitFingerprint {
778 events: [u32; PRESPLIT_HASH_TABLE_SIZE],
779 nb_events: usize,
780}
781
782impl Default for PreSplitFingerprint {
783 fn default() -> Self {
784 Self {
785 events: [0; PRESPLIT_HASH_TABLE_SIZE],
786 nb_events: 0,
787 }
788 }
789}
790
791/// Grow `out` ahead of the next block so block emission never lands on an
792/// amortized-doubling reallocation mid-frame (whose transient old+new copy
793/// spikes peak memory to ~3x the output), sizing the reservation from the
794/// compression ratio observed so far instead of the whole-input worst case.
795///
796/// `blocks_start` is where this frame's blocks begin in `out`, `consumed`
797/// the input bytes already emitted as blocks, `remaining` the input
798/// bytes still to compress (an estimate is fine: a low one only means one
799/// more re-estimate later), and `block_capacity` the active block-size cap
800/// (`FrameCompressor::block_capacity`) so a small `targetCBlockSize` does
801/// not keep a 128 KiB floor in the buffer or undercount header density.
802/// Incompressible input re-estimates to ~the full `compress_bound` after
803/// the first block — the old up-front policy's worst case — while
804/// compressible input stays at output scale.
805fn reserve_for_next_block(
806 out: &mut Vec<u8>,
807 blocks_start: usize,
808 consumed: u64,
809 remaining: usize,
810 block_capacity: usize,
811) {
812 // Worst-case single-block output: 3-byte header + raw payload, plus
813 // slack for the 4-byte frame checksum trailer and a few extra sub-block
814 // headers from the post-split emitters, so neither can reallocate.
815 let block_bound = remaining.min(block_capacity) + 3 + 16;
816 if out.capacity() - out.len() >= block_bound {
817 return;
818 }
819 let produced = (out.len() - blocks_start) as u64;
820 let estimate = if consumed == 0 {
821 // No ratio signal yet (capacity exhausted before the first block —
822 // only reachable with a caller-shrunk `out`): one block's bound.
823 block_bound
824 } else {
825 // remaining * observed ratio + per-block headers + 1/16 slack so a
826 // slightly-worsening tail doesn't force a reallocation per block.
827 // u128 keeps the product exact for multi-GiB frames.
828 let scaled = ((remaining as u128 * produced as u128) / consumed as u128) as u64;
829 let headers = (remaining as u64 / block_capacity.max(1) as u64 + 1) * 3;
830 usize::try_from(scaled + scaled / 16 + headers + 64).unwrap_or(usize::MAX)
831 };
832 // `reserve_exact`: the estimate already carries its own slack, and the
833 // whole-buffer doubling policy is exactly what this function exists to
834 // avoid. The `produced`-sized floor keeps growth geometric when the
835 // ratio estimate lands BELOW one block's bound (highly compressible
836 // input): without it every block would trigger a block-sized
837 // reallocation — O(blocks) buffer copies — while with it the buffer at
838 // least doubles its produced span per reallocation (O(log) copies) and
839 // the peak stays at output scale.
840 out.reserve_exact(estimate.max(block_bound + produced as usize));
841}
842
843/// The rate and the width stay ARGUMENTS here, though upstream generates one
844/// function per tier and says the speed of the pass relies on compile-time
845/// constant propagation (`zstd_preSplit.c:32`, `ZSTD_GEN_RECORD_FINGERPRINT` at
846/// `:87`). Monomorphising the walk on both was tried and measured on 8 MiB of
847/// repeated log lines at level 6, three interleaved readings a side: cycles
848/// overlapping (966-984 M against 973-997 M) and instructions UP, 1,252 M to
849/// 1,268 M. Rust inlines this into a walk the caller already picked a tier for,
850/// so the propagation is there without the four copies.
851fn presplit_hash2(bytes: &[u8], hash_log: usize) -> usize {
852 debug_assert!(hash_log >= 8);
853 if hash_log == 8 {
854 return bytes[0] as usize;
855 }
856 debug_assert!(hash_log <= PRESPLIT_HASH_LOG_MAX);
857 let value = u16::from_le_bytes([bytes[0], bytes[1]]) as u32;
858 (value.wrapping_mul(PRESPLIT_KNUTH) >> (32 - hash_log)) as usize
859}
860
861fn presplit_record_fingerprint(
862 fp: &mut PreSplitFingerprint,
863 src: &[u8],
864 sampling_rate: usize,
865 hash_log: usize,
866) {
867 // Only the slots this hash can reach. The table is sized for the widest
868 // hash the splitter uses; a tier on a narrower one was clearing four times
869 // the memory it then touched, sixteen times a block.
870 fp.events[..1usize << hash_log].fill(0);
871 fp.nb_events = 0;
872 if src.len() < 2 {
873 return;
874 }
875 let limit = src.len() - 1;
876 let mut n = 0usize;
877 while n < limit {
878 fp.events[presplit_hash2(&src[n..], hash_log)] += 1;
879 n += sampling_rate;
880 }
881 // Upstream zstd parity: zstd_preSplit.c records the integer division, not the
882 // rounded-up number of sampled events from the loop above.
883 fp.nb_events += limit / sampling_rate;
884}
885
886/// Single-byte histogram pass — matches upstream zstd `HIST_add` over a small
887/// segment with `hashLog == 8` (the `hash2` shortcut at
888/// `zstd_preSplit.c:36` returns the raw byte). The byChunks path uses
889/// 2-byte hashing for `hashLog >= 9`; this helper exists so the borders
890/// heuristic doesn't pay for that wider hash on its 512-byte windows.
891fn presplit_record_byte_histogram(fp: &mut PreSplitFingerprint, src: &[u8]) {
892 fp.events.fill(0);
893 for &b in src {
894 fp.events[b as usize] += 1;
895 }
896 // Upstream zstd `HIST_add` returns the maximum symbol; the caller then sets
897 // `nbEvents = SEGMENT_SIZE` explicitly (see `zstd_preSplit.c:213`).
898 fp.nb_events = src.len();
899}
900
901fn presplit_distance(lhs: &PreSplitFingerprint, rhs: &PreSplitFingerprint, hash_log: usize) -> u64 {
902 let slots = 1usize << hash_log;
903 // 64-bit, as upstream's `fpDistance` is (`zstd_preSplit.c`). Both factors
904 // are per-block sample counts bounded by the block size, so each product is
905 // under 2^34 and the sum over at most 2^10 slots is under 2^44 — the 128-bit
906 // arithmetic this used to do could not overflow either, it just cost several
907 // instructions a slot on a loop the splitter runs sixteen times a block.
908 let rn = rhs.nb_events as u64;
909 let ln = lhs.nb_events as u64;
910 // Four accumulators over slices the optimiser can see the length of, which
911 // is what lets this go wide: the splitter runs this loop once per chunk and
912 // sixteen times a block, so it counts more slots than the sampling reads
913 // bytes.
914 let (l, r) = (&lhs.events[..slots], &rhs.events[..slots]);
915 let mut acc = [0u64; 4];
916 let mut idx = 0;
917 while idx + 4 <= slots {
918 for lane in 0..4 {
919 let left = u64::from(l[idx + lane]) * rn;
920 let right = u64::from(r[idx + lane]) * ln;
921 acc[lane] += left.abs_diff(right);
922 }
923 idx += 4;
924 }
925 let mut distance = acc[0] + acc[1] + acc[2] + acc[3];
926 while idx < slots {
927 distance += (u64::from(l[idx]) * rn).abs_diff(u64::from(r[idx]) * ln);
928 idx += 1;
929 }
930 distance
931}
932
933fn presplit_fingerprints_differ(
934 reference: &PreSplitFingerprint,
935 new_fp: &PreSplitFingerprint,
936 penalty: i32,
937 hash_log: usize,
938) -> bool {
939 debug_assert!(reference.nb_events > 0);
940 debug_assert!(new_fp.nb_events > 0);
941 let p50 = reference.nb_events as u64 * new_fp.nb_events as u64;
942 let deviation = presplit_distance(reference, new_fp, hash_log);
943 // Plain `*`: p50 <= (block-sample-count)^2 and the (base+penalty) factor is
944 // a small constant, so the product stays well under u64::MAX.
945 let threshold =
946 p50 * (PRESPLIT_THRESHOLD_BASE + penalty as u64) / PRESPLIT_THRESHOLD_PENALTY_RATE;
947 deviation >= threshold
948}
949
950fn presplit_merge_events(
951 acc: &mut PreSplitFingerprint,
952 new_fp: &PreSplitFingerprint,
953 hash_log: usize,
954) {
955 // Plain `+`: `acc` accumulates only the chunks of a single block (caller
956 // loops within one block, <= MAX_BLOCK_SIZE), so the merged sample counts
957 // stay far under u32 / usize bounds — no overflow.
958 //
959 // Only the slots this tier's hash reaches: the rest are never recorded into
960 // and never read, so merging them is a quarter of the table for nothing.
961 // Paired slices rather than indices, so the bound is one check instead of
962 // one per slot and the adds can go wide.
963 let slots = 1usize << hash_log;
964 for (into, from) in acc.events[..slots].iter_mut().zip(&new_fp.events[..slots]) {
965 *into += *from;
966 }
967 acc.nb_events += new_fp.nb_events;
968}
969
970fn split_block_by_chunks(block: &[u8], level: usize) -> usize {
971 debug_assert_eq!(block.len(), MAX_BLOCK_SIZE as usize);
972 debug_assert!((1..=4).contains(&level));
973 let (sampling_rate, hash_log) = match level - 1 {
974 0 => (43, 8),
975 1 => (11, 9),
976 2 => (5, 10),
977 _ => (1, 10),
978 };
979
980 let mut past = PreSplitFingerprint::default();
981 let mut new_events = PreSplitFingerprint::default();
982 let mut penalty = PRESPLIT_THRESHOLD_PENALTY;
983 presplit_record_fingerprint(
984 &mut past,
985 &block[..PRESPLIT_CHUNK_SIZE],
986 sampling_rate,
987 hash_log,
988 );
989 // No pre-check on the ends before the walk. It reads as free — two
990 // fingerprints against the sixty-three the walk takes — but a sample of the
991 // ends cannot stand in for the walk: an A-B-A block has matching ends and a
992 // boundary in the middle, and four megabytes repeated at nearly the window
993 // distance lost 13% to exactly that. Upstream has no such check either; the
994 // cheap gate that keeps the walk off hopeless input is `savings`, which the
995 // caller already applies.
996 let mut pos = PRESPLIT_CHUNK_SIZE;
997 while pos <= block.len() - PRESPLIT_CHUNK_SIZE {
998 presplit_record_fingerprint(
999 &mut new_events,
1000 &block[pos..pos + PRESPLIT_CHUNK_SIZE],
1001 sampling_rate,
1002 hash_log,
1003 );
1004 if presplit_fingerprints_differ(&past, &new_events, penalty, hash_log) {
1005 return pos;
1006 }
1007 presplit_merge_events(&mut past, &new_events, hash_log);
1008 if penalty > 0 {
1009 penalty -= 1;
1010 }
1011 pos += PRESPLIT_CHUNK_SIZE;
1012 }
1013 block.len()
1014}
1015
1016/// Upstream zstd port of `ZSTD_splitBlock_fromBorders` (`zstd_preSplit.c:198`).
1017/// Records two 512-byte byte-histograms — one from each end of a 128 KB
1018/// block — and a third from the middle as a tie-breaker; returns either
1019/// a quantised split point (32 KB / 64 KB / 96 KB) or the full block
1020/// size when the two ends look indistinguishable. Cheaper than the
1021/// chunk-based path because it touches at most 1.5 KB of input
1022/// regardless of block size.
1023fn split_block_from_borders(block: &[u8]) -> usize {
1024 debug_assert_eq!(block.len(), MAX_BLOCK_SIZE as usize);
1025 let block_size = block.len();
1026 let mut past = PreSplitFingerprint::default();
1027 let mut new_fp = PreSplitFingerprint::default();
1028 presplit_record_byte_histogram(&mut past, &block[..PRESPLIT_BORDERS_SEGMENT]);
1029 presplit_record_byte_histogram(&mut new_fp, &block[block_size - PRESPLIT_BORDERS_SEGMENT..]);
1030 // Upstream zstd uses `penalty = 0, hash_log = 8` — i.e. raw byte histogram
1031 // distance with no threshold padding (`zstd_preSplit.c:214`).
1032 if !presplit_fingerprints_differ(&past, &new_fp, 0, 8) {
1033 return block_size;
1034 }
1035
1036 let mut middle = PreSplitFingerprint::default();
1037 let mid_start = block_size / 2 - PRESPLIT_BORDERS_SEGMENT / 2;
1038 presplit_record_byte_histogram(
1039 &mut middle,
1040 &block[mid_start..mid_start + PRESPLIT_BORDERS_SEGMENT],
1041 );
1042
1043 let dist_from_begin = presplit_distance(&past, &middle, 8);
1044 let dist_from_end = presplit_distance(&new_fp, &middle, 8);
1045 // Upstream zstd `SEGMENT_SIZE * SEGMENT_SIZE / 3` (`zstd_preSplit.c:221`):
1046 // if the middle is roughly equidistant from both ends, the change
1047 // sits near the centre — split at the midpoint.
1048 let min_distance = (PRESPLIT_BORDERS_SEGMENT as u64) * (PRESPLIT_BORDERS_SEGMENT as u64) / 3;
1049 if dist_from_begin.abs_diff(dist_from_end) < min_distance {
1050 return 64 * 1024;
1051 }
1052 // Larger `dist_from_begin` (i.e. `middle` farther from the head
1053 // fingerprint, equivalently closer to the tail) means the new
1054 // statistics already dominate the centre — the transition
1055 // happened EARLY → emit a small 32 KB head and let the 96 KB
1056 // tail absorb the rest. Inverse case: `dist_from_end` larger
1057 // (middle still resembles the head) means the transition is
1058 // LATE → emit a 96 KB head so the trailing 32 KB carries the
1059 // new statistics alone.
1060 if dist_from_begin > dist_from_end {
1061 32 * 1024
1062 } else {
1063 96 * 1024
1064 }
1065}
1066
1067/// XXH64 (low 32 bits, seed 0) over `data`. Shared helper for the
1068/// per-physical-block checksum sidecar so encoder and decoder hash
1069/// the exact same byte ranges with the exact same parameters. Gated
1070/// at `all(lsm, hash)` because the only consumer is the lsm-side
1071/// `block_checksums` sidecar; non-lsm builds carry no reference to
1072/// this helper at all.
1073#[cfg(all(feature = "lsm", feature = "hash"))]
1074#[inline]
1075pub(crate) fn xxh64_block_low32(data: &[u8]) -> u32 {
1076 let mut h = XxHash64::with_seed(0);
1077 h.write(data);
1078 h.finish() as u32
1079}
1080
1081/// Bench-only entry point for the upstream zstd-parity comparator test in
1082/// `tests/block_splitter_parity.rs`. Dispatches to the same
1083/// `_from_borders` (split_level == 0) / `_by_chunks` (split_level ∈
1084/// 1..=4) ports that `optimal_block_size` itself routes
1085/// through. Caller is responsible for passing exactly
1086/// `MAX_BLOCK_SIZE` bytes (per upstream zstd `ZSTD_splitBlock` contract —
1087/// "@blockSize must be == 128 KB" in `zstd_preSplit.h`).
1088#[cfg(feature = "bench-internals")]
1089pub(crate) fn block_splitter_decision_for_bench(block: &[u8], split_level: usize) -> usize {
1090 assert_eq!(
1091 block.len(),
1092 MAX_BLOCK_SIZE as usize,
1093 "block_splitter_decision_for_bench expects exactly MAX_BLOCK_SIZE bytes"
1094 );
1095 assert!(
1096 split_level <= 4,
1097 "block_splitter_decision_for_bench: split_level must be in 0..=4, got {split_level}"
1098 );
1099 if split_level == 0 {
1100 split_block_from_borders(block)
1101 } else {
1102 split_block_by_chunks(block, split_level)
1103 }
1104}
1105
1106/// Pull a pre-split window into cache with one bandwidth-bound sequential
1107/// pass before the strided fingerprint histogram + match scan read it.
1108///
1109/// The borrowed (no-copy) over-window path matches in place on the caller's
1110/// input, so the pre-split fingerprint is the FIRST touch of that 128 KiB
1111/// region — a cache-cold read. `presplit_record_fingerprint` reads it with a
1112/// `sampling_rate` stride and interleaved random writes into the 1 KiB events
1113/// table, a latency-bound pattern that pays full DRAM miss latency per line
1114/// (measured ~3x the cost of an ERMS streaming read of the same bytes). The
1115/// owned path never hits this because its history-mirror copy already warmed
1116/// the bytes; this restores that warmth without the copy's write half. One
1117/// dependent load per 64-byte line (the i9 line size) streams under the
1118/// hardware prefetcher, so the cold read is paid once at memory bandwidth and
1119/// every subsequent strided sample lands in L1/L2. `black_box` keeps the loop
1120/// from being optimized away as a dead read.
1121#[inline]
1122fn warm_presplit_window(window: &[u8]) {
1123 let mut acc = 0u8;
1124 let mut i = 0usize;
1125 while i < window.len() {
1126 acc ^= window[i];
1127 i += 64;
1128 }
1129 core::hint::black_box(acc);
1130}
1131
1132/// [`optimal_block_size_with`] at a level's default pre-split tier; the
1133/// frame loop resolves the tier itself, so only tests and the
1134/// `bench-internals` block-boundary probe read this form.
1135#[cfg(any(test, feature = "bench-internals"))]
1136pub(crate) fn optimal_block_size(
1137 level: CompressionLevel,
1138 block: &[u8],
1139 remaining_src_size: usize,
1140 block_size_max: usize,
1141 savings: i64,
1142) -> usize {
1143 optimal_block_size_with(
1144 crate::encoding::levels::config::level_pre_split(level),
1145 block,
1146 remaining_src_size,
1147 block_size_max,
1148 savings,
1149 )
1150}
1151
1152/// Size of the next block to cut from `block`, given the pre-split level
1153/// already resolved for the frame (`None` = never split, only full blocks).
1154///
1155/// Out of line on purpose: inlined into the per-block frame loop it grew the
1156/// loop body and shifted the code layout around `run_fast_kernel_block`,
1157/// costing the Fast levels 17-24 % on 1 MiB+ inputs on x86 (measured on the
1158/// i9; the kernel's own instructions were byte-identical). One call per block
1159/// is noise; the compact caller is not.
1160#[inline(never)]
1161pub(crate) fn optimal_block_size_with(
1162 pre_split: Option<usize>,
1163 block: &[u8],
1164 remaining_src_size: usize,
1165 block_size_max: usize,
1166 savings: i64,
1167) -> usize {
1168 let Some(split_level) = pre_split else {
1169 return remaining_src_size.min(block_size_max);
1170 };
1171 if remaining_src_size < MAX_BLOCK_SIZE as usize || block_size_max < MAX_BLOCK_SIZE as usize {
1172 return remaining_src_size.min(block_size_max);
1173 }
1174 if savings < 3 {
1175 return MAX_BLOCK_SIZE as usize;
1176 }
1177 if block.len() < MAX_BLOCK_SIZE as usize {
1178 return remaining_src_size.min(block_size_max);
1179 }
1180 // Upstream zstd `ZSTD_splitBlock` dispatch (`zstd_preSplit.c:234`):
1181 // `split_level == 0` → cheap borders heuristic;
1182 // `split_level == 1..=4` → byChunks with internal sampling level
1183 // `split_level - 1`.
1184 // NOTE: gating the sampling tier behind the cheap borders tier (run it
1185 // only when the block's ends already disagree) was measured and REJECTED:
1186 // it costs 2.8-3.4% of the decode corpus across levels 3-15, because the
1187 // boundaries the sampling finds are mostly inside blocks whose ends look
1188 // alike. The cost of the sampling on uniform input has to come off some
1189 // other way.
1190 let raw_split = if split_level == 0 {
1191 split_block_from_borders(&block[..MAX_BLOCK_SIZE as usize])
1192 } else {
1193 split_block_by_chunks(&block[..MAX_BLOCK_SIZE as usize], split_level)
1194 };
1195 raw_split
1196 .max(PRESPLIT_BLOCK_MIN)
1197 .min(MAX_BLOCK_SIZE as usize)
1198}
1199
1200/// Load `dict` into the matcher for the frame about to start, `size_hint`
1201/// bytes long when known, once `state.strategy_tag` holds the strategy the
1202/// frame runs. Shared by the frame compressor and the streaming context so a
1203/// reused one of either loads its dictionary the same way.
1204///
1205/// A dictionary a reused matcher kept resident across the reset (its bytes
1206/// and index still in place) only gets its offset history back: priming it
1207/// again would commit it into the window a second time. Otherwise it is
1208/// primed, or restored from the matcher's copy-mode snapshot when the source
1209/// is past the strategy's attach cutoff.
1210pub(crate) fn load_frame_dictionary<M: Matcher>(
1211 state: &mut CompressState<M>,
1212 level: CompressionLevel,
1213 dict: &EncoderDictionary,
1214 size_hint: Option<u64>,
1215) {
1216 // Upstream zstd `ZSTD_shouldAttachDict` (`zstd_compress.c`): a
1217 // precomputed-dictionary table is COPIED into the working context
1218 // only when the source is larger than a per-strategy cutoff; at or
1219 // below it (and for unknown size) the upstream zstd ATTACHES the dictionary
1220 // tables by reference (no per-frame table touch at all). We don't
1221 // have an attach-by-reference path yet, so:
1222 // - large source (> cutoff): reuse the captured prime snapshot
1223 // (a table copy) instead of re-hashing the dictionary — the
1224 // upstream zstd COPY regime, where the copy is cheaper than re-priming;
1225 // - small / unknown source: re-prime (the snapshot copy of the
1226 // whole table would cost MORE than the sparse re-prime here,
1227 // which is exactly why the upstream zstd attaches by reference instead).
1228 // `attachDictSizeCutoffs` per strategy: fast 8K, dfast 16K,
1229 // greedy/lazy/btopt 32K, btultra/btultra2 8K. Expressed as the
1230 // ceil-log bucket (8K = 2^13, 16K = 2^14, 32K = 2^15) so the
1231 // decision uses the SAME bucketed representation as the driver's
1232 // attach/copy gate (`reset_size_log`) — comparing
1233 // `source_size_ceil_log(hint)` on the full u64 avoids the `as usize`
1234 // truncation that could diverge from the driver on 32-bit targets.
1235 // For a power-of-two cutoff `2^k`, `ceil_log2(hint) > k` is exactly
1236 // `hint > 2^k`, so this is identical to the raw `hint > cutoff` on
1237 // 64-bit.
1238 let cutoff_log = match state.strategy_tag {
1239 // Keep the copy-snapshot gate in sync with the matcher's own
1240 // attach cutoff, so Fast never captures or restores a snapshot
1241 // for a mode it did not resolve.
1242 crate::encoding::strategy::StrategyTag::Fast => {
1243 crate::encoding::levels::config::FAST_ATTACH_DICT_CUTOFF_LOG
1244 }
1245 crate::encoding::strategy::StrategyTag::BtUltra
1246 | crate::encoding::strategy::StrategyTag::BtUltra2 => 13,
1247 crate::encoding::strategy::StrategyTag::Dfast => 14,
1248 crate::encoding::strategy::StrategyTag::Greedy
1249 | crate::encoding::strategy::StrategyTag::Lazy
1250 | crate::encoding::strategy::StrategyTag::Btlazy2
1251 | crate::encoding::strategy::StrategyTag::BtOpt => 15,
1252 };
1253 if state.matcher.dictionary_is_resident() {
1254 // Re-borrow fast path: the previous frame's reset kept this
1255 // dict's bytes + cached index resident, so skip the re-commit /
1256 // re-index and only reapply the offset history.
1257 state
1258 .matcher
1259 .reapply_resident_dictionary(dict.inner.offset_hist);
1260 return;
1261 }
1262 let prefer_copy_snapshot = crate::encoding::levels::config::restores_primed_snapshot(
1263 size_hint.map(crate::encoding::levels::config::source_size_ceil_log),
1264 cutoff_log,
1265 );
1266 let restored = prefer_copy_snapshot && state.matcher.restore_primed_dictionary(level);
1267 if !restored {
1268 state
1269 .matcher
1270 .prime_with_dictionary(dict.inner.dict_content.as_slice(), dict.inner.offset_hist);
1271 if prefer_copy_snapshot {
1272 state.matcher.capture_primed_dictionary(level);
1273 }
1274 }
1275}
1276
1277/// Record in `state` the strategy the matcher runs for the next frame (the
1278/// honoured public override, else the size- and dictionary-adaptive
1279/// resolution in `params`) and its pre-split tier; the literal gates and the
1280/// block splitter read these, and upstream indexes `splitLevels` by the
1281/// effective strategy too. Shared by the frame compressor and the streaming
1282/// encoder so both entry points cut and gate blocks identically.
1283pub(crate) fn sync_effective_strategy<M: Matcher>(
1284 state: &mut CompressState<M>,
1285 level: CompressionLevel,
1286 params: &crate::encoding::levels::config::LevelParams,
1287 strategy_override: Option<(crate::encoding::strategy::StrategyTag, u8)>,
1288) {
1289 match strategy_override {
1290 Some((tag, lazy_depth)) => {
1291 state.strategy_tag = tag;
1292 state.pre_split = Some(crate::encoding::levels::config::pre_split_for(
1293 tag, lazy_depth,
1294 ));
1295 }
1296 None => {
1297 state.strategy_tag = params.strategy_tag;
1298 state.pre_split = if matches!(level, CompressionLevel::Uncompressed) {
1299 None
1300 } else {
1301 params.pre_split()
1302 };
1303 }
1304 }
1305}
1306
1307/// Upstream `ZSTD_literalsCompressionIsDisabled`: an explicit
1308/// [`LiteralCompressionMode`] decides outright; under `Auto`, raw literals iff
1309/// the EFFECTIVE cParams are the fast strategy with `targetLength > 0`.
1310/// The effective strategy tag gates this (a strategy override can move a
1311/// negative level off fast). For the fast strategy the level table sets
1312/// `targetLength > 0` exactly on the negative (acceleration) rows (a CDict's
1313/// too), so where [`gate_target_length`] has no value `level < 0` is that
1314/// test.
1315pub(crate) fn literal_compression_disabled(
1316 strategy_tag: crate::encoding::strategy::StrategyTag,
1317 level: CompressionLevel,
1318 target_length_override: Option<u32>,
1319 mode: LiteralCompressionMode,
1320) -> bool {
1321 match mode {
1322 LiteralCompressionMode::Disable => true,
1323 LiteralCompressionMode::Enable => false,
1324 LiteralCompressionMode::Auto => {
1325 strategy_tag == crate::encoding::strategy::StrategyTag::Fast
1326 && target_length_override.map_or_else(
1327 || matches!(level, CompressionLevel::Level(n) if n < 0),
1328 |tl| tl > 0,
1329 )
1330 }
1331 }
1332}
1333
1334/// The `target_length` the raw-literals gate reads for a frame: the caller's,
1335/// else, for a dictionary frame a strategy knob moves onto the fast strategy,
1336/// the CDict row's, which the fast matcher runs as its step (upstream
1337/// `ZSTD_overrideCParams` replaces only the strategy, and
1338/// `ZSTD_literalsCompressionIsDisabled` reads the resulting cParams). Every
1339/// other frame's fast targetLength is what the level says, which
1340/// [`literal_compression_disabled`] derives itself.
1341pub(crate) fn gate_target_length(
1342 level: CompressionLevel,
1343 tuning: &FrameTuning,
1344 dictionary: Option<&EncoderDictionary>,
1345) -> Option<u32> {
1346 match (tuning.target_length, tuning.strategy, dictionary) {
1347 (Some(target_length), _, _) => Some(target_length),
1348 (None, Some((crate::encoding::strategy::StrategyTag::Fast, _)), Some(dict))
1349 if !dict.inner.dict_content.is_empty() =>
1350 {
1351 Some(
1352 crate::encoding::cparams::get_cdict_cparams(
1353 crate::encoding::levels::config::numeric_level(level),
1354 dict.sizes().serialized,
1355 &crate::encoding::parameters::ParamOverrides::default(),
1356 )
1357 .target_length,
1358 )
1359 }
1360 _ => None,
1361 }
1362}
1363
1364/// The level params the matcher's reset resolves for a frame: through the
1365/// dictionary's CDict tier when a dictionary is in play, else by source size.
1366/// Resolved without the caller's knobs: the frame gates read only the
1367/// strategy from these, and a strategy override replaces it
1368/// ([`sync_effective_strategy`]) on a dictionary frame as on any other.
1369pub(crate) fn resolve_frame_params(
1370 level: CompressionLevel,
1371 hint: Option<u64>,
1372 dictionary: Option<&EncoderDictionary>,
1373) -> crate::encoding::levels::config::LevelParams {
1374 match dictionary {
1375 Some(dict) if !dict.inner.dict_content.is_empty() => {
1376 crate::encoding::levels::config::resolve_level_params_with_dict(
1377 level,
1378 hint,
1379 dict.sizes(),
1380 &crate::encoding::parameters::ParamOverrides::default(),
1381 )
1382 .0
1383 }
1384 _ => crate::encoding::levels::config::resolve_level_params(level, hint),
1385 }
1386}
1387
1388pub(crate) struct CompressState<M: Matcher> {
1389 pub(crate) matcher: M,
1390 /// The parse every block took, recorded while this is `Some`, each block
1391 /// closed by a delimiter carrying its trailing literals.
1392 pub(crate) sequence_capture: Option<Vec<crate::encoding::sequences::ExternalSequence>>,
1393 /// Grid fingerprints of the frame's blocks, which is what tells the
1394 /// raw-skip that a block duplicating an earlier one is not the noise it
1395 /// looks like. See [`SeenContentGrid`](crate::encoding::incompressible::SeenContentGrid).
1396 pub(crate) seen_content: crate::encoding::incompressible::SeenContentGrid,
1397 /// CPU tier the literal gather runs under, resolved once when the
1398 /// compressor is built and dispatched on once per block.
1399 pub(crate) copy_kernel: crate::encoding::fastpath::FastpathKernel,
1400 pub(crate) last_huff_table: Option<crate::huff0::huff0_encoder::HuffmanTable>,
1401 /// Recycled `HuffmanTable` buffers: when a block clears or replaces
1402 /// `last_huff_table`, the old table parks here instead of dropping, so
1403 /// the next frame's dictionary entropy seed `clone_from`s into existing
1404 /// allocations. Without this, every dict-seeded frame whose last block
1405 /// ended raw/RLE paid a fresh two-Vec table clone per frame.
1406 pub(crate) huff_table_spare: Option<crate::huff0::huff0_encoder::HuffmanTable>,
1407 /// The Huffman weight builder's three buffers, kept across blocks and
1408 /// frames. The cheap build path takes a tree and two weight buffers per
1409 /// call, and it runs once per block plus once per split candidate wherever
1410 /// the block splitter probes, so taking them fresh each time was the single
1411 /// largest source of per-frame allocations: 1,880 of a frame's 4,000 at
1412 /// level 3. Lives here rather than in the per-block scratch, which the
1413 /// block emitter takes out of this state while a block is in flight.
1414 pub(crate) huff_weights: crate::huff0::huff0_encoder::WeightScratch,
1415 pub(crate) fse_tables: FseTables,
1416 pub(crate) block_scratch: crate::encoding::blocks::CompressedBlockScratch,
1417 /// The one allocation the match finder's tables and the per-block buffers
1418 /// are carved from, laid out at each frame start by
1419 /// [`Self::reset_for_frame`] and [`Self::finish_layout`].
1420 pub(crate) workspace: crate::encoding::workspace::Workspace,
1421 /// Offset history for repeat offset encoding: [rep0, rep1, rep2].
1422 /// Initialized to [1, 4, 8] per RFC 8878 §3.1.2.5.
1423 pub(crate) offset_hist: [u32; 3],
1424 /// Strategy tag resolved from the current `CompressionLevel` at every
1425 /// `matcher.reset()` call. Used by the literal-compression gates
1426 /// (`min_literals_to_compress`, `min_gain`) in
1427 /// `encoding::blocks::compressed` to mirror upstream zstd's strategy-aware
1428 /// thresholds (`zstd_compress_literals.c:114-127, 187-188`).
1429 ///
1430 /// **Invariant (required of every construction site):** must be
1431 /// initialized from the active `CompressionLevel` via
1432 /// `StrategyTag::for_compression_level`, and re-synced from the
1433 /// active level alongside every `matcher.reset()` call so the
1434 /// level-aware gates stay correct after a level change. The two
1435 /// reset sites that own this sync are `FrameCompressor::compress`
1436 /// and `StreamingEncoder::ensure_frame_started`. There is no
1437 /// `Default` impl — production constructors
1438 /// (`FrameCompressor::new`, `new_with_matcher`, the streaming
1439 /// encoder constructor) plumb this explicitly. Tests that build
1440 /// `CompressState` by hand must also supply a value.
1441 pub(crate) strategy_tag: crate::encoding::strategy::StrategyTag,
1442 /// Pre-split tier of the effective strategy (upstream `splitLevels`),
1443 /// synced with `strategy_tag`; `None` never pre-splits (raw frames).
1444 pub(crate) pre_split: Option<u8>,
1445 /// Whether the HUF literal table build runs the #167 table-log search
1446 /// (`true`) or the cheap single-build (`false`). The search is a clean
1447 /// ratio win over upstream zstd but costs ~1.5 us per literal section —
1448 /// negligible on large inputs, ~20% on small ones. The Fast and DoubleFast
1449 /// matchers are byte-faithful to upstream zstd, so the cheap path ties them;
1450 /// the search is therefore gated ON only for large (> 128 KiB) Fast and
1451 /// DoubleFast frames. Higher strategies always keep it (their matchers
1452 /// diverge, making the search load-bearing for ratio). Set per frame
1453 /// alongside `strategy_tag` via [`huf_search_enabled`].
1454 pub(crate) huf_optimal_search: bool,
1455 /// Mirror of upstream zstd's `ZSTD_literalsCompressionIsDisabled`
1456 /// (zstd_compress_internal.h): in the default (`auto`) literal-compression
1457 /// mode the literals section is emitted RAW (no Huffman) when
1458 /// `strategy == ZSTD_fast && targetLength > 0`. For the levels we resolve,
1459 /// that is exactly the negative levels (Fast strategy with `targetLength =
1460 /// -level > 0`; L1/L2 are Fast with `targetLength == 0`). C trades the
1461 /// literal-Huffman pass for speed there, so matching it keeps both the frame
1462 /// size and the encode cost in parity on the negative band. Set per frame
1463 /// alongside `strategy_tag`.
1464 pub(crate) literal_compression_disabled: bool,
1465 /// Whether a matched block may be cut into several blocks after matching,
1466 /// where its strategy and window call for it. `true` for every frame but a
1467 /// dictionary's analysis, which has to see one written block per matched
1468 /// block, as upstream's single-block analysis does (`ZDICT_countEStats`
1469 /// compresses with `ZSTD_compressBlock`, which never splits).
1470 pub(crate) post_split_allowed: bool,
1471}
1472
1473/// Whether the HUF literal build should run the #167 table-log search for a
1474/// frame of `source_size` bytes (see [`CompressState::huf_optimal_search`]).
1475/// Upstream gates the optimal-depth tableLog probe to
1476/// `HUF_OPTIMAL_DEPTH_THRESHOLD = ZSTD_btultra` (huf.h:117): only btultra /
1477/// btultra2 search the tableLog, every lower strategy (fast .. btopt) takes the
1478/// single-shot fast path (`HUF_optimalTableLog`, huf_compress.c:1284-1287).
1479/// Mirror that so our literal tableLog choice tracks upstream's instead of
1480/// spending the search to beat it on ratio at a speed cost.
1481pub(crate) fn huf_search_enabled(
1482 strategy: crate::encoding::strategy::StrategyTag,
1483 _source_size: Option<u64>,
1484) -> bool {
1485 use crate::encoding::strategy::StrategyTag;
1486 matches!(strategy, StrategyTag::BtUltra | StrategyTag::BtUltra2)
1487}
1488
1489impl<M: Matcher> CompressState<M> {
1490 /// Resets the matcher for the next frame at `level` and lets it lay its
1491 /// tables and history out in the workspace, reserving room behind them for
1492 /// the block buffers of blocks up to `block_target` bytes (capped by the
1493 /// frame's window). `ingest` is how the frame's input reaches the matcher,
1494 /// which sizes its history. [`Self::finish_layout`] completes the layout.
1495 pub(crate) fn reset_for_frame(
1496 &mut self,
1497 level: CompressionLevel,
1498 block_target: usize,
1499 ingest: crate::encoding::workspace::IngestPlan,
1500 ) {
1501 // A raw frame builds no compressed block, so it reserves no buffers
1502 // for one.
1503 let trailing_for = if ingest == crate::encoding::workspace::IngestPlan::Raw {
1504 crate::encoding::workspace::no_trailing
1505 } else {
1506 crate::encoding::blocks::CompressedBlockScratch::workspace_bytes
1507 };
1508 self.workspace
1509 .begin_layout(block_target, trailing_for, ingest);
1510 self.matcher.reset_in_workspace(level, &mut self.workspace);
1511 }
1512
1513 /// Carves the block buffers for blocks of up to `block_capacity` bytes, or
1514 /// of the frame's length when that is exact and shorter, opening the layout
1515 /// first when the matcher took no tables from it.
1516 ///
1517 /// # Panics
1518 ///
1519 /// Panics when `block_capacity` exceeds what the layout reserved for, which
1520 /// would mean the frame and the matcher disagree on the window.
1521 pub(crate) fn finish_layout(&mut self, block_capacity: usize) {
1522 if !self.workspace.is_open() {
1523 self.workspace.open(0, block_capacity);
1524 }
1525 let reserved = self.workspace.block_capacity();
1526 // A frame of exact length never carries a block longer than itself, and
1527 // the layout reserved for no more.
1528 let largest = block_capacity.min(self.workspace.ingest().exact_len().unwrap_or(usize::MAX));
1529 assert!(
1530 largest <= reserved,
1531 "a {largest}-byte block exceeds the {reserved} bytes the workspace reserved"
1532 );
1533 if self.workspace.ingest() == crate::encoding::workspace::IngestPlan::Raw {
1534 self.block_scratch.unbind();
1535 } else {
1536 self.block_scratch.bind(&mut self.workspace, reserved);
1537 }
1538 // Everything is laid out in the current allocation by now.
1539 self.workspace.release_retired();
1540 }
1541
1542 /// Heap bytes the compressor keeps between blocks and frames beyond the
1543 /// match finder: the FSE tables both slots of each axis hold, the block
1544 /// scratch with everything it holds (the splitter's workspace and the
1545 /// nested estimator scratch), and the context workspace the literal and
1546 /// sequence buffers are carved from.
1547 ///
1548 /// All of it survives a frame, so a caller sizing a context has to see it.
1549 pub(crate) fn retained_scratch_heap_size(&self) -> usize {
1550 self.fse_tables.heap_size()
1551 + self.block_scratch.retained_heap_size()
1552 + self.workspace.heap_bytes()
1553 }
1554
1555 /// Heap bytes of the parse recorded while capture is on, held until taken.
1556 pub(crate) fn captured_parse_heap_size(&self) -> usize {
1557 self.sequence_capture.as_ref().map_or(0, |capture| {
1558 capture.capacity()
1559 * core::mem::size_of::<crate::encoding::sequences::ExternalSequence>()
1560 })
1561 }
1562
1563 /// Clears `last_huff_table`, parking the table's buffers in
1564 /// `huff_table_spare` for reuse instead of dropping them.
1565 #[inline]
1566 pub(crate) fn clear_huff_table(&mut self) {
1567 if let Some(table) = self.last_huff_table.take() {
1568 self.park_huff_table(table);
1569 }
1570 }
1571
1572 /// Replaces `last_huff_table` with `table`, parking any displaced table
1573 /// in `huff_table_spare` for reuse.
1574 #[inline]
1575 pub(crate) fn replace_huff_table(&mut self, table: crate::huff0::huff0_encoder::HuffmanTable) {
1576 if let Some(old) = self.last_huff_table.replace(table) {
1577 self.park_huff_table(old);
1578 }
1579 }
1580
1581 /// Keeps a table's buffers rather than dropping them. The dictionary seed
1582 /// wants one spare to `clone_from` into, once per frame; every further
1583 /// table a block displaces goes to the weight builder instead, which takes
1584 /// one per block and per split candidate. Overwriting the single spare
1585 /// dropped the previous table on every block, so the builds that followed
1586 /// allocated their buffers again.
1587 #[inline]
1588 pub(crate) fn park_huff_table(&mut self, table: crate::huff0::huff0_encoder::HuffmanTable) {
1589 if self.huff_table_spare.is_none() {
1590 self.huff_table_spare = Some(table);
1591 } else {
1592 self.huff_weights.recycle(table);
1593 }
1594 }
1595}
1596
1597/// Per-frame setup resolved once by [`FrameCompressor::prepare_frame`] and
1598/// consumed by the block loop + [`FrameCompressor::finish_frame`]. Lets the
1599/// owned `compress()` and the borrowed one-shot path share identical
1600/// reset / dict-prime / entropy-seed setup and frame-tail emission.
1601struct FramePrep {
1602 window_size: u64,
1603 use_dictionary_state: bool,
1604 initial_size_hint: Option<u64>,
1605}
1606
1607/// Initial capacity for the `all_blocks` accumulator, by source-size hint.
1608/// The frame header is written only after all input is read (so
1609/// Frame_Content_Size is known), so compressed blocks accumulate in memory
1610/// first. Seed-size tiers (mirrors upstream zstd `ZSTD_CStreamOutSize` naming):
1611/// - tiny (`<= 4 KiB` hint): payload-bound seed, `>=` anything a tiny input's
1612/// compressed output could need.
1613/// - small (`<= 64 KiB` hint): absorbs one or two `Vec::extend` doublings
1614/// without over-allocating.
1615/// - default (one upstream zstd block, `130 KiB`): the value the rest of the encoder
1616/// is sized around; larger inputs amortise the first doublings cheaply and
1617/// the residue is dominated by internal `compress_block_encoded` buffers.
1618///
1619/// Shared by the owned (`run_owned_block_loop`) and borrowed
1620/// (`run_borrowed_block_loop`) paths so the tier table can't drift between them.
1621///
1622/// `block_capacity` (the active `targetCBlockSize` cap, or the 128 KiB
1623/// format ceiling) bounds every tier: with a small target the first
1624/// allocation tracks one capped block + header/checksum slack instead of
1625/// keeping the upstream zstd-sized floor that only later growth respects.
1626fn initial_all_blocks_cap(initial_size_hint: Option<u64>, block_capacity: usize) -> usize {
1627 const TINY_THRESHOLD: u64 = 4 * 1024;
1628 const SMALL_THRESHOLD: u64 = 64 * 1024;
1629 const TINY_CAP: usize = 4 * 1024;
1630 const SMALL_CAP: usize = 16 * 1024;
1631 const DEFAULT_CAP: usize = 130 * 1024;
1632 let first_block_cap = block_capacity + 3 + 16;
1633 match initial_size_hint {
1634 Some(h) if h <= TINY_THRESHOLD => TINY_CAP.min(first_block_cap),
1635 Some(h) if h <= SMALL_THRESHOLD => SMALL_CAP.min(first_block_cap),
1636 _ => DEFAULT_CAP.min(first_block_cap),
1637 }
1638}
1639
1640/// The block ceiling a target block size and a maximum block size leave
1641/// together: the smaller one that is set, or the format's 128 KiB.
1642pub(crate) fn block_ceiling(target: Option<u32>, max: Option<u32>) -> usize {
1643 let ceiling = match (target, max) {
1644 (Some(target), Some(max)) => target.min(max),
1645 (Some(size), None) | (None, Some(size)) => size,
1646 (None, None) => crate::common::MAX_BLOCK_SIZE,
1647 };
1648 ceiling as usize
1649}
1650
1651/// Per-block feeder for `run_owned_block_loop`.
1652///
1653/// `fill_block` appends source bytes to `buf` (which already holds any
1654/// carried pre-split suffix) until `buf.len() == block_capacity` or the
1655/// source is exhausted, returning `(bytes_appended, reached_eof)`.
1656/// `reached_eof` is true when no more input follows this block: either the
1657/// block could not be filled to `block_capacity`, or it filled exactly and the
1658/// source is confirmed exhausted (the slice knows its length; the reader probes
1659/// one byte ahead). An input that is an exact multiple of the block size
1660/// therefore marks its final full block `last_block` rather than emitting a
1661/// spurious trailing empty block.
1662///
1663/// The slice impl exists so the slice entry points
1664/// (`compress_independent_frame_into`, `compress_oneshot_*` fallbacks)
1665/// append with one `extend_from_slice` — the generic reader impl must
1666/// `resize` an initialized target region before `Read::read` can fill it,
1667/// which costs a zero-fill memset of the whole block on every frame.
1668///
1669/// `buf` is the matcher's history on the in-place path and a staging `Vec`
1670/// otherwise.
1671pub(crate) trait OwnedBlockSource {
1672 fn fill_block<B: IngestBuffer>(
1673 &mut self,
1674 buf: &mut B,
1675 block_capacity: usize,
1676 size_hint_remaining: Option<u64>,
1677 ) -> (usize, bool);
1678
1679 /// Bytes left, when the source knows them exactly. The matcher reserves
1680 /// room for a read before it is made, so a read held to this asks for no
1681 /// room past the end of the input.
1682 fn exact_remaining(&self) -> Option<usize> {
1683 None
1684 }
1685}
1686
1687impl OwnedBlockSource for &[u8] {
1688 fn exact_remaining(&self) -> Option<usize> {
1689 Some(self.len())
1690 }
1691
1692 fn fill_block<B: IngestBuffer>(
1693 &mut self,
1694 buf: &mut B,
1695 block_capacity: usize,
1696 _size_hint_remaining: Option<u64>,
1697 ) -> (usize, bool) {
1698 let want = block_capacity - buf.len();
1699 let take = want.min(self.len());
1700 buf.extend_from_slice(&self[..take]);
1701 *self = &self[take..];
1702 // EOF when this fill could not top the block to `block_capacity`
1703 // (`take < want`) OR it exactly consumed the last input bytes
1704 // (`self` now empty). The slice knows its own length, so a block that
1705 // exactly fills capacity at end-of-input is reported as the final
1706 // block here — the loop marks it `last_block` instead of emitting a
1707 // spurious trailing empty block on the next iteration. Mirrors the C
1708 // encoder, which marks the last real block last on `ZSTD_e_end`.
1709 (take, take < want || self.is_empty())
1710 }
1711}
1712
1713/// Adapter routing a generic [`Read`] source through [`OwnedBlockSource`]:
1714/// preserves the historical sizing behaviour — an initialized target region
1715/// bounded by the source-size hint, grown (doubling, capped) only when the
1716/// hint under-counted.
1717/// `peeked` holds a single look-ahead byte: when a block fills exactly to
1718/// `block_capacity`, `fill_block` reads one more byte to learn whether the
1719/// stream ended on that boundary. A `None` from that probe sets EOF (so the
1720/// just-filled block is marked last, mirroring the C encoder on `ZSTD_e_end`);
1721/// a byte is stashed here and prepended to the next block instead of leaking a
1722/// spurious trailing empty block when the input is an exact multiple of the
1723/// block size.
1724pub(crate) struct ReaderBlockSource<Rd> {
1725 pub(crate) reader: Rd,
1726 peeked: Option<u8>,
1727}
1728
1729impl<Rd> ReaderBlockSource<Rd> {
1730 pub(crate) fn new(reader: Rd) -> Self {
1731 Self {
1732 reader,
1733 peeked: None,
1734 }
1735 }
1736}
1737
1738impl<Rd: Read> OwnedBlockSource for ReaderBlockSource<Rd> {
1739 fn fill_block<B: IngestBuffer>(
1740 &mut self,
1741 buf: &mut B,
1742 block_capacity: usize,
1743 size_hint_remaining: Option<u64>,
1744 ) -> (usize, bool) {
1745 let start = buf.len();
1746 let mut filled = start;
1747 let mut reached_eof = false;
1748 // Prepend the look-ahead byte read past the previous full block. In
1749 // stream order it follows any carried pre-split suffix already in
1750 // `buf`, so it is appended after that suffix and counted as part of
1751 // this block's appended bytes.
1752 if let Some(b) = self.peeked.take() {
1753 buf.push(b);
1754 filled += 1;
1755 }
1756 // Size the read buffer to the bytes this block actually expects
1757 // rather than always zero-filling a full MAX_BLOCK_SIZE: a small
1758 // frame otherwise pays a 128 KiB `resize(_, 0)` memset per block
1759 // just to read a few KiB (the zero-fill past `filled` is then
1760 // truncated away).
1761 //
1762 // Overflow-free by construction (no `saturating_*` masking):
1763 // `filled <= block_capacity` always (the read only ever targets
1764 // `[filled..len]` with `len <= block_capacity`, and a carried-over
1765 // pre-split suffix is a `split_off` below `block_capacity`), so
1766 // `block_capacity - filled` never underflows; pinning `remaining`
1767 // to `block_capacity` before the `usize` cast keeps the cast and
1768 // the final add within `usize` on every target.
1769 let initial_target = match size_hint_remaining {
1770 Some(remaining) => {
1771 let remaining = remaining.min(block_capacity as u64) as usize;
1772 filled + remaining.min(block_capacity - filled)
1773 }
1774 // Unknown hint, or an inexact hint already met by prior blocks:
1775 // read against the full block window.
1776 None => block_capacity,
1777 };
1778 if buf.len() < initial_target {
1779 buf.resize(initial_target, 0);
1780 }
1781 loop {
1782 if reached_eof || filled == block_capacity {
1783 break;
1784 }
1785 if filled == buf.len() {
1786 // Hint under-counted the block; grow toward block_capacity
1787 // (doubling, capped) so reading continues without paying a
1788 // full-buffer zero up front. `len <= block_capacity` so the
1789 // double stays well within `usize`; `filled < block_capacity`
1790 // here (the `== block_capacity` break fired otherwise), so
1791 // `filled + 1 <= block_capacity`.
1792 let grow_to = (buf.len() * 2).clamp(filled + 1, block_capacity);
1793 buf.resize(grow_to, 0);
1794 }
1795 let read_end = buf.len();
1796 let new_bytes = self.reader.read(&mut buf[filled..read_end]).unwrap();
1797 if new_bytes == 0 {
1798 reached_eof = true;
1799 break;
1800 }
1801 filled += new_bytes;
1802 }
1803 // Look ahead one byte when the block filled exactly to capacity: a
1804 // 0-byte read means the stream ended on the block boundary, so this
1805 // block is the last one (the loop marks it `last_block`); otherwise
1806 // stash the byte for the next block. Without this, an input that is an
1807 // exact multiple of the block size would emit a spurious trailing
1808 // empty block (the next iteration reads 0 and serializes an empty
1809 // last Raw block). A blocking reader's probe read is consistent with
1810 // the existing pull model — the next `fill_block` would block on the
1811 // same byte anyway.
1812 if !reached_eof && filled == block_capacity {
1813 let mut probe = [0u8; 1];
1814 if self.reader.read(&mut probe).unwrap() == 0 {
1815 reached_eof = true;
1816 } else {
1817 self.peeked = Some(probe[0]);
1818 }
1819 }
1820 buf.truncate(filled);
1821 (filled - start, reached_eof)
1822 }
1823}
1824
1825impl<R: Read, W: Write> FrameCompressor<R, W, MatchGeneratorDriver> {
1826 /// Create a new `FrameCompressor`
1827 pub fn new(compression_level: CompressionLevel) -> Self {
1828 Self {
1829 uncompressed_data: None,
1830 compressed_data: None,
1831 compression_level,
1832 dictionary: None,
1833 source_size_hint: None,
1834 state: CompressState {
1835 matcher: MatchGeneratorDriver::new(1024 * 128, 1),
1836 sequence_capture: None,
1837 copy_kernel: crate::encoding::fastpath::select_kernel(),
1838 last_huff_table: None,
1839 huff_table_spare: None,
1840 huff_weights: Default::default(),
1841 seen_content: Default::default(),
1842 fse_tables: FseTables::new(),
1843 block_scratch: crate::encoding::blocks::CompressedBlockScratch::new(),
1844 workspace: crate::encoding::workspace::Workspace::new(),
1845 offset_hist: [1, 4, 8],
1846 strategy_tag: crate::encoding::strategy::StrategyTag::for_compression_level(
1847 compression_level,
1848 ),
1849 pre_split: crate::encoding::levels::config::level_pre_split(compression_level)
1850 .map(|tier| tier as u8),
1851 huf_optimal_search: true,
1852 literal_compression_disabled: matches!(
1853 compression_level,
1854 crate::encoding::CompressionLevel::Level(n) if n < 0
1855 ),
1856 post_split_allowed: true,
1857 },
1858 magicless: false,
1859 content_checksum: false,
1860 pre_split_disabled: false,
1861 content_size_flag: true,
1862 dict_id_flag: true,
1863 target_block_size: None,
1864 max_block_size: None,
1865 #[cfg(feature = "hash")]
1866 hasher: XxHash64::with_seed(0),
1867 #[cfg(feature = "lsm")]
1868 frame_emit_info: None,
1869 #[cfg(all(feature = "lsm", feature = "hash"))]
1870 per_block_checksums_enabled: false,
1871 #[cfg(all(feature = "lsm", feature = "hash"))]
1872 block_checksums: None,
1873 #[cfg(feature = "lsm")]
1874 block_decompressed_sizes: alloc::vec::Vec::new(),
1875 tuning: FrameTuning::default(),
1876 }
1877 }
1878
1879 /// Ask `producer` for each block's parse in place of the built-in
1880 /// matchers; `None` removes it. With `fallback` a block the producer fails
1881 /// on is matched as usual; without, it goes out uncompressed and
1882 /// [`Self::take_sequence_producer_failure`] reports it. With long-distance
1883 /// matching on, the long matches are taken outright and the producer
1884 /// parses the literals between them.
1885 pub fn set_sequence_producer(
1886 &mut self,
1887 producer: Option<alloc::boxed::Box<dyn crate::encoding::sequences::SequenceProducer>>,
1888 fallback: bool,
1889 ) {
1890 self.state.matcher.set_sequence_producer(producer, fallback);
1891 }
1892
1893 /// Whether a block went out uncompressed because the producer failed on it
1894 /// with no fallback, since this was last asked.
1895 pub fn take_sequence_producer_failure(&mut self) -> bool {
1896 self.state.matcher.take_producer_failure()
1897 }
1898
1899 /// Configure fine-grained compression parameters.
1900 ///
1901 /// Resets the base [`CompressionLevel`] to the parameters' level and installs the per-knob overrides
1902 /// (window/hash/chain/search logs, strategy, LDM) applied at the next
1903 /// frame. Pass `None`-equivalent (a builder that overrides nothing)
1904 /// to fall back to plain level-based compression.
1905 ///
1906 /// ```rust
1907 /// use structured_zstd::encoding::{
1908 /// CompressionLevel, CompressionParameters, FrameCompressor, Strategy,
1909 /// };
1910 /// let params = CompressionParameters::builder(CompressionLevel::Level(19))
1911 /// .strategy(Strategy::Btultra2)
1912 /// .enable_long_distance_matching(true)
1913 /// .build()
1914 /// .unwrap();
1915 /// let mut compressor: FrameCompressor = FrameCompressor::new(CompressionLevel::Default);
1916 /// compressor.set_parameters(¶ms);
1917 /// let compressed = compressor.compress_independent_frame(b"some data to compress");
1918 /// assert!(!compressed.is_empty());
1919 /// ```
1920 pub fn set_parameters(&mut self, params: &crate::encoding::CompressionParameters) {
1921 self.compression_level = params.level();
1922 let overrides = params.overrides();
1923 self.tuning = FrameTuning::from_overrides(&overrides);
1924 // Keep `state.strategy_tag` consistent immediately so the borrowed
1925 // one-shot eligibility gate (`borrowed_eligible`) and literal gates
1926 // are correct even before the next `compress()` re-sync. Resolve it
1927 // size-adaptively (same `resolve_level_params` path `prepare_frame`
1928 // uses) so a hint already set here yields the same strategy the matcher
1929 // will run, not the bare level-only mapping.
1930 // The dictionary counts only when the frame will prime it (same gate
1931 // as `prepare_frame`'s `use_dictionary_state`): uncompressed mode
1932 // ignores an attached dictionary and has no CDict tier to resolve.
1933 let with_dictionary = !matches!(self.compression_level, CompressionLevel::Uncompressed)
1934 && self.state.matcher.supports_dictionary_priming();
1935 let params = self.resolve_frame_params(self.source_size_hint, with_dictionary);
1936 self.sync_effective_strategy(¶ms);
1937 self.state.huf_optimal_search =
1938 huf_search_enabled(self.state.strategy_tag, self.source_size_hint);
1939 self.state.literal_compression_disabled = literal_compression_disabled(
1940 self.state.strategy_tag,
1941 self.compression_level,
1942 gate_target_length(
1943 self.compression_level,
1944 &self.tuning,
1945 self.dictionary.as_ref().filter(|_| with_dictionary),
1946 ),
1947 self.tuning.literal_compression,
1948 );
1949 self.state.matcher.set_param_overrides(Some(overrides));
1950 }
1951
1952 /// Whether the borrowed (no per-block history copy) one-shot loop is
1953 /// valid for an `input_len`-byte slice under the resolved `prep`.
1954 ///
1955 /// `Uncompressed` resolves to `StrategyTag::Fast` but must emit stored
1956 /// Raw blocks, which the borrowed loop's
1957 /// `compress_block_encoded_borrowed` (RLE/raw-fast/compressed) does NOT
1958 /// do, so exclude it; it then takes the owned path's dedicated
1959 /// Uncompressed arm.
1960 ///
1961 /// No window-size gate: over-window inputs are handled too. The owned
1962 /// path bounds matches to the last `advertised_window` bytes via
1963 /// `window_low` and evicts its history, sliding the table's stored
1964 /// positions down with it; the borrowed path
1965 /// computes the identical `window_low = block_end - advertised_window`
1966 /// and the kernel rejects any hash candidate below it, while the
1967 /// per-position `put` during the scan keeps in-window slots current,
1968 /// so it produces byte-identical output to the owned (evicting) path
1969 /// without ever copying the input into `history`, even when the input
1970 /// far exceeds the window.
1971 ///
1972 /// BUT gate on `input_len <= u32::MAX`: the Fast kernel stores ABSOLUTE
1973 /// positions in a `u32` hash table, and the borrowed scan walks
1974 /// absolute input offsets up to `block_end == input.len()`. Past 4 GiB
1975 /// those offsets truncate / overflow the `u32` position math
1976 /// (`base_off + ip0 as u32`, `window_low`), panicking or corrupting.
1977 /// The owned/evicting path keeps the scanned window bounded (positions
1978 /// stay small), so >4 GiB inputs fall back to it.
1979 fn borrowed_eligible(&self, input_len: usize, prep: &FramePrep) -> bool {
1980 if matches!(self.compression_level, CompressionLevel::Uncompressed)
1981 || input_len > u32::MAX as usize
1982 {
1983 return false;
1984 }
1985 if prep.use_dictionary_state {
1986 // The borrowed dict scan runs in VIRTUAL `[dict][input]` coordinates,
1987 // so the position space is `dict_content.len() + input_len`, not just
1988 // `input_len`. A large attached dictionary plus an otherwise-allowed
1989 // input can exceed the `u32` floor the kernel asserts — fall back to
1990 // the owned (copy) path in that case.
1991 let fits_u32 = self
1992 .dictionary
1993 .as_ref()
1994 .and_then(|dict| dict.inner.dict_content.len().checked_add(input_len))
1995 .is_some_and(|virtual_len| virtual_len <= u32::MAX as usize);
1996 if !fits_u32 {
1997 return false;
1998 }
1999 // Dictionary frames: only the Simple (Fast) backend in attach mode
2000 // has a borrowed (no input copy) dict scan. Copy-mode dict frames
2001 // and the other backends still take the owned path. The reset laid
2002 // the history out on a prediction of this, which must hold, or the
2003 // history is either unused or outgrown.
2004 let attached = self.state.matcher.borrowed_dict_supported();
2005 debug_assert_eq!(
2006 attached,
2007 self.state.matcher.frame_scans_in_place(),
2008 "the reset's in-place prediction disagrees with the primed dictionary mode",
2009 );
2010 return attached;
2011 }
2012 // The borrowed (no-copy, in-place over-window) scan exists for the
2013 // Simple (Fast), Dfast, and Row backends, and for the HashChain
2014 // backend's lazy CHAIN parser; BT/optimal (BinaryTree search) stay on
2015 // the owned path. Every borrowed scan applies the per-position
2016 // `window_low = abs_ip - advertised_window` offset cap so over-window
2017 // inputs are matched in place (no input->history copy), matching C's
2018 // continuous-index + windowLow one-shot behaviour. The reset decided
2019 // it, and laid the history out to match.
2020 self.state.matcher.frame_scans_in_place()
2021 }
2022
2023 /// Compress `input` as one frame's worth of blocks into `out` (appended
2024 /// from its current end): the borrowed in-place loop when
2025 /// [`Self::borrowed_eligible`], else the owned (history-copying) loop fed
2026 /// an in-place `&[u8]` cursor. Returns `total_uncompressed`; the caller
2027 /// emits the frame header (before this call, when the content size is
2028 /// known) or the drain tail.
2029 fn run_one_frame(&mut self, input: &[u8], prep: &FramePrep, out: &mut Vec<u8>) -> u64 {
2030 if self.borrowed_eligible(input.len(), prep) {
2031 // A reused Dfast matcher's tables hold earlier frames, which its
2032 // borrowed kernel (numbering this input from zero) would read as
2033 // its own: emptied, the frame is the one a fresh matcher writes
2034 // (upstream: first and later uses of a context compress the
2035 // same). Measured cheaper than the owned path, which retires them
2036 // by moving the floor but copies the input and runs slower: level 3
2037 // on a reused ZSTD_compress2 context, the owned path costs 3.5% at
2038 // 1 KiB frames, 4.1% at 4 KiB and 10.8% at 1 MiB, and gains at most
2039 // 1% below 256 bytes, within noise. The emptied tables are sized
2040 // from the frame's own source, so a tiny frame clears a tiny table.
2041 if !self.state.matcher.borrowed_frame_is_independent() {
2042 self.state.matcher.forget_earlier_frames();
2043 }
2044 self.run_borrowed_block_loop(input, out)
2045 } else {
2046 let mut cursor: &[u8] = input;
2047 self.run_owned_block_loop(&mut cursor, prep.initial_size_hint, true, out)
2048 }
2049 }
2050
2051 /// Compress one contiguous `&[u8]` as a single independent Zstd frame,
2052 /// writing the frame bytes into `out` (its previous contents are
2053 /// replaced and its allocation reused), reusing this compressor's heavy
2054 /// state across calls.
2055 ///
2056 /// This is the reusable-compression-context (CCtx-equivalent) entry
2057 /// point, mirroring C `ZSTD_compress2` over a reused `ZSTD_CCtx`:
2058 /// construct ONE `FrameCompressor` and call this in a loop to emit N
2059 /// independent, self-describing frames (each carrying its own header,
2060 /// blocks, and checksum, decodable in isolation, with no cross-frame
2061 /// match history). Every call resets the per-frame state: only the
2062 /// allocations are kept, so the
2063 /// dominant per-frame setup cost (table allocation + dictionary prime)
2064 /// is paid once instead of N times. Passing the same `out` buffer each
2065 /// call additionally reuses the output allocation, matching C's
2066 /// caller-owned `dst` buffer (no per-frame output allocation).
2067 ///
2068 /// Reusing the context + `out` across many small frames (the typical
2069 /// per-block-frame workload) is far cheaper than a fresh
2070 /// [`compress_slice_to_vec`](crate::encoding::compress_slice_to_vec)
2071 /// per block, which allocates and primes from scratch each time.
2072 ///
2073 /// The input is read in place: no [`Self::set_source`] /
2074 /// [`Self::set_drain`] setup is required, and the input lifetime is not
2075 /// baked into the compressor type, so successive calls may pass slices
2076 /// with unrelated lifetimes. When the Fast (Simple) backend is active
2077 /// and no dictionary is set, the matcher references the input directly
2078 /// (no per-block history copy); other backends / dictionary use copy
2079 /// each block into history exactly as the streaming
2080 /// [`compress`](Self::compress) path does. The source-size hint is
2081 /// derived from the input length on every call, so per-frame table
2082 /// sizing tracks each frame's actual size regardless of any earlier
2083 /// hint.
2084 ///
2085 /// A sticky dictionary set via
2086 /// [`set_dictionary`](Self::set_dictionary) (or its variants) is primed
2087 /// into every frame, mirroring `ZSTD_CCtx_loadDictionary` /
2088 /// `ZSTD_CCtx_refCDict`.
2089 ///
2090 /// # Panics
2091 ///
2092 /// Panics on encoder error, matching [`Self::compress`] and
2093 /// [`compress_slice_to_vec`](crate::encoding::compress_slice_to_vec).
2094 pub fn compress_independent_frame_into(&mut self, input: &[u8], out: &mut Vec<u8>) {
2095 // Size the next frame from the actual payload, not a stale hint a
2096 // previous call may have left behind (a wrong hint would change the
2097 // resolved window/header and could flip borrowed eligibility).
2098 self.source_size_hint = Some(input.len() as u64);
2099 let prep = self.prepare_frame(crate::encoding::workspace::IngestPlan::Slice(input.len()));
2100 // Content size is known up front (one-shot), so write the frame
2101 // header FIRST and emit blocks STRAIGHT into `out` — no separate
2102 // `all_blocks` accumulator and no header+blocks copy (which was the
2103 // dominant per-frame memmove + the only un-amortized per-frame alloc
2104 // even when the compressor is reused).
2105 let total_uncompressed = input.len() as u64;
2106 let emit_checksum = cfg!(feature = "hash") && self.content_checksum;
2107 let checksum_len = if emit_checksum { 4 } else { 0 };
2108 out.clear();
2109 // Reserve the header plus ONE block's worst case up front; the block
2110 // loops then grow `out` from the compression ratio observed so far
2111 // (`reserve_for_next_block`). Reserving `compress_bound(input_len)`
2112 // here held a whole-input-sized allocation for the entire frame —
2113 // ~100 MiB peak on a 100 MiB stream whose compressed output is a few
2114 // MiB, where the reference implementation's context peaks at
2115 // window-sized state. Small frames (<= one block) still get their
2116 // full bound in one shot, so the reused-`out` steady state is
2117 // unchanged. 18 = max frame header (magic 4 + descriptor 1 + window
2118 // 1 + dict id 4 + FCS 8).
2119 let first_block_bound = input.len().min(self.block_capacity()) + 3;
2120 out.reserve(18 + first_block_bound + checksum_len);
2121 self.append_frame_header(total_uncompressed, &prep, out);
2122 let header_len = out.len();
2123 let _ = self.run_one_frame(input, &prep, out);
2124 #[cfg(feature = "hash")]
2125 if self.content_checksum {
2126 out.extend_from_slice(&(self.hasher.finish() as u32).to_le_bytes());
2127 }
2128 #[cfg(feature = "lsm")]
2129 {
2130 let blocks_end = out.len() - checksum_len;
2131 self.populate_frame_emit_info(header_len, &out[header_len..blocks_end], emit_checksum);
2132 }
2133 #[cfg(not(feature = "lsm"))]
2134 let _ = header_len;
2135 }
2136
2137 /// Convenience wrapper over [`Self::compress_independent_frame_into`]
2138 /// that allocates and returns a fresh `Vec` per call. Prefer the
2139 /// `_into` form in tight per-block-frame loops to reuse one output
2140 /// buffer across frames (the CCtx-equivalent zero-per-call-alloc
2141 /// output, matching C's caller-owned `dst`).
2142 ///
2143 /// ```rust
2144 /// use structured_zstd::encoding::{FrameCompressor, CompressionLevel};
2145 /// let mut cctx: FrameCompressor = FrameCompressor::new(CompressionLevel::Default);
2146 /// let frame_a = cctx.compress_independent_frame(b"first block payload");
2147 /// let frame_b = cctx.compress_independent_frame(b"second block payload");
2148 /// assert!(!frame_a.is_empty() && !frame_b.is_empty());
2149 /// ```
2150 pub fn compress_independent_frame(&mut self, input: &[u8]) -> Vec<u8> {
2151 let mut out = Vec::new();
2152 self.compress_independent_frame_into(input, &mut out);
2153 out
2154 }
2155
2156 /// Borrowed one-shot block loop: walks `input` in `MAX_BLOCK_SIZE`
2157 /// strides (the Fast backend never pre-splits, so boundaries match the
2158 /// owned loop), scanning each block range in place against the
2159 /// borrowed window via `compress_block_encoded_borrowed`, with no
2160 /// per-block copy into the history. Returns `(all_blocks,
2161 /// total_uncompressed)`. Caller guarantees Fast backend + no
2162 /// dictionary; over-window inputs are fine (matches are bounded by
2163 /// `window_low` exactly as the owned evicting path).
2164 fn run_borrowed_block_loop(&mut self, input: &[u8], out: &mut Vec<u8>) -> u64 {
2165 // Blocks are appended to `out` starting here. `out` may already hold
2166 // the frame header (the one-shot compress-into-Vec path writes it
2167 // first, since the content size is known up front, and the loop
2168 // emits blocks straight after it — no separate `all_blocks` Vec and
2169 // no header+blocks copy). Output-size reads below are taken RELATIVE
2170 // to `blocks_start` so a header prefix never skews the upstream zstd split
2171 // `savings` gate (which would change block boundaries / wire output).
2172 let blocks_start = out.len();
2173 let total_uncompressed = input.len() as u64;
2174 // Empty input: emit a single empty last Raw block (mirrors the
2175 // owned loop's empty-file special case).
2176 if input.is_empty() {
2177 let header = BlockHeader {
2178 last_block: true,
2179 block_type: crate::blocks::block::BlockType::Raw,
2180 block_size: 0,
2181 };
2182 header.serialize(out);
2183 #[cfg(feature = "lsm")]
2184 self.block_decompressed_sizes.push(0);
2185 #[cfg(all(feature = "lsm", feature = "hash"))]
2186 if let Some(checksums) = self.block_checksums.as_mut() {
2187 checksums.push(xxh64_block_low32(&[]));
2188 }
2189 return total_uncompressed;
2190 }
2191 // SAFETY: `input` outlives this call (held by the caller across
2192 // the call) and is not mutated. Only the Simple backend is active
2193 // (gated by `compress_oneshot_borrowed`).
2194 unsafe {
2195 self.state.matcher.set_borrowed_window(input);
2196 }
2197 // Fixed for the whole frame, so resolved before the loop takes the
2198 // state.
2199 let block_capacity = self.block_capacity();
2200 let pre_split = self.pre_split_level();
2201 let dict_active =
2202 self.dictionary.is_some() && self.state.matcher.supports_dictionary_priming();
2203 // Panic-safety: clear the borrowed `(ptr, len)` on EVERY exit,
2204 // including an unwind from an `assert!` inside the block loop, so
2205 // a caught-and-reused compressor never retains a dangling window.
2206 // (The next frame's `reset()` also clears it before any read, but
2207 // this guard makes the invariant local and unwind-proof.) The guard
2208 // holds the state's `&mut` and the loop reaches the state through it:
2209 // a raw pointer kept beside the loop's own `&mut self.state` would be
2210 // invalidated by it (Stacked Borrows), making the drop's access
2211 // undefined.
2212 struct ClearBorrowedOnDrop<'a>(&'a mut CompressState<MatchGeneratorDriver>);
2213 impl Drop for ClearBorrowedOnDrop<'_> {
2214 fn drop(&mut self) {
2215 self.0.matcher.clear_borrowed_window();
2216 }
2217 }
2218 let guard = ClearBorrowedOnDrop(&mut self.state);
2219 let mut start = 0usize;
2220 while start < input.len() {
2221 reserve_for_next_block(
2222 out,
2223 blocks_start,
2224 start as u64,
2225 input.len() - start,
2226 block_capacity,
2227 );
2228 // Upstream zstd `ZSTD_compress_frameChunk`: size each block via the cheap
2229 // fingerprint pre-splitter so a full 128 KiB block is cut at a
2230 // statistical boundary when it pays. `savings = consumed -
2231 // produced` mirrors the upstream zstd gate (the first block and
2232 // incompressible input keep the full 128 KiB). The borrowed window
2233 // already spans the whole input, so a smaller block is just a
2234 // narrower `(block_start, block_end)` range into it.
2235 let savings = start as i64 - (out.len() - blocks_start) as i64;
2236 // Borrowed path only: warm the pre-split window before the
2237 // cache-cold strided fingerprint read. Gated to exactly the
2238 // conditions under which `optimal_block_size` reads `block`
2239 // (a pre-split level, a full 128 KiB block remaining, the
2240 // block-size cap admits a full block, and `savings >= 3` so the
2241 // splitter actually runs) — so non-pre-split levels, the first
2242 // block, and the trailing partial block pay nothing. See
2243 // `warm_presplit_window`.
2244 if savings >= 3
2245 && input.len() - start >= MAX_BLOCK_SIZE as usize
2246 && block_capacity >= MAX_BLOCK_SIZE as usize
2247 && pre_split.is_some()
2248 {
2249 warm_presplit_window(&input[start..start + MAX_BLOCK_SIZE as usize]);
2250 }
2251 let block_len = optimal_block_size_with(
2252 pre_split,
2253 &input[start..],
2254 input.len() - start,
2255 block_capacity,
2256 savings,
2257 );
2258 let end = (start + block_len).min(input.len());
2259 let block = &input[start..end];
2260 let last_block = end == input.len();
2261 #[cfg(feature = "hash")]
2262 if self.content_checksum {
2263 self.hasher.write(block);
2264 }
2265 crate::encoding::levels::compress_block_encoded_borrowed(
2266 &mut *guard.0,
2267 self.compression_level,
2268 last_block,
2269 block,
2270 start,
2271 end,
2272 out,
2273 dict_active,
2274 #[cfg(feature = "lsm")]
2275 Some(&mut self.block_decompressed_sizes),
2276 #[cfg(all(feature = "lsm", feature = "hash"))]
2277 self.block_checksums.as_mut(),
2278 );
2279 start = end;
2280 }
2281 // `guard` drops here, clearing the borrowed window.
2282 drop(guard);
2283 total_uncompressed
2284 }
2285}
2286
2287impl<R: Read, W: Write, M: Matcher> FrameCompressor<R, W, M> {
2288 /// Create a new `FrameCompressor` with a custom matching algorithm implementation
2289 pub fn new_with_matcher(matcher: M, compression_level: CompressionLevel) -> Self {
2290 Self::with_matcher(matcher, compression_level)
2291 }
2292
2293 /// Attach a pre-parsed dictionary to be used for subsequent compressions.
2294 ///
2295 /// In compressed modes, the dictionary id is written only when the active
2296 /// matcher supports dictionary priming.
2297 /// Uncompressed mode and non-priming matchers ignore the attached dictionary
2298 /// at encode time.
2299 pub fn set_dictionary(
2300 &mut self,
2301 dictionary: crate::decoding::Dictionary,
2302 ) -> Result<Option<EncoderDictionary>, crate::decoding::errors::DictionaryDecodeError> {
2303 self.attach_dictionary(EncoderDictionary::from_dictionary(dictionary))
2304 }
2305
2306 /// Parse and attach a dictionary blob, in either of the two forms upstream
2307 /// `ZSTD_CCtx_loadDictionary` takes (`ZSTD_dct_auto`): a
2308 /// [serialized](crate::decoding::Dictionary::is_serialized) dictionary,
2309 /// and anything else is raw content, which is why any file can
2310 /// be handed to `zstd -D`. Raw content has no id, so the frame records none
2311 /// and the decoder must be given the same bytes explicitly.
2312 ///
2313 /// A serialized blob parses through the encoder-only path (skips the
2314 /// FSE/HUF decode lookup-table build the encoder never reads); the entropy
2315 /// ENCODER tables — and thus the emitted frame — are identical to a full
2316 /// parse. To reject anything but a serialized dictionary, parse with
2317 /// [`EncoderDictionary::from_bytes`] (upstream `ZSTD_dct_fullDict`) and
2318 /// attach the result.
2319 ///
2320 /// An empty buffer is how the same upstream entry point is told there is
2321 /// no dictionary: it clears whatever was attached and succeeds, returning
2322 /// it, rather than reporting a dictionary too small to use.
2323 pub fn set_dictionary_from_bytes(
2324 &mut self,
2325 raw_dictionary: &[u8],
2326 ) -> Result<Option<EncoderDictionary>, crate::decoding::errors::DictionaryDecodeError> {
2327 if raw_dictionary.is_empty() {
2328 return Ok(self.clear_dictionary());
2329 }
2330 self.attach_dictionary(EncoderDictionary::from_serialized_or_raw_content(
2331 raw_dictionary,
2332 )?)
2333 }
2334}
2335
2336impl<R: Read, W: Write, M: Matcher, D: DictionaryHold> FrameCompressor<R, W, M, D> {
2337 /// A compressor with a custom matcher that compresses every frame with
2338 /// `dictionary`, or with none, held as given: a `&EncoderDictionary` is
2339 /// borrowed for the compressor's lifetime, so a compressor built for one
2340 /// frame reads a dictionary prepared once without taking a reference on it.
2341 ///
2342 /// # Errors
2343 /// As [`Self::set_encoder_dictionary`].
2344 ///
2345 /// # Examples
2346 /// ```
2347 /// use structured_zstd::encoding::sequences::{ExternalSequence, SequenceReplay};
2348 /// use structured_zstd::encoding::{CompressionLevel, EncoderDictionary, FrameCompressor};
2349 ///
2350 /// let dictionary = EncoderDictionary::from_serialized_or_raw_content(b"shared history").unwrap();
2351 /// let input = b"shared history, once more";
2352 /// // One block of literals; a parse may also reach into the dictionary.
2353 /// let parse = [ExternalSequence::delimiter(input.len() as u32)];
2354 /// let mut frame = Vec::new();
2355 /// let mut compressor: FrameCompressor<&[u8], &mut Vec<u8>, _, _> =
2356 /// FrameCompressor::new_with_matcher_and_dictionary(
2357 /// SequenceReplay::new(&parse, 1 << 17).with_dictionary(),
2358 /// CompressionLevel::Default,
2359 /// Some(&dictionary),
2360 /// )
2361 /// .unwrap();
2362 /// compressor.set_source(&input[..]);
2363 /// compressor.set_drain(&mut frame);
2364 /// compressor.compress();
2365 /// assert!(compressor.dictionary().is_some());
2366 /// drop(compressor);
2367 /// assert!(!frame.is_empty());
2368 /// ```
2369 pub fn new_with_matcher_and_dictionary(
2370 matcher: M,
2371 compression_level: CompressionLevel,
2372 dictionary: Option<D>,
2373 ) -> Result<Self, crate::decoding::errors::DictionaryDecodeError> {
2374 let mut compressor = Self::with_matcher(matcher, compression_level);
2375 if let Some(dictionary) = dictionary {
2376 compressor.attach_dictionary(dictionary)?;
2377 }
2378 Ok(compressor)
2379 }
2380
2381 fn with_matcher(matcher: M, compression_level: CompressionLevel) -> Self {
2382 Self {
2383 uncompressed_data: None,
2384 compressed_data: None,
2385 dictionary: None,
2386 source_size_hint: None,
2387 state: CompressState {
2388 matcher,
2389 sequence_capture: None,
2390 copy_kernel: crate::encoding::fastpath::select_kernel(),
2391 last_huff_table: None,
2392 huff_table_spare: None,
2393 huff_weights: Default::default(),
2394 seen_content: Default::default(),
2395 fse_tables: FseTables::new(),
2396 block_scratch: crate::encoding::blocks::CompressedBlockScratch::new(),
2397 workspace: crate::encoding::workspace::Workspace::new(),
2398 offset_hist: [1, 4, 8],
2399 strategy_tag: crate::encoding::strategy::StrategyTag::for_compression_level(
2400 compression_level,
2401 ),
2402 pre_split: crate::encoding::levels::config::level_pre_split(compression_level)
2403 .map(|tier| tier as u8),
2404 huf_optimal_search: true,
2405 literal_compression_disabled: matches!(
2406 compression_level,
2407 crate::encoding::CompressionLevel::Level(n) if n < 0
2408 ),
2409 post_split_allowed: true,
2410 },
2411 compression_level,
2412 magicless: false,
2413 content_checksum: false,
2414 pre_split_disabled: false,
2415 content_size_flag: true,
2416 dict_id_flag: true,
2417 target_block_size: None,
2418 max_block_size: None,
2419 #[cfg(feature = "hash")]
2420 hasher: XxHash64::with_seed(0),
2421 #[cfg(feature = "lsm")]
2422 frame_emit_info: None,
2423 #[cfg(all(feature = "lsm", feature = "hash"))]
2424 per_block_checksums_enabled: false,
2425 #[cfg(all(feature = "lsm", feature = "hash"))]
2426 block_checksums: None,
2427 #[cfg(feature = "lsm")]
2428 block_decompressed_sizes: alloc::vec::Vec::new(),
2429 tuning: FrameTuning::default(),
2430 }
2431 }
2432
2433 /// Enable or disable magicless frame format (`ZSTD_f_zstd1_magicless`).
2434 ///
2435 /// When set to `true`, emitted frames omit the 4-byte magic number
2436 /// prefix. The matching decoder must be configured to expect a
2437 /// magicless stream — wire-format only round-trips with a
2438 /// magicless-aware decoder.
2439 pub fn set_magicless(&mut self, magicless: bool) {
2440 self.magicless = magicless;
2441 }
2442
2443 /// Enable or disable the trailing XXH64 content checksum
2444 /// (semantics of upstream `ZSTD_c_checksumFlag`). Default `false`,
2445 /// matching the upstream library default (`ZSTD_c_checksumFlag = 0`)
2446 /// so out-of-the-box frames carry the same layout and pay the same
2447 /// costs as the reference implementation.
2448 ///
2449 /// When `false`, emitted frames set `Content_Checksum_flag = 0` and carry
2450 /// no trailing digest; such frames are valid (RFC 8878) and decode
2451 /// correctly in any [`ContentChecksum`](crate::decoding::ContentChecksum)
2452 /// mode. Without the `hash` feature no checksum is emitted regardless of
2453 /// this setting.
2454 pub fn set_content_checksum(&mut self, emit: bool) {
2455 self.content_checksum = emit;
2456 }
2457
2458 /// Enable or disable recording `Frame_Content_Size` in the frame header
2459 /// when the total size is known (semantics of upstream
2460 /// `ZSTD_c_contentSizeFlag`). Default `true`, matching upstream. With
2461 /// the flag off the header carries a window descriptor instead (and the
2462 /// single-segment layout, which requires an FCS, is disabled).
2463 pub fn set_content_size_flag(&mut self, emit: bool) {
2464 self.content_size_flag = emit;
2465 }
2466
2467 /// Enable or disable recording the dictionary ID in the frame header
2468 /// when a dictionary is attached (semantics of upstream
2469 /// `ZSTD_c_dictIDFlag`). Default `true`, matching upstream. Frames
2470 /// emitted with the flag off still decode when the decoder is handed
2471 /// the dictionary explicitly.
2472 pub fn set_dictionary_id_flag(&mut self, emit: bool) {
2473 self.dict_id_flag = emit;
2474 }
2475
2476 /// Set an upper bound on emitted block sizes (semantics of upstream
2477 /// `ZSTD_c_targetCBlockSize`): every physical block's payload is capped
2478 /// at `target` bytes (+3-byte block header on the wire), trading some
2479 /// ratio for bounded per-block latency. The value is clamped to
2480 /// `[MIN_TARGET_BLOCK_SIZE, MAX_BLOCK_SIZE]` (the upstream bounds).
2481 /// `None` removes the target.
2482 pub fn set_target_block_size(&mut self, target: Option<u32>) {
2483 self.target_block_size = target.map(|t| {
2484 t.clamp(
2485 crate::common::MIN_TARGET_BLOCK_SIZE,
2486 crate::common::MAX_BLOCK_SIZE,
2487 )
2488 });
2489 }
2490
2491 /// Cap every block of the frame at `size` bytes (semantics of upstream
2492 /// `ZSTD_c_maxBlockSize`), for a decoder that holds only that much of a
2493 /// block at a time. Clamped to `[MIN_MAX_BLOCK_SIZE, MAX_BLOCK_SIZE]`;
2494 /// `None` restores the format's 128 KiB. Combines with
2495 /// [`Self::set_target_block_size`]: the smaller of the two applies.
2496 ///
2497 /// ```
2498 /// use structured_zstd::encoding::{CompressionLevel, FrameCompressor};
2499 ///
2500 /// let mut compressor = FrameCompressor::new(CompressionLevel::Default);
2501 /// compressor.set_max_block_size(Some(1024));
2502 /// let mut frame = Vec::new();
2503 /// compressor.set_source(&[7u8; 4096][..]);
2504 /// compressor.set_drain(&mut frame);
2505 /// compressor.compress();
2506 /// ```
2507 pub fn set_max_block_size(&mut self, size: Option<u32>) {
2508 self.max_block_size = size.map(|s| {
2509 s.clamp(
2510 crate::common::MIN_MAX_BLOCK_SIZE,
2511 crate::common::MAX_BLOCK_SIZE,
2512 )
2513 });
2514 }
2515
2516 /// Record the parse every block takes, from the next one on, until turned
2517 /// off: its sequences with plain offsets, each block closed by a delimiter
2518 /// carrying its trailing literals, and a block written out unsearched as a
2519 /// delimiter alone (C `ZSTD_generateSequences`). An empty input records
2520 /// nothing, as upstream's does. The frames themselves are unchanged.
2521 ///
2522 /// ```
2523 /// use structured_zstd::encoding::{CompressionLevel, FrameCompressor};
2524 ///
2525 /// let input = b"capture capture capture capture".repeat(10);
2526 /// let mut compressor: FrameCompressor = FrameCompressor::new(CompressionLevel::Default);
2527 /// compressor.set_sequence_capture(true);
2528 /// compressor.compress_independent_frame(&input);
2529 /// let parse = compressor.take_captured_sequences();
2530 /// let covered: usize = parse.iter().map(|s| (s.lit_len + s.match_len) as usize).sum();
2531 /// assert_eq!(covered, input.len());
2532 /// ```
2533 pub fn set_sequence_capture(&mut self, capture: bool) {
2534 self.state.sequence_capture = capture.then(Vec::new);
2535 }
2536
2537 /// The parse recorded since [`Self::set_sequence_capture`] turned it on,
2538 /// or since it was last taken; empty when nothing records it.
2539 pub fn take_captured_sequences(&mut self) -> Vec<crate::encoding::sequences::ExternalSequence> {
2540 self.state
2541 .sequence_capture
2542 .as_mut()
2543 .map(core::mem::take)
2544 .unwrap_or_default()
2545 }
2546
2547 /// The configured block ceiling before the window is considered: the
2548 /// smaller of the target and the maximum, or the format's 128 KiB.
2549 fn block_ceiling(&self) -> usize {
2550 block_ceiling(self.target_block_size, self.max_block_size)
2551 }
2552
2553 /// The active block-size cap: the configured ceiling, held to the window.
2554 fn block_capacity(&self) -> usize {
2555 let requested = self.block_ceiling();
2556 // Upstream zstd sizes a block as `MIN(maxBlockSize, windowSize)`
2557 // (`ZSTD_compress.c`). A block wider than the window can never be
2558 // held by the matcher, which asserts on it, so a small `window_log`
2559 // must shrink the block rather than overrun the window.
2560 let window = self.state.matcher.window_size() as usize;
2561 if window == 0 {
2562 requested
2563 } else {
2564 requested.min(window)
2565 }
2566 }
2567
2568 /// Before calling [FrameCompressor::compress] you need to set the source.
2569 ///
2570 /// This is the data that is compressed and written into the drain.
2571 pub fn set_source(&mut self, uncompressed_data: R) -> Option<R> {
2572 self.uncompressed_data.replace(uncompressed_data)
2573 }
2574
2575 /// Before calling [FrameCompressor::compress] you need to set the drain.
2576 ///
2577 /// As the compressor compresses data, the drain serves as a place for the output to be writte.
2578 pub fn set_drain(&mut self, compressed_data: W) -> Option<W> {
2579 self.compressed_data.replace(compressed_data)
2580 }
2581
2582 /// Diagnostic switch: cut full blocks only, never pre-split. Used by
2583 /// the sequence capture so its block structure matches upstream's
2584 /// `ZSTD_generateSequences` (see `pre_split_disabled`).
2585 #[cfg(feature = "bench-internals")]
2586 pub fn set_pre_split_disabled(&mut self, disabled: bool) {
2587 self.pre_split_disabled = disabled;
2588 }
2589
2590 /// The pre-split level the block loops apply: the effective strategy's
2591 /// tier (`state.pre_split`) unless the diagnostic switch is on.
2592 fn pre_split_level(&self) -> Option<usize> {
2593 if self.pre_split_disabled {
2594 None
2595 } else {
2596 self.state.pre_split.map(usize::from)
2597 }
2598 }
2599
2600 /// The level params the matcher's reset resolves for the next frame
2601 /// (dictionary-aware when a dictionary will be used).
2602 fn resolve_frame_params(
2603 &self,
2604 hint: Option<u64>,
2605 with_dictionary: bool,
2606 ) -> crate::encoding::levels::config::LevelParams {
2607 resolve_frame_params(
2608 self.compression_level,
2609 hint,
2610 attached(&self.dictionary).filter(|_| with_dictionary),
2611 )
2612 }
2613
2614 /// Record the strategy the matcher actually runs for the next frame (a
2615 /// public-parameter override, else the size- and dictionary-adaptive
2616 /// resolution in `params`) and its pre-split tier: the literal gates and
2617 /// the block splitter read these, and upstream indexes `splitLevels` by
2618 /// the effective strategy too.
2619 fn sync_effective_strategy(&mut self, params: &crate::encoding::levels::config::LevelParams) {
2620 sync_effective_strategy(
2621 &mut self.state,
2622 self.compression_level,
2623 params,
2624 self.tuning.strategy,
2625 );
2626 }
2627
2628 /// Provide a hint about the total uncompressed size for the next frame.
2629 ///
2630 /// When set, the encoder selects smaller hash tables and windows for
2631 /// small inputs, matching the C zstd source-size-class behavior.
2632 ///
2633 /// This hint applies only to frame payload bytes (`size`). Dictionary
2634 /// history is primed separately and does not inflate the hinted size or
2635 /// advertised frame window.
2636 /// Must be called before [`compress`](Self::compress).
2637 pub fn set_source_size_hint(&mut self, size: u64) {
2638 self.source_size_hint = Some(size);
2639 }
2640
2641 /// Total heap bytes this compressor's allocations hold, excluding the
2642 /// inline struct: the match-finder tables / history / recycled buffers and
2643 /// the primed-dictionary snapshot (via the matcher), the retained
2644 /// Huffman tables (active + recycled spare), the retained dictionary
2645 /// content, the cached dictionary entropy tables (literals Huffman +
2646 /// LL/ML/OF FSE), and the per-block sidecar buffers. Lets a context
2647 /// report its true footprint through `ZSTD_sizeof_CCtx`.
2648 pub fn heap_size(&self) -> usize {
2649 let mut total = self.state.matcher.heap_size();
2650 total += self
2651 .state
2652 .last_huff_table
2653 .as_ref()
2654 .map_or(0, |table| table.heap_size());
2655 total += self
2656 .state
2657 .huff_table_spare
2658 .as_ref()
2659 .map_or(0, |table| table.heap_size());
2660 // The weight builder's buffers are kept between blocks and frames, so
2661 // a reused compressor holds them for as long as it lives.
2662 total += self.state.huff_weights.heap_size();
2663 total += self.state.retained_scratch_heap_size();
2664 total += self.state.seen_content.heap_size();
2665 total += self.state.captured_parse_heap_size();
2666 // A borrowed dictionary belongs to its owner, as upstream's
2667 // ZSTD_sizeof_CCtx counts a local dictionary and not a referenced CDict.
2668 if D::OWNED {
2669 total += attached(&self.dictionary).map_or(0, EncoderDictionary::heap_size);
2670 }
2671 #[cfg(all(feature = "lsm", feature = "hash"))]
2672 {
2673 total += self
2674 .block_checksums
2675 .as_ref()
2676 .map_or(0, |v| v.capacity() * core::mem::size_of::<u32>());
2677 }
2678 #[cfg(feature = "lsm")]
2679 {
2680 total += self.block_decompressed_sizes.capacity() * core::mem::size_of::<u32>();
2681 }
2682 total
2683 }
2684
2685 /// Compress the uncompressed data from the provided source as one Zstd frame and write it to the provided drain
2686 ///
2687 /// This will repeatedly call [Read::read] on the source to fill up blocks until the source returns 0 on the read call.
2688 /// All compressed blocks are buffered in memory so that the frame header can include the
2689 /// `Frame_Content_Size` field (which requires knowing the total uncompressed size). The
2690 /// entire frame — header, blocks, and optional checksum — is then written to the drain
2691 /// at the end. This means peak memory usage is O(compressed_size).
2692 ///
2693 /// To avoid endlessly encoding from a potentially endless source (like a network socket) you can use the
2694 /// [Read::take] function.
2695 pub fn compress(&mut self) {
2696 // `prepare_frame` / `finish_frame` are shared with the borrowed
2697 // one-shot path, so both run the same reset / dict-prime /
2698 // entropy-seed setup and frame tail.
2699 let prep = self.prepare_frame(crate::encoding::workspace::IngestPlan::Stream);
2700 // Take the reader out so `run_owned_block_loop` can borrow it
2701 // mutably alongside `&mut self` (the rest of the loop touches
2702 // `self.state` / `self.hasher`, disjoint from the reader). Restored
2703 // before the frame tail so a reused compressor keeps its source.
2704 //
2705 // Deliberately NOT restored on unwind: if the block loop panics the
2706 // source has been partially consumed, so handing it back would let a
2707 // `catch_unwind` caller "successfully" compress the remaining tail
2708 // from an arbitrary midpoint — silent data corruption. Leaving the
2709 // slot empty makes any post-panic reuse fail loudly at the `expect`
2710 // below (matcher/entropy state is equally unre-usable after an
2711 // unwind; the reference implementation likewise requires a context
2712 // reset after an error).
2713 let mut source = self
2714 .uncompressed_data
2715 .take()
2716 .expect("source must be set via set_source before compress()");
2717 // Streaming drain: the content size is only known at EOF, so the
2718 // frame header can't precede the blocks — accumulate them in a local
2719 // buffer and let `finish_frame` write header + blocks to the drain.
2720 let mut all_blocks: Vec<u8> = Vec::with_capacity(initial_all_blocks_cap(
2721 prep.initial_size_hint,
2722 self.block_capacity(),
2723 ));
2724 let mut block_source = ReaderBlockSource::new(&mut source);
2725 let total_uncompressed = self.run_owned_block_loop(
2726 &mut block_source,
2727 prep.initial_size_hint,
2728 false,
2729 &mut all_blocks,
2730 );
2731 self.uncompressed_data = Some(source);
2732 self.finish_frame(all_blocks, total_uncompressed, &prep);
2733 }
2734
2735 /// Write every matched block as one block, never cut after matching: what
2736 /// a dictionary's analysis needs to tell each block's kind from the frame.
2737 #[cfg(feature = "dict-builder")]
2738 pub(crate) fn forbid_post_split(&mut self) {
2739 self.state.post_split_allowed = false;
2740 }
2741
2742 /// [`Self::compress`] for a source of `total` bytes, the frame written
2743 /// over `out` rather than to the drain: with the content size known up
2744 /// front the header goes first and the blocks straight after it, with no
2745 /// block accumulator and no copy into the drain. The frame is the one
2746 /// `compress` writes for the same source, hint and settings.
2747 #[cfg(feature = "dict-builder")]
2748 pub(crate) fn compress_known_into(&mut self, total: u64, out: &mut Vec<u8>) {
2749 let prep = self.prepare_frame(crate::encoding::workspace::IngestPlan::Stream);
2750 let mut source = self
2751 .uncompressed_data
2752 .take()
2753 .expect("source must be set via set_source before compress_known_into()");
2754 out.clear();
2755 self.append_frame_header(total, &prep, out);
2756 let header_len = out.len();
2757 let mut block_source = ReaderBlockSource::new(&mut source);
2758 let read = self.run_owned_block_loop(&mut block_source, prep.initial_size_hint, false, out);
2759 self.uncompressed_data = Some(source);
2760 assert_eq!(
2761 read, total,
2762 "the source held another length than its frame header"
2763 );
2764 #[cfg(feature = "hash")]
2765 if self.content_checksum {
2766 out.extend_from_slice(&(self.hasher.finish() as u32).to_le_bytes());
2767 }
2768 #[cfg(feature = "lsm")]
2769 {
2770 let emit_checksum = cfg!(feature = "hash") && self.content_checksum;
2771 let blocks_end = out.len() - if emit_checksum { 4 } else { 0 };
2772 self.populate_frame_emit_info(header_len, &out[header_len..blocks_end], emit_checksum);
2773 }
2774 #[cfg(not(feature = "lsm"))]
2775 let _ = header_len;
2776 }
2777
2778 /// Resets the compressor for the next frame, whose input reaches the
2779 /// match finder as `ingest` says.
2780 fn prepare_frame(&mut self, ingest: crate::encoding::workspace::IngestPlan) -> FramePrep {
2781 // The raw-skip's memory of what this frame has already emitted. Frames
2782 // are independent, so carrying it over would let one frame's content
2783 // hold the skip off for the next; the allocation is kept.
2784 self.state.seen_content.reset_for_frame();
2785 // Reset per-frame introspection state so a re-used compressor
2786 // doesn't carry over the previous frame's layout/checksums.
2787 #[cfg(feature = "lsm")]
2788 {
2789 self.frame_emit_info = None;
2790 // Always captured under lsm (drives `decompressed_byte_range`);
2791 // clear, keep the allocation for a reused compressor.
2792 self.block_decompressed_sizes.clear();
2793 }
2794 #[cfg(all(feature = "lsm", feature = "hash"))]
2795 {
2796 if self.per_block_checksums_enabled {
2797 self.block_checksums = Some(alloc::vec::Vec::new());
2798 } else {
2799 self.block_checksums = None;
2800 }
2801 }
2802 let initial_size_hint = self.source_size_hint;
2803 let use_dictionary_state =
2804 !matches!(self.compression_level, CompressionLevel::Uncompressed)
2805 && self.state.matcher.supports_dictionary_priming()
2806 && self.dictionary.is_some();
2807 if let Some(size_hint) = self.source_size_hint.take() {
2808 // Keep source-size hint scoped to payload bytes; dictionary priming
2809 // is applied separately and should not force larger matcher sizing.
2810 self.state.matcher.set_source_size_hint(size_hint);
2811 }
2812 // Hand the matcher the dictionary's sizes so the frame runs the CDict's
2813 // cParams tier and sizes its dictionary tables from the content. Set
2814 // before `reset` (which consumes it) and only when a dictionary will
2815 // actually be primed.
2816 if use_dictionary_state && let Some(dict) = attached(&self.dictionary) {
2817 self.state.matcher.set_dictionary_size_hint(dict.sizes());
2818 }
2819 // Clearing buffers to allow re-using of the compressor
2820 let block_target = self.block_ceiling();
2821 // Raw frames emit straight from the staged buffer and never consult
2822 // the match finder, so they keep no history.
2823 let ingest = if matches!(self.compression_level, CompressionLevel::Uncompressed) {
2824 crate::encoding::workspace::IngestPlan::Raw
2825 } else {
2826 ingest
2827 };
2828 self.state
2829 .reset_for_frame(self.compression_level, block_target, ingest);
2830 let block_capacity = self.block_capacity();
2831 self.state.finish_layout(block_capacity);
2832 self.state.offset_hist = [1, 4, 8];
2833 // Sync `state.strategy_tag` to the level resolved at this reset so
2834 // the literal-compression gates (`min_literals_to_compress` /
2835 // `min_gain` in `encoding::blocks::compressed`) see the correct
2836 // strategy for the next frame. Frame-by-frame level changes go
2837 // through this same `compress()` entry point, so re-syncing here
2838 // covers level switches without touching the matcher dispatch.
2839 // A public-parameter strategy override (#27) wins over the level's
2840 // derived tag so the literal-compression gates and dict-attach cutoff
2841 // below see the strategy the matcher actually runs. Otherwise resolve
2842 // the strategy SIZE-ADAPTIVELY through the same path the matcher's reset
2843 // used (`resolve_level_params` -> `get_cparams`, the port of upstream
2844 // `ZSTD_getCParams`): a small frame promotes a level to a higher
2845 // strategy (e.g. L13 over a <=16 KiB frame becomes btultra). Re-deriving
2846 // from the bare level would make the literal-compression / HUF-search
2847 // gates disagree with the matcher's actual parse on small frames (the
2848 // gate would think btlazy2 and skip the HUF table-log search the btultra
2849 // frame runs, costing a few bytes on small literal sections).
2850 // A dictionary frame runs the CDict's strategy (upstream
2851 // `ZSTD_resetCCtx_usingCDict`), so resolve through the same
2852 // dictionary-aware path the matcher's reset took.
2853 let params = self.resolve_frame_params(initial_size_hint, use_dictionary_state);
2854 self.sync_effective_strategy(¶ms);
2855 // `initial_size_hint` (captured before the `.take()` above) — by here
2856 // `self.source_size_hint` is None.
2857 self.state.huf_optimal_search =
2858 huf_search_enabled(self.state.strategy_tag, initial_size_hint);
2859 // Recomputed per frame from the persisted override: attaching or
2860 // clearing a dictionary after `set_parameters` can move the strategy
2861 // this gate reads.
2862 self.state.literal_compression_disabled = literal_compression_disabled(
2863 self.state.strategy_tag,
2864 self.compression_level,
2865 gate_target_length(
2866 self.compression_level,
2867 &self.tuning,
2868 attached(&self.dictionary).filter(|_| use_dictionary_state),
2869 ),
2870 self.tuning.literal_compression,
2871 );
2872 let cached_entropy = if use_dictionary_state {
2873 attached(&self.dictionary).map(|dict| &dict.inner.entropy)
2874 } else {
2875 None
2876 };
2877 if use_dictionary_state && let Some(dict) = attached(&self.dictionary) {
2878 // This state drives sequence encoding, while matcher priming below updates
2879 // the match generator's internal repeat-offset history for match finding.
2880 self.state.offset_hist = dict.inner.offset_hist;
2881 load_frame_dictionary(
2882 &mut self.state,
2883 self.compression_level,
2884 dict,
2885 initial_size_hint,
2886 );
2887 }
2888 if let Some(cache) = cached_entropy {
2889 // Refill an empty slot from the recycled spare before
2890 // `clone_from`: `Option::clone_from(None ← Some)` falls back to
2891 // a fresh clone (two Vec allocations), while `Some ← Some`
2892 // delegates to the table's buffer-reusing `clone_from`. Frames
2893 // whose last block cleared the table would otherwise re-clone
2894 // the dict seed every frame.
2895 match &cache.huff {
2896 Some(src) => {
2897 if self.state.last_huff_table.is_none() {
2898 self.state.last_huff_table = self.state.huff_table_spare.take();
2899 }
2900 match &mut self.state.last_huff_table {
2901 Some(dst) => dst.clone_from(src),
2902 slot => *slot = Some(src.clone()),
2903 }
2904 }
2905 None => self.state.clear_huff_table(),
2906 }
2907 } else {
2908 self.state.clear_huff_table();
2909 }
2910 // Whatever the last frame ended on is about to be replaced. Keep it as
2911 // this frame's build buffer rather than dropping it, or a reused
2912 // compressor that emits one custom-table block per frame allocates a
2913 // table per axis every frame.
2914 self.state.fse_tables.park_previous_before_frame();
2915 // `clone_from` keeps frame-to-frame seeding cheap for reused compressors by
2916 // reusing existing allocations where possible instead of reallocating every frame.
2917 if let Some(cache) = cached_entropy {
2918 self.state
2919 .fse_tables
2920 .ll_previous
2921 .clone_from(&cache.ll_previous);
2922 self.state
2923 .fse_tables
2924 .ml_previous
2925 .clone_from(&cache.ml_previous);
2926 self.state
2927 .fse_tables
2928 .of_previous
2929 .clone_from(&cache.of_previous);
2930 } else {
2931 self.state.fse_tables.ll_previous = None;
2932 self.state.fse_tables.ml_previous = None;
2933 self.state.fse_tables.of_previous = None;
2934 }
2935 let ll_entropy = cached_entropy.and_then(|cache| match cache.ll_previous.as_ref() {
2936 Some(PreviousFseTable::Custom(table)) => Some(table.as_ref()),
2937 _ => None,
2938 });
2939 let ml_entropy = cached_entropy.and_then(|cache| match cache.ml_previous.as_ref() {
2940 Some(PreviousFseTable::Custom(table)) => Some(table.as_ref()),
2941 _ => None,
2942 });
2943 let of_entropy = cached_entropy.and_then(|cache| match cache.of_previous.as_ref() {
2944 Some(PreviousFseTable::Custom(table)) => Some(table.as_ref()),
2945 _ => None,
2946 });
2947 self.state.matcher.seed_dictionary_entropy(
2948 self.state.last_huff_table.as_ref(),
2949 ll_entropy,
2950 ml_entropy,
2951 of_entropy,
2952 );
2953 #[cfg(feature = "hash")]
2954 {
2955 self.hasher = XxHash64::with_seed(0);
2956 }
2957 let window_size = self.state.matcher.window_size();
2958 assert!(
2959 window_size != 0,
2960 "matcher reported window_size == 0, which is invalid"
2961 );
2962 FramePrep {
2963 window_size,
2964 use_dictionary_state,
2965 initial_size_hint,
2966 }
2967 }
2968
2969 /// Owned streaming block loop: reads blocks from the caller-provided
2970 /// `source` reader, optionally pre-splits, hashes for the content
2971 /// checksum, and emits each block via `compress_block_encoded`,
2972 /// accumulating the block bytes. Returns `(all_blocks,
2973 /// total_uncompressed)`. The source is passed in (rather than read
2974 /// from `self.uncompressed_data`) so the streaming `compress` path can
2975 /// feed the configured reader while the slice paths
2976 /// (`compress_oneshot_borrowed`, `compress_independent_frame`) feed an
2977 /// in-place `&[u8]` cursor without baking its lifetime into the
2978 /// compressor type.
2979 fn run_owned_block_loop<S: OwnedBlockSource>(
2980 &mut self,
2981 source: &mut S,
2982 initial_size_hint: Option<u64>,
2983 // Whether `initial_size_hint` is the input's exact length (the
2984 // one-shot slice paths) or a caller-provided estimate (the streaming
2985 // `Read` path, where `set_source_size_hint` is advisory). An exact
2986 // hint drives the one-shot ratio reservation; an estimate is only
2987 // trusted up to a small lookahead past the bytes actually read.
2988 hint_is_exact: bool,
2989 out: &mut Vec<u8>,
2990 ) -> u64 {
2991 if matches!(self.compression_level, CompressionLevel::Uncompressed) {
2992 return self.run_raw_block_loop(source, initial_size_hint, out);
2993 }
2994 // Compressed blocks are appended to `out` from its current end. The
2995 // streaming drain path passes a fresh buffer (the frame header is
2996 // written to the drain afterward, since Frame_Content_Size is only
2997 // known once the reader hits EOF); the one-shot compress-into-Vec
2998 // path passes `out` already holding the header. The upstream zstd split
2999 // `savings` gate below accumulates block-relative (`before_len`)
3000 // output deltas, so a header prefix never skews it.
3001 let blocks_start = out.len();
3002 let mut total_uncompressed: u64 = 0;
3003 let mut reached_eof = false;
3004 let mut savings = 0i64;
3005 // The matcher's history was laid out for the whole frame when the
3006 // context was (`frame_history_bytes`), so the blocks read into it below
3007 // never reallocate it.
3008 let block_capacity = self.block_capacity();
3009 loop {
3010 // Top the matcher's uncommitted input back up to one block. Reading
3011 // straight into its history means neither the block nor a pre-split
3012 // remainder is ever copied: the remainder of the previous iteration
3013 // is already there, uncommitted, and heads this block, as upstream's
3014 // ZSTD_compress_frameChunk() runs over one contiguous input buffer.
3015 if !reached_eof {
3016 // Remaining-bytes expectation for the reader source's sizing
3017 // (`None` = unknown, or an inexact hint already met by prior
3018 // blocks). The slice source appends directly and ignores it.
3019 let size_hint_remaining = match initial_size_hint {
3020 Some(hint) if hint > total_uncompressed => Some(hint - total_uncompressed),
3021 _ => None,
3022 };
3023 // A carried remainder is what a pre-split left of a full block,
3024 // so it is shorter than one. Held to what an exact source has
3025 // left: its history was laid out for its bytes and no more.
3026 let want = (block_capacity - self.state.matcher.uncommitted_input().len())
3027 .min(source.exact_remaining().unwrap_or(usize::MAX));
3028 let (appended, eof) = self.state.matcher.fill_in_place(want, &mut |buf| {
3029 source.fill_block(buf, buf.len() + want, size_hint_remaining)
3030 });
3031 total_uncompressed += appended as u64;
3032 reached_eof = eof;
3033 }
3034 let available = self.state.matcher.uncommitted_input().len();
3035 let mut last_block = reached_eof;
3036 let remaining_for_split = if reached_eof {
3037 available
3038 } else {
3039 block_capacity
3040 };
3041 // Length this block will actually claim. The pre-split pass may
3042 // shorten it; the remainder stays uncommitted and heads the next
3043 // block.
3044 let mut block_len = available;
3045 let fixed_len = self.state.matcher.fixed_block_len();
3046 if let Some(len) = fixed_len {
3047 // A block of no bytes would read below as the empty frame's
3048 // terminal block and drop the input; such a cut is ignored.
3049 if len > 0 && len < available {
3050 block_len = len;
3051 last_block = false;
3052 }
3053 } else if available == block_capacity {
3054 let split_at = optimal_block_size_with(
3055 self.pre_split_level(),
3056 self.state.matcher.uncommitted_input(),
3057 remaining_for_split,
3058 block_capacity,
3059 savings,
3060 );
3061 // The splitter sees byte statistics only; a cut it proposes
3062 // inside a match the matcher already knows of is moved out.
3063 let split_at = if split_at < available {
3064 self.state.matcher.settle_block_cut(available, split_at)
3065 } else {
3066 split_at
3067 };
3068 if split_at < available {
3069 block_len = split_at;
3070 last_block = false;
3071 }
3072 }
3073 // As we read, hash that data too (skipped when the content
3074 // checksum is disabled).
3075 #[cfg(feature = "hash")]
3076 if self.content_checksum {
3077 self.hasher
3078 .write(&self.state.matcher.uncommitted_input()[..block_len]);
3079 }
3080 // Per-physical-block XXH64 (low 32 bits) for the optional
3081 // per-block checksum sidecar. Hashing happens INSIDE the
3082 // block emitters (RLE / Raw fast-path / Compressed /
3083 // post-split partitions), so the digests vector has
3084 // exactly one entry per physical Block_Header written to
3085 // `all_blocks` — 1:1 with `FrameEmitInfo.blocks`. See
3086 // `enable_per_block_checksums` rustdoc.
3087 // Size the output ahead of this block's emission from the ratio
3088 // observed so far (see `reserve_for_next_block`); with no usable
3089 // size hint, ensure one block's worst case and let the doubling
3090 // growth policy amortize across blocks.
3091 // Bytes already emitted as blocks: everything read so far minus what
3092 // is still uncommitted, this block included.
3093 let emitted = total_uncompressed - available as u64;
3094 match initial_size_hint {
3095 Some(hint) if hint >= total_uncompressed => {
3096 // An advisory hint (streaming path) is only trusted up to
3097 // a small lookahead past the bytes actually read: a hint
3098 // far above the real input would otherwise reserve the
3099 // whole phantom remainder up front.
3100 let hint_remaining = hint - emitted;
3101 let remaining = if hint_is_exact {
3102 hint_remaining
3103 } else {
3104 let buffered = total_uncompressed - emitted;
3105 const HINT_LOOKAHEAD: u64 = 64 * 1024;
3106 hint_remaining.min(buffered + HINT_LOOKAHEAD)
3107 };
3108 reserve_for_next_block(
3109 out,
3110 blocks_start,
3111 emitted,
3112 remaining as usize,
3113 self.block_capacity(),
3114 );
3115 }
3116 _ => {
3117 out.reserve(block_len + 3 + 16);
3118 }
3119 }
3120 // Special handling is needed for compression of a totally empty file
3121 if block_len == 0 {
3122 let header = BlockHeader {
3123 last_block: true,
3124 block_type: crate::blocks::block::BlockType::Raw,
3125 block_size: 0,
3126 };
3127 header.serialize(out);
3128 #[cfg(feature = "lsm")]
3129 self.block_decompressed_sizes.push(0);
3130 #[cfg(all(feature = "lsm", feature = "hash"))]
3131 if let Some(checksums) = self.block_checksums.as_mut() {
3132 checksums.push(xxh64_block_low32(&[]));
3133 }
3134 break;
3135 }
3136
3137 let before_len = out.len();
3138 // A primed dictionary makes "incompressible-looking" blocks
3139 // matchable against the dict, so the raw-fast-path inside must be
3140 // bypassed (it skips matching). Mirror prepare_frame's
3141 // `use_dictionary_state`: a dict is only PRIMED (and thus matchable)
3142 // when the matcher supports priming; a non-priming matcher ignores
3143 // an attached dictionary, so the raw-fast-path must stay enabled
3144 // for it.
3145 let dict_active =
3146 self.dictionary.is_some() && self.state.matcher.supports_dictionary_priming();
3147 // A block the matcher laid out stays one physical block.
3148 let post_split = self.state.post_split_allowed;
3149 self.state.post_split_allowed = post_split && fixed_len.is_none();
3150 compress_block_encoded(
3151 &mut self.state,
3152 self.compression_level,
3153 last_block,
3154 block_len,
3155 out,
3156 dict_active,
3157 #[cfg(feature = "lsm")]
3158 Some(&mut self.block_decompressed_sizes),
3159 #[cfg(all(feature = "lsm", feature = "hash"))]
3160 self.block_checksums.as_mut(),
3161 );
3162 self.state.post_split_allowed = post_split;
3163 savings += block_len as i64 - (out.len() - before_len) as i64;
3164 // A block is last only when it took everything read, so nothing
3165 // uncommitted is left behind it.
3166 if last_block {
3167 break;
3168 }
3169 }
3170 total_uncompressed
3171 }
3172
3173 /// Block loop of an uncompressed frame: each block is read straight into
3174 /// `out` behind its header, which is filled in once the block's length is
3175 /// known, so the payload is never staged and the match finder never sees
3176 /// it.
3177 fn run_raw_block_loop<S: OwnedBlockSource>(
3178 &mut self,
3179 source: &mut S,
3180 initial_size_hint: Option<u64>,
3181 out: &mut Vec<u8>,
3182 ) -> u64 {
3183 let block_capacity = self.block_capacity();
3184 let mut total_uncompressed: u64 = 0;
3185 loop {
3186 let size_hint_remaining = match initial_size_hint {
3187 Some(hint) if hint > total_uncompressed => Some(hint - total_uncompressed),
3188 _ => None,
3189 };
3190 let header_at = out.len();
3191 out.extend_from_slice(&[0; 3]);
3192 let payload_at = out.len();
3193 let (block_len, last_block) =
3194 source.fill_block(out, payload_at + block_capacity, size_hint_remaining);
3195 total_uncompressed += block_len as u64;
3196 let header = BlockHeader {
3197 last_block,
3198 block_type: crate::blocks::block::BlockType::Raw,
3199 // At most one block, which a block header's 21-bit size holds.
3200 block_size: block_len as u32,
3201 };
3202 out[header_at..payload_at].copy_from_slice(&header.to_le_bytes());
3203 // A stored block is a delimiter alone in a captured parse. The
3204 // empty block of an empty input records none, as upstream's
3205 // block loop never runs for one (zstd_compress.c:4610).
3206 if block_len > 0
3207 && let Some(capture) = self.state.sequence_capture.as_mut()
3208 {
3209 capture.push(crate::encoding::sequences::ExternalSequence::delimiter(
3210 block_len as u32,
3211 ));
3212 }
3213 #[cfg(feature = "hash")]
3214 if self.content_checksum {
3215 self.hasher.write(&out[payload_at..]);
3216 }
3217 #[cfg(feature = "lsm")]
3218 self.block_decompressed_sizes.push(block_len as u32);
3219 #[cfg(all(feature = "lsm", feature = "hash"))]
3220 if let Some(checksums) = self.block_checksums.as_mut() {
3221 checksums.push(xxh64_block_low32(&out[payload_at..]));
3222 }
3223 if last_block {
3224 break;
3225 }
3226 }
3227 total_uncompressed
3228 }
3229
3230 /// Append the frame header bytes onto `out` once the total payload size
3231 /// is known (so `Frame_Content_Size` / `single_segment` can be set).
3232 /// Appends rather than returns so the one-shot path serializes straight
3233 /// into the reused output buffer with no per-frame header `Vec`.
3234 fn append_frame_header(&self, total_uncompressed: u64, prep: &FramePrep, out: &mut Vec<u8>) {
3235 // The header goes out after the content, so its size is known whether
3236 // or not it was hinted. A windowed frame cannot encode a content size
3237 // below 256 in fewer than 4 FCS bytes (the 1-byte class is
3238 // single-segment-only, see `find_fcs_field_size`), so small payloads
3239 // gain the most.
3240 let frame_content_size = self.content_size_flag.then_some(total_uncompressed);
3241 let single_segment =
3242 crate::encoding::frame_header::single_segment(frame_content_size, prep.window_size);
3243 let header = FrameHeader {
3244 frame_content_size,
3245 single_segment,
3246 content_checksum: cfg!(feature = "hash") && self.content_checksum,
3247 dictionary_id: if prep.use_dictionary_state && self.dict_id_flag {
3248 // Id 0 is a raw-content dictionary: RFC 8878 spells "no
3249 // dictionary ID" as an absent field, not as a stored zero.
3250 attached(&self.dictionary)
3251 .map(|dict| dict.inner.id)
3252 .filter(|id| *id != 0)
3253 .map(u64::from)
3254 } else {
3255 None
3256 },
3257 window_size: if single_segment {
3258 None
3259 } else {
3260 Some(prep.window_size)
3261 },
3262 magicless: self.magicless,
3263 };
3264 header.serialize(out);
3265 }
3266
3267 /// Write the frame header, accumulated block bytes, and optional
3268 /// trailing content checksum to the configured drain; populate
3269 /// `frame_emit_info` (lsm). Header and blocks are written separately to
3270 /// avoid shifting `all_blocks` to prepend the header. Used by
3271 /// `compress` and `compress_oneshot_borrowed`.
3272 fn finish_frame(&mut self, all_blocks: Vec<u8>, total_uncompressed: u64, prep: &FramePrep) {
3273 let mut header_buf: Vec<u8> = Vec::with_capacity(18);
3274 self.append_frame_header(total_uncompressed, prep, &mut header_buf);
3275 // Snapshot the checksum before borrowing the drain field so the
3276 // `self.hasher` read and the `self.compressed_data` write don't
3277 // both need `&mut self` simultaneously.
3278 #[cfg(feature = "hash")]
3279 let checksum_bytes = self
3280 .content_checksum
3281 .then(|| (self.hasher.finish() as u32).to_le_bytes());
3282 let drain = self.compressed_data.as_mut().unwrap();
3283 drain.write_all(&header_buf).unwrap();
3284 drain.write_all(&all_blocks).unwrap();
3285 // With the `hash` feature AND the content checksum enabled, the header
3286 // set `Content_Checksum_flag` and the 32-bit digest is written at the
3287 // end of the frame. Disabled => no trailing bytes, flag stays 0.
3288 #[cfg(feature = "hash")]
3289 if let Some(checksum_bytes) = checksum_bytes {
3290 drain.write_all(&checksum_bytes).unwrap();
3291 }
3292 #[cfg(feature = "lsm")]
3293 {
3294 let emit_checksum = cfg!(feature = "hash") && self.content_checksum;
3295 self.populate_frame_emit_info(header_buf.len(), &all_blocks, emit_checksum);
3296 }
3297 }
3298
3299 /// Assemble the frame (header + blocks + optional checksum) into the
3300 /// caller-provided `out` buffer, replacing its contents, and populate
3301 /// `frame_emit_info` (lsm). `out` is cleared first (its allocation is
3302 /// reused, the CCtx-equivalent zero-per-call-alloc output path) then
3303 /// grown once to the exact frame size. Used by
3304 /// `compress_independent_frame_into`. The single `all_blocks` copy into
3305 /// `out` is the same one copy `finish_frame` performs writing
3306 /// `all_blocks` into a `Vec` drain, no extra buffering vs the drain
3307 /// path.
3308 /// Walk `all_blocks` to recover per-block layout and store it in
3309 /// `frame_emit_info`. Each Block_Header is 3 bytes LE packing
3310 /// `(block_size << 3) | (block_type << 1) | last_block`. Physical body
3311 /// size differs by type: RLE bodies are always 1 byte (the repeated
3312 /// byte), Raw/Compressed bodies span `block_size`. `header_len` is the
3313 /// serialized frame-header length (frame offset of the first block).
3314 #[cfg(feature = "lsm")]
3315 fn populate_frame_emit_info(
3316 &mut self,
3317 header_len: usize,
3318 all_blocks: &[u8],
3319 emit_checksum: bool,
3320 ) {
3321 use crate::blocks::block::BlockType as BT;
3322 use crate::encoding::frame_emit_info::{FrameBlock, FrameEmitInfo};
3323 // All frame-offset arithmetic below is bounded by u32 on the wire
3324 // (Block_Size is a 21-bit field, frames bounded by MAX_BLOCK_SIZE *
3325 // #blocks). A pathologically large frame whose total emitted size
3326 // exceeds u32::MAX would overflow the cast; bail out by leaving
3327 // `frame_emit_info` at `None` rather than handing the caller a
3328 // silently-truncated layout. The overflow path is statically
3329 // unreachable on every realistic frame so the predictor amortises
3330 // the branch to zero cost.
3331 let frame_header_len: u32 = match u32::try_from(header_len) {
3332 Ok(v) => v,
3333 Err(_) => return,
3334 };
3335 let all_blocks_len_u32: u32 = match u32::try_from(all_blocks.len()) {
3336 Ok(v) => v,
3337 Err(_) => return,
3338 };
3339 let mut blocks: Vec<FrameBlock> = Vec::new();
3340 let mut cursor: usize = 0;
3341 while cursor + 3 <= all_blocks.len() {
3342 let mut header_u32 = [0u8; 4];
3343 header_u32[..3].copy_from_slice(&all_blocks[cursor..cursor + 3]);
3344 let raw = u32::from_le_bytes(header_u32);
3345 let last_block = (raw & 1) != 0;
3346 let block_type = match (raw >> 1) & 0b11 {
3347 0 => BT::Raw,
3348 1 => BT::RLE,
3349 2 => BT::Compressed,
3350 _ => BT::Reserved,
3351 };
3352 let block_size_field = raw >> 3;
3353 // RLE bodies are always 1 byte physical on the wire (the single
3354 // repeated byte); the spec's Block_Size field carries the
3355 // logical repeat count. Raw and Compressed bodies physically
3356 // span block_size_field bytes. Store the physical length in
3357 // body_size so the 'offset + header + body_size' arithmetic
3358 // always lands on the next block boundary, and surface the raw
3359 // spec field separately as block_size_field.
3360 let physical_body: u32 = match block_type {
3361 BT::RLE => 1,
3362 _ => block_size_field,
3363 };
3364 let cursor_u32: u32 = match u32::try_from(cursor) {
3365 Ok(v) => v,
3366 Err(_) => return,
3367 };
3368 let offset_in_frame = match frame_header_len.checked_add(cursor_u32) {
3369 Some(v) => v,
3370 None => return,
3371 };
3372 // Decompressed (regenerated) size, captured per physical block
3373 // during emit (1:1 with the wire blocks scanned here). Raw/RLE are
3374 // wire-derivable (`block_size_field`), so a short sidecar still
3375 // yields the correct value for them. A Compressed block's size is
3376 // NOT on the wire: if the sidecar is missing its entry, fabricating
3377 // 0 would publish a silently-wrong `decompressed_byte_range`. Since
3378 // this metadata is the authoritative mapping for a successful
3379 // encode, bail out (leave `frame_emit_info` at `None`) rather than
3380 // hand back a corrupt layout; the 1:1 push invariant makes this
3381 // unreachable in practice (debug_assert catches a regression).
3382 let decompressed_size = match self.block_decompressed_sizes.get(blocks.len()).copied() {
3383 Some(size) => size,
3384 None if matches!(block_type, BT::Raw | BT::RLE) => block_size_field,
3385 None => {
3386 debug_assert!(
3387 false,
3388 "missing decompressed-size sidecar entry for compressed block {}",
3389 blocks.len()
3390 );
3391 return;
3392 }
3393 };
3394 blocks.push(FrameBlock {
3395 offset_in_frame,
3396 header_size: 3,
3397 body_size: physical_body,
3398 block_size_field,
3399 block_type,
3400 last_block,
3401 decompressed_size,
3402 });
3403 cursor += 3 + physical_body as usize;
3404 if last_block {
3405 break;
3406 }
3407 }
3408 // Fail closed on a structurally incomplete scan: the loop must have
3409 // consumed the whole block section AND ended on a parsed last block.
3410 // A premature `last_block` (bytes left over) or a run-off without any
3411 // last block would otherwise publish an invalid public `FrameEmitInfo`.
3412 // Unreachable for a well-formed self-produced frame (debug_assert
3413 // catches a regression); on release we bail, leaving `frame_emit_info`
3414 // at `None` rather than handing back a corrupt layout.
3415 if cursor != all_blocks.len() || !blocks.last().is_some_and(|b| b.last_block) {
3416 debug_assert!(
3417 false,
3418 "incomplete block scan in populate_frame_emit_info: cursor={} len={} last_block={:?}",
3419 cursor,
3420 all_blocks.len(),
3421 blocks.last().map(|b| b.last_block)
3422 );
3423 return;
3424 }
3425 let checksum_range = if emit_checksum {
3426 let cs_start = match frame_header_len.checked_add(all_blocks_len_u32) {
3427 Some(v) => v,
3428 None => return,
3429 };
3430 let cs_end = match cs_start.checked_add(4) {
3431 Some(v) => v,
3432 None => return,
3433 };
3434 Some(cs_start..cs_end)
3435 } else {
3436 None
3437 };
3438 let body_total = match frame_header_len.checked_add(all_blocks_len_u32) {
3439 Some(v) => v,
3440 None => return,
3441 };
3442 let total_size = if checksum_range.is_some() {
3443 match body_total.checked_add(4) {
3444 Some(v) => v,
3445 None => return,
3446 }
3447 } else {
3448 body_total
3449 };
3450 self.frame_emit_info = Some(FrameEmitInfo {
3451 frame_header_range: 0..frame_header_len,
3452 blocks,
3453 checksum_range,
3454 total_size,
3455 });
3456 }
3457
3458 /// Layout of the most recently emitted frame.
3459 ///
3460 /// Returns `None` if [`compress`](Self::compress) has not been
3461 /// called yet on this compressor. After a successful `compress()`
3462 /// the returned `FrameEmitInfo` describes the frame header range,
3463 /// every emitted block's offset / size / type, and the optional
3464 /// trailing content-checksum range — all in frame-absolute byte
3465 /// offsets matching the bytes written to the drain.
3466 ///
3467 /// Behind the `lsm` Cargo feature.
3468 #[cfg(feature = "lsm")]
3469 pub fn last_frame_emit_info(&self) -> Option<&crate::encoding::frame_emit_info::FrameEmitInfo> {
3470 self.frame_emit_info.as_ref()
3471 }
3472
3473 /// Opt in to per-block XXH64 checksum computation during
3474 /// [`compress`](Self::compress). Default off; zero cost when
3475 /// disabled. The captured digests are accessible via
3476 /// [`last_frame_block_checksums`](Self::last_frame_block_checksums).
3477 ///
3478 /// One checksum is emitted per physical FrameBlock written to
3479 /// the drain: 1:1 cardinality with
3480 /// [`last_frame_emit_info`](Self::last_frame_emit_info)'s
3481 /// `blocks` vector. On the post-split optimization path
3482 /// (Level 16-22 with large window) the per-partition decompressed
3483 /// range is hashed inside the partition loop so the digest count
3484 /// still matches the emitted block count. The decoder collects
3485 /// per-physical-block digests on the same granularity, so
3486 /// element-wise equality holds round-trip.
3487 ///
3488 /// Behind `all(feature = "lsm", feature = "hash")` — the XXH64
3489 /// primitive lives behind the `hash` feature, so this method only
3490 /// compiles when both are enabled.
3491 #[cfg(all(feature = "lsm", feature = "hash"))]
3492 pub fn enable_per_block_checksums(&mut self) {
3493 self.per_block_checksums_enabled = true;
3494 }
3495
3496 /// Per-block XXH64 (low 32 bits) digests captured during the most
3497 /// recent `compress()` call. `None` unless
3498 /// [`enable_per_block_checksums`](Self::enable_per_block_checksums)
3499 /// was called before `compress()`.
3500 ///
3501 /// Behind `all(feature = "lsm", feature = "hash")`.
3502 #[cfg(all(feature = "lsm", feature = "hash"))]
3503 pub fn last_frame_block_checksums(&self) -> Option<&[u32]> {
3504 self.block_checksums.as_deref()
3505 }
3506
3507 /// Get a mutable reference to the source
3508 pub fn source_mut(&mut self) -> Option<&mut R> {
3509 self.uncompressed_data.as_mut()
3510 }
3511
3512 /// Get a mutable reference to the drain
3513 pub fn drain_mut(&mut self) -> Option<&mut W> {
3514 self.compressed_data.as_mut()
3515 }
3516
3517 /// Get a reference to the source
3518 pub fn source(&self) -> Option<&R> {
3519 self.uncompressed_data.as_ref()
3520 }
3521
3522 /// Get a reference to the drain
3523 pub fn drain(&self) -> Option<&W> {
3524 self.compressed_data.as_ref()
3525 }
3526
3527 /// Retrieve the source
3528 pub fn take_source(&mut self) -> Option<R> {
3529 self.uncompressed_data.take()
3530 }
3531
3532 /// Retrieve the drain
3533 pub fn take_drain(&mut self) -> Option<W> {
3534 self.compressed_data.take()
3535 }
3536
3537 /// The matcher, for a caller that reads what it recorded between frames.
3538 #[cfg(feature = "dict-builder")]
3539 pub(crate) fn matcher_mut(&mut self) -> &mut M {
3540 &mut self.state.matcher
3541 }
3542
3543 /// Whether the last frame's blocks coded offsets with the fast matcher's
3544 /// repeat policy, for a caller counting offset codes as they did.
3545 #[cfg(feature = "dict-builder")]
3546 pub(crate) fn uses_fast_offset_codes(&self) -> bool {
3547 crate::encoding::blocks::uses_fast_offset_codes(self.state.strategy_tag)
3548 }
3549
3550 /// Before calling [FrameCompressor::compress] you can replace the matcher
3551 pub fn replace_matcher(&mut self, mut match_generator: M) -> M {
3552 core::mem::swap(&mut match_generator, &mut self.state.matcher);
3553 // The outgoing matcher may hold tables and history in this
3554 // compressor's workspace, which it must not outlive.
3555 match_generator.leave_workspace();
3556 match_generator
3557 }
3558
3559 /// Before calling [FrameCompressor::compress] you can replace the compression level.
3560 ///
3561 /// This also clears any fine-grained parameter overrides installed via
3562 /// [`set_parameters`](Self::set_parameters): reverting to a bare level
3563 /// means plain level-based tuning, not the previous frame's customized
3564 /// strategy / LDM / log / literal-mode overrides. To keep overriding, call
3565 /// [`set_parameters`](Self::set_parameters) again with the new base level.
3566 pub fn set_compression_level(
3567 &mut self,
3568 compression_level: CompressionLevel,
3569 ) -> CompressionLevel {
3570 let old = self.compression_level;
3571 self.compression_level = compression_level;
3572 // Resync the raw-literals gate: negative levels disable literal (Huffman)
3573 // compression (C `ZSTD_literalsCompressionIsDisabled`). `prepare_frame`
3574 // never recomputes this, so it must be refreshed on the level switch the
3575 // same way the constructors and `set_parameters` do.
3576 self.state.literal_compression_disabled = matches!(
3577 compression_level,
3578 CompressionLevel::Level(n) if n < 0
3579 );
3580 // Drop sticky overrides so the level switch yields plain geometry,
3581 // the literal mode included: the gate above is the bare level's rule.
3582 self.tuning = FrameTuning::default();
3583 self.state.matcher.clear_param_overrides();
3584 old
3585 }
3586
3587 /// Get the current compression level
3588 pub fn compression_level(&self) -> CompressionLevel {
3589 self.compression_level
3590 }
3591
3592 /// Attach an already-parsed [`EncoderDictionary`] without reparsing a raw
3593 /// blob, held the way this compressor holds one (see [`DictionaryHold`]).
3594 ///
3595 /// Accepts an `EncoderDictionary` produced once via
3596 /// [`EncoderDictionary::from_bytes`] / [`EncoderDictionary::from_dictionary`]
3597 /// or handed back by [`Self::clear_dictionary`] / the `set_dictionary*`
3598 /// return value, so callers can reattach or reuse a prepared dictionary
3599 /// across compressions without re-running the dictionary parse each time.
3600 /// Returns the previously-attached dictionary, if any.
3601 ///
3602 /// # Errors
3603 /// [`ZeroRepeatOffsetInDictionary`](crate::decoding::errors::DictionaryDecodeError::ZeroRepeatOffsetInDictionary)
3604 /// when a repeat offset of the dictionary is zero.
3605 pub fn set_encoder_dictionary(
3606 &mut self,
3607 dictionary: D,
3608 ) -> Result<Option<D>, crate::decoding::errors::DictionaryDecodeError> {
3609 self.attach_dictionary(dictionary)
3610 }
3611
3612 /// The dictionary frames are compressed with, if one is attached.
3613 ///
3614 /// # Examples
3615 /// ```
3616 /// use structured_zstd::encoding::{CompressionLevel, EncoderDictionary, FrameCompressor};
3617 ///
3618 /// let dictionary = EncoderDictionary::from_serialized_or_raw_content(b"some shared history").unwrap();
3619 /// let mut compressor: FrameCompressor = FrameCompressor::new(CompressionLevel::Default);
3620 /// assert!(compressor.dictionary().is_none());
3621 /// compressor.set_encoder_dictionary(dictionary).unwrap();
3622 /// assert_eq!(compressor.dictionary().map(EncoderDictionary::id), Some(0));
3623 /// ```
3624 pub fn dictionary(&self) -> Option<&EncoderDictionary> {
3625 attached(&self.dictionary)
3626 }
3627
3628 /// Remove the attached dictionary, returning it as it was held.
3629 pub fn clear_dictionary(&mut self) -> Option<D> {
3630 // Drop the CDict prime snapshot — it is keyed to the dictionary
3631 // being removed and must not be restored against a different (or no)
3632 // dictionary on the next frame.
3633 self.state.matcher.invalidate_primed_dictionary();
3634 self.dictionary.take()
3635 }
3636
3637 /// Validate `enc`, store it, and return the previously-attached
3638 /// dictionary; its entropy tables come with it, built when it was
3639 /// prepared. Shared by every public attach entry point: `set_dictionary`,
3640 /// `set_dictionary_from_bytes`, and `set_encoder_dictionary`.
3641 fn attach_dictionary(
3642 &mut self,
3643 enc: D,
3644 ) -> Result<Option<D>, crate::decoding::errors::DictionaryDecodeError> {
3645 // A zero id is not an error here: it marks a raw-content dictionary,
3646 // which has no header to carry one. The frame then records no
3647 // dictionary ID, so the decoder has to be handed the same bytes.
3648 let prepared: &EncoderDictionary = core::borrow::Borrow::borrow(&enc);
3649 let dictionary = &prepared.inner;
3650 if let Some(index) = dictionary.offset_hist.iter().position(|&rep| rep == 0) {
3651 return Err(
3652 crate::decoding::errors::DictionaryDecodeError::ZeroRepeatOffsetInDictionary {
3653 index: index as u8,
3654 },
3655 );
3656 }
3657 // A previously-captured CDict prime snapshot belongs to the OLD
3658 // dictionary; drop it so the first frame with the new dictionary
3659 // re-primes (and re-captures) instead of restoring stale tables.
3660 self.state.matcher.invalidate_primed_dictionary();
3661 if !prepared.is_used_for_compression() {
3662 return Ok(self.dictionary.take());
3663 }
3664 Ok(self.dictionary.replace(enc))
3665 }
3666}
3667
3668#[cfg(test)]
3669mod tests;