Skip to main content

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