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;
4use core::convert::TryInto;
5#[cfg(feature = "hash")]
6use twox_hash::XxHash64;
7
8#[cfg(feature = "hash")]
9use core::hash::Hasher;
10
11use super::{
12    CompressionLevel, Matcher, block_header::BlockHeader, frame_header::FrameHeader, levels::*,
13    match_generator::MatchGeneratorDriver,
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#[derive(Clone)]
33pub struct EncoderDictionary {
34    pub(crate) inner: crate::decoding::Dictionary,
35}
36
37impl EncoderDictionary {
38    /// Wrap an already-parsed [`Dictionary`](crate::decoding::Dictionary) for
39    /// encoder use. A fully-decoded dictionary is valid here; only the encoder
40    /// entropy tables, content, and offset history are read.
41    pub fn from_dictionary(dictionary: crate::decoding::Dictionary) -> Self {
42        Self { inner: dictionary }
43    }
44
45    /// Parse a serialized dictionary blob for encoder use, skipping the decode
46    /// lookup-table build the encoder never reads (see
47    /// `Dictionary::decode_dict_for_encoding`). The encoder entropy tables — and
48    /// thus the emitted frame — are identical to a full parse.
49    pub fn from_bytes(
50        raw_dictionary: &[u8],
51    ) -> Result<Self, crate::decoding::errors::DictionaryDecodeError> {
52        Ok(Self {
53            inner: crate::decoding::Dictionary::decode_dict_for_encoding(raw_dictionary)?,
54        })
55    }
56
57    /// The dictionary id.
58    ///
59    /// A dictionary attached for encoding always has a non-zero id (the
60    /// `set_dictionary*` / `set_encoder_dictionary` attach path rejects a
61    /// zero id). This getter, however, reflects the wrapped dictionary as-is:
62    /// an `EncoderDictionary` built via [`Self::from_dictionary`] from a raw
63    /// `Dictionary` with `id == 0` reports `0` here until it is attached.
64    pub fn id(&self) -> u32 {
65        self.inner.id
66    }
67}
68
69/// An interface for compressing arbitrary data with the ZStandard compression algorithm.
70///
71/// `FrameCompressor` will generally be used by:
72/// 1. Initializing a compressor by providing a buffer of data using `FrameCompressor::new()`
73/// 2. Starting compression and writing that compression into a vec using `FrameCompressor::begin`
74///
75/// # Examples
76/// ```
77/// use structured_zstd::encoding::{FrameCompressor, CompressionLevel};
78/// let mock_data: &[_] = &[0x1, 0x2, 0x3, 0x4];
79/// let mut output = std::vec::Vec::new();
80/// // Initialize a compressor.
81/// let mut compressor = FrameCompressor::new(CompressionLevel::Uncompressed);
82/// compressor.set_source(mock_data);
83/// compressor.set_drain(&mut output);
84///
85/// // `compress` writes the compressed output into the provided buffer.
86/// compressor.compress();
87/// ```
88pub struct FrameCompressor<
89    R: Read = &'static [u8],
90    W: Write = Vec<u8>,
91    M: Matcher = MatchGeneratorDriver,
92> {
93    uncompressed_data: Option<R>,
94    compressed_data: Option<W>,
95    compression_level: CompressionLevel,
96    dictionary: Option<EncoderDictionary>,
97    dictionary_entropy_cache: Option<CachedDictionaryEntropy>,
98    source_size_hint: Option<u64>,
99    state: CompressState<M>,
100    /// When true, emitted frames omit the 4-byte magic number prefix
101    /// (`ZSTD_f_zstd1_magicless`). Default false. The caller is
102    /// responsible for ensuring the decoder is configured for the
103    /// matching format — wire-format only round-trips with a
104    /// magicless-aware decoder.
105    magicless: bool,
106    /// Whether to emit a trailing XXH64 content checksum and set the frame
107    /// header's `Content_Checksum_flag` (semantics of upstream
108    /// `ZSTD_c_checksumFlag`). Default `false`, matching the upstream
109    /// library default; combined with the `hash` feature at frame-build
110    /// time, so without `hash` no checksum is emitted regardless. Set via
111    /// [`Self::set_content_checksum`].
112    content_checksum: bool,
113    /// Whether to record `Frame_Content_Size` in the frame header when the
114    /// total size is known (semantics of upstream `ZSTD_c_contentSizeFlag`).
115    /// Default `true`, matching upstream. With the flag off the header
116    /// carries a window descriptor instead (single-segment requires an FCS,
117    /// so it is disabled too). Set via [`Self::set_content_size_flag`].
118    content_size_flag: bool,
119    /// Whether to record the dictionary ID in the frame header when a
120    /// dictionary is attached (semantics of upstream `ZSTD_c_dictIDFlag`).
121    /// Default `true`, matching upstream. Decoders can still decode the
122    /// frame by being handed the right dictionary explicitly. Set via
123    /// [`Self::set_dictionary_id_flag`].
124    dict_id_flag: bool,
125    /// Upper bound on emitted block sizes (semantics of upstream
126    /// `ZSTD_c_targetCBlockSize`): capping the RAW block length at the
127    /// target bounds every physical block's compressed payload at the
128    /// target too (a compressed block never exceeds its raw input — the
129    /// raw-block fallback fires otherwise), so blocks land at or under
130    /// `target + 3` header bytes on the wire. `None` = no target (full
131    /// 128 KiB blocks). Set via [`Self::set_target_block_size`].
132    target_block_size: Option<u32>,
133    #[cfg(feature = "hash")]
134    hasher: XxHash64,
135    /// Block-layout introspection populated at the end of every
136    /// successful `compress()`. `None` until the first call.
137    /// Behind the `lsm` feature gate.
138    #[cfg(feature = "lsm")]
139    frame_emit_info: Option<crate::encoding::frame_emit_info::FrameEmitInfo>,
140    /// When `true`, `compress()` XXH64-hashes each block's
141    /// uncompressed bytes and appends the low-32-bit digest to
142    /// `block_checksums`. Default `false` (zero cost). Gated on
143    /// `all(lsm, hash)` because XXH64 lives behind the `hash`
144    /// feature; an `lsm`-only build has no way to compute digests.
145    #[cfg(all(feature = "lsm", feature = "hash"))]
146    per_block_checksums_enabled: bool,
147    /// Per-block XXH64 (low 32 bits) digests captured during
148    /// `compress()` when `per_block_checksums_enabled` is set. Ordered
149    /// by block-emit order. `None` until the first call after enabling.
150    /// Gated on `all(lsm, hash)` (see `per_block_checksums_enabled`).
151    #[cfg(all(feature = "lsm", feature = "hash"))]
152    block_checksums: Option<alloc::vec::Vec<u32>>,
153    /// Per-physical-block decompressed (regenerated) sizes captured
154    /// during `compress()`, in block-emit order (1:1 with
155    /// `frame_emit_info.blocks`). Always captured under `lsm` (no
156    /// opt-in, unlike `block_checksums`) because `FrameEmitInfo` is
157    /// always built under `lsm` and `decompressed_byte_range` needs
158    /// the per-block sizes. Cleared and refilled per frame.
159    #[cfg(feature = "lsm")]
160    block_decompressed_sizes: alloc::vec::Vec<u32>,
161    /// Effective strategy tag when a public-parameter
162    /// [`Strategy`](crate::encoding::Strategy) override (#27) is active.
163    /// `Some` overrides the level-derived `state.strategy_tag` so the
164    /// literal-compression gates and dict-attach cutoff see the strategy
165    /// the matcher actually runs, not the base level's. `None` keeps the
166    /// level-derived tag.
167    strategy_override: Option<crate::encoding::strategy::StrategyTag>,
168}
169
170#[derive(Clone, Default)]
171pub(crate) struct CachedDictionaryEntropy {
172    pub(crate) huff: Option<crate::huff0::huff0_encoder::HuffmanTable>,
173    pub(crate) ll_previous: Option<PreviousFseTable>,
174    pub(crate) ml_previous: Option<PreviousFseTable>,
175    pub(crate) of_previous: Option<PreviousFseTable>,
176}
177
178impl CachedDictionaryEntropy {
179    /// Heap bytes the cached dictionary entropy holds: the literals Huffman
180    /// table plus any `Custom` LL/ML/OF FSE tables (the `Arc`-boxed `FSETable`
181    /// payload and its flat state array). `Default` / `Rle` variants own no heap.
182    pub(crate) fn heap_size(&self) -> usize {
183        let mut total = self.huff.as_ref().map_or(0, |h| h.heap_size());
184        for prev in [&self.ll_previous, &self.ml_previous, &self.of_previous] {
185            if let Some(PreviousFseTable::Custom(table)) = prev {
186                total +=
187                    core::mem::size_of::<crate::fse::fse_encoder::FSETable>() + table.heap_size();
188            }
189        }
190        total
191    }
192
193    /// Derive the encoder-side entropy tables a dictionary seeds for the first
194    /// block of each frame (the upstream zstd `cdict->cBlockState`): the literals
195    /// Huffman table plus the literal-length / match-length / offset FSE
196    /// "previous" tables. Shared by [`FrameCompressor`] and
197    /// [`crate::encoding::StreamingEncoder`] so both seed identically.
198    pub(crate) fn from_dictionary(dictionary: &crate::decoding::Dictionary) -> Self {
199        Self {
200            huff: dictionary.huf.table.to_encoder_table(),
201            ll_previous: dictionary
202                .fse
203                .literal_lengths
204                .to_encoder_table()
205                .map(|table| PreviousFseTable::Custom(SharedFseTable::new(table))),
206            ml_previous: dictionary
207                .fse
208                .match_lengths
209                .to_encoder_table()
210                .map(|table| PreviousFseTable::Custom(SharedFseTable::new(table))),
211            of_previous: dictionary
212                .fse
213                .offsets
214                .to_encoder_table()
215                .map(|table| PreviousFseTable::Custom(SharedFseTable::new(table))),
216        }
217    }
218}
219
220/// Shared owner for a custom "previous" FSE encoder table. `Arc` on
221/// atomic-pointer targets, `Rc` otherwise (keeps `no_std` no-atomics
222/// builds compiling, single-thread there anyway), mirroring
223/// `decoding::dictionary::SharedDictionary`. Cloning the cached
224/// dictionary entropy into the per-frame state is then a refcount bump,
225/// not a full `FSETable` copy — the upstream zstd references `cdict->cBlockState`
226/// instead of rebuilding it per frame.
227#[cfg(target_has_atomic = "ptr")]
228pub(crate) type SharedFseTable = alloc::sync::Arc<FSETable>;
229#[cfg(not(target_has_atomic = "ptr"))]
230pub(crate) type SharedFseTable = alloc::rc::Rc<FSETable>;
231
232#[derive(Clone)]
233pub(crate) enum PreviousFseTable {
234    // Default tables are immutable and already stored alongside the state, so
235    // repeating them only needs a lightweight marker instead of cloning FSETable.
236    Default,
237    // Shared handle: cloning (per-frame dictionary entropy seed) is a refcount
238    // bump. The table is only ever read or REPLACED wholesale (a block that
239    // builds a new table swaps in a fresh `SharedFseTable`), never mutated in
240    // place, so sharing is sound.
241    Custom(SharedFseTable),
242    Rle(u8),
243}
244
245impl PreviousFseTable {
246    pub(crate) fn as_table<'a>(&'a self, default: &'a FSETable) -> Option<&'a FSETable> {
247        match self {
248            Self::Default => Some(default),
249            Self::Custom(table) => Some(table),
250            Self::Rle(_) => None,
251        }
252    }
253}
254
255pub(crate) struct FseTables {
256    /// The three predefined LL/ML/OF tables are functions of
257    /// compile-time-constant distributions. The
258    /// [`fse_encoder::FseDefaultTable`] type alias resolves to
259    /// `&'static FSETable` when a process-wide cache is available
260    /// (atomic-pointer targets, or no-atomic targets with the
261    /// `critical-section` feature) and to `Box<FSETable>` on the
262    /// cache-less no-atomic path (one per-frame allocation, dropped
263    /// with the compressor — no `Box::leak`, no unbounded growth).
264    /// Both arms `Deref` to `FSETable`, so consumers in
265    /// `encoding/blocks/compressed.rs` borrow through `&` uniformly
266    /// without seeing the per-target divergence.
267    pub(crate) ll_default: crate::fse::fse_encoder::FseDefaultTable,
268    pub(crate) ll_previous: Option<PreviousFseTable>,
269    pub(crate) ml_default: crate::fse::fse_encoder::FseDefaultTable,
270    pub(crate) ml_previous: Option<PreviousFseTable>,
271    pub(crate) of_default: crate::fse::fse_encoder::FseDefaultTable,
272    pub(crate) of_previous: Option<PreviousFseTable>,
273}
274
275impl FseTables {
276    pub fn new() -> Self {
277        Self {
278            ll_default: default_ll_table(),
279            ll_previous: None,
280            ml_default: default_ml_table(),
281            ml_previous: None,
282            of_default: default_of_table(),
283            of_previous: None,
284        }
285    }
286
287    /// Borrow the LL default table as `&FSETable`. Abstracts the cfg
288    /// split in [`crate::fse::fse_encoder::FseDefaultTable`] —
289    /// `&'static FSETable` (atomic / `critical-section`) auto-derefs
290    /// directly; `Box<FSETable>` (cache-less no-atomic) derefs
291    /// through `Box`. Both arms yield `&FSETable` uniformly so
292    /// downstream consumers can stay cfg-agnostic.
293    #[inline]
294    #[allow(clippy::borrow_deref_ref)]
295    pub(crate) fn ll_default_ref(&self) -> &FSETable {
296        &*self.ll_default
297    }
298
299    /// Borrow the ML default table as `&FSETable`. See [`Self::ll_default_ref`].
300    #[inline]
301    #[allow(clippy::borrow_deref_ref)]
302    pub(crate) fn ml_default_ref(&self) -> &FSETable {
303        &*self.ml_default
304    }
305
306    /// Borrow the OF default table as `&FSETable`. See [`Self::ll_default_ref`].
307    #[inline]
308    #[allow(clippy::borrow_deref_ref)]
309    pub(crate) fn of_default_ref(&self) -> &FSETable {
310        &*self.of_default
311    }
312}
313
314const PRESPLIT_BLOCK_MIN: usize = 3500;
315const PRESPLIT_THRESHOLD_PENALTY_RATE: u64 = 16;
316const PRESPLIT_THRESHOLD_BASE: u64 = PRESPLIT_THRESHOLD_PENALTY_RATE - 2;
317const PRESPLIT_THRESHOLD_PENALTY: i32 = 3;
318const PRESPLIT_CHUNK_SIZE: usize = 8 << 10;
319const PRESPLIT_HASH_LOG_MAX: usize = 10;
320const PRESPLIT_HASH_TABLE_SIZE: usize = 1 << PRESPLIT_HASH_LOG_MAX;
321const PRESPLIT_KNUTH: u32 = 0x9E37_79B9;
322/// Upstream zstd `SEGMENT_SIZE` in `ZSTD_splitBlock_fromBorders` (`zstd_preSplit.c:201`).
323/// Two `SEGMENT_SIZE`-byte fingerprints — one from the start, one from the end —
324/// drive the cheap border heuristic; a third one from the middle disambiguates
325/// where in the block the transition sits.
326const PRESPLIT_BORDERS_SEGMENT: usize = 512;
327
328#[derive(Clone)]
329struct PreSplitFingerprint {
330    events: [u32; PRESPLIT_HASH_TABLE_SIZE],
331    nb_events: usize,
332}
333
334impl Default for PreSplitFingerprint {
335    fn default() -> Self {
336        Self {
337            events: [0; PRESPLIT_HASH_TABLE_SIZE],
338            nb_events: 0,
339        }
340    }
341}
342
343/// Grow `out` ahead of the next block so block emission never lands on an
344/// amortized-doubling reallocation mid-frame (whose transient old+new copy
345/// spikes peak memory to ~3x the output), sizing the reservation from the
346/// compression ratio observed so far instead of the whole-input worst case.
347///
348/// `blocks_start` is where this frame's blocks begin in `out`, `consumed`
349/// the input bytes already emitted as blocks, `remaining` the input
350/// bytes still to compress (an estimate is fine: a low one only means one
351/// more re-estimate later), and `block_capacity` the active block-size cap
352/// (`FrameCompressor::block_capacity`) so a small `targetCBlockSize` does
353/// not keep a 128 KiB floor in the buffer or undercount header density.
354/// Incompressible input re-estimates to ~the full `compress_bound` after
355/// the first block — the old up-front policy's worst case — while
356/// compressible input stays at output scale.
357fn reserve_for_next_block(
358    out: &mut Vec<u8>,
359    blocks_start: usize,
360    consumed: u64,
361    remaining: usize,
362    block_capacity: usize,
363) {
364    // Worst-case single-block output: 3-byte header + raw payload, plus
365    // slack for the 4-byte frame checksum trailer and a few extra sub-block
366    // headers from the post-split emitters, so neither can reallocate.
367    let block_bound = remaining.min(block_capacity) + 3 + 16;
368    if out.capacity() - out.len() >= block_bound {
369        return;
370    }
371    let produced = (out.len() - blocks_start) as u64;
372    let estimate = if consumed == 0 {
373        // No ratio signal yet (capacity exhausted before the first block —
374        // only reachable with a caller-shrunk `out`): one block's bound.
375        block_bound
376    } else {
377        // remaining * observed ratio + per-block headers + 1/16 slack so a
378        // slightly-worsening tail doesn't force a reallocation per block.
379        // u128 keeps the product exact for multi-GiB frames.
380        let scaled = ((remaining as u128 * produced as u128) / consumed as u128) as u64;
381        let headers = (remaining as u64 / block_capacity.max(1) as u64 + 1) * 3;
382        usize::try_from(scaled + scaled / 16 + headers + 64).unwrap_or(usize::MAX)
383    };
384    // `reserve_exact`: the estimate already carries its own slack, and the
385    // whole-buffer doubling policy is exactly what this function exists to
386    // avoid. The `produced`-sized floor keeps growth geometric when the
387    // ratio estimate lands BELOW one block's bound (highly compressible
388    // input): without it every block would trigger a block-sized
389    // reallocation — O(blocks) buffer copies — while with it the buffer at
390    // least doubles its produced span per reallocation (O(log) copies) and
391    // the peak stays at output scale.
392    out.reserve_exact(estimate.max(block_bound + produced as usize));
393}
394
395fn presplit_hash2(bytes: &[u8], hash_log: usize) -> usize {
396    debug_assert!(hash_log >= 8);
397    if hash_log == 8 {
398        return bytes[0] as usize;
399    }
400    debug_assert!(hash_log <= PRESPLIT_HASH_LOG_MAX);
401    let value = u16::from_le_bytes([bytes[0], bytes[1]]) as u32;
402    (value.wrapping_mul(PRESPLIT_KNUTH) >> (32 - hash_log)) as usize
403}
404
405fn presplit_record_fingerprint(
406    fp: &mut PreSplitFingerprint,
407    src: &[u8],
408    sampling_rate: usize,
409    hash_log: usize,
410) {
411    fp.events.fill(0);
412    fp.nb_events = 0;
413    if src.len() < 2 {
414        return;
415    }
416    let limit = src.len() - 1;
417    let mut n = 0usize;
418    while n < limit {
419        fp.events[presplit_hash2(&src[n..], hash_log)] += 1;
420        n += sampling_rate;
421    }
422    // Upstream zstd parity: zstd_preSplit.c records the integer division, not the
423    // rounded-up number of sampled events from the loop above.
424    fp.nb_events += limit / sampling_rate;
425}
426
427/// Single-byte histogram pass — matches upstream zstd `HIST_add` over a small
428/// segment with `hashLog == 8` (the `hash2` shortcut at
429/// `zstd_preSplit.c:36` returns the raw byte). The byChunks path uses
430/// 2-byte hashing for `hashLog >= 9`; this helper exists so the borders
431/// heuristic doesn't pay for that wider hash on its 512-byte windows.
432fn presplit_record_byte_histogram(fp: &mut PreSplitFingerprint, src: &[u8]) {
433    fp.events.fill(0);
434    for &b in src {
435        fp.events[b as usize] += 1;
436    }
437    // Upstream zstd `HIST_add` returns the maximum symbol; the caller then sets
438    // `nbEvents = SEGMENT_SIZE` explicitly (see `zstd_preSplit.c:213`).
439    fp.nb_events = src.len();
440}
441
442fn presplit_distance(lhs: &PreSplitFingerprint, rhs: &PreSplitFingerprint, hash_log: usize) -> u64 {
443    let slots = 1usize << hash_log;
444    let mut distance = 0u64;
445    for idx in 0..slots {
446        let left = lhs.events[idx] as i128 * rhs.nb_events as i128;
447        let right = rhs.events[idx] as i128 * lhs.nb_events as i128;
448        // Plain `+`: events/nb_events are per-block sample counts (<= block
449        // size), so each |left-right| <= (2^17)^2 and the sum over <= 2^hash_log
450        // slots stays far under u64::MAX — no overflow.
451        distance += left.abs_diff(right) as u64;
452    }
453    distance
454}
455
456fn presplit_fingerprints_differ(
457    reference: &PreSplitFingerprint,
458    new_fp: &PreSplitFingerprint,
459    penalty: i32,
460    hash_log: usize,
461) -> bool {
462    debug_assert!(reference.nb_events > 0);
463    debug_assert!(new_fp.nb_events > 0);
464    let p50 = reference.nb_events as u64 * new_fp.nb_events as u64;
465    let deviation = presplit_distance(reference, new_fp, hash_log);
466    // Plain `*`: p50 <= (block-sample-count)^2 and the (base+penalty) factor is
467    // a small constant, so the product stays well under u64::MAX.
468    let threshold =
469        p50 * (PRESPLIT_THRESHOLD_BASE + penalty as u64) / PRESPLIT_THRESHOLD_PENALTY_RATE;
470    deviation >= threshold
471}
472
473fn presplit_merge_events(acc: &mut PreSplitFingerprint, new_fp: &PreSplitFingerprint) {
474    // Plain `+`: `acc` accumulates only the chunks of a single block (caller
475    // loops within one block, <= MAX_BLOCK_SIZE), so the merged sample counts
476    // stay far under u32 / usize bounds — no overflow.
477    for idx in 0..PRESPLIT_HASH_TABLE_SIZE {
478        acc.events[idx] += new_fp.events[idx];
479    }
480    acc.nb_events += new_fp.nb_events;
481}
482
483fn split_block_by_chunks(block: &[u8], level: usize) -> usize {
484    debug_assert_eq!(block.len(), MAX_BLOCK_SIZE as usize);
485    debug_assert!((1..=4).contains(&level));
486    let (sampling_rate, hash_log) = match level - 1 {
487        0 => (43, 8),
488        1 => (11, 9),
489        2 => (5, 10),
490        _ => (1, 10),
491    };
492
493    let mut past = PreSplitFingerprint::default();
494    let mut new_events = PreSplitFingerprint::default();
495    let mut penalty = PRESPLIT_THRESHOLD_PENALTY;
496    presplit_record_fingerprint(
497        &mut past,
498        &block[..PRESPLIT_CHUNK_SIZE],
499        sampling_rate,
500        hash_log,
501    );
502    let mut pos = PRESPLIT_CHUNK_SIZE;
503    while pos <= block.len() - PRESPLIT_CHUNK_SIZE {
504        presplit_record_fingerprint(
505            &mut new_events,
506            &block[pos..pos + PRESPLIT_CHUNK_SIZE],
507            sampling_rate,
508            hash_log,
509        );
510        if presplit_fingerprints_differ(&past, &new_events, penalty, hash_log) {
511            return pos;
512        }
513        presplit_merge_events(&mut past, &new_events);
514        if penalty > 0 {
515            penalty -= 1;
516        }
517        pos += PRESPLIT_CHUNK_SIZE;
518    }
519    block.len()
520}
521
522/// Upstream zstd port of `ZSTD_splitBlock_fromBorders` (`zstd_preSplit.c:198`).
523/// Records two 512-byte byte-histograms — one from each end of a 128 KB
524/// block — and a third from the middle as a tie-breaker; returns either
525/// a quantised split point (32 KB / 64 KB / 96 KB) or the full block
526/// size when the two ends look indistinguishable. Cheaper than the
527/// chunk-based path because it touches at most 1.5 KB of input
528/// regardless of block size.
529fn split_block_from_borders(block: &[u8]) -> usize {
530    debug_assert_eq!(block.len(), MAX_BLOCK_SIZE as usize);
531    let block_size = block.len();
532    let mut past = PreSplitFingerprint::default();
533    let mut new_fp = PreSplitFingerprint::default();
534    presplit_record_byte_histogram(&mut past, &block[..PRESPLIT_BORDERS_SEGMENT]);
535    presplit_record_byte_histogram(&mut new_fp, &block[block_size - PRESPLIT_BORDERS_SEGMENT..]);
536    // Upstream zstd uses `penalty = 0, hash_log = 8` — i.e. raw byte histogram
537    // distance with no threshold padding (`zstd_preSplit.c:214`).
538    if !presplit_fingerprints_differ(&past, &new_fp, 0, 8) {
539        return block_size;
540    }
541
542    let mut middle = PreSplitFingerprint::default();
543    let mid_start = block_size / 2 - PRESPLIT_BORDERS_SEGMENT / 2;
544    presplit_record_byte_histogram(
545        &mut middle,
546        &block[mid_start..mid_start + PRESPLIT_BORDERS_SEGMENT],
547    );
548
549    let dist_from_begin = presplit_distance(&past, &middle, 8);
550    let dist_from_end = presplit_distance(&new_fp, &middle, 8);
551    // Upstream zstd `SEGMENT_SIZE * SEGMENT_SIZE / 3` (`zstd_preSplit.c:221`):
552    // if the middle is roughly equidistant from both ends, the change
553    // sits near the centre — split at the midpoint.
554    let min_distance = (PRESPLIT_BORDERS_SEGMENT as u64) * (PRESPLIT_BORDERS_SEGMENT as u64) / 3;
555    if dist_from_begin.abs_diff(dist_from_end) < min_distance {
556        return 64 * 1024;
557    }
558    // Larger `dist_from_begin` (i.e. `middle` farther from the head
559    // fingerprint, equivalently closer to the tail) means the new
560    // statistics already dominate the centre — the transition
561    // happened EARLY → emit a small 32 KB head and let the 96 KB
562    // tail absorb the rest. Inverse case: `dist_from_end` larger
563    // (middle still resembles the head) means the transition is
564    // LATE → emit a 96 KB head so the trailing 32 KB carries the
565    // new statistics alone.
566    if dist_from_begin > dist_from_end {
567        32 * 1024
568    } else {
569        96 * 1024
570    }
571}
572
573/// XXH64 (low 32 bits, seed 0) over `data`. Shared helper for the
574/// per-physical-block checksum sidecar so encoder and decoder hash
575/// the exact same byte ranges with the exact same parameters. Gated
576/// at `all(lsm, hash)` because the only consumer is the lsm-side
577/// `block_checksums` sidecar; non-lsm builds carry no reference to
578/// this helper at all.
579#[cfg(all(feature = "lsm", feature = "hash"))]
580#[inline]
581pub(crate) fn xxh64_block_low32(data: &[u8]) -> u32 {
582    let mut h = XxHash64::with_seed(0);
583    h.write(data);
584    h.finish() as u32
585}
586
587/// Bench-only entry point for the upstream zstd-parity comparator test in
588/// `tests/block_splitter_parity.rs`. Dispatches to the same
589/// `_from_borders` (split_level == 0) / `_by_chunks` (split_level ∈
590/// 1..=4) ports that `optimal_block_size` itself routes
591/// through. Caller is responsible for passing exactly
592/// `MAX_BLOCK_SIZE` bytes (per upstream zstd `ZSTD_splitBlock` contract —
593/// "@blockSize must be == 128 KB" in `zstd_preSplit.h`).
594#[cfg(feature = "bench_internals")]
595pub(crate) fn block_splitter_decision_for_bench(block: &[u8], split_level: usize) -> usize {
596    assert_eq!(
597        block.len(),
598        MAX_BLOCK_SIZE as usize,
599        "block_splitter_decision_for_bench expects exactly MAX_BLOCK_SIZE bytes"
600    );
601    assert!(
602        split_level <= 4,
603        "block_splitter_decision_for_bench: split_level must be in 0..=4, got {split_level}"
604    );
605    if split_level == 0 {
606        split_block_from_borders(block)
607    } else {
608        split_block_by_chunks(block, split_level)
609    }
610}
611
612/// Pull a pre-split window into cache with one bandwidth-bound sequential
613/// pass before the strided fingerprint histogram + match scan read it.
614///
615/// The borrowed (no-copy) over-window path matches in place on the caller's
616/// input, so the pre-split fingerprint is the FIRST touch of that 128 KiB
617/// region — a cache-cold read. `presplit_record_fingerprint` reads it with a
618/// `sampling_rate` stride and interleaved random writes into the 1 KiB events
619/// table, a latency-bound pattern that pays full DRAM miss latency per line
620/// (measured ~3x the cost of an ERMS streaming read of the same bytes). The
621/// owned path never hits this because its history-mirror copy already warmed
622/// the bytes; this restores that warmth without the copy's write half. One
623/// dependent load per 64-byte line (the i9 line size) streams under the
624/// hardware prefetcher, so the cold read is paid once at memory bandwidth and
625/// every subsequent strided sample lands in L1/L2. `black_box` keeps the loop
626/// from being optimized away as a dead read.
627#[inline]
628fn warm_presplit_window(window: &[u8]) {
629    let mut acc = 0u8;
630    let mut i = 0usize;
631    while i < window.len() {
632        acc ^= window[i];
633        i += 64;
634    }
635    core::hint::black_box(acc);
636}
637
638pub(crate) fn optimal_block_size(
639    level: CompressionLevel,
640    block: &[u8],
641    remaining_src_size: usize,
642    block_size_max: usize,
643    savings: i64,
644) -> usize {
645    let Some(split_level) = crate::encoding::levels::config::level_pre_split(level) else {
646        return remaining_src_size.min(block_size_max);
647    };
648    if remaining_src_size < MAX_BLOCK_SIZE as usize || block_size_max < MAX_BLOCK_SIZE as usize {
649        return remaining_src_size.min(block_size_max);
650    }
651    if savings < 3 {
652        return MAX_BLOCK_SIZE as usize;
653    }
654    if block.len() < MAX_BLOCK_SIZE as usize {
655        return remaining_src_size.min(block_size_max);
656    }
657    // Upstream zstd `ZSTD_splitBlock` dispatch (`zstd_preSplit.c:234`):
658    // `split_level == 0` → cheap borders heuristic;
659    // `split_level == 1..=4` → byChunks with internal sampling level
660    // `split_level - 1`.
661    let raw_split = if split_level == 0 {
662        split_block_from_borders(&block[..MAX_BLOCK_SIZE as usize])
663    } else {
664        split_block_by_chunks(&block[..MAX_BLOCK_SIZE as usize], split_level)
665    };
666    raw_split
667        .max(PRESPLIT_BLOCK_MIN)
668        .min(MAX_BLOCK_SIZE as usize)
669}
670
671pub(crate) struct CompressState<M: Matcher> {
672    pub(crate) matcher: M,
673    pub(crate) last_huff_table: Option<crate::huff0::huff0_encoder::HuffmanTable>,
674    /// Recycled `HuffmanTable` buffers: when a block clears or replaces
675    /// `last_huff_table`, the old table parks here instead of dropping, so
676    /// the next frame's dictionary entropy seed `clone_from`s into existing
677    /// allocations. Without this, every dict-seeded frame whose last block
678    /// ended raw/RLE paid a fresh two-Vec table clone per frame.
679    pub(crate) huff_table_spare: Option<crate::huff0::huff0_encoder::HuffmanTable>,
680    pub(crate) fse_tables: FseTables,
681    pub(crate) block_scratch: crate::encoding::blocks::CompressedBlockScratch,
682    /// Offset history for repeat offset encoding: [rep0, rep1, rep2].
683    /// Initialized to [1, 4, 8] per RFC 8878 §3.1.2.5.
684    pub(crate) offset_hist: [u32; 3],
685    /// Strategy tag resolved from the current `CompressionLevel` at every
686    /// `matcher.reset()` call. Used by the literal-compression gates
687    /// (`min_literals_to_compress`, `min_gain`) in
688    /// `encoding::blocks::compressed` to mirror upstream zstd's strategy-aware
689    /// thresholds (`zstd_compress_literals.c:114-127, 187-188`).
690    ///
691    /// **Invariant (required of every construction site):** must be
692    /// initialized from the active `CompressionLevel` via
693    /// `StrategyTag::for_compression_level`, and re-synced from the
694    /// active level alongside every `matcher.reset()` call so the
695    /// level-aware gates stay correct after a level change. The two
696    /// reset sites that own this sync are `FrameCompressor::compress`
697    /// and `StreamingEncoder::ensure_frame_started`. There is no
698    /// `Default` impl — production constructors
699    /// (`FrameCompressor::new`, `new_with_matcher`, the streaming
700    /// encoder constructor) plumb this explicitly. Tests that build
701    /// `CompressState` by hand must also supply a value.
702    pub(crate) strategy_tag: crate::encoding::strategy::StrategyTag,
703    /// Whether the HUF literal table build runs the #167 table-log search
704    /// (`true`) or the cheap single-build (`false`). The search is a clean
705    /// ratio win over upstream zstd but costs ~1.5 us per literal section —
706    /// negligible on large inputs, ~20% on small ones. The Fast and DoubleFast
707    /// matchers are byte-faithful to upstream zstd, so the cheap path ties them;
708    /// the search is therefore gated ON only for large (> 128 KiB) Fast and
709    /// DoubleFast frames. Higher strategies always keep it (their matchers
710    /// diverge, making the search load-bearing for ratio). Set per frame
711    /// alongside `strategy_tag` via [`huf_search_enabled`].
712    pub(crate) huf_optimal_search: bool,
713    /// Mirror of upstream zstd's `ZSTD_literalsCompressionIsDisabled`
714    /// (zstd_compress_internal.h): in the default (`auto`) literal-compression
715    /// mode the literals section is emitted RAW (no Huffman) when
716    /// `strategy == ZSTD_fast && targetLength > 0`. For the levels we resolve,
717    /// that is exactly the negative levels (Fast strategy with `targetLength =
718    /// -level > 0`; L1/L2 are Fast with `targetLength == 0`). C trades the
719    /// literal-Huffman pass for speed there, so matching it keeps both the frame
720    /// size and the encode cost in parity on the negative band. Set per frame
721    /// alongside `strategy_tag`.
722    pub(crate) literal_compression_disabled: bool,
723}
724
725/// Whether the HUF literal build should run the #167 table-log search for a
726/// frame of `source_size` bytes (see [`CompressState::huf_optimal_search`]).
727/// Upstream gates the optimal-depth tableLog probe to
728/// `HUF_OPTIMAL_DEPTH_THRESHOLD = ZSTD_btultra` (huf.h:117): only btultra /
729/// btultra2 search the tableLog, every lower strategy (fast .. btopt) takes the
730/// single-shot fast path (`HUF_optimalTableLog`, huf_compress.c:1284-1287).
731/// Mirror that so our literal tableLog choice tracks upstream's instead of
732/// spending the search to beat it on ratio at a speed cost.
733pub(crate) fn huf_search_enabled(
734    strategy: crate::encoding::strategy::StrategyTag,
735    _source_size: Option<u64>,
736) -> bool {
737    use crate::encoding::strategy::StrategyTag;
738    matches!(strategy, StrategyTag::BtUltra | StrategyTag::BtUltra2)
739}
740
741impl<M: Matcher> CompressState<M> {
742    /// Clears `last_huff_table`, parking the table's buffers in
743    /// `huff_table_spare` for reuse instead of dropping them.
744    #[inline]
745    pub(crate) fn clear_huff_table(&mut self) {
746        if let Some(table) = self.last_huff_table.take() {
747            self.huff_table_spare = Some(table);
748        }
749    }
750
751    /// Replaces `last_huff_table` with `table`, parking any displaced table
752    /// in `huff_table_spare` for reuse.
753    #[inline]
754    pub(crate) fn replace_huff_table(&mut self, table: crate::huff0::huff0_encoder::HuffmanTable) {
755        if let Some(old) = self.last_huff_table.replace(table) {
756            self.huff_table_spare = Some(old);
757        }
758    }
759}
760
761/// Per-frame setup resolved once by [`FrameCompressor::prepare_frame`] and
762/// consumed by the block loop + [`FrameCompressor::finish_frame`]. Lets the
763/// owned `compress()` and the borrowed one-shot path share identical
764/// reset / dict-prime / entropy-seed setup and frame-tail emission.
765struct FramePrep {
766    window_size: u64,
767    use_dictionary_state: bool,
768    source_size_hint_known: bool,
769    initial_size_hint: Option<u64>,
770}
771
772/// Initial capacity for the `all_blocks` accumulator, by source-size hint.
773/// The frame header is written only after all input is read (so
774/// Frame_Content_Size is known), so compressed blocks accumulate in memory
775/// first. Seed-size tiers (mirrors upstream zstd `ZSTD_CStreamOutSize` naming):
776/// - tiny (`<= 4 KiB` hint): payload-bound seed, `>=` anything a tiny input's
777///   compressed output could need.
778/// - small (`<= 64 KiB` hint): absorbs one or two `Vec::extend` doublings
779///   without over-allocating.
780/// - default (one upstream zstd block, `130 KiB`): the value the rest of the encoder
781///   is sized around; larger inputs amortise the first doublings cheaply and
782///   the residue is dominated by internal `compress_block_encoded` buffers.
783///
784/// Shared by the owned (`run_owned_block_loop`) and borrowed
785/// (`run_borrowed_block_loop`) paths so the tier table can't drift between them.
786///
787/// `block_capacity` (the active `targetCBlockSize` cap, or the 128 KiB
788/// format ceiling) bounds every tier: with a small target the first
789/// allocation tracks one capped block + header/checksum slack instead of
790/// keeping the upstream zstd-sized floor that only later growth respects.
791fn initial_all_blocks_cap(initial_size_hint: Option<u64>, block_capacity: usize) -> usize {
792    const TINY_THRESHOLD: u64 = 4 * 1024;
793    const SMALL_THRESHOLD: u64 = 64 * 1024;
794    const TINY_CAP: usize = 4 * 1024;
795    const SMALL_CAP: usize = 16 * 1024;
796    const DEFAULT_CAP: usize = 130 * 1024;
797    let first_block_cap = block_capacity + 3 + 16;
798    match initial_size_hint {
799        Some(h) if h <= TINY_THRESHOLD => TINY_CAP.min(first_block_cap),
800        Some(h) if h <= SMALL_THRESHOLD => SMALL_CAP.min(first_block_cap),
801        _ => DEFAULT_CAP.min(first_block_cap),
802    }
803}
804
805/// Per-block feeder for `run_owned_block_loop`.
806///
807/// `fill_block` appends source bytes to `buf` (which already holds any
808/// carried pre-split suffix) until `buf.len() == block_capacity` or the
809/// source is exhausted, returning `(bytes_appended, reached_eof)`.
810/// `reached_eof` is true when no more input follows this block: either the
811/// block could not be filled to `block_capacity`, or it filled exactly and the
812/// source is confirmed exhausted (the slice knows its length; the reader probes
813/// one byte ahead). An input that is an exact multiple of the block size
814/// therefore marks its final full block `last_block` rather than emitting a
815/// spurious trailing empty block.
816///
817/// The slice impl exists so the slice entry points
818/// (`compress_independent_frame_into`, `compress_oneshot_*` fallbacks)
819/// append with one `extend_from_slice` — the generic reader impl must
820/// `resize` an initialized target region before `Read::read` can fill it,
821/// which costs a zero-fill memset of the whole block on every frame.
822pub(crate) trait OwnedBlockSource {
823    fn fill_block(
824        &mut self,
825        buf: &mut Vec<u8>,
826        block_capacity: usize,
827        size_hint_remaining: Option<u64>,
828    ) -> (usize, bool);
829}
830
831impl OwnedBlockSource for &[u8] {
832    fn fill_block(
833        &mut self,
834        buf: &mut Vec<u8>,
835        block_capacity: usize,
836        _size_hint_remaining: Option<u64>,
837    ) -> (usize, bool) {
838        let want = block_capacity - buf.len();
839        let take = want.min(self.len());
840        buf.extend_from_slice(&self[..take]);
841        *self = &self[take..];
842        // EOF when this fill could not top the block to `block_capacity`
843        // (`take < want`) OR it exactly consumed the last input bytes
844        // (`self` now empty). The slice knows its own length, so a block that
845        // exactly fills capacity at end-of-input is reported as the final
846        // block here — the loop marks it `last_block` instead of emitting a
847        // spurious trailing empty block on the next iteration. Mirrors the C
848        // encoder, which marks the last real block last on `ZSTD_e_end`.
849        (take, take < want || self.is_empty())
850    }
851}
852
853/// Adapter routing a generic [`Read`] source through [`OwnedBlockSource`]:
854/// preserves the historical sizing behaviour — an initialized target region
855/// bounded by the source-size hint, grown (doubling, capped) only when the
856/// hint under-counted.
857/// `peeked` holds a single look-ahead byte: when a block fills exactly to
858/// `block_capacity`, `fill_block` reads one more byte to learn whether the
859/// stream ended on that boundary. A `None` from that probe sets EOF (so the
860/// just-filled block is marked last, mirroring the C encoder on `ZSTD_e_end`);
861/// a byte is stashed here and prepended to the next block instead of leaking a
862/// spurious trailing empty block when the input is an exact multiple of the
863/// block size.
864pub(crate) struct ReaderBlockSource<Rd> {
865    pub(crate) reader: Rd,
866    peeked: Option<u8>,
867}
868
869impl<Rd> ReaderBlockSource<Rd> {
870    pub(crate) fn new(reader: Rd) -> Self {
871        Self {
872            reader,
873            peeked: None,
874        }
875    }
876}
877
878impl<Rd: Read> OwnedBlockSource for ReaderBlockSource<Rd> {
879    fn fill_block(
880        &mut self,
881        buf: &mut Vec<u8>,
882        block_capacity: usize,
883        size_hint_remaining: Option<u64>,
884    ) -> (usize, bool) {
885        let start = buf.len();
886        let mut filled = start;
887        let mut reached_eof = false;
888        // Prepend the look-ahead byte read past the previous full block. In
889        // stream order it follows any carried pre-split suffix already in
890        // `buf`, so it is appended after that suffix and counted as part of
891        // this block's appended bytes.
892        if let Some(b) = self.peeked.take() {
893            buf.push(b);
894            filled += 1;
895        }
896        // Size the read buffer to the bytes this block actually expects
897        // rather than always zero-filling a full MAX_BLOCK_SIZE: a small
898        // frame otherwise pays a 128 KiB `resize(_, 0)` memset per block
899        // just to read a few KiB (the zero-fill past `filled` is then
900        // truncated away).
901        //
902        // Overflow-free by construction (no `saturating_*` masking):
903        // `filled <= block_capacity` always (the read only ever targets
904        // `[filled..len]` with `len <= block_capacity`, and a carried-over
905        // pre-split suffix is a `split_off` below `block_capacity`), so
906        // `block_capacity - filled` never underflows; pinning `remaining`
907        // to `block_capacity` before the `usize` cast keeps the cast and
908        // the final add within `usize` on every target.
909        let initial_target = match size_hint_remaining {
910            Some(remaining) => {
911                let remaining = remaining.min(block_capacity as u64) as usize;
912                filled + remaining.min(block_capacity - filled)
913            }
914            // Unknown hint, or an inexact hint already met by prior blocks:
915            // read against the full block window.
916            None => block_capacity,
917        };
918        if buf.len() < initial_target {
919            buf.resize(initial_target, 0);
920        }
921        loop {
922            if reached_eof || filled == block_capacity {
923                break;
924            }
925            if filled == buf.len() {
926                // Hint under-counted the block; grow toward block_capacity
927                // (doubling, capped) so reading continues without paying a
928                // full-buffer zero up front. `len <= block_capacity` so the
929                // double stays well within `usize`; `filled < block_capacity`
930                // here (the `== block_capacity` break fired otherwise), so
931                // `filled + 1 <= block_capacity`.
932                let grow_to = (buf.len() * 2).clamp(filled + 1, block_capacity);
933                buf.resize(grow_to, 0);
934            }
935            let read_end = buf.len();
936            let new_bytes = self.reader.read(&mut buf[filled..read_end]).unwrap();
937            if new_bytes == 0 {
938                reached_eof = true;
939                break;
940            }
941            filled += new_bytes;
942        }
943        // Look ahead one byte when the block filled exactly to capacity: a
944        // 0-byte read means the stream ended on the block boundary, so this
945        // block is the last one (the loop marks it `last_block`); otherwise
946        // stash the byte for the next block. Without this, an input that is an
947        // exact multiple of the block size would emit a spurious trailing
948        // empty block (the next iteration reads 0 and serializes an empty
949        // last Raw block). A blocking reader's probe read is consistent with
950        // the existing pull model — the next `fill_block` would block on the
951        // same byte anyway.
952        if !reached_eof && filled == block_capacity {
953            let mut probe = [0u8; 1];
954            if self.reader.read(&mut probe).unwrap() == 0 {
955                reached_eof = true;
956            } else {
957                self.peeked = Some(probe[0]);
958            }
959        }
960        buf.truncate(filled);
961        (filled - start, reached_eof)
962    }
963}
964
965impl<R: Read, W: Write> FrameCompressor<R, W, MatchGeneratorDriver> {
966    /// Create a new `FrameCompressor`
967    pub fn new(compression_level: CompressionLevel) -> Self {
968        Self {
969            uncompressed_data: None,
970            compressed_data: None,
971            compression_level,
972            dictionary: None,
973            dictionary_entropy_cache: None,
974            source_size_hint: None,
975            state: CompressState {
976                matcher: MatchGeneratorDriver::new(1024 * 128, 1),
977                last_huff_table: None,
978                huff_table_spare: None,
979                fse_tables: FseTables::new(),
980                block_scratch: crate::encoding::blocks::CompressedBlockScratch::new(),
981                offset_hist: [1, 4, 8],
982                strategy_tag: crate::encoding::strategy::StrategyTag::for_compression_level(
983                    compression_level,
984                ),
985                huf_optimal_search: true,
986                literal_compression_disabled: matches!(
987                    compression_level,
988                    crate::encoding::CompressionLevel::Level(n) if n < 0
989                ),
990            },
991            magicless: false,
992            content_checksum: false,
993            content_size_flag: true,
994            dict_id_flag: true,
995            target_block_size: None,
996            #[cfg(feature = "hash")]
997            hasher: XxHash64::with_seed(0),
998            #[cfg(feature = "lsm")]
999            frame_emit_info: None,
1000            #[cfg(all(feature = "lsm", feature = "hash"))]
1001            per_block_checksums_enabled: false,
1002            #[cfg(all(feature = "lsm", feature = "hash"))]
1003            block_checksums: None,
1004            #[cfg(feature = "lsm")]
1005            block_decompressed_sizes: alloc::vec::Vec::new(),
1006            strategy_override: None,
1007        }
1008    }
1009
1010    /// Configure fine-grained compression parameters (#27).
1011    ///
1012    /// Resets the base [`CompressionLevel`](crate::encoding::CompressionLevel)
1013    /// to the parameters' level and installs the per-knob overrides
1014    /// (window/hash/chain/search logs, strategy, LDM) applied at the next
1015    /// frame. Pass `None`-equivalent (a builder that overrides nothing)
1016    /// to fall back to plain level-based compression.
1017    ///
1018    /// ```rust
1019    /// use structured_zstd::encoding::{
1020    ///     CompressionLevel, CompressionParameters, FrameCompressor, Strategy,
1021    /// };
1022    /// let params = CompressionParameters::builder(CompressionLevel::Level(19))
1023    ///     .strategy(Strategy::Btultra2)
1024    ///     .enable_long_distance_matching(true)
1025    ///     .build()
1026    ///     .unwrap();
1027    /// let mut compressor: FrameCompressor = FrameCompressor::new(CompressionLevel::Default);
1028    /// compressor.set_parameters(&params);
1029    /// let compressed = compressor.compress_independent_frame(b"some data to compress");
1030    /// assert!(!compressed.is_empty());
1031    /// ```
1032    pub fn set_parameters(&mut self, params: &crate::encoding::CompressionParameters) {
1033        self.compression_level = params.level();
1034        let overrides = params.overrides();
1035        self.strategy_override = overrides.strategy.map(|s| s.tag());
1036        // Keep `state.strategy_tag` consistent immediately so the borrowed
1037        // one-shot eligibility gate (`borrowed_eligible`) and literal gates
1038        // are correct even before the next `compress()` re-sync. Resolve it
1039        // size-adaptively (same `resolve_level_params` path `prepare_frame`
1040        // uses) so a hint already set here yields the same strategy the matcher
1041        // will run, not the bare level-only mapping.
1042        self.state.strategy_tag = self.strategy_override.unwrap_or_else(|| {
1043            crate::encoding::levels::config::resolve_level_params(
1044                self.compression_level,
1045                self.source_size_hint,
1046            )
1047            .strategy_tag
1048        });
1049        self.state.huf_optimal_search =
1050            huf_search_enabled(self.state.strategy_tag, self.source_size_hint);
1051        // C `ZSTD_literalsCompressionIsDisabled` (ps_auto): raw literals iff the
1052        // EFFECTIVE cParams are the fast strategy with targetLength > 0. A
1053        // strategy override (e.g. negative level + BtUltra2) flips `strategy_tag`
1054        // away from fast, so the resolved tag — not the signed level — gates
1055        // this. For the fast strategy the level table sets targetLength > 0
1056        // exactly on the negative (acceleration) rows, so absent an explicit
1057        // targetLength override `level < 0` is that test; an override wins.
1058        self.state.literal_compression_disabled = self.state.strategy_tag
1059            == crate::encoding::strategy::StrategyTag::Fast
1060            && overrides.target_length.map_or_else(
1061                || {
1062                    matches!(
1063                        self.compression_level,
1064                        crate::encoding::CompressionLevel::Level(n) if n < 0
1065                    )
1066                },
1067                |tl| tl > 0,
1068            );
1069        self.state.matcher.set_param_overrides(Some(overrides));
1070    }
1071
1072    /// Whether the borrowed (no per-block history copy) one-shot loop is
1073    /// valid for an `input_len`-byte slice under the resolved `prep`.
1074    ///
1075    /// `Uncompressed` resolves to `StrategyTag::Fast` but must emit stored
1076    /// Raw blocks, which the borrowed loop's
1077    /// `compress_block_encoded_borrowed` (RLE/raw-fast/compressed) does NOT
1078    /// do, so exclude it; it then takes the owned path's dedicated
1079    /// Uncompressed arm.
1080    ///
1081    /// No window-size gate: over-window inputs are handled too. The owned
1082    /// path bounds matches to the last `advertised_window` bytes via
1083    /// `window_low` and evicts/rehashes its history; the borrowed path
1084    /// computes the identical `window_low = block_end - advertised_window`
1085    /// and the kernel rejects any hash candidate below it, while the
1086    /// per-position `put` during the scan keeps in-window slots current,
1087    /// so it produces byte-identical output to the owned (evicting) path
1088    /// without ever copying the input into `history`, even when the input
1089    /// far exceeds the window.
1090    ///
1091    /// BUT gate on `input_len <= u32::MAX`: the Fast kernel stores ABSOLUTE
1092    /// positions in a `u32` hash table, and the borrowed scan walks
1093    /// absolute input offsets up to `block_end == input.len()`. Past 4 GiB
1094    /// those offsets truncate / overflow the `u32` position math
1095    /// (`base_off + ip0 as u32`, `window_low`), panicking or corrupting.
1096    /// The owned/evicting path keeps the scanned window bounded (positions
1097    /// stay small), so >4 GiB inputs fall back to it.
1098    fn borrowed_eligible(&self, input_len: usize, prep: &FramePrep) -> bool {
1099        if matches!(self.compression_level, CompressionLevel::Uncompressed)
1100            || input_len > u32::MAX as usize
1101        {
1102            return false;
1103        }
1104        if prep.use_dictionary_state {
1105            // The borrowed dict scan runs in VIRTUAL `[dict][input]` coordinates,
1106            // so the position space is `dict_content.len() + input_len`, not just
1107            // `input_len`. A large attached dictionary plus an otherwise-allowed
1108            // input can exceed the `u32` floor the kernel asserts — fall back to
1109            // the owned (copy) path in that case.
1110            let fits_u32 = self
1111                .dictionary
1112                .as_ref()
1113                .and_then(|dict| dict.inner.dict_content.len().checked_add(input_len))
1114                .is_some_and(|virtual_len| virtual_len <= u32::MAX as usize);
1115            if !fits_u32 {
1116                return false;
1117            }
1118            // Dictionary frames: only the Simple (Fast) backend in attach mode
1119            // has a borrowed (no input copy) dict scan. Copy-mode dict frames
1120            // and the other backends still take the owned path.
1121            return self.state.matcher.borrowed_dict_supported();
1122        }
1123        // The borrowed (no-copy, in-place over-window) scan exists for the
1124        // Simple (Fast), Dfast, and Row backends, and for the HashChain
1125        // backend's lazy CHAIN parser; BT/optimal (BinaryTree search) stay on
1126        // the owned path. Every borrowed scan applies the per-position
1127        // `window_low = abs_ip - advertised_window` offset cap so over-window
1128        // inputs are matched in place (no input->history copy), matching C's
1129        // continuous-index + windowLow one-shot behaviour.
1130        self.state.matcher.borrowed_supported()
1131    }
1132
1133    /// Compress `input` as one frame's worth of blocks into `out` (appended
1134    /// from its current end): the borrowed in-place loop when
1135    /// [`Self::borrowed_eligible`], else the owned (history-copying) loop fed
1136    /// an in-place `&[u8]` cursor. Returns `total_uncompressed`; the caller
1137    /// emits the frame header (before this call, when the content size is
1138    /// known) or the drain tail.
1139    fn run_one_frame(&mut self, input: &[u8], prep: &FramePrep, out: &mut Vec<u8>) -> u64 {
1140        if self.borrowed_eligible(input.len(), prep) {
1141            self.run_borrowed_block_loop(input, out)
1142        } else {
1143            let mut cursor: &[u8] = input;
1144            self.run_owned_block_loop(&mut cursor, prep.initial_size_hint, true, out)
1145        }
1146    }
1147
1148    /// Compress one contiguous `&[u8]` as a single independent Zstd frame,
1149    /// writing the frame bytes into `out` (its previous contents are
1150    /// replaced and its allocation reused), reusing this compressor's heavy
1151    /// state across calls.
1152    ///
1153    /// This is the reusable-compression-context (CCtx-equivalent) entry
1154    /// point, mirroring C `ZSTD_compress2` over a reused `ZSTD_CCtx`:
1155    /// construct ONE `FrameCompressor` and call this in a loop to emit N
1156    /// independent, self-describing frames (each carrying its own header,
1157    /// blocks, and checksum, decodable in isolation, with no cross-frame
1158    /// match history). Every call resets the per-frame state via
1159    /// [`Self::prepare_frame`]: only the allocations are kept, so the
1160    /// dominant per-frame setup cost (table allocation + dictionary prime)
1161    /// is paid once instead of N times. Passing the same `out` buffer each
1162    /// call additionally reuses the output allocation, matching C's
1163    /// caller-owned `dst` buffer (no per-frame output allocation).
1164    ///
1165    /// Reusing the context + `out` across many small frames (the typical
1166    /// per-block-frame workload) is far cheaper than a fresh
1167    /// [`compress_slice_to_vec`](crate::encoding::compress_slice_to_vec)
1168    /// per block, which allocates and primes from scratch each time.
1169    ///
1170    /// The input is read in place: no [`Self::set_source`] /
1171    /// [`Self::set_drain`] setup is required, and the input lifetime is not
1172    /// baked into the compressor type, so successive calls may pass slices
1173    /// with unrelated lifetimes. When the Fast (Simple) backend is active
1174    /// and no dictionary is set, the matcher references the input directly
1175    /// (no per-block history copy); other backends / dictionary use copy
1176    /// each block into history exactly as the streaming
1177    /// [`compress`](Self::compress) path does. The source-size hint is
1178    /// derived from the input length on every call, so per-frame table
1179    /// sizing tracks each frame's actual size regardless of any earlier
1180    /// hint.
1181    ///
1182    /// A sticky dictionary set via
1183    /// [`set_dictionary`](Self::set_dictionary) (or its variants) is primed
1184    /// into every frame, mirroring `ZSTD_CCtx_loadDictionary` /
1185    /// `ZSTD_CCtx_refCDict`.
1186    ///
1187    /// # Panics
1188    ///
1189    /// Panics on encoder error, matching [`Self::compress`] and
1190    /// [`compress_slice_to_vec`](crate::encoding::compress_slice_to_vec).
1191    pub fn compress_independent_frame_into(&mut self, input: &[u8], out: &mut Vec<u8>) {
1192        // Size the next frame from the actual payload, not a stale hint a
1193        // previous call may have left behind (a wrong hint would change the
1194        // resolved window/header and could flip borrowed eligibility).
1195        self.source_size_hint = Some(input.len() as u64);
1196        let prep = self.prepare_frame();
1197        // Content size is known up front (one-shot), so write the frame
1198        // header FIRST and emit blocks STRAIGHT into `out` — no separate
1199        // `all_blocks` accumulator and no header+blocks copy (which was the
1200        // dominant per-frame memmove + the only un-amortized per-frame alloc
1201        // even when the compressor is reused).
1202        let total_uncompressed = input.len() as u64;
1203        let emit_checksum = cfg!(feature = "hash") && self.content_checksum;
1204        let checksum_len = if emit_checksum { 4 } else { 0 };
1205        out.clear();
1206        // Reserve the header plus ONE block's worst case up front; the block
1207        // loops then grow `out` from the compression ratio observed so far
1208        // (`reserve_for_next_block`). Reserving `compress_bound(input_len)`
1209        // here held a whole-input-sized allocation for the entire frame —
1210        // ~100 MiB peak on a 100 MiB stream whose compressed output is a few
1211        // MiB, where the reference implementation's context peaks at
1212        // window-sized state. Small frames (<= one block) still get their
1213        // full bound in one shot, so the reused-`out` steady state is
1214        // unchanged. 18 = max frame header (magic 4 + descriptor 1 + window
1215        // 1 + dict id 4 + FCS 8).
1216        let first_block_bound = input.len().min(self.block_capacity()) + 3;
1217        out.reserve(18 + first_block_bound + checksum_len);
1218        self.append_frame_header(total_uncompressed, &prep, out);
1219        let header_len = out.len();
1220        let _ = self.run_one_frame(input, &prep, out);
1221        #[cfg(feature = "hash")]
1222        if self.content_checksum {
1223            out.extend_from_slice(&(self.hasher.finish() as u32).to_le_bytes());
1224        }
1225        #[cfg(feature = "lsm")]
1226        {
1227            let blocks_end = out.len() - checksum_len;
1228            self.populate_frame_emit_info(header_len, &out[header_len..blocks_end], emit_checksum);
1229        }
1230        #[cfg(not(feature = "lsm"))]
1231        let _ = header_len;
1232    }
1233
1234    /// Convenience wrapper over [`Self::compress_independent_frame_into`]
1235    /// that allocates and returns a fresh `Vec` per call. Prefer the
1236    /// `_into` form in tight per-block-frame loops to reuse one output
1237    /// buffer across frames (the CCtx-equivalent zero-per-call-alloc
1238    /// output, matching C's caller-owned `dst`).
1239    ///
1240    /// ```rust
1241    /// use structured_zstd::encoding::{FrameCompressor, CompressionLevel};
1242    /// let mut cctx: FrameCompressor = FrameCompressor::new(CompressionLevel::Default);
1243    /// let frame_a = cctx.compress_independent_frame(b"first block payload");
1244    /// let frame_b = cctx.compress_independent_frame(b"second block payload");
1245    /// assert!(!frame_a.is_empty() && !frame_b.is_empty());
1246    /// ```
1247    pub fn compress_independent_frame(&mut self, input: &[u8]) -> Vec<u8> {
1248        let mut out = Vec::new();
1249        self.compress_independent_frame_into(input, &mut out);
1250        out
1251    }
1252
1253    /// Borrowed one-shot block loop: walks `input` in `MAX_BLOCK_SIZE`
1254    /// strides (the Fast backend never pre-splits, so boundaries match the
1255    /// owned loop), scanning each block range in place against the
1256    /// borrowed window via `compress_block_encoded_borrowed` — no
1257    /// per-block `commit_space` copy. Returns `(all_blocks,
1258    /// total_uncompressed)`. Caller guarantees Fast backend + no
1259    /// dictionary; over-window inputs are fine (matches are bounded by
1260    /// `window_low` exactly as the owned evicting path).
1261    fn run_borrowed_block_loop(&mut self, input: &[u8], out: &mut Vec<u8>) -> u64 {
1262        // Blocks are appended to `out` starting here. `out` may already hold
1263        // the frame header (the one-shot compress-into-Vec path writes it
1264        // first, since the content size is known up front, and the loop
1265        // emits blocks straight after it — no separate `all_blocks` Vec and
1266        // no header+blocks copy). Output-size reads below are taken RELATIVE
1267        // to `blocks_start` so a header prefix never skews the upstream zstd split
1268        // `savings` gate (which would change block boundaries / wire output).
1269        let blocks_start = out.len();
1270        let total_uncompressed = input.len() as u64;
1271        // Empty input: emit a single empty last Raw block (mirrors the
1272        // owned loop's empty-file special case).
1273        if input.is_empty() {
1274            let header = BlockHeader {
1275                last_block: true,
1276                block_type: crate::blocks::block::BlockType::Raw,
1277                block_size: 0,
1278            };
1279            header.serialize(out);
1280            #[cfg(feature = "lsm")]
1281            self.block_decompressed_sizes.push(0);
1282            #[cfg(all(feature = "lsm", feature = "hash"))]
1283            if let Some(checksums) = self.block_checksums.as_mut() {
1284                checksums.push(xxh64_block_low32(&[]));
1285            }
1286            return total_uncompressed;
1287        }
1288        // SAFETY: `input` outlives this call (held by the caller across
1289        // the call) and is not mutated. Only the Simple backend is active
1290        // (gated by `compress_oneshot_borrowed`).
1291        unsafe {
1292            self.state.matcher.set_borrowed_window(input);
1293        }
1294        // Panic-safety: clear the borrowed `(ptr, len)` on EVERY exit,
1295        // including an unwind from an `assert!` inside the block loop, so
1296        // a caught-and-reused compressor never retains a dangling window.
1297        // (The next frame's `reset()` also clears it before any read, but
1298        // this guard makes the invariant local and unwind-proof.)
1299        struct ClearBorrowedOnDrop(*mut MatchGeneratorDriver);
1300        impl Drop for ClearBorrowedOnDrop {
1301            fn drop(&mut self) {
1302                // SAFETY: at drop (normal return or unwind) the loop's
1303                // borrows of the matcher have ended, so this is the only
1304                // access. `addr_of_mut!` produced this pointer without an
1305                // intermediate `&mut`, so the interleaved `&mut` uses in
1306                // the loop did not invalidate it.
1307                unsafe { (*self.0).clear_borrowed_window() };
1308            }
1309        }
1310        let _clear_guard = ClearBorrowedOnDrop(core::ptr::addr_of_mut!(self.state.matcher));
1311        let block_capacity = self.block_capacity();
1312        let mut start = 0usize;
1313        while start < input.len() {
1314            reserve_for_next_block(
1315                out,
1316                blocks_start,
1317                start as u64,
1318                input.len() - start,
1319                block_capacity,
1320            );
1321            // Upstream zstd `ZSTD_compress_frameChunk`: size each block via the cheap
1322            // fingerprint pre-splitter so a full 128 KiB block is cut at a
1323            // statistical boundary when it pays. `savings = consumed -
1324            // produced` mirrors the upstream zstd gate (the first block and
1325            // incompressible input keep the full 128 KiB). The borrowed window
1326            // already spans the whole input, so a smaller block is just a
1327            // narrower `(block_start, block_end)` range into it.
1328            let savings = start as i64 - (out.len() - blocks_start) as i64;
1329            // Borrowed path only: warm the pre-split window before the
1330            // cache-cold strided fingerprint read. Gated to exactly the
1331            // conditions under which `optimal_block_size` reads `block`
1332            // (a pre-split level, a full 128 KiB block remaining, the
1333            // block-size cap admits a full block, and `savings >= 3` so the
1334            // splitter actually runs) — so non-pre-split levels, the first
1335            // block, and the trailing partial block pay nothing. See
1336            // `warm_presplit_window`.
1337            if savings >= 3
1338                && input.len() - start >= MAX_BLOCK_SIZE as usize
1339                && block_capacity >= MAX_BLOCK_SIZE as usize
1340                && crate::encoding::levels::config::level_pre_split(self.compression_level)
1341                    .is_some()
1342            {
1343                warm_presplit_window(&input[start..start + MAX_BLOCK_SIZE as usize]);
1344            }
1345            let block_len = optimal_block_size(
1346                self.compression_level,
1347                &input[start..],
1348                input.len() - start,
1349                block_capacity,
1350                savings,
1351            );
1352            let end = (start + block_len).min(input.len());
1353            let block = &input[start..end];
1354            let last_block = end == input.len();
1355            #[cfg(feature = "hash")]
1356            if self.content_checksum {
1357                self.hasher.write(block);
1358            }
1359            let dict_active =
1360                self.dictionary.is_some() && self.state.matcher.supports_dictionary_priming();
1361            crate::encoding::levels::compress_block_encoded_borrowed(
1362                &mut self.state,
1363                self.compression_level,
1364                last_block,
1365                block,
1366                start,
1367                end,
1368                dict_active,
1369                out,
1370                #[cfg(feature = "lsm")]
1371                Some(&mut self.block_decompressed_sizes),
1372                #[cfg(all(feature = "lsm", feature = "hash"))]
1373                self.block_checksums.as_mut(),
1374            );
1375            start = end;
1376        }
1377        // `_clear_guard` drops here, clearing the borrowed window.
1378        total_uncompressed
1379    }
1380}
1381
1382impl<R: Read, W: Write, M: Matcher> FrameCompressor<R, W, M> {
1383    /// Create a new `FrameCompressor` with a custom matching algorithm implementation
1384    pub fn new_with_matcher(matcher: M, compression_level: CompressionLevel) -> Self {
1385        Self {
1386            uncompressed_data: None,
1387            compressed_data: None,
1388            dictionary: None,
1389            dictionary_entropy_cache: None,
1390            source_size_hint: None,
1391            state: CompressState {
1392                matcher,
1393                last_huff_table: None,
1394                huff_table_spare: None,
1395                fse_tables: FseTables::new(),
1396                block_scratch: crate::encoding::blocks::CompressedBlockScratch::new(),
1397                offset_hist: [1, 4, 8],
1398                strategy_tag: crate::encoding::strategy::StrategyTag::for_compression_level(
1399                    compression_level,
1400                ),
1401                huf_optimal_search: true,
1402                literal_compression_disabled: matches!(
1403                    compression_level,
1404                    crate::encoding::CompressionLevel::Level(n) if n < 0
1405                ),
1406            },
1407            compression_level,
1408            magicless: false,
1409            content_checksum: false,
1410            content_size_flag: true,
1411            dict_id_flag: true,
1412            target_block_size: None,
1413            #[cfg(feature = "hash")]
1414            hasher: XxHash64::with_seed(0),
1415            #[cfg(feature = "lsm")]
1416            frame_emit_info: None,
1417            #[cfg(all(feature = "lsm", feature = "hash"))]
1418            per_block_checksums_enabled: false,
1419            #[cfg(all(feature = "lsm", feature = "hash"))]
1420            block_checksums: None,
1421            #[cfg(feature = "lsm")]
1422            block_decompressed_sizes: alloc::vec::Vec::new(),
1423            strategy_override: None,
1424        }
1425    }
1426
1427    /// Enable or disable magicless frame format (`ZSTD_f_zstd1_magicless`).
1428    ///
1429    /// When set to `true`, emitted frames omit the 4-byte magic number
1430    /// prefix. The matching decoder must be configured to expect a
1431    /// magicless stream — wire-format only round-trips with a
1432    /// magicless-aware decoder.
1433    pub fn set_magicless(&mut self, magicless: bool) {
1434        self.magicless = magicless;
1435    }
1436
1437    /// Enable or disable the trailing XXH64 content checksum
1438    /// (semantics of upstream `ZSTD_c_checksumFlag`). Default `false`,
1439    /// matching the upstream library default (`ZSTD_c_checksumFlag = 0`)
1440    /// so out-of-the-box frames carry the same layout and pay the same
1441    /// costs as the reference implementation.
1442    ///
1443    /// When `false`, emitted frames set `Content_Checksum_flag = 0` and carry
1444    /// no trailing digest; such frames are valid (RFC 8878) and decode
1445    /// correctly in any [`ContentChecksum`](crate::decoding::ContentChecksum)
1446    /// mode. Without the `hash` feature no checksum is emitted regardless of
1447    /// this setting.
1448    pub fn set_content_checksum(&mut self, emit: bool) {
1449        self.content_checksum = emit;
1450    }
1451
1452    /// Enable or disable recording `Frame_Content_Size` in the frame header
1453    /// when the total size is known (semantics of upstream
1454    /// `ZSTD_c_contentSizeFlag`). Default `true`, matching upstream. With
1455    /// the flag off the header carries a window descriptor instead (and the
1456    /// single-segment layout, which requires an FCS, is disabled).
1457    pub fn set_content_size_flag(&mut self, emit: bool) {
1458        self.content_size_flag = emit;
1459    }
1460
1461    /// Enable or disable recording the dictionary ID in the frame header
1462    /// when a dictionary is attached (semantics of upstream
1463    /// `ZSTD_c_dictIDFlag`). Default `true`, matching upstream. Frames
1464    /// emitted with the flag off still decode when the decoder is handed
1465    /// the dictionary explicitly.
1466    pub fn set_dictionary_id_flag(&mut self, emit: bool) {
1467        self.dict_id_flag = emit;
1468    }
1469
1470    /// Set an upper bound on emitted block sizes (semantics of upstream
1471    /// `ZSTD_c_targetCBlockSize`): every physical block's payload is capped
1472    /// at `target` bytes (+3-byte block header on the wire), trading some
1473    /// ratio for bounded per-block latency. The value is clamped to
1474    /// `[MIN_TARGET_BLOCK_SIZE, MAX_BLOCK_SIZE]` (the upstream bounds).
1475    /// `None` removes the target.
1476    pub fn set_target_block_size(&mut self, target: Option<u32>) {
1477        self.target_block_size = target.map(|t| {
1478            t.clamp(
1479                crate::common::MIN_TARGET_BLOCK_SIZE,
1480                crate::common::MAX_BLOCK_SIZE,
1481            )
1482        });
1483    }
1484
1485    /// The active block-size cap: the configured target, or the format's
1486    /// 128 KiB block ceiling.
1487    fn block_capacity(&self) -> usize {
1488        self.target_block_size
1489            .map_or(crate::common::MAX_BLOCK_SIZE as usize, |t| t as usize)
1490    }
1491
1492    /// Before calling [FrameCompressor::compress] you need to set the source.
1493    ///
1494    /// This is the data that is compressed and written into the drain.
1495    pub fn set_source(&mut self, uncompressed_data: R) -> Option<R> {
1496        self.uncompressed_data.replace(uncompressed_data)
1497    }
1498
1499    /// Before calling [FrameCompressor::compress] you need to set the drain.
1500    ///
1501    /// As the compressor compresses data, the drain serves as a place for the output to be writte.
1502    pub fn set_drain(&mut self, compressed_data: W) -> Option<W> {
1503        self.compressed_data.replace(compressed_data)
1504    }
1505
1506    /// Provide a hint about the total uncompressed size for the next frame.
1507    ///
1508    /// When set, the encoder selects smaller hash tables and windows for
1509    /// small inputs, matching the C zstd source-size-class behavior.
1510    ///
1511    /// This hint applies only to frame payload bytes (`size`). Dictionary
1512    /// history is primed separately and does not inflate the hinted size or
1513    /// advertised frame window.
1514    /// Must be called before [`compress`](Self::compress).
1515    pub fn set_source_size_hint(&mut self, size: u64) {
1516        self.source_size_hint = Some(size);
1517    }
1518
1519    /// Total heap bytes this compressor's allocations hold, excluding the
1520    /// inline struct: the match-finder tables / history / recycled buffers and
1521    /// the primed-dictionary snapshot (via the matcher), the retained
1522    /// Huffman tables (active + recycled spare), the retained dictionary
1523    /// content, the cached dictionary entropy tables (literals Huffman +
1524    /// LL/ML/OF FSE), and the per-block sidecar buffers. Lets a context
1525    /// report its true footprint through `ZSTD_sizeof_CCtx`.
1526    pub fn heap_size(&self) -> usize {
1527        let mut total = self.state.matcher.heap_size();
1528        total += self
1529            .state
1530            .last_huff_table
1531            .as_ref()
1532            .map_or(0, |table| table.heap_size());
1533        total += self
1534            .state
1535            .huff_table_spare
1536            .as_ref()
1537            .map_or(0, |table| table.heap_size());
1538        total += self
1539            .dictionary
1540            .as_ref()
1541            .map_or(0, |d| d.inner.dict_content.capacity());
1542        total += self
1543            .dictionary_entropy_cache
1544            .as_ref()
1545            .map_or(0, CachedDictionaryEntropy::heap_size);
1546        #[cfg(all(feature = "lsm", feature = "hash"))]
1547        {
1548            total += self
1549                .block_checksums
1550                .as_ref()
1551                .map_or(0, |v| v.capacity() * core::mem::size_of::<u32>());
1552        }
1553        #[cfg(feature = "lsm")]
1554        {
1555            total += self.block_decompressed_sizes.capacity() * core::mem::size_of::<u32>();
1556        }
1557        total
1558    }
1559
1560    /// Compress the uncompressed data from the provided source as one Zstd frame and write it to the provided drain
1561    ///
1562    /// This will repeatedly call [Read::read] on the source to fill up blocks until the source returns 0 on the read call.
1563    /// All compressed blocks are buffered in memory so that the frame header can include the
1564    /// `Frame_Content_Size` field (which requires knowing the total uncompressed size). The
1565    /// entire frame — header, blocks, and optional checksum — is then written to the drain
1566    /// at the end. This means peak memory usage is O(compressed_size).
1567    ///
1568    /// To avoid endlessly encoding from a potentially endless source (like a network socket) you can use the
1569    /// [Read::take] function
1570    /// Per-frame setup values resolved by [`Self::prepare_frame`] and
1571    /// consumed by the block loop + [`Self::finish_frame`]. Lets the
1572    /// owned `compress()` and the borrowed one-shot path share the exact
1573    /// same reset / dict-prime / entropy-seed setup and frame tail.
1574    pub fn compress(&mut self) {
1575        let prep = self.prepare_frame();
1576        // Take the reader out so `run_owned_block_loop` can borrow it
1577        // mutably alongside `&mut self` (the rest of the loop touches
1578        // `self.state` / `self.hasher`, disjoint from the reader). Restored
1579        // before the frame tail so a reused compressor keeps its source.
1580        //
1581        // Deliberately NOT restored on unwind: if the block loop panics the
1582        // source has been partially consumed, so handing it back would let a
1583        // `catch_unwind` caller "successfully" compress the remaining tail
1584        // from an arbitrary midpoint — silent data corruption. Leaving the
1585        // slot empty makes any post-panic reuse fail loudly at the `expect`
1586        // below (matcher/entropy state is equally unre-usable after an
1587        // unwind; the reference implementation likewise requires a context
1588        // reset after an error).
1589        let mut source = self
1590            .uncompressed_data
1591            .take()
1592            .expect("source must be set via set_source before compress()");
1593        // Streaming drain: the content size is only known at EOF, so the
1594        // frame header can't precede the blocks — accumulate them in a local
1595        // buffer and let `finish_frame` write header + blocks to the drain.
1596        let mut all_blocks: Vec<u8> = Vec::with_capacity(initial_all_blocks_cap(
1597            prep.initial_size_hint,
1598            self.block_capacity(),
1599        ));
1600        let mut block_source = ReaderBlockSource::new(&mut source);
1601        let total_uncompressed = self.run_owned_block_loop(
1602            &mut block_source,
1603            prep.initial_size_hint,
1604            false,
1605            &mut all_blocks,
1606        );
1607        self.uncompressed_data = Some(source);
1608        self.finish_frame(all_blocks, total_uncompressed, &prep);
1609    }
1610
1611    fn prepare_frame(&mut self) -> FramePrep {
1612        // Reset per-frame introspection state so a re-used compressor
1613        // doesn't carry over the previous frame's layout/checksums.
1614        #[cfg(feature = "lsm")]
1615        {
1616            self.frame_emit_info = None;
1617            // Always captured under lsm (drives `decompressed_byte_range`);
1618            // clear, keep the allocation for a reused compressor.
1619            self.block_decompressed_sizes.clear();
1620        }
1621        #[cfg(all(feature = "lsm", feature = "hash"))]
1622        {
1623            if self.per_block_checksums_enabled {
1624                self.block_checksums = Some(alloc::vec::Vec::new());
1625            } else {
1626                self.block_checksums = None;
1627            }
1628        }
1629        let initial_size_hint = self.source_size_hint;
1630        let source_size_hint_known = initial_size_hint.is_some();
1631        let use_dictionary_state =
1632            !matches!(self.compression_level, CompressionLevel::Uncompressed)
1633                && self.state.matcher.supports_dictionary_priming()
1634                && self.dictionary.is_some();
1635        if let Some(size_hint) = self.source_size_hint.take() {
1636            // Keep source-size hint scoped to payload bytes; dictionary priming
1637            // is applied separately and should not force larger matcher sizing.
1638            self.state.matcher.set_source_size_hint(size_hint);
1639        }
1640        // Hand the matcher the dictionary's content size so its binary-tree /
1641        // hash-chain tables shrink to the dictionary's cParams tier (upstream zstd CDict
1642        // economics: the dictionary supplies long matches, so a source-sized live
1643        // table is wasted peak memory). The eviction window stays source-sized so
1644        // the dictionary bytes remain referenceable. Set before `reset` (which
1645        // consumes it) and only when a dictionary will actually be primed.
1646        if use_dictionary_state && let Some(dict) = self.dictionary.as_ref() {
1647            self.state
1648                .matcher
1649                .set_dictionary_size_hint(dict.inner.dict_content.len());
1650        }
1651        // Clearing buffers to allow re-using of the compressor
1652        self.state.matcher.reset(self.compression_level);
1653        self.state.offset_hist = [1, 4, 8];
1654        // Sync `state.strategy_tag` to the level resolved at this reset so
1655        // the literal-compression gates (`min_literals_to_compress` /
1656        // `min_gain` in `encoding::blocks::compressed`) see the correct
1657        // strategy for the next frame. Frame-by-frame level changes go
1658        // through this same `compress()` entry point, so re-syncing here
1659        // covers level switches without touching the matcher dispatch.
1660        // A public-parameter strategy override (#27) wins over the level's
1661        // derived tag so the literal-compression gates and dict-attach cutoff
1662        // below see the strategy the matcher actually runs. Otherwise resolve
1663        // the strategy SIZE-ADAPTIVELY through the same path the matcher's reset
1664        // used (`resolve_level_params` -> `get_cparams`, the port of upstream
1665        // `ZSTD_getCParams`): a small frame promotes a level to a higher
1666        // strategy (e.g. L13 over a <=16 KiB frame becomes btultra). Re-deriving
1667        // from the bare level would make the literal-compression / HUF-search
1668        // gates disagree with the matcher's actual parse on small frames (the
1669        // gate would think btlazy2 and skip the HUF table-log search the btultra
1670        // frame runs, costing a few bytes on small literal sections).
1671        self.state.strategy_tag = self.strategy_override.unwrap_or_else(|| {
1672            crate::encoding::levels::config::resolve_level_params(
1673                self.compression_level,
1674                initial_size_hint,
1675            )
1676            .strategy_tag
1677        });
1678        // `initial_size_hint` (captured before the `.take()` above) — by here
1679        // `self.source_size_hint` is None.
1680        self.state.huf_optimal_search =
1681            huf_search_enabled(self.state.strategy_tag, initial_size_hint);
1682        let cached_entropy = if use_dictionary_state {
1683            self.dictionary_entropy_cache.as_ref()
1684        } else {
1685            None
1686        };
1687        if use_dictionary_state && let Some(dict) = self.dictionary.as_ref() {
1688            // This state drives sequence encoding, while matcher priming below updates
1689            // the match generator's internal repeat-offset history for match finding.
1690            self.state.offset_hist = dict.inner.offset_hist;
1691            // Upstream zstd `ZSTD_shouldAttachDict` (`zstd_compress.c`): a
1692            // precomputed-dictionary table is COPIED into the working context
1693            // only when the source is larger than a per-strategy cutoff; at or
1694            // below it (and for unknown size) the upstream zstd ATTACHES the dictionary
1695            // tables by reference (no per-frame table touch at all). We don't
1696            // have an attach-by-reference path yet, so:
1697            //   - large source (> cutoff): reuse the captured prime snapshot
1698            //     (a table copy) instead of re-hashing the dictionary — the
1699            //     upstream zstd COPY regime, where the copy is cheaper than re-priming;
1700            //   - small / unknown source: re-prime (the snapshot copy of the
1701            //     whole table would cost MORE than the sparse re-prime here,
1702            //     which is exactly why the upstream zstd attaches by reference instead).
1703            // `attachDictSizeCutoffs` per strategy: fast 8K, dfast 16K,
1704            // greedy/lazy/btopt 32K, btultra/btultra2 8K. Expressed as the
1705            // ceil-log bucket (8K = 2^13, 16K = 2^14, 32K = 2^15) so the
1706            // decision uses the SAME bucketed representation as the driver's
1707            // attach/copy gate (`reset_size_log`) — comparing
1708            // `source_size_ceil_log(hint)` on the full u64 avoids the `as usize`
1709            // truncation that could diverge from the driver on 32-bit targets.
1710            // For a power-of-two cutoff `2^k`, `ceil_log2(hint) > k` is exactly
1711            // `hint > 2^k`, so this is identical to the raw `hint > cutoff` on
1712            // 64-bit.
1713            let cutoff_log = match self.state.strategy_tag {
1714                // Fast always attaches now (the copy-mode owned path memmoved the
1715                // whole input into history every frame); keep the copy-snapshot
1716                // gate in sync with the matcher's attach cutoff so Fast never
1717                // captures/restores a copy snapshot it can no longer use.
1718                crate::encoding::strategy::StrategyTag::Fast => {
1719                    crate::encoding::levels::config::FAST_ATTACH_DICT_CUTOFF_LOG
1720                }
1721                crate::encoding::strategy::StrategyTag::BtUltra
1722                | crate::encoding::strategy::StrategyTag::BtUltra2 => 13,
1723                crate::encoding::strategy::StrategyTag::Dfast => 14,
1724                crate::encoding::strategy::StrategyTag::Greedy
1725                | crate::encoding::strategy::StrategyTag::Lazy
1726                | crate::encoding::strategy::StrategyTag::Btlazy2
1727                | crate::encoding::strategy::StrategyTag::BtOpt => 15,
1728            };
1729            if self.state.matcher.dictionary_is_resident() {
1730                // Re-borrow fast path: the previous frame's reset kept this
1731                // dict's bytes + cached index resident, so skip the re-commit /
1732                // re-index and only reapply the offset history.
1733                self.state
1734                    .matcher
1735                    .reapply_resident_dictionary(dict.inner.offset_hist);
1736            } else {
1737                let prefer_copy_snapshot = initial_size_hint.is_some_and(|s| {
1738                    crate::encoding::levels::config::source_size_ceil_log(s) > cutoff_log
1739                });
1740                let restored = prefer_copy_snapshot
1741                    && self
1742                        .state
1743                        .matcher
1744                        .restore_primed_dictionary(self.compression_level);
1745                if !restored {
1746                    self.state.matcher.prime_with_dictionary(
1747                        dict.inner.dict_content.as_slice(),
1748                        dict.inner.offset_hist,
1749                    );
1750                    if prefer_copy_snapshot {
1751                        self.state
1752                            .matcher
1753                            .capture_primed_dictionary(self.compression_level);
1754                    }
1755                }
1756            }
1757        }
1758        if let Some(cache) = cached_entropy {
1759            // Refill an empty slot from the recycled spare before
1760            // `clone_from`: `Option::clone_from(None ← Some)` falls back to
1761            // a fresh clone (two Vec allocations), while `Some ← Some`
1762            // delegates to the table's buffer-reusing `clone_from`. Frames
1763            // whose last block cleared the table would otherwise re-clone
1764            // the dict seed every frame.
1765            match &cache.huff {
1766                Some(src) => {
1767                    if self.state.last_huff_table.is_none() {
1768                        self.state.last_huff_table = self.state.huff_table_spare.take();
1769                    }
1770                    match &mut self.state.last_huff_table {
1771                        Some(dst) => dst.clone_from(src),
1772                        slot => *slot = Some(src.clone()),
1773                    }
1774                }
1775                None => self.state.clear_huff_table(),
1776            }
1777        } else {
1778            self.state.clear_huff_table();
1779        }
1780        // `clone_from` keeps frame-to-frame seeding cheap for reused compressors by
1781        // reusing existing allocations where possible instead of reallocating every frame.
1782        if let Some(cache) = cached_entropy {
1783            self.state
1784                .fse_tables
1785                .ll_previous
1786                .clone_from(&cache.ll_previous);
1787            self.state
1788                .fse_tables
1789                .ml_previous
1790                .clone_from(&cache.ml_previous);
1791            self.state
1792                .fse_tables
1793                .of_previous
1794                .clone_from(&cache.of_previous);
1795        } else {
1796            self.state.fse_tables.ll_previous = None;
1797            self.state.fse_tables.ml_previous = None;
1798            self.state.fse_tables.of_previous = None;
1799        }
1800        let ll_entropy = cached_entropy.and_then(|cache| match cache.ll_previous.as_ref() {
1801            Some(PreviousFseTable::Custom(table)) => Some(table.as_ref()),
1802            _ => None,
1803        });
1804        let ml_entropy = cached_entropy.and_then(|cache| match cache.ml_previous.as_ref() {
1805            Some(PreviousFseTable::Custom(table)) => Some(table.as_ref()),
1806            _ => None,
1807        });
1808        let of_entropy = cached_entropy.and_then(|cache| match cache.of_previous.as_ref() {
1809            Some(PreviousFseTable::Custom(table)) => Some(table.as_ref()),
1810            _ => None,
1811        });
1812        self.state.matcher.seed_dictionary_entropy(
1813            self.state.last_huff_table.as_ref(),
1814            ll_entropy,
1815            ml_entropy,
1816            of_entropy,
1817        );
1818        #[cfg(feature = "hash")]
1819        {
1820            self.hasher = XxHash64::with_seed(0);
1821        }
1822        let window_size = self.state.matcher.window_size();
1823        assert!(
1824            window_size != 0,
1825            "matcher reported window_size == 0, which is invalid"
1826        );
1827        FramePrep {
1828            window_size,
1829            use_dictionary_state,
1830            source_size_hint_known,
1831            initial_size_hint,
1832        }
1833    }
1834
1835    /// Owned streaming block loop: reads blocks from the caller-provided
1836    /// `source` reader, optionally pre-splits, hashes for the content
1837    /// checksum, and emits each block via `compress_block_encoded`,
1838    /// accumulating the block bytes. Returns `(all_blocks,
1839    /// total_uncompressed)`. The source is passed in (rather than read
1840    /// from `self.uncompressed_data`) so the streaming `compress` path can
1841    /// feed the configured reader while the slice paths
1842    /// (`compress_oneshot_borrowed`, `compress_independent_frame`) feed an
1843    /// in-place `&[u8]` cursor without baking its lifetime into the
1844    /// compressor type.
1845    fn run_owned_block_loop<S: OwnedBlockSource>(
1846        &mut self,
1847        source: &mut S,
1848        initial_size_hint: Option<u64>,
1849        // Whether `initial_size_hint` is the input's exact length (the
1850        // one-shot slice paths) or a caller-provided estimate (the streaming
1851        // `Read` path, where `set_source_size_hint` is advisory). An exact
1852        // hint drives the one-shot ratio reservation; an estimate is only
1853        // trusted up to a small lookahead past the bytes actually read.
1854        hint_is_exact: bool,
1855        out: &mut Vec<u8>,
1856    ) -> u64 {
1857        // Compressed blocks are appended to `out` from its current end. The
1858        // streaming drain path passes a fresh buffer (the frame header is
1859        // written to the drain afterward, since Frame_Content_Size is only
1860        // known once the reader hits EOF); the one-shot compress-into-Vec
1861        // path passes `out` already holding the header. The upstream zstd split
1862        // `savings` gate below accumulates block-relative (`before_len`)
1863        // output deltas, so a header prefix never skews it.
1864        let blocks_start = out.len();
1865        let mut total_uncompressed: u64 = 0;
1866        let mut pending_input: Vec<u8> = Vec::new();
1867        let mut reached_eof = false;
1868        let mut savings = 0i64;
1869        // Compress block by block
1870        loop {
1871            // Read up to one upstream zstd block. When the pre-block splitter keeps a
1872            // suffix, top it back up before compressing the next block, matching
1873            // ZSTD_compress_frameChunk() over a contiguous input buffer.
1874            let block_capacity = self.block_capacity();
1875            // Always draw the block buffer from the matcher's recycled pool
1876            // (its capacity already covers the block size, so the resize below
1877            // stays in-place). Any carried pre-split suffix is copied in, and
1878            // `pending_input` is retained as a reusable carry buffer. The prior
1879            // approach `split_off`'d a fresh suffix Vec per pre-split and
1880            // `reserve_exact`-grew it to `block_capacity` every block; on a
1881            // heavily pre-split frame that churned one block-sized allocation
1882            // per split (~12 MB over ~90 splits on a 1 MiB corpus input).
1883            let mut uncompressed_data = self.state.matcher.get_next_space();
1884            uncompressed_data.clear();
1885            uncompressed_data.extend_from_slice(&pending_input);
1886            pending_input.clear();
1887            if !reached_eof {
1888                // Remaining-bytes expectation for the reader source's sizing
1889                // (`None` = unknown, or an inexact hint already met by prior
1890                // blocks). The slice source appends directly and ignores it.
1891                let size_hint_remaining = match initial_size_hint {
1892                    Some(hint) if hint > total_uncompressed => Some(hint - total_uncompressed),
1893                    _ => None,
1894                };
1895                let (appended, eof) =
1896                    source.fill_block(&mut uncompressed_data, block_capacity, size_hint_remaining);
1897                total_uncompressed += appended as u64;
1898                reached_eof = eof;
1899            }
1900            let mut last_block = reached_eof;
1901            let remaining_for_split = if reached_eof {
1902                uncompressed_data.len()
1903            } else {
1904                block_capacity
1905            };
1906            if !matches!(self.compression_level, CompressionLevel::Uncompressed)
1907                && uncompressed_data.len() == block_capacity
1908            {
1909                let block_len = optimal_block_size(
1910                    self.compression_level,
1911                    &uncompressed_data,
1912                    remaining_for_split,
1913                    block_capacity,
1914                    savings,
1915                );
1916                if block_len < uncompressed_data.len() {
1917                    // Carry the kept suffix into the reusable `pending_input`
1918                    // buffer (cleared, capacity retained) instead of allocating
1919                    // a fresh Vec via `split_off`. Next iteration copies it back
1920                    // into a pooled block buffer. The block currently being
1921                    // compressed is truncated to the chosen split length.
1922                    pending_input.clear();
1923                    pending_input.extend_from_slice(&uncompressed_data[block_len..]);
1924                    uncompressed_data.truncate(block_len);
1925                    last_block = false;
1926                }
1927            }
1928            // As we read, hash that data too (skipped when the content
1929            // checksum is disabled).
1930            #[cfg(feature = "hash")]
1931            if self.content_checksum {
1932                self.hasher.write(&uncompressed_data);
1933            }
1934            // Per-physical-block XXH64 (low 32 bits) for the optional
1935            // per-block checksum sidecar. Hashing happens INSIDE the
1936            // block emitters (RLE / Raw fast-path / Compressed /
1937            // post-split partitions), so the digests vector has
1938            // exactly one entry per physical Block_Header written to
1939            // `all_blocks` — 1:1 with `FrameEmitInfo.blocks`. See
1940            // `enable_per_block_checksums` rustdoc.
1941            // Size the output ahead of this block's emission from the ratio
1942            // observed so far (see `reserve_for_next_block`); with no usable
1943            // size hint, ensure one block's worst case and let the doubling
1944            // growth policy amortize across blocks.
1945            let emitted =
1946                total_uncompressed - uncompressed_data.len() as u64 - pending_input.len() as u64;
1947            match initial_size_hint {
1948                Some(hint) if hint >= total_uncompressed => {
1949                    // An advisory hint (streaming path) is only trusted up to
1950                    // a small lookahead past the bytes actually read: a hint
1951                    // far above the real input would otherwise reserve the
1952                    // whole phantom remainder up front.
1953                    let hint_remaining = hint - emitted;
1954                    let remaining = if hint_is_exact {
1955                        hint_remaining
1956                    } else {
1957                        let buffered = total_uncompressed - emitted;
1958                        const HINT_LOOKAHEAD: u64 = 64 * 1024;
1959                        hint_remaining.min(buffered + HINT_LOOKAHEAD)
1960                    };
1961                    reserve_for_next_block(
1962                        out,
1963                        blocks_start,
1964                        emitted,
1965                        remaining as usize,
1966                        self.block_capacity(),
1967                    );
1968                }
1969                _ => {
1970                    out.reserve(uncompressed_data.len() + 3 + 16);
1971                }
1972            }
1973            // Special handling is needed for compression of a totally empty file
1974            if uncompressed_data.is_empty() {
1975                let header = BlockHeader {
1976                    last_block: true,
1977                    block_type: crate::blocks::block::BlockType::Raw,
1978                    block_size: 0,
1979                };
1980                header.serialize(out);
1981                #[cfg(feature = "lsm")]
1982                self.block_decompressed_sizes.push(0);
1983                #[cfg(all(feature = "lsm", feature = "hash"))]
1984                if let Some(checksums) = self.block_checksums.as_mut() {
1985                    checksums.push(xxh64_block_low32(&[]));
1986                }
1987                break;
1988            }
1989
1990            match self.compression_level {
1991                CompressionLevel::Uncompressed => {
1992                    let header = BlockHeader {
1993                        last_block,
1994                        block_type: crate::blocks::block::BlockType::Raw,
1995                        block_size: uncompressed_data.len().try_into().unwrap(),
1996                    };
1997                    header.serialize(out);
1998                    #[cfg(feature = "lsm")]
1999                    self.block_decompressed_sizes
2000                        .push(uncompressed_data.len() as u32);
2001                    #[cfg(all(feature = "lsm", feature = "hash"))]
2002                    if let Some(checksums) = self.block_checksums.as_mut() {
2003                        checksums.push(xxh64_block_low32(&uncompressed_data));
2004                    }
2005                    out.extend_from_slice(&uncompressed_data);
2006                    savings +=
2007                        uncompressed_data.len() as i64 - (3 + uncompressed_data.len()) as i64;
2008                }
2009                CompressionLevel::Fastest
2010                | CompressionLevel::Default
2011                | CompressionLevel::Better
2012                | CompressionLevel::Best
2013                | CompressionLevel::Level(_) => {
2014                    let before_len = out.len();
2015                    let block_len = uncompressed_data.len();
2016                    // A primed dictionary makes "incompressible-looking"
2017                    // blocks matchable against the dict, so the raw-fast-
2018                    // path inside must be bypassed (it skips matching).
2019                    // Mirror prepare_frame's `use_dictionary_state`: a dict
2020                    // is only PRIMED (and thus matchable) when the matcher
2021                    // supports priming — a non-priming matcher ignores an
2022                    // attached dictionary, so the raw-fast-path must stay
2023                    // enabled for it. (This arm is already non-Uncompressed.)
2024                    let dict_active = self.dictionary.is_some()
2025                        && self.state.matcher.supports_dictionary_priming();
2026                    compress_block_encoded(
2027                        &mut self.state,
2028                        self.compression_level,
2029                        last_block,
2030                        uncompressed_data,
2031                        out,
2032                        dict_active,
2033                        #[cfg(feature = "lsm")]
2034                        Some(&mut self.block_decompressed_sizes),
2035                        #[cfg(all(feature = "lsm", feature = "hash"))]
2036                        self.block_checksums.as_mut(),
2037                    );
2038                    savings += block_len as i64 - (out.len() - before_len) as i64;
2039                }
2040            }
2041            if last_block && pending_input.is_empty() {
2042                break;
2043            }
2044        }
2045        total_uncompressed
2046    }
2047
2048    /// Append the frame header bytes onto `out` once the total payload size
2049    /// is known (so `Frame_Content_Size` / `single_segment` can be set).
2050    /// Appends rather than returns so the one-shot path serializes straight
2051    /// into the reused output buffer with no per-frame header `Vec`.
2052    fn append_frame_header(&self, total_uncompressed: u64, prep: &FramePrep, out: &mut Vec<u8>) {
2053        // Match the upstream zstd framing policy (`ZSTD_writeFrameHeader`):
2054        // single-segment whenever the content size is known and the whole
2055        // source fits the active window (`contentSizeFlag && windowSize >=
2056        // srcSize`). A single-segment frame REQUIRES an FCS field, so
2057        // suppressing the content size (`content_size_flag` off) forces the
2058        // windowed layout. There is no lower size bound: small payloads
2059        // benefit most, since a windowed frame cannot encode a content size
2060        // below 256 in fewer than 4 FCS bytes (the 1-byte FCS class is
2061        // single-segment-only, see `find_fcs_field_size`), whereas a
2062        // single-segment frame stores it in one byte and omits the window
2063        // descriptor. The single-segment window equals the FCS, so a block
2064        // must never reference past the content: the post-hoc raw fallback in
2065        // the block emitters guarantees any non-shrinking block is stored raw,
2066        // and genuine matches stay within the already-emitted output.
2067        // Dictionary frames qualify too (the dictionary is decoder setup
2068        // state, not part of the regenerated segment), keeping the decoder's
2069        // single-allocation path (our decoder caps reservation to
2070        // min(window, FCS) either way).
2071        let single_segment = self.content_size_flag
2072            && prep.source_size_hint_known
2073            && total_uncompressed <= prep.window_size;
2074        let header = FrameHeader {
2075            frame_content_size: self.content_size_flag.then_some(total_uncompressed),
2076            single_segment,
2077            content_checksum: cfg!(feature = "hash") && self.content_checksum,
2078            dictionary_id: if prep.use_dictionary_state && self.dict_id_flag {
2079                self.dictionary.as_ref().map(|dict| dict.inner.id as u64)
2080            } else {
2081                None
2082            },
2083            window_size: if single_segment {
2084                None
2085            } else {
2086                Some(prep.window_size)
2087            },
2088            magicless: self.magicless,
2089        };
2090        header.serialize(out);
2091    }
2092
2093    /// Write the frame header, accumulated block bytes, and optional
2094    /// trailing content checksum to the configured drain; populate
2095    /// `frame_emit_info` (lsm). Header and blocks are written separately to
2096    /// avoid shifting `all_blocks` to prepend the header. Used by
2097    /// `compress` and `compress_oneshot_borrowed`.
2098    fn finish_frame(&mut self, all_blocks: Vec<u8>, total_uncompressed: u64, prep: &FramePrep) {
2099        let mut header_buf: Vec<u8> = Vec::with_capacity(18);
2100        self.append_frame_header(total_uncompressed, prep, &mut header_buf);
2101        // Snapshot the checksum before borrowing the drain field so the
2102        // `self.hasher` read and the `self.compressed_data` write don't
2103        // both need `&mut self` simultaneously.
2104        #[cfg(feature = "hash")]
2105        let checksum_bytes = self
2106            .content_checksum
2107            .then(|| (self.hasher.finish() as u32).to_le_bytes());
2108        let drain = self.compressed_data.as_mut().unwrap();
2109        drain.write_all(&header_buf).unwrap();
2110        drain.write_all(&all_blocks).unwrap();
2111        // With the `hash` feature AND the content checksum enabled, the header
2112        // set `Content_Checksum_flag` and the 32-bit digest is written at the
2113        // end of the frame. Disabled => no trailing bytes, flag stays 0.
2114        #[cfg(feature = "hash")]
2115        if let Some(checksum_bytes) = checksum_bytes {
2116            drain.write_all(&checksum_bytes).unwrap();
2117        }
2118        #[cfg(feature = "lsm")]
2119        {
2120            let emit_checksum = cfg!(feature = "hash") && self.content_checksum;
2121            self.populate_frame_emit_info(header_buf.len(), &all_blocks, emit_checksum);
2122        }
2123    }
2124
2125    /// Assemble the frame (header + blocks + optional checksum) into the
2126    /// caller-provided `out` buffer, replacing its contents, and populate
2127    /// `frame_emit_info` (lsm). `out` is cleared first (its allocation is
2128    /// reused, the CCtx-equivalent zero-per-call-alloc output path) then
2129    /// grown once to the exact frame size. Used by
2130    /// `compress_independent_frame_into`. The single `all_blocks` copy into
2131    /// `out` is the same one copy `finish_frame` performs writing
2132    /// `all_blocks` into a `Vec` drain, no extra buffering vs the drain
2133    /// path.
2134    /// Walk `all_blocks` to recover per-block layout and store it in
2135    /// `frame_emit_info`. Each Block_Header is 3 bytes LE packing
2136    /// `(block_size << 3) | (block_type << 1) | last_block`. Physical body
2137    /// size differs by type: RLE bodies are always 1 byte (the repeated
2138    /// byte), Raw/Compressed bodies span `block_size`. `header_len` is the
2139    /// serialized frame-header length (frame offset of the first block).
2140    #[cfg(feature = "lsm")]
2141    fn populate_frame_emit_info(
2142        &mut self,
2143        header_len: usize,
2144        all_blocks: &[u8],
2145        emit_checksum: bool,
2146    ) {
2147        use crate::blocks::block::BlockType as BT;
2148        use crate::encoding::frame_emit_info::{FrameBlock, FrameEmitInfo};
2149        // All frame-offset arithmetic below is bounded by u32 on the wire
2150        // (Block_Size is a 21-bit field, frames bounded by MAX_BLOCK_SIZE *
2151        // #blocks). A pathologically large frame whose total emitted size
2152        // exceeds u32::MAX would overflow the cast; bail out by leaving
2153        // `frame_emit_info` at `None` rather than handing the caller a
2154        // silently-truncated layout. The overflow path is statically
2155        // unreachable on every realistic frame so the predictor amortises
2156        // the branch to zero cost.
2157        let frame_header_len: u32 = match u32::try_from(header_len) {
2158            Ok(v) => v,
2159            Err(_) => return,
2160        };
2161        let all_blocks_len_u32: u32 = match u32::try_from(all_blocks.len()) {
2162            Ok(v) => v,
2163            Err(_) => return,
2164        };
2165        let mut blocks: Vec<FrameBlock> = Vec::new();
2166        let mut cursor: usize = 0;
2167        while cursor + 3 <= all_blocks.len() {
2168            let mut header_u32 = [0u8; 4];
2169            header_u32[..3].copy_from_slice(&all_blocks[cursor..cursor + 3]);
2170            let raw = u32::from_le_bytes(header_u32);
2171            let last_block = (raw & 1) != 0;
2172            let block_type = match (raw >> 1) & 0b11 {
2173                0 => BT::Raw,
2174                1 => BT::RLE,
2175                2 => BT::Compressed,
2176                _ => BT::Reserved,
2177            };
2178            let block_size_field = raw >> 3;
2179            // RLE bodies are always 1 byte physical on the wire (the single
2180            // repeated byte); the spec's Block_Size field carries the
2181            // logical repeat count. Raw and Compressed bodies physically
2182            // span block_size_field bytes. Store the physical length in
2183            // body_size so the 'offset + header + body_size' arithmetic
2184            // always lands on the next block boundary, and surface the raw
2185            // spec field separately as block_size_field.
2186            let physical_body: u32 = match block_type {
2187                BT::RLE => 1,
2188                _ => block_size_field,
2189            };
2190            let cursor_u32: u32 = match u32::try_from(cursor) {
2191                Ok(v) => v,
2192                Err(_) => return,
2193            };
2194            let offset_in_frame = match frame_header_len.checked_add(cursor_u32) {
2195                Some(v) => v,
2196                None => return,
2197            };
2198            // Decompressed (regenerated) size, captured per physical block
2199            // during emit (1:1 with the wire blocks scanned here). Raw/RLE are
2200            // wire-derivable (`block_size_field`), so a short sidecar still
2201            // yields the correct value for them. A Compressed block's size is
2202            // NOT on the wire: if the sidecar is missing its entry, fabricating
2203            // 0 would publish a silently-wrong `decompressed_byte_range`. Since
2204            // this metadata is the authoritative mapping for a successful
2205            // encode, bail out (leave `frame_emit_info` at `None`) rather than
2206            // hand back a corrupt layout; the 1:1 push invariant makes this
2207            // unreachable in practice (debug_assert catches a regression).
2208            let decompressed_size = match self.block_decompressed_sizes.get(blocks.len()).copied() {
2209                Some(size) => size,
2210                None if matches!(block_type, BT::Raw | BT::RLE) => block_size_field,
2211                None => {
2212                    debug_assert!(
2213                        false,
2214                        "missing decompressed-size sidecar entry for compressed block {}",
2215                        blocks.len()
2216                    );
2217                    return;
2218                }
2219            };
2220            blocks.push(FrameBlock {
2221                offset_in_frame,
2222                header_size: 3,
2223                body_size: physical_body,
2224                block_size_field,
2225                block_type,
2226                last_block,
2227                decompressed_size,
2228            });
2229            cursor += 3 + physical_body as usize;
2230            if last_block {
2231                break;
2232            }
2233        }
2234        // Fail closed on a structurally incomplete scan: the loop must have
2235        // consumed the whole block section AND ended on a parsed last block.
2236        // A premature `last_block` (bytes left over) or a run-off without any
2237        // last block would otherwise publish an invalid public `FrameEmitInfo`.
2238        // Unreachable for a well-formed self-produced frame (debug_assert
2239        // catches a regression); on release we bail, leaving `frame_emit_info`
2240        // at `None` rather than handing back a corrupt layout.
2241        if cursor != all_blocks.len() || !blocks.last().is_some_and(|b| b.last_block) {
2242            debug_assert!(
2243                false,
2244                "incomplete block scan in populate_frame_emit_info: cursor={} len={} last_block={:?}",
2245                cursor,
2246                all_blocks.len(),
2247                blocks.last().map(|b| b.last_block)
2248            );
2249            return;
2250        }
2251        let checksum_range = if emit_checksum {
2252            let cs_start = match frame_header_len.checked_add(all_blocks_len_u32) {
2253                Some(v) => v,
2254                None => return,
2255            };
2256            let cs_end = match cs_start.checked_add(4) {
2257                Some(v) => v,
2258                None => return,
2259            };
2260            Some(cs_start..cs_end)
2261        } else {
2262            None
2263        };
2264        let body_total = match frame_header_len.checked_add(all_blocks_len_u32) {
2265            Some(v) => v,
2266            None => return,
2267        };
2268        let total_size = if checksum_range.is_some() {
2269            match body_total.checked_add(4) {
2270                Some(v) => v,
2271                None => return,
2272            }
2273        } else {
2274            body_total
2275        };
2276        self.frame_emit_info = Some(FrameEmitInfo {
2277            frame_header_range: 0..frame_header_len,
2278            blocks,
2279            checksum_range,
2280            total_size,
2281        });
2282    }
2283
2284    /// Layout of the most recently emitted frame.
2285    ///
2286    /// Returns `None` if [`compress`](Self::compress) has not been
2287    /// called yet on this compressor. After a successful `compress()`
2288    /// the returned `FrameEmitInfo` describes the frame header range,
2289    /// every emitted block's offset / size / type, and the optional
2290    /// trailing content-checksum range — all in frame-absolute byte
2291    /// offsets matching the bytes written to the drain.
2292    ///
2293    /// Behind the `lsm` Cargo feature.
2294    #[cfg(feature = "lsm")]
2295    pub fn last_frame_emit_info(&self) -> Option<&crate::encoding::frame_emit_info::FrameEmitInfo> {
2296        self.frame_emit_info.as_ref()
2297    }
2298
2299    /// Opt in to per-block XXH64 checksum computation during
2300    /// [`compress`](Self::compress). Default off; zero cost when
2301    /// disabled. The captured digests are accessible via
2302    /// [`last_frame_block_checksums`](Self::last_frame_block_checksums).
2303    ///
2304    /// One checksum is emitted per physical FrameBlock written to
2305    /// the drain: 1:1 cardinality with
2306    /// [`last_frame_emit_info`](Self::last_frame_emit_info)'s
2307    /// `blocks` vector. On the post-split optimization path
2308    /// (Level 16-22 with large window) the per-partition decompressed
2309    /// range is hashed inside the partition loop so the digest count
2310    /// still matches the emitted block count. The decoder collects
2311    /// per-physical-block digests on the same granularity, so
2312    /// element-wise equality holds round-trip.
2313    ///
2314    /// Behind `all(feature = "lsm", feature = "hash")` — the XXH64
2315    /// primitive lives behind the `hash` feature, so this method only
2316    /// compiles when both are enabled.
2317    #[cfg(all(feature = "lsm", feature = "hash"))]
2318    pub fn enable_per_block_checksums(&mut self) {
2319        self.per_block_checksums_enabled = true;
2320    }
2321
2322    /// Per-block XXH64 (low 32 bits) digests captured during the most
2323    /// recent `compress()` call. `None` unless
2324    /// [`enable_per_block_checksums`](Self::enable_per_block_checksums)
2325    /// was called before `compress()`.
2326    ///
2327    /// Behind `all(feature = "lsm", feature = "hash")`.
2328    #[cfg(all(feature = "lsm", feature = "hash"))]
2329    pub fn last_frame_block_checksums(&self) -> Option<&[u32]> {
2330        self.block_checksums.as_deref()
2331    }
2332
2333    /// Get a mutable reference to the source
2334    pub fn source_mut(&mut self) -> Option<&mut R> {
2335        self.uncompressed_data.as_mut()
2336    }
2337
2338    /// Get a mutable reference to the drain
2339    pub fn drain_mut(&mut self) -> Option<&mut W> {
2340        self.compressed_data.as_mut()
2341    }
2342
2343    /// Get a reference to the source
2344    pub fn source(&self) -> Option<&R> {
2345        self.uncompressed_data.as_ref()
2346    }
2347
2348    /// Get a reference to the drain
2349    pub fn drain(&self) -> Option<&W> {
2350        self.compressed_data.as_ref()
2351    }
2352
2353    /// Retrieve the source
2354    pub fn take_source(&mut self) -> Option<R> {
2355        self.uncompressed_data.take()
2356    }
2357
2358    /// Retrieve the drain
2359    pub fn take_drain(&mut self) -> Option<W> {
2360        self.compressed_data.take()
2361    }
2362
2363    /// Before calling [FrameCompressor::compress] you can replace the matcher
2364    pub fn replace_matcher(&mut self, mut match_generator: M) -> M {
2365        core::mem::swap(&mut match_generator, &mut self.state.matcher);
2366        match_generator
2367    }
2368
2369    /// Before calling [FrameCompressor::compress] you can replace the compression level.
2370    ///
2371    /// This also clears any fine-grained parameter overrides installed via
2372    /// [`set_parameters`](Self::set_parameters): reverting to a bare level
2373    /// means plain level-based tuning, not the previous frame's customized
2374    /// strategy / LDM / log overrides. To keep overriding, call
2375    /// [`set_parameters`](Self::set_parameters) again with the new base level.
2376    pub fn set_compression_level(
2377        &mut self,
2378        compression_level: CompressionLevel,
2379    ) -> CompressionLevel {
2380        let old = self.compression_level;
2381        self.compression_level = compression_level;
2382        // Resync the raw-literals gate: negative levels disable literal (Huffman)
2383        // compression (C `ZSTD_literalsCompressionIsDisabled`). `prepare_frame`
2384        // never recomputes this, so it must be refreshed on the level switch the
2385        // same way the constructors and `set_parameters` do.
2386        self.state.literal_compression_disabled = matches!(
2387            compression_level,
2388            CompressionLevel::Level(n) if n < 0
2389        );
2390        // Drop sticky overrides so the level switch yields plain geometry.
2391        self.strategy_override = None;
2392        self.state.matcher.clear_param_overrides();
2393        old
2394    }
2395
2396    /// Get the current compression level
2397    pub fn compression_level(&self) -> CompressionLevel {
2398        self.compression_level
2399    }
2400
2401    /// Attach a pre-parsed dictionary to be used for subsequent compressions.
2402    ///
2403    /// In compressed modes, the dictionary id is written only when the active
2404    /// matcher supports dictionary priming.
2405    /// Uncompressed mode and non-priming matchers ignore the attached dictionary
2406    /// at encode time.
2407    pub fn set_dictionary(
2408        &mut self,
2409        dictionary: crate::decoding::Dictionary,
2410    ) -> Result<Option<EncoderDictionary>, crate::decoding::errors::DictionaryDecodeError> {
2411        self.attach_dictionary(EncoderDictionary::from_dictionary(dictionary))
2412    }
2413
2414    /// Parse and attach a serialized dictionary blob.
2415    ///
2416    /// Parses with the encoder-only path (skips the FSE/HUF decode lookup-table
2417    /// build the encoder never reads); the entropy ENCODER tables — and thus
2418    /// the emitted frame — are identical to a full parse.
2419    pub fn set_dictionary_from_bytes(
2420        &mut self,
2421        raw_dictionary: &[u8],
2422    ) -> Result<Option<EncoderDictionary>, crate::decoding::errors::DictionaryDecodeError> {
2423        self.attach_dictionary(EncoderDictionary::from_bytes(raw_dictionary)?)
2424    }
2425
2426    /// Attach an already-parsed [`EncoderDictionary`] without reparsing a raw
2427    /// blob.
2428    ///
2429    /// Accepts an `EncoderDictionary` produced once via
2430    /// [`EncoderDictionary::from_bytes`] / [`EncoderDictionary::from_dictionary`]
2431    /// or handed back by [`Self::clear_dictionary`] / the `set_dictionary*`
2432    /// return value, so callers can reattach or reuse a prepared dictionary
2433    /// across compressions without re-running the dictionary parse each time.
2434    /// Returns the previously-attached dictionary, if any.
2435    pub fn set_encoder_dictionary(
2436        &mut self,
2437        dictionary: EncoderDictionary,
2438    ) -> Result<Option<EncoderDictionary>, crate::decoding::errors::DictionaryDecodeError> {
2439        self.attach_dictionary(dictionary)
2440    }
2441
2442    /// Remove the attached dictionary, returning it as an [`EncoderDictionary`].
2443    pub fn clear_dictionary(&mut self) -> Option<EncoderDictionary> {
2444        self.dictionary_entropy_cache = None;
2445        // Drop the CDict prime snapshot — it is keyed to the dictionary
2446        // being removed and must not be restored against a different (or no)
2447        // dictionary on the next frame.
2448        self.state.matcher.invalidate_primed_dictionary();
2449        self.dictionary.take()
2450    }
2451
2452    /// Validate `enc`, build the encoder entropy cache from it, store it, and
2453    /// return the previously-attached dictionary. Shared by every public
2454    /// attach entry point: `set_dictionary`, `set_dictionary_from_bytes`, and
2455    /// `set_encoder_dictionary`.
2456    fn attach_dictionary(
2457        &mut self,
2458        enc: EncoderDictionary,
2459    ) -> Result<Option<EncoderDictionary>, crate::decoding::errors::DictionaryDecodeError> {
2460        let dictionary = &enc.inner;
2461        if dictionary.id == 0 {
2462            return Err(crate::decoding::errors::DictionaryDecodeError::ZeroDictionaryId);
2463        }
2464        if let Some(index) = dictionary.offset_hist.iter().position(|&rep| rep == 0) {
2465            return Err(
2466                crate::decoding::errors::DictionaryDecodeError::ZeroRepeatOffsetInDictionary {
2467                    index: index as u8,
2468                },
2469            );
2470        }
2471        self.dictionary_entropy_cache = Some(CachedDictionaryEntropy::from_dictionary(dictionary));
2472        // A previously-captured CDict prime snapshot belongs to the OLD
2473        // dictionary; drop it so the first frame with the new dictionary
2474        // re-primes (and re-captures) instead of restoring stale tables.
2475        self.state.matcher.invalidate_primed_dictionary();
2476        Ok(self.dictionary.replace(enc))
2477    }
2478}
2479
2480#[cfg(test)]
2481mod tests;