Skip to main content

libzstd_bitexact_rs/
compress.rs

1//! One-shot compression (`ZSTD_compress`), aiming for **byte-identical output
2//! to C libzstd 1.5.7**: parameter derivation (`ZSTD_getCParams` /
3//! `ZSTD_adjustCParams`), the match finders, and frame assembly
4//! (`ZSTD_writeFrameHeader` / `ZSTD_compress_frameChunk` /
5//! `ZSTD_compressBlock_internal`).
6//!
7//! Current scope: **every compression level** (1-22 and the negative /
8//! acceleration levels), any input size, no dictionary, no checksum — the
9//! `ZSTD_compress` defaults. [`compress_with_dict`] additionally primes the
10//! match finder from a raw dictionary (`ZSTD_compress_usingDict`, extDict
11//! path), for raw dictionaries at every strategy (fast through btultra2). This
12//! includes the configurations where C
13//! auto-enables long-distance matching (`strategy >= btopt && windowLog >=
14//! 27`, i.e. level 22 beyond 64 MiB), whose match finder ([`crate::ldm`]) is
15//! bit-exact. All nine strategies are implemented: fast and
16//! dfast here, greedy/lazy/lazy2/btlazy2 in [`crate::lazy`], and
17//! btopt/btultra/btultra2 in [`crate::opt`]. Block boundaries follow
18//! `ZSTD_optimalBlockSize` (the 1.5.7 pre-block splitter,
19//! [`crate::pre_split`]); the bt-opt strategies additionally run the
20//! post-block splitter ([`crate::post_split`]).
21
22use crate::error::Error;
23use crate::pre_split;
24use crate::sequences_encode::{self, FseEntropyState, SeqStore};
25
26/// `ZSTD_WINDOW_START_INDEX`: indices 0 and 1 are reserved, so a hash-table
27/// entry of 0 always reads as "empty / out of window".
28const WINDOW_START_INDEX: usize = 2;
29const K_SEARCH_STRENGTH: u32 = 8;
30const HASH_READ_SIZE: usize = 8;
31const BLOCK_SIZE_MAX: usize = 128 * 1024;
32const MIN_CBLOCK_SIZE: usize = 2;
33const BLOCK_HEADER_SIZE: usize = 3;
34const WINDOWLOG_ABSOLUTEMIN: u32 = 10;
35const HASHLOG_MIN: u32 = 6;
36/// `ZSTD_SHORT_CACHE_TAG_BITS`: the low bits of a CDict tagged-table entry hold
37/// a hash tag; the high bits hold the index. Used by the `dictMatchState`
38/// (CDict) match finders for fast/dfast.
39const SHORT_CACHE_TAG_BITS: u32 = 8;
40/// `ZSTD_WINDOWLOG_MAX` on 64-bit targets (`ZSTD_WINDOWLOG_MAX_64`).
41const WINDOWLOG_MAX: u32 = 31;
42/// `ZSTD_CONTENTSIZE_UNKNOWN`: the source size is not known in advance.
43const CONTENTSIZE_UNKNOWN: u64 = u64::MAX;
44const ZSTD_MAGIC: u32 = 0xFD2F_B528;
45/// `ZSTD_MAGIC_DICTIONARY`: a dictionary of at least 8 bytes beginning with
46/// this magic is a trained (ZDICT) dictionary rather than raw content.
47const MAGIC_DICTIONARY: u32 = 0xEC30_A437;
48
49// --- Compression parameters --------------------------------------------------
50
51/// `ZSTD_strategy`.
52#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Debug)]
53pub(crate) enum Strategy {
54    Fast = 1,
55    Dfast = 2,
56    Greedy = 3,
57    Lazy = 4,
58    Lazy2 = 5,
59    Btlazy2 = 6,
60    Btopt = 7,
61    Btultra = 8,
62    Btultra2 = 9,
63}
64
65/// `ZSTD_compressionParameters`.
66#[derive(Clone, Copy, Debug)]
67pub(crate) struct CParams {
68    pub window_log: u32,
69    pub chain_log: u32,
70    pub hash_log: u32,
71    /// Unused by the fast strategy; read once the search strategies land.
72    #[allow(dead_code)]
73    pub search_log: u32,
74    pub min_match: u32,
75    pub target_length: u32,
76    pub strategy: Strategy,
77}
78
79/// One `ZSTD_defaultCParameters` row: (W, C, H, S, L, TL, strategy).
80type CParamsRow = (u32, u32, u32, u32, u32, u32, Strategy);
81
82/// `ZSTD_defaultCParameters` (clevels.h, zstd 1.5.7): rows 0..=22 for the four
83/// source-size classes. Row 0 is the base for negative levels.
84#[rustfmt::skip]
85const DEFAULT_CPARAMETERS: [[CParamsRow; 23]; 4] = {
86    use Strategy::*;
87    [
88    [   /* "default" - for any srcSize > 256 KB */
89        (19, 12, 13,  1,  6,  1, Fast),
90        (19, 13, 14,  1,  7,  0, Fast),
91        (20, 15, 16,  1,  6,  0, Fast),
92        (21, 16, 17,  1,  5,  0, Dfast),
93        (21, 18, 18,  1,  5,  0, Dfast),
94        (21, 18, 19,  3,  5,  2, Greedy),
95        (21, 18, 19,  3,  5,  4, Lazy),
96        (21, 19, 20,  4,  5,  8, Lazy),
97        (21, 19, 20,  4,  5, 16, Lazy2),
98        (22, 20, 21,  4,  5, 16, Lazy2),
99        (22, 21, 22,  5,  5, 16, Lazy2),
100        (22, 21, 22,  6,  5, 16, Lazy2),
101        (22, 22, 23,  6,  5, 32, Lazy2),
102        (22, 22, 22,  4,  5, 32, Btlazy2),
103        (22, 22, 23,  5,  5, 32, Btlazy2),
104        (22, 23, 23,  6,  5, 32, Btlazy2),
105        (22, 22, 22,  5,  5, 48, Btopt),
106        (23, 23, 22,  5,  4, 64, Btopt),
107        (23, 23, 22,  6,  3, 64, Btultra),
108        (23, 24, 22,  7,  3, 256, Btultra2),
109        (25, 25, 23,  7,  3, 256, Btultra2),
110        (26, 26, 24,  7,  3, 512, Btultra2),
111        (27, 27, 25,  9,  3, 999, Btultra2),
112    ],
113    [   /* for srcSize <= 256 KB */
114        (18, 12, 13,  1,  5,  1, Fast),
115        (18, 13, 14,  1,  6,  0, Fast),
116        (18, 14, 14,  1,  5,  0, Dfast),
117        (18, 16, 16,  1,  4,  0, Dfast),
118        (18, 16, 17,  3,  5,  2, Greedy),
119        (18, 17, 18,  5,  5,  2, Greedy),
120        (18, 18, 19,  3,  5,  4, Lazy),
121        (18, 18, 19,  4,  4,  4, Lazy),
122        (18, 18, 19,  4,  4,  8, Lazy2),
123        (18, 18, 19,  5,  4,  8, Lazy2),
124        (18, 18, 19,  6,  4,  8, Lazy2),
125        (18, 18, 19,  5,  4, 12, Btlazy2),
126        (18, 19, 19,  7,  4, 12, Btlazy2),
127        (18, 18, 19,  4,  4, 16, Btopt),
128        (18, 18, 19,  4,  3, 32, Btopt),
129        (18, 18, 19,  6,  3, 128, Btopt),
130        (18, 19, 19,  6,  3, 128, Btultra),
131        (18, 19, 19,  8,  3, 256, Btultra),
132        (18, 19, 19,  6,  3, 128, Btultra2),
133        (18, 19, 19,  8,  3, 256, Btultra2),
134        (18, 19, 19, 10,  3, 512, Btultra2),
135        (18, 19, 19, 12,  3, 512, Btultra2),
136        (18, 19, 19, 13,  3, 999, Btultra2),
137    ],
138    [   /* for srcSize <= 128 KB */
139        (17, 12, 12,  1,  5,  1, Fast),
140        (17, 12, 13,  1,  6,  0, Fast),
141        (17, 13, 15,  1,  5,  0, Fast),
142        (17, 15, 16,  2,  5,  0, Dfast),
143        (17, 17, 17,  2,  4,  0, Dfast),
144        (17, 16, 17,  3,  4,  2, Greedy),
145        (17, 16, 17,  3,  4,  4, Lazy),
146        (17, 16, 17,  3,  4,  8, Lazy2),
147        (17, 16, 17,  4,  4,  8, Lazy2),
148        (17, 16, 17,  5,  4,  8, Lazy2),
149        (17, 16, 17,  6,  4,  8, Lazy2),
150        (17, 17, 17,  5,  4,  8, Btlazy2),
151        (17, 18, 17,  7,  4, 12, Btlazy2),
152        (17, 18, 17,  3,  4, 12, Btopt),
153        (17, 18, 17,  4,  3, 32, Btopt),
154        (17, 18, 17,  6,  3, 256, Btopt),
155        (17, 18, 17,  6,  3, 128, Btultra),
156        (17, 18, 17,  8,  3, 256, Btultra),
157        (17, 18, 17, 10,  3, 512, Btultra),
158        (17, 18, 17,  5,  3, 256, Btultra2),
159        (17, 18, 17,  7,  3, 512, Btultra2),
160        (17, 18, 17,  9,  3, 512, Btultra2),
161        (17, 18, 17, 11,  3, 999, Btultra2),
162    ],
163    [   /* for srcSize <= 16 KB */
164        (14, 12, 13,  1,  5,  1, Fast),
165        (14, 14, 15,  1,  5,  0, Fast),
166        (14, 14, 15,  1,  4,  0, Fast),
167        (14, 14, 15,  2,  4,  0, Dfast),
168        (14, 14, 14,  4,  4,  2, Greedy),
169        (14, 14, 14,  3,  4,  4, Lazy),
170        (14, 14, 14,  4,  4,  8, Lazy2),
171        (14, 14, 14,  6,  4,  8, Lazy2),
172        (14, 14, 14,  8,  4,  8, Lazy2),
173        (14, 15, 14,  5,  4,  8, Btlazy2),
174        (14, 15, 14,  9,  4,  8, Btlazy2),
175        (14, 15, 14,  3,  4, 12, Btopt),
176        (14, 15, 14,  4,  3, 24, Btopt),
177        (14, 15, 14,  5,  3, 32, Btultra),
178        (14, 15, 15,  6,  3, 64, Btultra),
179        (14, 15, 15,  7,  3, 256, Btultra),
180        (14, 15, 15,  5,  3, 48, Btultra2),
181        (14, 15, 15,  6,  3, 128, Btultra2),
182        (14, 15, 15,  7,  3, 256, Btultra2),
183        (14, 15, 15,  8,  3, 256, Btultra2),
184        (14, 15, 15,  8,  3, 512, Btultra2),
185        (14, 15, 15,  9,  3, 512, Btultra2),
186        (14, 15, 15, 10,  3, 999, Btultra2),
187    ],
188    ]
189};
190
191const ZSTD_MAX_CLEVEL: i32 = 22;
192const ZSTD_CLEVEL_DEFAULT: i32 = 3;
193/// `ZSTD_minCLevel` = -(1 << 17).
194const ZSTD_MIN_CLEVEL: i32 = -(1 << 17);
195
196fn highbit32(x: u32) -> u32 {
197    debug_assert!(x >= 1);
198    31 - x.leading_zeros()
199}
200
201/// `ZSTD_cycleLog`.
202fn cycle_log(hash_log: u32, strat: Strategy) -> u32 {
203    let bt_scale = (strat as i32 >= Strategy::Btlazy2 as i32) as u32;
204    hash_log - bt_scale
205}
206
207/// `ZSTD_dictAndWindowLog`: the window log enlarged so the hash/chain logs can
208/// reference both the dictionary and the live window (the zstd format treats the
209/// whole dictionary as in-window if any one byte of it is). Used only to
210/// downsize hashLog/chainLog. `src_size` must not be `CONTENTSIZE_UNKNOWN` — the
211/// caller guards, as the C `assert` documents.
212fn dict_and_window_log(window_log: u32, src_size: u64, dict_size: u64) -> u32 {
213    // No dictionary ==> no change.
214    if dict_size == 0 {
215        return window_log;
216    }
217    let max_window_size = 1u64 << WINDOWLOG_MAX;
218    let window_size = 1u64 << window_log;
219    let dict_and_window_size = dict_size.wrapping_add(window_size);
220    if window_size >= dict_size.wrapping_add(src_size) {
221        // Window already large enough for dict + src.
222        window_log
223    } else if dict_and_window_size >= max_window_size {
224        WINDOWLOG_MAX
225    } else {
226        // C truncates dictAndWindowSize to U32 before highbit32, then +1.
227        highbit32((dict_and_window_size as u32).wrapping_sub(1)) + 1
228    }
229}
230
231/// Which `ZSTD_CParamMode_e` the cParam derivation models. The two we need
232/// derive the same row size but adjust the chosen parameters differently.
233#[derive(Clone, Copy, PartialEq, Eq)]
234pub(crate) enum CParamMode {
235    /// `ZSTD_cpm_noAttachDict` — `ZSTD_compress` and the `compress_usingDict`
236    /// (extDict) path.
237    NoAttachDict,
238    /// `ZSTD_cpm_createCDict` — the parameters a CDict's own matchState is built
239    /// with (an assumed small source, plus the short-cache hash/chain caps).
240    CreateCDict,
241}
242
243/// `ZSTD_getCParams_internal` + `ZSTD_adjustCParams_internal` for mode
244/// `ZSTD_cpm_noAttachDict` — the derivation used by `ZSTD_compress` and the
245/// `ZSTD_compress_usingDict` (extDict) path. `src_size == CONTENTSIZE_UNKNOWN`
246/// selects the unknown-size behavior; `dict_size == 0` means no dictionary.
247/// Yields the same cParams as the public `ZSTD_getCParams` once that maps a 0
248/// srcSizeHint to UNKNOWN (`cpm_unknown` and `cpm_noAttachDict` derive identical
249/// cParams); `tests/cparams_differential.rs` checks this field-by-field.
250pub(crate) fn get_cparams(level: i32, src_size: u64, dict_size: u64) -> CParams {
251    get_cparams_mode(level, src_size, dict_size, CParamMode::NoAttachDict)
252}
253
254/// cParams for a CDict's own matchState (`ZSTD_getCParams_internal` mode
255/// `ZSTD_cpm_createCDict`, source size unknown). These size the dictionary's
256/// (tagged) tables and become the working tables on the CDict *copy* path.
257pub(crate) fn get_cparams_create_cdict(level: i32, dict_size: u64) -> CParams {
258    get_cparams_mode(
259        level,
260        CONTENTSIZE_UNKNOWN,
261        dict_size,
262        CParamMode::CreateCDict,
263    )
264}
265
266fn get_cparams_mode(level: i32, src_size: u64, dict_size: u64, mode: CParamMode) -> CParams {
267    // --- ZSTD_getCParamRowSize (createCDict & noAttachDict keep dictSize) ---
268    let unknown = src_size == CONTENTSIZE_UNKNOWN;
269    let r_size = if unknown && dict_size == 0 {
270        CONTENTSIZE_UNKNOWN
271    } else {
272        // C: srcSizeHint + dictSize + (unknown && dict>0 ? 500 : 0) in U64.
273        // When unknown, srcSizeHint is U64::MAX and the sum wraps exactly as C,
274        // so an unknown src with a dict yields rSize = dictSize + 499.
275        let added: u64 = if unknown && dict_size > 0 { 500 } else { 0 };
276        src_size.wrapping_add(dict_size).wrapping_add(added)
277    };
278    let table_id = (r_size <= 256 * 1024) as usize
279        + (r_size <= 128 * 1024) as usize
280        + (r_size <= 16 * 1024) as usize;
281    // Level 0 means "default"; negative levels use row 0 as their base.
282    let row = if level == 0 {
283        ZSTD_CLEVEL_DEFAULT
284    } else {
285        level.clamp(0, ZSTD_MAX_CLEVEL)
286    } as usize;
287
288    let (w, c, h, s, l, t, strat) = DEFAULT_CPARAMETERS[table_id][row];
289    let mut cp = CParams {
290        window_log: w,
291        chain_log: c,
292        hash_log: h,
293        search_log: s,
294        min_match: l,
295        target_length: t,
296        strategy: strat,
297    };
298    if level < 0 {
299        // Acceleration factor for negative levels (clamp to ZSTD_minCLevel
300        // before negating so i32::MIN can't overflow).
301        cp.target_length = (-level.max(ZSTD_MIN_CLEVEL)) as u32;
302    }
303
304    adjust_cparams_internal(cp, src_size, dict_size, mode)
305}
306
307/// `ZSTD_adjustCParams_internal`: downsize a base parameter set for the actual
308/// source and dictionary sizes. Reused with a CDict's own cParams to size the
309/// attach path's working tables (`ZSTD_resetCCtx_byAttachingCDict`).
310fn adjust_cparams_internal(
311    mut cp: CParams,
312    src_size: u64,
313    dict_size: u64,
314    mode: CParamMode,
315) -> CParams {
316    // `cpm_createCDict` assumes a small source (`minSrcSize = 513`) when the
317    // size is unknown but a dictionary is present, so the window/hash downsizing
318    // below runs against that assumed source instead of being skipped.
319    let adj_src =
320        if mode == CParamMode::CreateCDict && dict_size != 0 && src_size == CONTENTSIZE_UNKNOWN {
321            513
322        } else {
323            src_size
324        };
325    // Resize windowLog down when the input (src + dict) is small.
326    let max_window_resize = 1u64 << (WINDOWLOG_MAX - 1);
327    if adj_src <= max_window_resize && dict_size <= max_window_resize {
328        let t_size = (adj_src + dict_size) as u32;
329        let hash_size_min = 1u32 << HASHLOG_MIN;
330        let src_log = if t_size < hash_size_min {
331            HASHLOG_MIN
332        } else {
333            highbit32(t_size - 1) + 1
334        };
335        if cp.window_log > src_log {
336            cp.window_log = src_log;
337        }
338    }
339    // Downsize hashLog/chainLog to the dict-aware window log. Skipped when the
340    // source size is unknown, matching the C guard (dictAndWindowLog asserts a
341    // known srcSize).
342    if adj_src != CONTENTSIZE_UNKNOWN {
343        let daw_log = dict_and_window_log(cp.window_log, adj_src, dict_size);
344        let cyc_log = cycle_log(cp.chain_log, cp.strategy);
345        if cp.hash_log > daw_log + 1 {
346            cp.hash_log = daw_log + 1;
347        }
348        if cyc_log > daw_log {
349            cp.chain_log -= cyc_log - daw_log;
350        }
351    }
352    if cp.window_log < WINDOWLOG_ABSOLUTEMIN {
353        cp.window_log = WINDOWLOG_ABSOLUTEMIN;
354    }
355    // `cpm_createCDict` with tagged indices (fast/dfast use the short cache):
356    // hashLog and chainLog can use at most `32 - SHORT_CACHE_TAG_BITS(8)` bits.
357    if mode == CParamMode::CreateCDict && matches!(cp.strategy, Strategy::Fast | Strategy::Dfast) {
358        let max_short_cache_hash_log = 32 - SHORT_CACHE_TAG_BITS;
359        cp.hash_log = cp.hash_log.min(max_short_cache_hash_log);
360        cp.chain_log = cp.chain_log.min(max_short_cache_hash_log);
361    }
362    // Row-match-finder hashLog cap: (hashLog - rowLog + 8) <= 32. C
363    // conservatively assumes row mode is on for the strategies that support it
364    // (greedy..lazy2; useRowMatchFinder auto -> enable).
365    if matches!(
366        cp.strategy,
367        Strategy::Greedy | Strategy::Lazy | Strategy::Lazy2
368    ) {
369        let row_log = cp.search_log.clamp(4, 6);
370        let max_hash_log = (32 - 8) + row_log;
371        if cp.hash_log > max_hash_log {
372            cp.hash_log = max_hash_log;
373        }
374    }
375    cp
376}
377
378/// Test hook for `tests/cparams_differential.rs`: `get_cparams` as
379/// `[windowLog, chainLog, hashLog, searchLog, minMatch, targetLength,
380/// strategy]` (strategy as its `ZSTD_strategy` discriminant, 1..=9). The
381/// differential oracle is raw `unsafe` FFI, which `#![forbid(unsafe_code)]`
382/// bars from the library, so it must run from `tests/` — and integration tests
383/// can't reach these `pub(crate)` internals. Not part of the stable public API.
384#[doc(hidden)]
385pub fn cparams_for_testing(level: i32, src_size: u64, dict_size: u64) -> [u32; 7] {
386    let cp = get_cparams(level, src_size, dict_size);
387    [
388        cp.window_log,
389        cp.chain_log,
390        cp.hash_log,
391        cp.search_log,
392        cp.min_match,
393        cp.target_length,
394        cp.strategy as u32,
395    ]
396}
397
398/// Test hook for `tests/cdict_compress_differential.rs`: the CDict's own
399/// `cpm_createCDict` cParams (so a test can pick levels whose CDict uses the
400/// fast strategy). Same `[wlog, clog, hlog, slog, mml, tlen, strategy]` shape.
401#[doc(hidden)]
402pub fn cparams_create_cdict_for_testing(level: i32, dict_size: u64) -> [u32; 7] {
403    let cp = get_cparams_create_cdict(level, dict_size);
404    [
405        cp.window_log,
406        cp.chain_log,
407        cp.hash_log,
408        cp.search_log,
409        cp.min_match,
410        cp.target_length,
411        cp.strategy as u32,
412    ]
413}
414
415// --- Small shared helpers ------------------------------------------------------
416
417#[inline]
418pub(crate) fn read32(data: &[u8], at: usize) -> u32 {
419    u32::from_le_bytes(data[at..at + 4].try_into().unwrap())
420}
421
422#[inline]
423pub(crate) fn read64(data: &[u8], at: usize) -> u64 {
424    u64::from_le_bytes(data[at..at + 8].try_into().unwrap())
425}
426
427/// `ZSTD_count`: length of the common run of `data[a..]` and `data[b..]`,
428/// reading no further than `limit` for the `a` cursor.
429///
430/// Hot match-extension primitive (every match across every strategy). Compares
431/// eight bytes at a time like `ZSTD_count`: XOR two little-endian `u64`s and the
432/// first differing byte is at `trailing_zeros / 8` (LSB ⇒ lowest offset). The
433/// word loop only runs while `a + 8 <= limit`, so it reads no further than the
434/// byte loop would (callers always pass `limit <= data.len()` and a match source
435/// `b < a`), and the count is identical to the byte-at-a-time version.
436#[inline]
437pub(crate) fn count_eq(data: &[u8], mut a: usize, mut b: usize, limit: usize) -> usize {
438    let start = a;
439    while a + 8 <= limit {
440        let diff = read64(data, a) ^ read64(data, b);
441        if diff != 0 {
442            return a - start + (diff.trailing_zeros() / 8) as usize;
443        }
444        a += 8;
445        b += 8;
446    }
447    while a < limit && data[a] == data[b] {
448        a += 1;
449        b += 1;
450    }
451    a - start
452}
453
454/// `ZSTD_hashPtr` for `mls` in 4..=8 (8 is the double-fast long hash), hashing
455/// the bytes of `data` at `at`.
456#[inline]
457pub(crate) fn hash_ptr(data: &[u8], at: usize, hlog: u32, mls: u32) -> usize {
458    const PRIME4: u32 = 2654435761;
459    const PRIME5: u64 = 889523592379;
460    const PRIME6: u64 = 227718039650203;
461    const PRIME7: u64 = 58295818150454627;
462    const PRIME8: u64 = 0xCF1B_BCDC_B7A5_6463;
463    match mls {
464        5 => ((read64(data, at) << (64 - 40)).wrapping_mul(PRIME5) >> (64 - hlog)) as usize,
465        6 => ((read64(data, at) << (64 - 48)).wrapping_mul(PRIME6) >> (64 - hlog)) as usize,
466        7 => ((read64(data, at) << (64 - 56)).wrapping_mul(PRIME7) >> (64 - hlog)) as usize,
467        8 => (read64(data, at).wrapping_mul(PRIME8) >> (64 - hlog)) as usize,
468        _ => (read32(data, at).wrapping_mul(PRIME4) >> (32 - hlog)) as usize,
469    }
470}
471
472/// `ZSTD_index_overlap_check`: whether a repcode index is far enough below the
473/// prefix start that its 4-byte read stays clear of the dict/prefix seam (admits
474/// prefix-side repcodes outright and dict-side ones only when safe).
475pub(crate) fn index_overlap_check(prefix_lowest_index: u32, rep_index: u32) -> bool {
476    prefix_lowest_index.wrapping_sub(1).wrapping_sub(rep_index) >= 3
477}
478
479// --- The window (ZSTD_window_t) ----------------------------------------------------
480
481/// `ZSTD_window_t`, with the C `base`/`dictBase` pointers replaced by index
482/// biases into the caller's history buffer: index `i` of the *current*
483/// segment lives at `buf[i - seg_bias]` (so `base = buf - seg_bias`), and the
484/// previous segment — the extDict, `i` in `[low_limit, dict_limit)` — lives
485/// at `buf[i - dict_bias]`.
486///
487/// One-shot compression is a single contiguous segment with `seg_bias == 2`
488/// (`ZSTD_WINDOW_START_INDEX`). Streaming wraps its input buffer once full;
489/// the wrap makes the next chunk non-contiguous, which turns the whole live
490/// window into the extDict exactly as `ZSTD_window_update` does.
491pub(crate) struct Window {
492    pub(crate) seg_bias: u32,
493    pub(crate) dict_bias: u32,
494    /// `window.lowLimit`: lowest valid index overall (extDict start).
495    pub(crate) low_limit: u32,
496    /// `window.dictLimit`: first index of the current segment.
497    pub(crate) dict_limit: u32,
498    /// `window.nextSrc` as (buffer position, index): where the next chunk
499    /// must start to be contiguous, and the index it would get.
500    next_src_pos: usize,
501    next_src_idx: u32,
502    /// `ms->loadedDictEnd`: the index at the end of a loaded dictionary, in the
503    /// context's referential (so directly comparable to a block-end index).
504    /// Nonzero only while a dictionary is loaded *and still valid*: when it is,
505    /// the **entire** dictionary stays a valid match target as long as its last
506    /// byte is within the window (`ZSTD_getLowestMatchIndex`'s `isDictionary`
507    /// branch), and `ZSTD_window_enforceMaxDist` / `ZSTD_checkDictValidity` clear
508    /// it once the dict falls out of distance. Zero for no dictionary, for the
509    /// CDict *attach* path (the dict lives in a separate match state), and for a
510    /// streaming-wrap extDict (which is aged history, not a dictionary).
511    pub(crate) loaded_dict_end: u32,
512}
513
514impl Window {
515    /// `ZSTD_window_init` (plus the first-update segment flip baked in: the
516    /// first chunk always starts a fresh segment at index 2 with an empty
517    /// dict, so the flip is a no-op).
518    pub(crate) fn new() -> Self {
519        Window {
520            seg_bias: WINDOW_START_INDEX as u32,
521            dict_bias: WINDOW_START_INDEX as u32,
522            low_limit: WINDOW_START_INDEX as u32,
523            dict_limit: WINDOW_START_INDEX as u32,
524            next_src_pos: 0,
525            next_src_idx: WINDOW_START_INDEX as u32,
526            loaded_dict_end: 0,
527        }
528    }
529
530    /// The window state `ZSTD_compress_usingDict` reaches just before
531    /// compressing `src`: the dictionary has been loaded (a contiguous first
532    /// chunk) and the separately-buffered `src` appended non-contiguously, so
533    /// the dict became the extDict. We model the two C buffers as one
534    /// concatenated `dict ++ src`, so both segments share `pos = index -
535    /// ZSTD_WINDOW_START_INDEX` and `seg_bias == dict_bias == 2`:
536    /// `dict` is indices `[2, 2+dict_len)` (the extDict), `src` is
537    /// `[2+dict_len, 2+dict_len+src_len)` (the current segment). `nextSrc`
538    /// points one past `src`, so a later contiguous append would extend it.
539    pub(crate) fn preloaded_ext_dict(dict_len: usize, src_len: usize) -> Self {
540        let start = WINDOW_START_INDEX as u32;
541        Window {
542            seg_bias: start,
543            dict_bias: start,
544            low_limit: start,
545            dict_limit: start + dict_len as u32,
546            next_src_pos: dict_len + src_len,
547            next_src_idx: start + (dict_len + src_len) as u32,
548            // The dict is a loaded dictionary (extDict prefix): keep it fully
549            // valid while its last byte is in window (`loadedDictEnd == dictLimit`).
550            loaded_dict_end: start + dict_len as u32,
551        }
552    }
553
554    /// The window state a ZSTDMT job (after the first) reaches: the previous
555    /// job's overlap tail is supplied as a raw-content prefix that is
556    /// **contiguous** with the segment in C's round buffer, so the window does
557    /// *not* flip to extDict — it stays noDict, with the prefix as ordinary
558    /// in-window history. (Contrast [`preloaded_ext_dict`](Self::preloaded_ext_dict),
559    /// where the dict is a separate buffer.) The whole `prefix ++ segment` buffer
560    /// is the current segment at indices `[2, 2 + total_len)`, with
561    /// `lowLimit == dictLimit == 2`, so the noDict matchers reach back into the
562    /// prefix as far as `maxDist` allows.
563    pub(crate) fn preloaded_contiguous_prefix(total_len: usize) -> Self {
564        let start = WINDOW_START_INDEX as u32;
565        Window {
566            seg_bias: start,
567            dict_bias: start,
568            low_limit: start,
569            dict_limit: start,
570            next_src_pos: total_len,
571            next_src_idx: start + total_len as u32,
572            loaded_dict_end: 0,
573        }
574    }
575
576    /// The window state `ZSTD_resetCCtx_byAttachingCDict` reaches: the
577    /// dictionary lives in a *separate* match state (the attached CDict), so the
578    /// working window is non-extDict with `src` as its only segment, starting at
579    /// `cdictEnd`. We model `content ++ src` as one buffer, so `src` is the
580    /// current segment `[2 + content_len, ..)` and `lowLimit == dictLimit`
581    /// (no extDict — dict matches come from the attached state instead).
582    pub(crate) fn preloaded_attached_dict(content_len: usize, src_len: usize) -> Self {
583        let start = WINDOW_START_INDEX as u32;
584        let src_start = start + content_len as u32;
585        Window {
586            seg_bias: start,
587            dict_bias: start,
588            low_limit: src_start,
589            dict_limit: src_start,
590            next_src_pos: content_len + src_len,
591            next_src_idx: start + (content_len + src_len) as u32,
592            // Attach: the dict lives in a separate match state, not as an extDict
593            // of this window, so there is no in-window loadedDictEnd to track.
594            loaded_dict_end: 0,
595        }
596    }
597
598    /// Like [`preloaded_attached_dict`](Self::preloaded_attached_dict) but for
599    /// *streaming*: the source length is not known up front, so `nextSrc` points
600    /// at the *start* of the source region (position `content_len`) rather than
601    /// past it, and `window_preloaded` is left off so the first
602    /// `compress_continue` registers each chunk contiguously (no extDict flip)
603    /// via `ZSTD_window_update`. The dict still lives in the attached match state
604    /// (`lowLimit == dictLimit`, no extDict); the concatenated `content ++ input`
605    /// staging buffer keeps `dictIndexDelta == 0`.
606    pub(crate) fn streaming_attached_dict(content_len: usize) -> Self {
607        let start = WINDOW_START_INDEX as u32;
608        let src_start = start + content_len as u32;
609        Window {
610            seg_bias: start,
611            dict_bias: start,
612            low_limit: src_start,
613            dict_limit: src_start,
614            next_src_pos: content_len,
615            next_src_idx: src_start,
616            // Attach: dict lives in a separate match state (see above).
617            loaded_dict_end: 0,
618        }
619    }
620
621    /// Like [`preloaded_ext_dict`](Self::preloaded_ext_dict) but for *streaming*:
622    /// the CDict **copy** path reaches here. The de-tagged CDict tables were copied
623    /// into the working context, so the dict is an ordinary **extDict** prefix
624    /// (`lowLimit < dictLimit`) rather than living in a separate attached match
625    /// state. The source length is not known up front, so `nextSrc` points at the
626    /// source start (`content_len`) and `window_preloaded` is left off; the first
627    /// `compress_continue` registers the segment contiguously — `start ==
628    /// next_src_pos`, so `ZSTD_window_update` does *not* flip (the dict is already
629    /// the extDict), reproducing exactly what `preloaded_ext_dict` bakes in.
630    pub(crate) fn streaming_ext_dict(content_len: usize) -> Self {
631        let start = WINDOW_START_INDEX as u32;
632        Window {
633            seg_bias: start,
634            dict_bias: start,
635            low_limit: start,
636            dict_limit: start + content_len as u32,
637            next_src_pos: content_len,
638            next_src_idx: start + content_len as u32,
639            // Loaded dictionary as an extDict prefix (`loadedDictEnd == dictLimit`).
640            loaded_dict_end: start + content_len as u32,
641        }
642    }
643
644    /// `ZSTD_window_update`: register `buf[start..end]` as the next chunk.
645    /// Returns whether it was contiguous with the previous one; if not, the
646    /// current prefix becomes the extDict. Also shrinks the extDict when the
647    /// new chunk overwrites part of it in the (shared) buffer.
648    pub(crate) fn update(&mut self, start: usize, end: usize) -> bool {
649        if start == end {
650            return true;
651        }
652        let mut contiguous = true;
653        if start != self.next_src_pos {
654            self.low_limit = self.dict_limit;
655            self.dict_limit = self.next_src_idx;
656            self.dict_bias = self.seg_bias;
657            // `base = ip - distanceFromBase`, i.e. `buf[start]` gets the next
658            // index. C does this as pointer arithmetic, so `base` may legitimately
659            // point *before* the buffer: a separate window (e.g. the LDM window,
660            // updated over the same `dict ++ src` staging buffer but whose input
661            // must start at `WINDOW_START_INDEX`) yields `seg_bias = 2 - content_len`,
662            // which wraps. The U32 index domain wraps consistently, so the
663            // downstream `pos + seg_bias` recovers the right indices — match
664            // `wrapping_sub`/`wrapping_add` to C rather than tripping the debug
665            // overflow check (release already wraps).
666            self.seg_bias = self.next_src_idx.wrapping_sub(start as u32);
667            if self.dict_limit - self.low_limit < HASH_READ_SIZE as u32 {
668                // Too small extDict: forget it.
669                self.low_limit = self.dict_limit;
670            }
671            contiguous = false;
672        }
673        self.next_src_pos = end;
674        self.next_src_idx = self.seg_bias.wrapping_add(end as u32);
675        // If input and dictionary overlap (same buffer), reduce the
676        // dictionary to the part not overwritten by the input. Pointer
677        // comparisons in C; signed offsets here since the dict bias can
678        // exceed the buffer positions involved.
679        let dict_bias = i64::from(self.dict_bias);
680        if (end as i64 > i64::from(self.low_limit) - dict_bias)
681            && ((start as i64) < i64::from(self.dict_limit) - dict_bias)
682        {
683            let high_input_idx = end as i64 + dict_bias;
684            self.low_limit = if high_input_idx > i64::from(self.dict_limit) {
685                self.dict_limit
686            } else {
687                high_input_idx as u32
688            };
689        }
690        contiguous
691    }
692
693    /// `ZSTD_window_enforceMaxDist`: called before each block; slides `lowLimit`
694    /// to `idx - maxDist` and drags `dictLimit` along once the extDict falls out
695    /// of the window. (The C parameter is named `blockEnd`, but
696    /// `ZSTD_compress_frameChunk` anchors it at the block *start* `ip`.)
697    ///
698    /// With a loaded dictionary (`loaded_dict_end != 0`) the threshold is pushed
699    /// out by `loadedDictEnd` — a dictionary stays fully referenceable until its
700    /// last byte leaves the window — and once it does fall out the dict is
701    /// cleared (`loadedDictEnd = 0`; the matchers then revert to the windowed
702    /// lowest index). With no dictionary the test is just the overflow guard on
703    /// `idx - maxDist`.
704    pub(crate) fn enforce_max_dist(&mut self, idx: u32, max_dist: u32) {
705        // `maxDist + loadedDictEnd` is a U32 sum in C; wrap to match (and to keep
706        // the debug overflow checks quiet for a huge dict + window).
707        if idx > max_dist.wrapping_add(self.loaded_dict_end) {
708            let new_low_limit = idx - max_dist;
709            if self.low_limit < new_low_limit {
710                self.low_limit = new_low_limit;
711            }
712            if self.dict_limit < self.low_limit {
713                self.dict_limit = self.low_limit;
714            }
715            self.loaded_dict_end = 0;
716        }
717    }
718
719    /// `ZSTD_checkDictValidity`: run before [`enforce_max_dist`](Self::enforce_max_dist)
720    /// per block, anchored at the block *end* (`ip + blockSize`). It only
721    /// invalidates a loaded dictionary — without touching `lowLimit` — once the
722    /// window size is reached anywhere within the next block, or once the dict is
723    /// no longer contiguous with the current segment (`loadedDictEnd !=
724    /// dictLimit`, e.g. after a streaming wrap). A no-op when no dictionary is
725    /// loaded. (Dropping an *attached* `dictMatchState` is the caller's concern;
726    /// the attach path never reaches a window-filling size here.)
727    pub(crate) fn check_dict_validity(&mut self, block_end_idx: u32, max_dist: u32) {
728        if self.loaded_dict_end != 0
729            && (block_end_idx > self.loaded_dict_end.wrapping_add(max_dist)
730                || self.loaded_dict_end != self.dict_limit)
731        {
732            self.loaded_dict_end = 0;
733        }
734    }
735
736    /// `ZSTD_window_hasExtDict`, which decides the dict mode
737    /// (`ZSTD_matchState_dictMode`): extDict iff `lowLimit < dictLimit`.
738    pub(crate) fn has_ext_dict(&self) -> bool {
739        self.low_limit < self.dict_limit
740    }
741
742    /// The window mutation of `ZSTD_initStats_ultra` (btultra2's first-block
743    /// double pass): forget the first pass's match history by sliding the
744    /// index space past the block (`window.base -= srcSize; dictLimit +=
745    /// srcSize; lowLimit = dictLimit`). `nextSrc` stays put as a pointer, so
746    /// its index moves with the base.
747    pub(crate) fn slide_for_init_stats(&mut self, src_size: usize) {
748        self.seg_bias += src_size as u32;
749        self.dict_limit += src_size as u32;
750        self.low_limit = self.dict_limit;
751        self.next_src_idx += src_size as u32;
752    }
753
754    /// `ZSTD_window_needOverflowCorrection`: the running index reached
755    /// `ZSTD_CURRENT_MAX` (3500 MiB on 64-bit), so the 32-bit index space must
756    /// be recycled. `block_end_pos` is the block end as a buffer position;
757    /// its index is `block_end_pos + seg_bias`.
758    pub(crate) fn needs_overflow_correction(&self, block_end_pos: usize) -> bool {
759        const CURRENT_MAX: u32 = 3500 * (1 << 20);
760        (block_end_pos as u32).wrapping_add(self.seg_bias) > CURRENT_MAX
761    }
762
763    /// `ZSTD_window_correctOverflow`: reduce every index by the returned
764    /// `correction` so the window's high index drops back to roughly
765    /// `maxDist`, keeping the low `cycleLog` bits unchanged (the chains and
766    /// binary trees index modulo a power of two). The caller must apply the
767    /// same `correction` to every stored table index. `block_start_pos` is
768    /// the block start as a buffer position (`src` in C).
769    pub(crate) fn correct_overflow(
770        &mut self,
771        cycle_log: u32,
772        max_dist: u32,
773        block_start_pos: usize,
774    ) -> u32 {
775        let cycle_size = 1u32 << cycle_log;
776        let cycle_mask = cycle_size - 1;
777        let curr = block_start_pos as u32 + self.seg_bias;
778        let current_cycle = curr & cycle_mask;
779        // Ensure newCurrent - maxDist >= ZSTD_WINDOW_START_INDEX.
780        let current_cycle_correction = if current_cycle < WINDOW_START_INDEX as u32 {
781            cycle_size.max(WINDOW_START_INDEX as u32)
782        } else {
783            0
784        };
785        let new_current = current_cycle + current_cycle_correction + max_dist.max(cycle_size);
786        let correction = curr - new_current;
787
788        // `base += correction` (indices drop by `correction`) becomes
789        // `seg_bias -= correction` here; the dict segment moves in lockstep.
790        self.seg_bias -= correction;
791        self.dict_bias -= correction;
792        self.next_src_idx -= correction;
793        let floor = correction + WINDOW_START_INDEX as u32;
794        self.low_limit = if self.low_limit < floor {
795            WINDOW_START_INDEX as u32
796        } else {
797            self.low_limit - correction
798        };
799        self.dict_limit = if self.dict_limit < floor {
800            WINDOW_START_INDEX as u32
801        } else {
802            self.dict_limit - correction
803        };
804        correction
805    }
806}
807
808/// `ZSTD_count_2segments`: count the match length when `match` lives in the
809/// extDict segment — count up to `mend`, then continue comparing from
810/// `istart` (the prefix start). All arguments are buffer positions.
811pub(crate) fn count_2segments(
812    buf: &[u8],
813    ip: usize,
814    matched: usize,
815    iend: usize,
816    mend: usize,
817    istart: usize,
818) -> usize {
819    let v_end = (ip + (mend - matched)).min(iend);
820    let match_length = count_eq(buf, ip, matched, v_end);
821    if matched + match_length != mend {
822        return match_length;
823    }
824    match_length + count_eq(buf, ip + match_length, istart, iend)
825}
826
827/// `ZSTD_reduceTable_internal`: subtract `correction` from every stored index
828/// after an overflow correction, squashing anything below the reserved
829/// `WINDOW_START_INDEX` floor to 0 (an empty slot). `preserve_mark` keeps the
830/// btlazy2 `DUBT_UNSORTED_MARK` (value 1) untouched.
831pub(crate) fn reduce_table(table: &mut [u32], correction: u32, preserve_mark: bool) {
832    const DUBT_UNSORTED_MARK: u32 = 1;
833    let threshold = correction + WINDOW_START_INDEX as u32;
834    for v in table.iter_mut() {
835        if preserve_mark && *v == DUBT_UNSORTED_MARK {
836            // keep the sort sentinel
837        } else if *v < threshold {
838            *v = 0;
839        } else {
840            *v -= correction;
841        }
842    }
843}
844
845// --- The ZSTD_fast match finder ---------------------------------------------------
846
847/// Position bias: index `i` of the input maps to match index `i + 2`
848/// (`ZSTD_WINDOW_START_INDEX`), so hash-table zeros are never valid matches.
849/// `idx_to_pos(idx) = idx - 2` and vice versa.
850struct FastCtx {
851    hash_table: Vec<u32>,
852    hlog: u32,
853    mls: u32,
854    step_size: usize,
855    window_log: u32,
856}
857
858impl FastCtx {
859    fn new(cparams: &CParams) -> Self {
860        FastCtx {
861            hash_table: vec![0u32; 1usize << cparams.hash_log],
862            hlog: cparams.hash_log,
863            // stepSize = targetLength + !targetLength + 1 (min 2).
864            step_size: cparams.target_length as usize + (cparams.target_length == 0) as usize + 1,
865            mls: cparams.min_match.clamp(4, 7),
866            window_log: cparams.window_log,
867        }
868    }
869
870    /// `ZSTD_reduceIndex` for the fast strategy: only the hash table.
871    fn reduce_indices(&mut self, correction: u32) {
872        reduce_table(&mut self.hash_table, correction, false);
873    }
874}
875
876/// `ZSTD_fillHashTableForCCtx` (fast strategy, `dtlm_fast`, untagged): seed the
877/// hash table from the dictionary content laid out at `data[0..dict_len]`,
878/// before compressing `src`. Each inserted entry is a *biased* index
879/// (`pos + ZSTD_WINDOW_START_INDEX`), matching how the matcher stores and reads
880/// positions, so the dict entries are found as extDict candidates.
881///
882/// `nextToUpdate` starts at dict position 0; C's loop `ip + 3 < iend + 2` with
883/// `iend = dictEnd - HASH_READ_SIZE` is, in buffer positions, `ip + 9 <
884/// dict_len`. The caller guarantees `dict_len > HASH_READ_SIZE` (otherwise C's
885/// `ZSTD_loadDictionaryContent` returns before the fill, leaving an empty
886/// table).
887fn fill_fast_hash_table_for_cctx(ctx: &mut FastCtx, data: &[u8], dict_len: usize) {
888    let hlog = ctx.hlog;
889    let mls = ctx.mls;
890    let mut ip = 0usize;
891    while ip + 9 < dict_len {
892        let h = hash_ptr(data, ip, hlog, mls);
893        ctx.hash_table[h] = (ip + WINDOW_START_INDEX) as u32;
894        ip += 3;
895    }
896}
897
898/// `ZSTD_writeTaggedIndex`: pack `(index, tag)` into a CDict hash bucket. The
899/// low [`SHORT_CACHE_TAG_BITS`] bits of `hash_and_tag` are the tag; the rest is
900/// the bucket. The index is stored shifted up, the tag in the low bits.
901fn write_tagged_index(table: &mut [u32], hash_and_tag: usize, index: u32) {
902    let hash = hash_and_tag >> SHORT_CACHE_TAG_BITS;
903    let tag = (hash_and_tag as u32) & ((1 << SHORT_CACHE_TAG_BITS) - 1);
904    table[hash] = (index << SHORT_CACHE_TAG_BITS) | tag;
905}
906
907/// `ZSTD_fillHashTableForCDict` (fast strategy, `dtlm_full`, **tagged**): seed a
908/// CDict's own hash table from the dictionary content at `data[0..dict_len]`.
909/// Buckets are addressed by `hashLog + SHORT_CACHE_TAG_BITS` bits and store
910/// `(biasedIndex << 8) | tag`; `dtlm_full` also inserts the p=1,2 positions when
911/// their bucket is still empty. The caller sizes `table` to `1 << hlog` and
912/// guarantees `dict_len > HASH_READ_SIZE`.
913fn fill_fast_hash_table_for_cdict(
914    table: &mut [u32],
915    data: &[u8],
916    dict_len: usize,
917    hlog: u32,
918    mls: u32,
919) {
920    let h_bits = hlog + SHORT_CACHE_TAG_BITS;
921    let mut ip = 0usize;
922    while ip + 9 < dict_len {
923        let curr = (ip + WINDOW_START_INDEX) as u32;
924        write_tagged_index(table, hash_ptr(data, ip, h_bits, mls), curr);
925        // dtlm_full: also load the in-between positions if their bucket is empty.
926        for p in 1..3usize {
927            let hash_and_tag = hash_ptr(data, ip + p, h_bits, mls);
928            if table[hash_and_tag >> SHORT_CACHE_TAG_BITS] == 0 {
929                write_tagged_index(table, hash_and_tag, curr + p as u32);
930            }
931        }
932        ip += 3;
933    }
934}
935
936/// `ZSTD_fillHashTableForCCtx` with `dtlm_full` (untagged): the de-tagged form of
937/// a CDict fast table is identical to an untagged `dtlm_full` fill, since
938/// `hashPtr(p, hLog+8) >> 8 == hashPtr(p, hLog)`. Used by the CDict **copy**
939/// path, which reproduces the CDict tables in the working context.
940fn fill_fast_hash_table_for_cctx_full(ctx: &mut FastCtx, data: &[u8], dict_len: usize) {
941    let hlog = ctx.hlog;
942    let mls = ctx.mls;
943    let mut ip = 0usize;
944    while ip + 9 < dict_len {
945        let curr = (ip + WINDOW_START_INDEX) as u32;
946        ctx.hash_table[hash_ptr(data, ip, hlog, mls)] = curr;
947        for p in 1..3usize {
948            let h = hash_ptr(data, ip + p, hlog, mls);
949            if ctx.hash_table[h] == 0 {
950                ctx.hash_table[h] = curr + p as u32;
951            }
952        }
953        ip += 3;
954    }
955}
956
957/// `ZSTD_compressBlock_fast_dictMatchState_generic`: the CDict **attach** match
958/// finder. The dictionary is a separate (tagged) match state; `ctx` holds the
959/// working context's own untagged table, which starts empty and fills as `src`
960/// is parsed. In our `content ++ src` buffer the working window is non-extDict
961/// (src begins at `dictEnd`), so `dictIndexDelta == 0` and both tables index the
962/// same buffer; a dict match is taken only when the working table found nothing
963/// (`matchIndex <= prefixStartIndex`), replicating the extDict parse. `data` is
964/// `content ++ src`; the block is `data[block_start..block_end]` (= `src`),
965/// `content_len` is the dictionary content length.
966#[allow(clippy::too_many_arguments)]
967fn compress_block_fast_dict_match_state(
968    ctx: &mut FastCtx,
969    store: &mut SeqStore,
970    rep: &mut [u32; 3],
971    data: &[u8],
972    block_start: usize,
973    block_end: usize,
974    content_len: usize,
975    dict_hash_table: &[u32],
976    dict_hlog: u32,
977) -> usize {
978    let bias = WINDOW_START_INDEX;
979    let hlog = ctx.hlog;
980    let mls = ctx.mls;
981    // dictMatchState uses stepSize = targetLength + !targetLength (no +1).
982    let step_size = ctx.step_size - 1;
983    let k_step_incr = 1usize << K_SEARCH_STRENGTH;
984    let dict_h_bits = dict_hlog + SHORT_CACHE_TAG_BITS;
985    let tag_mask = (1u32 << SHORT_CACHE_TAG_BITS) - 1;
986
987    // Window geometry (concat buffer, `base`/`dictBase` bias both = 2).
988    let prefix_start_index = (bias + content_len) as u32; // window.dictLimit (src start)
989    let dict_start_index = bias as u32; // dms.window.dictLimit
990    let prefix_start_pos = content_len; // prefix_start_index - bias
991    let dict_end_pos = content_len; // dictEnd - bias (== prefix start; dictIndexDelta 0)
992    let iend_pos = block_end;
993
994    let iend = block_end + bias;
995    if block_end - block_start < HASH_READ_SIZE {
996        return block_end - block_start;
997    }
998    let ilimit = iend - HASH_READ_SIZE;
999    let istart = block_start + bias;
1000    let dict_and_prefix_length = (istart - prefix_start_index as usize) + content_len;
1001
1002    let mut offset_1 = rep[0];
1003    let mut offset_2 = rep[1];
1004
1005    let mut anchor = istart;
1006    let mut ip0 = istart + (dict_and_prefix_length == 0) as usize;
1007    let mut ip1 = ip0 + step_size;
1008
1009    while ip1 <= ilimit {
1010        let mut hash0 = hash_ptr(data, ip0 - bias, hlog, mls);
1011        let dict_hat0 = hash_ptr(data, ip0 - bias, dict_h_bits, mls);
1012        let mut dict_idx_tag = dict_hash_table[dict_hat0 >> SHORT_CACHE_TAG_BITS];
1013        let mut dict_tags_match = (dict_idx_tag & tag_mask) == (dict_hat0 as u32 & tag_mask);
1014        let mut match_index = ctx.hash_table[hash0];
1015        let mut curr = ip0 as u32;
1016        let mut step = step_size;
1017        let mut next_step = ip0 + k_step_incr;
1018
1019        // The inner search loop yields the match length (with `ip0`/`anchor`/
1020        // offsets already updated and the sequence stored), or returns on
1021        // running out of input.
1022        let m_length = loop {
1023            let rep_index = curr.wrapping_add(1).wrapping_sub(offset_1);
1024            let hash1 = hash_ptr(data, ip1 - bias, hlog, mls);
1025            let dict_hat1 = hash_ptr(data, ip1 - bias, dict_h_bits, mls);
1026            ctx.hash_table[hash0] = curr;
1027
1028            // 1. repcode at ip0 + 1.
1029            if index_overlap_check(prefix_start_index, rep_index)
1030                && read32(data, rep_index as usize - bias) == read32(data, (ip0 + 1) - bias)
1031            {
1032                let mend = if rep_index < prefix_start_index {
1033                    dict_end_pos
1034                } else {
1035                    iend_pos
1036                };
1037                let ml = 4 + count_2segments(
1038                    data,
1039                    (ip0 + 1 - bias) + 4,
1040                    (rep_index as usize - bias) + 4,
1041                    iend_pos,
1042                    mend,
1043                    prefix_start_pos,
1044                );
1045                ip0 += 1;
1046                store.store_seq(&data[anchor - bias..ip0 - bias], 1, ml as u32);
1047                break ml;
1048            }
1049
1050            // 2. dictionary match (only when the working table found nothing).
1051            if dict_tags_match {
1052                let dict_match_index = dict_idx_tag >> SHORT_CACHE_TAG_BITS;
1053                if dict_match_index > dict_start_index
1054                    && read32(data, dict_match_index as usize - bias) == read32(data, ip0 - bias)
1055                    && match_index <= prefix_start_index
1056                {
1057                    let offset = curr - dict_match_index; // dictIndexDelta == 0
1058                    let mut ml = 4 + count_2segments(
1059                        data,
1060                        (ip0 - bias) + 4,
1061                        (dict_match_index as usize - bias) + 4,
1062                        iend_pos,
1063                        dict_end_pos,
1064                        prefix_start_pos,
1065                    );
1066                    let mut dm = dict_match_index as usize;
1067                    while ip0 > anchor
1068                        && dm > dict_start_index as usize
1069                        && data[(ip0 - bias) - 1] == data[(dm - bias) - 1]
1070                    {
1071                        ip0 -= 1;
1072                        dm -= 1;
1073                        ml += 1;
1074                    }
1075                    offset_2 = offset_1;
1076                    offset_1 = offset;
1077                    store.store_seq(&data[anchor - bias..ip0 - bias], offset + 3, ml as u32);
1078                    break ml;
1079                }
1080            }
1081
1082            // 3. ordinary match in the working context.
1083            if match_index >= prefix_start_index
1084                && read32(data, match_index as usize - bias) == read32(data, ip0 - bias)
1085            {
1086                let offset = curr - match_index;
1087                let mut ml = 4 + count_eq(
1088                    data,
1089                    (ip0 - bias) + 4,
1090                    (match_index as usize - bias) + 4,
1091                    iend_pos,
1092                );
1093                let mut m = match_index as usize;
1094                while ip0 > anchor
1095                    && m > prefix_start_index as usize
1096                    && data[(ip0 - bias) - 1] == data[(m - bias) - 1]
1097                {
1098                    ip0 -= 1;
1099                    m -= 1;
1100                    ml += 1;
1101                }
1102                offset_2 = offset_1;
1103                offset_1 = offset;
1104                store.store_seq(&data[anchor - bias..ip0 - bias], offset + 3, ml as u32);
1105                break ml;
1106            }
1107
1108            // Prepare the next iteration.
1109            dict_idx_tag = dict_hash_table[dict_hat1 >> SHORT_CACHE_TAG_BITS];
1110            dict_tags_match = (dict_idx_tag & tag_mask) == (dict_hat1 as u32 & tag_mask);
1111            match_index = ctx.hash_table[hash1];
1112            if ip1 >= next_step {
1113                step += 1;
1114                next_step += k_step_incr;
1115            }
1116            ip0 = ip1;
1117            ip1 += step;
1118            if ip1 > ilimit {
1119                rep[0] = offset_1;
1120                rep[1] = offset_2;
1121                return iend_pos - (anchor - bias);
1122            }
1123            curr = ip0 as u32;
1124            hash0 = hash1;
1125        };
1126
1127        // Match found: advance past it, then fill the table and run the
1128        // immediate-repcode loop.
1129        ip0 += m_length;
1130        anchor = ip0;
1131        if ip0 <= ilimit {
1132            ctx.hash_table[hash_ptr(data, (curr as usize + 2) - bias, hlog, mls)] = curr + 2;
1133            ctx.hash_table[hash_ptr(data, (ip0 - 2) - bias, hlog, mls)] = (ip0 - 2) as u32;
1134            while ip0 <= ilimit {
1135                let current2 = ip0 as u32;
1136                let rep_index2 = current2.wrapping_sub(offset_2);
1137                if index_overlap_check(prefix_start_index, rep_index2)
1138                    && read32(data, rep_index2 as usize - bias) == read32(data, ip0 - bias)
1139                {
1140                    let rep_end2 = if rep_index2 < prefix_start_index {
1141                        dict_end_pos
1142                    } else {
1143                        iend_pos
1144                    };
1145                    let rep_length2 = 4 + count_2segments(
1146                        data,
1147                        (ip0 - bias) + 4,
1148                        (rep_index2 as usize - bias) + 4,
1149                        iend_pos,
1150                        rep_end2,
1151                        prefix_start_pos,
1152                    );
1153                    std::mem::swap(&mut offset_1, &mut offset_2);
1154                    store.store_seq(&data[anchor - bias..ip0 - bias], 1, rep_length2 as u32);
1155                    ctx.hash_table[hash_ptr(data, ip0 - bias, hlog, mls)] = current2;
1156                    ip0 += rep_length2;
1157                    anchor = ip0;
1158                } else {
1159                    break;
1160                }
1161            }
1162        }
1163        ip1 = ip0 + step_size;
1164    }
1165
1166    rep[0] = offset_1;
1167    rep[1] = offset_2;
1168    iend_pos - (anchor - bias)
1169}
1170
1171/// `ZSTD_compressBlock_fast` (noDict path), operating on the history buffer
1172/// `data` with the current block being `data[block_start..block_end]`.
1173///
1174/// `seg_bias` maps buffer positions to window indices (`idx = pos +
1175/// seg_bias`; 2 for a frame's first segment) and `lowest_valid` is
1176/// `window.dictLimit`, the lowest index of the current segment — both only
1177/// differ from 2 after a streaming buffer wrap.
1178///
1179/// Returns the size of the trailing literals; emits sequences into `store` and
1180/// updates `rep`. The `useCmov`/branch variants of the candidate test are
1181/// semantically identical, so a single form is used.
1182#[allow(clippy::too_many_arguments)]
1183fn compress_block_fast(
1184    ctx: &mut FastCtx,
1185    store: &mut SeqStore,
1186    rep: &mut [u32; 3],
1187    data: &[u8],
1188    block_start: usize,
1189    block_end: usize,
1190    seg_bias: usize,
1191    lowest_valid: usize,
1192) -> usize {
1193    let src_size = block_end - block_start;
1194    let hlog = ctx.hlog;
1195    let mls = ctx.mls;
1196    let step_size = ctx.step_size;
1197    // `ZSTD_getLowestPrefixIndex(ms, endIndex, windowLog)`: once the frame
1198    // outgrows the window, the lowest valid match index slides to
1199    // blockEnd - windowSize. (Biased indices, like everything below.)
1200    let max_distance = 1usize << ctx.window_log;
1201    let end_index = block_end + seg_bias;
1202    let prefix_start_index = if end_index - lowest_valid > max_distance {
1203        end_index - max_distance
1204    } else {
1205        lowest_valid
1206    };
1207    let k_step_incr: usize = 1 << (K_SEARCH_STRENGTH - 1);
1208
1209    // All `ipN` variables are *biased* indices (buffer position + seg_bias),
1210    // matching the C pointer arithmetic `ip - base`.
1211    let bias = seg_bias;
1212    let to_pos = |idx: usize| idx - bias; // biased index -> data position
1213    let istart = block_start + bias;
1214    let iend = block_end + bias;
1215    if src_size < HASH_READ_SIZE {
1216        return src_size;
1217    }
1218    let ilimit = iend - HASH_READ_SIZE;
1219
1220    let mut anchor = istart;
1221    let mut ip0 = istart;
1222    let mut ip1: usize;
1223    let mut ip2: usize;
1224    let mut ip3: usize;
1225    let mut current0: u32;
1226
1227    let mut rep_offset1 = rep[0];
1228    let mut rep_offset2 = rep[1];
1229    let mut offset_saved1 = 0u32;
1230    let mut offset_saved2 = 0u32;
1231
1232    let mut hash0: usize;
1233    let mut hash1: usize;
1234    let mut match_idx: u32;
1235
1236    let mut step: usize;
1237    let mut next_step: usize;
1238
1239    // Candidate test (`ZSTD_match4Found_*`): valid window index and 4 equal
1240    // bytes. The C cmov/branch variants are semantically identical.
1241    let found = |data: &[u8], cur: usize, idx: u32| -> bool {
1242        idx as usize >= prefix_start_index
1243            && read32(data, to_pos(cur)) == read32(data, to_pos(idx as usize))
1244    };
1245
1246    ip0 += (ip0 == prefix_start_index) as usize; // skip the very first window position
1247    {
1248        // `ZSTD_getLowestPrefixIndex(ms, curr, windowLog)` — at the *block
1249        // start*, not the block end: when the window slides, maxRep is the
1250        // full window size, one block more than prefix_start_index allows.
1251        let curr = ip0;
1252        let window_low = if curr - lowest_valid > max_distance {
1253            curr - max_distance
1254        } else {
1255            lowest_valid
1256        };
1257        let max_rep = (curr - window_low) as u32;
1258        if rep_offset2 > max_rep {
1259            offset_saved2 = rep_offset2;
1260            rep_offset2 = 0;
1261        }
1262        if rep_offset1 > max_rep {
1263            offset_saved1 = rep_offset1;
1264            rep_offset1 = 0;
1265        }
1266    }
1267
1268    'outer: loop {
1269        // _start:
1270        step = step_size;
1271        next_step = ip0 + k_step_incr;
1272        ip1 = ip0 + 1;
1273        ip2 = ip0 + step;
1274        ip3 = ip2 + 1;
1275
1276        if ip3 >= ilimit {
1277            break 'outer;
1278        }
1279
1280        hash0 = hash_ptr(data, to_pos(ip0), hlog, mls);
1281        hash1 = hash_ptr(data, to_pos(ip1), hlog, mls);
1282        match_idx = ctx.hash_table[hash0];
1283
1284        loop {
1285            // Repcode candidate at ip2 (read before the table write, as in C).
1286            let rval = if rep_offset1 > 0 {
1287                read32(data, to_pos(ip2 - rep_offset1 as usize))
1288            } else {
1289                0
1290            };
1291
1292            current0 = ip0 as u32;
1293            ctx.hash_table[hash0] = current0;
1294
1295            // Check repcode at ip2.
1296            if rep_offset1 > 0 && read32(data, to_pos(ip2)) == rval {
1297                ip0 = ip2;
1298                let mut match0 = ip0 - rep_offset1 as usize;
1299                let ext = (data[to_pos(ip0) - 1] == data[to_pos(match0) - 1]) as usize;
1300                let mut m_length = ext;
1301                ip0 -= ext;
1302                match0 -= ext;
1303                let offcode = 1u32; // REPCODE1_TO_OFFBASE
1304                m_length += 4;
1305                ctx.hash_table[hash1] = ip1 as u32;
1306                // _match
1307                m_length += count_eq(
1308                    data,
1309                    to_pos(ip0) + m_length,
1310                    to_pos(match0) + m_length,
1311                    to_pos(iend),
1312                );
1313                store.store_seq(&data[to_pos(anchor)..to_pos(ip0)], offcode, m_length as u32);
1314                ip0 += m_length;
1315                anchor = ip0;
1316                post_match(
1317                    ctx,
1318                    store,
1319                    data,
1320                    &mut ip0,
1321                    &mut anchor,
1322                    current0,
1323                    &mut rep_offset1,
1324                    &mut rep_offset2,
1325                    ilimit,
1326                    iend,
1327                    hlog,
1328                    mls,
1329                    bias,
1330                );
1331                continue 'outer;
1332            }
1333
1334            if found(data, ip0, match_idx) {
1335                ctx.hash_table[hash1] = ip1 as u32;
1336                offset_and_match(
1337                    ctx,
1338                    store,
1339                    data,
1340                    &mut ip0,
1341                    &mut anchor,
1342                    current0,
1343                    &mut rep_offset1,
1344                    &mut rep_offset2,
1345                    match_idx,
1346                    prefix_start_index,
1347                    ilimit,
1348                    iend,
1349                    hlog,
1350                    mls,
1351                    bias,
1352                );
1353                continue 'outer;
1354            }
1355
1356            match_idx = ctx.hash_table[hash1];
1357            hash0 = hash1;
1358            hash1 = hash_ptr(data, to_pos(ip2), hlog, mls);
1359            ip0 = ip1;
1360            ip1 = ip2;
1361            ip2 = ip3;
1362
1363            current0 = ip0 as u32;
1364            ctx.hash_table[hash0] = current0;
1365
1366            if found(data, ip0, match_idx) {
1367                if step <= 4 {
1368                    ctx.hash_table[hash1] = ip1 as u32;
1369                }
1370                offset_and_match(
1371                    ctx,
1372                    store,
1373                    data,
1374                    &mut ip0,
1375                    &mut anchor,
1376                    current0,
1377                    &mut rep_offset1,
1378                    &mut rep_offset2,
1379                    match_idx,
1380                    prefix_start_index,
1381                    ilimit,
1382                    iend,
1383                    hlog,
1384                    mls,
1385                    bias,
1386                );
1387                continue 'outer;
1388            }
1389
1390            match_idx = ctx.hash_table[hash1];
1391            hash0 = hash1;
1392            hash1 = hash_ptr(data, to_pos(ip2), hlog, mls);
1393            ip0 = ip1;
1394            ip1 = ip2;
1395            ip2 = ip0 + step;
1396            ip3 = ip1 + step;
1397
1398            if ip2 >= next_step {
1399                step += 1;
1400                next_step += k_step_incr;
1401            }
1402            if ip3 >= ilimit {
1403                break 'outer;
1404            }
1405        }
1406    }
1407
1408    // _cleanup: restore any invalidated repcodes for the next block.
1409    offset_saved2 = if offset_saved1 != 0 && rep_offset1 != 0 {
1410        offset_saved1
1411    } else {
1412        offset_saved2
1413    };
1414    rep[0] = if rep_offset1 != 0 {
1415        rep_offset1
1416    } else {
1417        offset_saved1
1418    };
1419    rep[1] = if rep_offset2 != 0 {
1420        rep_offset2
1421    } else {
1422        offset_saved2
1423    };
1424
1425    to_pos(iend) - to_pos(anchor)
1426}
1427
1428/// The `_offset` + `_match` tail for an ordinary (non-repcode) match: compute
1429/// the offset, extend backward, count forward, store, then run the
1430/// post-match repcode loop. Control then re-enters `_start`, whose own
1431/// `ip3 >= ilimit` check decides whether to clean up.
1432#[allow(clippy::too_many_arguments)]
1433fn offset_and_match(
1434    ctx: &mut FastCtx,
1435    store: &mut SeqStore,
1436    data: &[u8],
1437    ip0: &mut usize,
1438    anchor: &mut usize,
1439    current0: u32,
1440    rep_offset1: &mut u32,
1441    rep_offset2: &mut u32,
1442    match_idx: u32,
1443    prefix_start_index: usize,
1444    ilimit: usize,
1445    iend: usize,
1446    hlog: u32,
1447    mls: u32,
1448    bias: usize,
1449) {
1450    let to_pos = |idx: usize| idx - bias;
1451    let mut match0 = match_idx as usize;
1452
1453    *rep_offset2 = *rep_offset1;
1454    *rep_offset1 = (*ip0 - match0) as u32;
1455    let offcode = *rep_offset1 + 3; // OFFSET_TO_OFFBASE
1456    let mut m_length = 4usize;
1457
1458    // Backward extension.
1459    while *ip0 > *anchor
1460        && match0 > prefix_start_index
1461        && data[to_pos(*ip0) - 1] == data[to_pos(match0) - 1]
1462    {
1463        *ip0 -= 1;
1464        match0 -= 1;
1465        m_length += 1;
1466    }
1467
1468    // Forward extension.
1469    m_length += count_eq(
1470        data,
1471        to_pos(*ip0) + m_length,
1472        to_pos(match0) + m_length,
1473        to_pos(iend),
1474    );
1475    store.store_seq(
1476        &data[to_pos(*anchor)..to_pos(*ip0)],
1477        offcode,
1478        m_length as u32,
1479    );
1480    *ip0 += m_length;
1481    *anchor = *ip0;
1482
1483    post_match(
1484        ctx,
1485        store,
1486        data,
1487        ip0,
1488        anchor,
1489        current0,
1490        rep_offset1,
1491        rep_offset2,
1492        ilimit,
1493        iend,
1494        hlog,
1495        mls,
1496        bias,
1497    );
1498}
1499
1500/// The shared post-match tail: fill the hash table at `current0+2` and
1501/// `ip0-2`, then greedily emit immediate repcode matches; control returns to
1502/// `_start`.
1503#[allow(clippy::too_many_arguments)]
1504fn post_match(
1505    ctx: &mut FastCtx,
1506    store: &mut SeqStore,
1507    data: &[u8],
1508    ip0: &mut usize,
1509    anchor: &mut usize,
1510    current0: u32,
1511    rep_offset1: &mut u32,
1512    rep_offset2: &mut u32,
1513    ilimit: usize,
1514    iend: usize,
1515    hlog: u32,
1516    mls: u32,
1517    bias: usize,
1518) {
1519    let to_pos = |idx: usize| idx - bias;
1520    if *ip0 <= ilimit {
1521        // Fill table.
1522        let c02 = current0 as usize + 2;
1523        let h = hash_ptr(data, to_pos(c02), hlog, mls);
1524        ctx.hash_table[h] = c02 as u32;
1525        let h2 = hash_ptr(data, to_pos(*ip0 - 2), hlog, mls);
1526        ctx.hash_table[h2] = (*ip0 - 2) as u32;
1527
1528        if *rep_offset2 > 0 {
1529            while *ip0 <= ilimit
1530                && read32(data, to_pos(*ip0)) == read32(data, to_pos(*ip0 - *rep_offset2 as usize))
1531            {
1532                let r_length = count_eq(
1533                    data,
1534                    to_pos(*ip0) + 4,
1535                    to_pos(*ip0 - *rep_offset2 as usize) + 4,
1536                    to_pos(iend),
1537                ) + 4;
1538                std::mem::swap(rep_offset1, rep_offset2);
1539                let h = hash_ptr(data, to_pos(*ip0), hlog, mls);
1540                ctx.hash_table[h] = *ip0 as u32;
1541                *ip0 += r_length;
1542                store.store_seq(&[], 1, r_length as u32); // REPCODE1, no literals
1543                *anchor = *ip0;
1544            }
1545        }
1546    }
1547}
1548
1549// --- The ZSTD_fast extDict match finder --------------------------------------------
1550
1551/// The per-block two-segment geometry of `ZSTD_compressBlock_fast_extDict`,
1552/// shared by the matcher body and its match tail. Index space is the window's
1553/// (`idx = pos + bias` per segment); `*_pos` fields are buffer positions.
1554struct ExtDictView {
1555    seg_bias: usize,
1556    dict_bias: usize,
1557    /// `prefixStartIndex` (== `dictLimit` while the extDict is live).
1558    prefix_start_index: usize,
1559    /// `dictStart`: lowest valid extDict byte, as a buffer position.
1560    dict_start_pos: usize,
1561    /// `dictEnd`: one-past-the-end of the extDict, as a buffer position.
1562    dict_end_pos: usize,
1563    prefix_start_pos: usize,
1564    iend_pos: usize,
1565    ilimit: usize,
1566}
1567
1568impl ExtDictView {
1569    fn pos_p(&self, idx: usize) -> usize {
1570        idx - self.seg_bias
1571    }
1572    /// Buffer position of `idx`, resolved through the segment the C code
1573    /// would pick (`idx < prefixStartIndex ? dictBase : base`).
1574    fn pos_seg(&self, idx: usize) -> usize {
1575        if idx < self.prefix_start_index {
1576            idx - self.dict_bias
1577        } else {
1578            idx - self.seg_bias
1579        }
1580    }
1581    fn match_end_pos(&self, idx: usize) -> usize {
1582        if idx < self.prefix_start_index {
1583            self.dict_end_pos
1584        } else {
1585            self.iend_pos
1586        }
1587    }
1588}
1589
1590/// `ZSTD_compressBlock_fast_extDict_generic`: the fast matcher over a
1591/// two-segment window — matches may start in the extDict (the pre-wrap
1592/// streaming history) and run across the seam into the prefix.
1593fn compress_block_fast_extdict(
1594    ctx: &mut FastCtx,
1595    store: &mut SeqStore,
1596    rep: &mut [u32; 3],
1597    data: &[u8],
1598    block_start: usize,
1599    block_end: usize,
1600    win: &Window,
1601) -> usize {
1602    let src_size = block_end - block_start;
1603    let hlog = ctx.hlog;
1604    let mls = ctx.mls;
1605    let step_size = ctx.step_size;
1606    let k_step_incr: usize = 1 << (K_SEARCH_STRENGTH - 1);
1607
1608    let seg_bias = win.seg_bias as usize;
1609    let istart = block_start + seg_bias;
1610    let iend = block_end + seg_bias;
1611    let end_index = iend;
1612
1613    // ZSTD_getLowestMatchIndex(ms, endIndex, windowLog): lowest valid index
1614    // in either segment. With a loaded dictionary (`loadedDictEnd != 0`) the
1615    // whole dict stays referenceable down to `lowLimit` regardless of maxDist;
1616    // the windowed floor applies only without one.
1617    let max_distance = 1usize << ctx.window_log;
1618    let lowest_valid = win.low_limit as usize;
1619    let dict_start_index = if win.loaded_dict_end != 0 {
1620        lowest_valid
1621    } else if end_index - lowest_valid > max_distance {
1622        end_index - max_distance
1623    } else {
1624        lowest_valid
1625    };
1626    let dict_limit = win.dict_limit as usize;
1627    let prefix_start_index = dict_start_index.max(dict_limit);
1628
1629    // Switch to the "regular" variant if extDict is invalidated by maxDistance.
1630    if prefix_start_index == dict_start_index {
1631        return compress_block_fast(
1632            ctx,
1633            store,
1634            rep,
1635            data,
1636            block_start,
1637            block_end,
1638            seg_bias,
1639            dict_limit,
1640        );
1641    }
1642    if src_size < HASH_READ_SIZE {
1643        return src_size;
1644    }
1645
1646    let w = ExtDictView {
1647        seg_bias,
1648        dict_bias: win.dict_bias as usize,
1649        prefix_start_index,
1650        dict_start_pos: dict_start_index - win.dict_bias as usize,
1651        dict_end_pos: prefix_start_index - win.dict_bias as usize,
1652        prefix_start_pos: prefix_start_index - seg_bias,
1653        iend_pos: block_end,
1654        ilimit: iend - HASH_READ_SIZE,
1655    };
1656
1657    let mut anchor = istart;
1658    let mut ip0 = istart;
1659    let mut ip1: usize;
1660    let mut ip2: usize;
1661    let mut ip3: usize;
1662    let mut current0: u32;
1663
1664    let mut offset_1 = rep[0];
1665    let mut offset_2 = rep[1];
1666    let mut offset_saved1 = 0u32;
1667    let mut offset_saved2 = 0u32;
1668
1669    // No first-position skip here; note `>=` where the noDict variant uses `>`.
1670    {
1671        let curr = ip0 as u32;
1672        let max_rep = curr - dict_start_index as u32;
1673        if offset_2 >= max_rep {
1674            offset_saved2 = offset_2;
1675            offset_2 = 0;
1676        }
1677        if offset_1 >= max_rep {
1678            offset_saved1 = offset_1;
1679            offset_1 = 0;
1680        }
1681    }
1682
1683    let mut hash0: usize;
1684    let mut hash1: usize;
1685    let mut idx: u32;
1686
1687    let mut step: usize;
1688    let mut next_step: usize;
1689
1690    'outer: loop {
1691        // _start:
1692        step = step_size;
1693        next_step = ip0 + k_step_incr;
1694        ip1 = ip0 + 1;
1695        ip2 = ip0 + step;
1696        ip3 = ip2 + 1;
1697
1698        if ip3 >= w.ilimit {
1699            break 'outer;
1700        }
1701
1702        hash0 = hash_ptr(data, w.pos_p(ip0), hlog, mls);
1703        hash1 = hash_ptr(data, w.pos_p(ip1), hlog, mls);
1704        idx = ctx.hash_table[hash0];
1705
1706        loop {
1707            // Load the repcode match for ip[2]. The `(U32)(prefixStartIndex -
1708            // repIndex) >= 4` intentional-underflow test admits prefix-side
1709            // repcodes outright and dict-side ones only when the 4-byte read
1710            // stays below the seam.
1711            let current2 = ip2;
1712            let rep_index = current2.wrapping_sub(offset_1 as usize);
1713            let rval = if (prefix_start_index as u32).wrapping_sub(rep_index as u32) >= 4
1714                && offset_1 > 0
1715            {
1716                read32(data, w.pos_seg(rep_index))
1717            } else {
1718                read32(data, w.pos_p(ip2)) ^ 1 // guaranteed to not match
1719            };
1720
1721            // Write back hash table entry.
1722            current0 = ip0 as u32;
1723            ctx.hash_table[hash0] = current0;
1724
1725            // Check repcode at ip[2].
1726            if read32(data, w.pos_p(ip2)) == rval {
1727                ip0 = ip2;
1728                let mut match0 = rep_index;
1729                let match_seg_pos = w.pos_seg(match0);
1730                let ext = (data[w.pos_p(ip0) - 1] == data[match_seg_pos - 1]) as usize;
1731                let mut m_length = ext;
1732                ip0 -= ext;
1733                match0 -= ext;
1734                m_length += 4;
1735                extdict_match_tail(
1736                    ctx,
1737                    store,
1738                    data,
1739                    &mut ip0,
1740                    &mut anchor,
1741                    current0,
1742                    &mut offset_1,
1743                    &mut offset_2,
1744                    m_length,
1745                    1, // REPCODE1_TO_OFFBASE
1746                    match0,
1747                    hash1,
1748                    ip1,
1749                    &w,
1750                );
1751                continue 'outer;
1752            }
1753
1754            {
1755                // Load + check match for ip[0] (validity vs dictStartIndex,
1756                // segment select vs prefixStartIndex, as in C).
1757                let mval = if idx as usize >= dict_start_index {
1758                    read32(data, w.pos_seg(idx as usize))
1759                } else {
1760                    read32(data, w.pos_p(ip0)) ^ 1
1761                };
1762                if read32(data, w.pos_p(ip0)) == mval {
1763                    extdict_offset_and_match(
1764                        ctx,
1765                        store,
1766                        data,
1767                        &mut ip0,
1768                        &mut anchor,
1769                        current0,
1770                        &mut offset_1,
1771                        &mut offset_2,
1772                        idx,
1773                        hash1,
1774                        ip1,
1775                        &w,
1776                    );
1777                    continue 'outer;
1778                }
1779            }
1780
1781            // Lookup ip[1], hash ip[2], advance.
1782            idx = ctx.hash_table[hash1];
1783            hash0 = hash1;
1784            hash1 = hash_ptr(data, w.pos_p(ip2), hlog, mls);
1785            ip0 = ip1;
1786            ip1 = ip2;
1787            ip2 = ip3;
1788
1789            current0 = ip0 as u32;
1790            ctx.hash_table[hash0] = current0;
1791
1792            {
1793                let mval = if idx as usize >= dict_start_index {
1794                    read32(data, w.pos_seg(idx as usize))
1795                } else {
1796                    read32(data, w.pos_p(ip0)) ^ 1
1797                };
1798                if read32(data, w.pos_p(ip0)) == mval {
1799                    extdict_offset_and_match(
1800                        ctx,
1801                        store,
1802                        data,
1803                        &mut ip0,
1804                        &mut anchor,
1805                        current0,
1806                        &mut offset_1,
1807                        &mut offset_2,
1808                        idx,
1809                        hash1,
1810                        ip1,
1811                        &w,
1812                    );
1813                    continue 'outer;
1814                }
1815            }
1816
1817            idx = ctx.hash_table[hash1];
1818            hash0 = hash1;
1819            hash1 = hash_ptr(data, w.pos_p(ip2), hlog, mls);
1820            ip0 = ip1;
1821            ip1 = ip2;
1822            ip2 = ip0 + step;
1823            ip3 = ip1 + step;
1824
1825            if ip2 >= next_step {
1826                step += 1;
1827                next_step += k_step_incr;
1828            }
1829            if ip3 >= w.ilimit {
1830                break 'outer;
1831            }
1832        }
1833    }
1834
1835    // _cleanup: same saved-offset rotation as the noDict variant.
1836    offset_saved2 = if offset_saved1 != 0 && offset_1 != 0 {
1837        offset_saved1
1838    } else {
1839        offset_saved2
1840    };
1841    rep[0] = if offset_1 != 0 {
1842        offset_1
1843    } else {
1844        offset_saved1
1845    };
1846    rep[1] = if offset_2 != 0 {
1847        offset_2
1848    } else {
1849        offset_saved2
1850    };
1851
1852    iend - anchor
1853}
1854
1855/// The `_offset` tail: compute the offset from `idx`, extend backward within
1856/// the match's segment, then fall into the shared `_match` tail.
1857#[allow(clippy::too_many_arguments)]
1858fn extdict_offset_and_match(
1859    ctx: &mut FastCtx,
1860    store: &mut SeqStore,
1861    data: &[u8],
1862    ip0: &mut usize,
1863    anchor: &mut usize,
1864    current0: u32,
1865    offset_1: &mut u32,
1866    offset_2: &mut u32,
1867    idx: u32,
1868    hash1: usize,
1869    ip1: usize,
1870    w: &ExtDictView,
1871) {
1872    let offset = current0 - idx;
1873    let mut match0 = idx as usize;
1874    let in_dict = match0 < w.prefix_start_index;
1875    let low_match_pos = if in_dict {
1876        w.dict_start_pos
1877    } else {
1878        w.prefix_start_pos
1879    };
1880    let match_bias = if in_dict { w.dict_bias } else { w.seg_bias };
1881
1882    *offset_2 = *offset_1;
1883    *offset_1 = offset;
1884    let offcode = offset + 3; // OFFSET_TO_OFFBASE
1885    let mut m_length = 4usize;
1886
1887    // Count the backwards match length (bounded by the match's segment).
1888    while *ip0 > *anchor
1889        && (match0 - match_bias) > low_match_pos
1890        && data[w.pos_p(*ip0) - 1] == data[match0 - match_bias - 1]
1891    {
1892        *ip0 -= 1;
1893        match0 -= 1;
1894        m_length += 1;
1895    }
1896
1897    extdict_match_tail(
1898        ctx, store, data, ip0, anchor, current0, offset_1, offset_2, m_length, offcode, match0,
1899        hash1, ip1, w,
1900    );
1901}
1902
1903/// The `_match` tail: two-segment forward count, sequence store, the
1904/// conditional ip1 hash write-back, table fill, and the immediate-repcode
1905/// loop with two-segment reads.
1906#[allow(clippy::too_many_arguments)]
1907fn extdict_match_tail(
1908    ctx: &mut FastCtx,
1909    store: &mut SeqStore,
1910    data: &[u8],
1911    ip0: &mut usize,
1912    anchor: &mut usize,
1913    current0: u32,
1914    offset_1: &mut u32,
1915    offset_2: &mut u32,
1916    mut m_length: usize,
1917    offcode: u32,
1918    match0: usize,
1919    hash1: usize,
1920    ip1: usize,
1921    w: &ExtDictView,
1922) {
1923    let hlog = ctx.hlog;
1924    let mls = ctx.mls;
1925
1926    // Count the forward length across the dict/prefix seam.
1927    m_length += count_2segments(
1928        data,
1929        w.pos_p(*ip0) + m_length,
1930        w.pos_seg(match0) + m_length,
1931        w.iend_pos,
1932        w.match_end_pos(match0),
1933        w.prefix_start_pos,
1934    );
1935    store.store_seq(
1936        &data[w.pos_p(*anchor)..w.pos_p(*ip0)],
1937        offcode,
1938        m_length as u32,
1939    );
1940    *ip0 += m_length;
1941    *anchor = *ip0;
1942
1943    // Write next hash table entry.
1944    if ip1 < *ip0 {
1945        ctx.hash_table[hash1] = ip1 as u32;
1946    }
1947
1948    // Fill table and check for immediate repcode.
1949    if *ip0 <= w.ilimit {
1950        let c02 = current0 as usize + 2;
1951        let h = hash_ptr(data, w.pos_p(c02), hlog, mls);
1952        ctx.hash_table[h] = c02 as u32;
1953        let h2 = hash_ptr(data, w.pos_p(*ip0 - 2), hlog, mls);
1954        ctx.hash_table[h2] = (*ip0 - 2) as u32;
1955
1956        while *ip0 <= w.ilimit {
1957            let rep_index2 = ip0.wrapping_sub(*offset_2 as usize);
1958            // ZSTD_index_overlap_check: repIndex2 must not straddle the seam.
1959            let no_overlap = (w.prefix_start_index as u32)
1960                .wrapping_sub(1)
1961                .wrapping_sub(rep_index2 as u32)
1962                >= 3;
1963            if !(no_overlap && *offset_2 > 0) {
1964                break;
1965            }
1966            let rep_match2_pos = w.pos_seg(rep_index2);
1967            if read32(data, rep_match2_pos) != read32(data, w.pos_p(*ip0)) {
1968                break;
1969            }
1970            let rep_length2 = count_2segments(
1971                data,
1972                w.pos_p(*ip0) + 4,
1973                rep_match2_pos + 4,
1974                w.iend_pos,
1975                w.match_end_pos(rep_index2),
1976                w.prefix_start_pos,
1977            ) + 4;
1978            std::mem::swap(offset_1, offset_2);
1979            store.store_seq(&[], 1, rep_length2 as u32);
1980            let h = hash_ptr(data, w.pos_p(*ip0), hlog, mls);
1981            ctx.hash_table[h] = *ip0 as u32;
1982            *ip0 += rep_length2;
1983            *anchor = *ip0;
1984        }
1985    }
1986}
1987
1988// --- The ZSTD_dfast match finder ----------------------------------------------------
1989
1990/// Double-fast keeps two tables: a long one hashing 8 bytes (`hashLog` bits)
1991/// and a short one hashing `mls` bytes (`chainLog` bits). Indices are biased
1992/// by [`WINDOW_START_INDEX`] like the fast matcher's.
1993struct DfastCtx {
1994    hash_long: Vec<u32>,
1995    hash_small: Vec<u32>,
1996    hlog_l: u32,
1997    hlog_s: u32,
1998    mls: u32,
1999    window_log: u32,
2000}
2001
2002impl DfastCtx {
2003    fn new(cparams: &CParams) -> Self {
2004        DfastCtx {
2005            hash_long: vec![0u32; 1usize << cparams.hash_log],
2006            hash_small: vec![0u32; 1usize << cparams.chain_log],
2007            hlog_l: cparams.hash_log,
2008            hlog_s: cparams.chain_log,
2009            mls: cparams.min_match.clamp(4, 7),
2010            window_log: cparams.window_log,
2011        }
2012    }
2013
2014    /// `ZSTD_reduceIndex` for dfast: the long (hash) and short (chain) tables.
2015    fn reduce_indices(&mut self, correction: u32) {
2016        reduce_table(&mut self.hash_long, correction, false);
2017        reduce_table(&mut self.hash_small, correction, false);
2018    }
2019}
2020
2021/// `ZSTD_fillDoubleHashTableForCCtx` (dfast, `dtlm_fast`, untagged): seed both
2022/// dict tables from `data[0..dict_len]` before compressing `src`. Each step-3
2023/// position is inserted into the short table (`chainLog` bits, `mls`) and the
2024/// long table (`hashLog` bits, `mls == 8`), as a biased index. With `dtlm_fast`
2025/// only the `i == 0` position of each step is loaded, so the loop bound is the
2026/// same as the single-hash fill: `ip + 9 < dict_len` (C's `ip + 2 <= dictEnd -
2027/// HASH_READ_SIZE`). The caller guarantees `dict_len > HASH_READ_SIZE`.
2028fn fill_dfast_hash_tables_for_cctx(ctx: &mut DfastCtx, data: &[u8], dict_len: usize) {
2029    let hlog_l = ctx.hlog_l;
2030    let hlog_s = ctx.hlog_s;
2031    let mls = ctx.mls;
2032    let mut ip = 0usize;
2033    while ip + 9 < dict_len {
2034        let curr = (ip + WINDOW_START_INDEX) as u32;
2035        ctx.hash_small[hash_ptr(data, ip, hlog_s, mls)] = curr;
2036        ctx.hash_long[hash_ptr(data, ip, hlog_l, 8)] = curr;
2037        ip += 3;
2038    }
2039}
2040
2041/// `ZSTD_fillDoubleHashTableForCDict` (dfast, `dtlm_full`, **tagged**): the short
2042/// table gets the `i == 0` position of each step; the long table gets `i == 0`
2043/// plus the `i == 1, 2` positions when their bucket is still empty. Both tables
2044/// are addressed with `+ SHORT_CACHE_TAG_BITS` extra bits and store tagged
2045/// indices. Caller sizes them `1 << hlog_l` / `1 << hlog_s`.
2046#[allow(clippy::too_many_arguments)]
2047fn fill_dfast_hash_tables_for_cdict(
2048    hash_long: &mut [u32],
2049    hash_small: &mut [u32],
2050    data: &[u8],
2051    dict_len: usize,
2052    hlog_l: u32,
2053    hlog_s: u32,
2054    mls: u32,
2055) {
2056    let h_bits_l = hlog_l + SHORT_CACHE_TAG_BITS;
2057    let h_bits_s = hlog_s + SHORT_CACHE_TAG_BITS;
2058    let mut ip = 0usize;
2059    while ip + 9 < dict_len {
2060        let curr = (ip + WINDOW_START_INDEX) as u32;
2061        for i in 0..3usize {
2062            let sm = hash_ptr(data, ip + i, h_bits_s, mls);
2063            let lg = hash_ptr(data, ip + i, h_bits_l, 8);
2064            if i == 0 {
2065                write_tagged_index(hash_small, sm, curr + i as u32);
2066            }
2067            if i == 0 || hash_long[lg >> SHORT_CACHE_TAG_BITS] == 0 {
2068                write_tagged_index(hash_long, lg, curr + i as u32);
2069            }
2070        }
2071        ip += 3;
2072    }
2073}
2074
2075/// `ZSTD_fillDoubleHashTableForCCtx` with `dtlm_full` (untagged): the de-tagged
2076/// form of a dfast CDict, used by the copy path.
2077fn fill_dfast_hash_tables_for_cctx_full(ctx: &mut DfastCtx, data: &[u8], dict_len: usize) {
2078    let hlog_l = ctx.hlog_l;
2079    let hlog_s = ctx.hlog_s;
2080    let mls = ctx.mls;
2081    let mut ip = 0usize;
2082    while ip + 9 < dict_len {
2083        let curr = (ip + WINDOW_START_INDEX) as u32;
2084        for i in 0..3usize {
2085            let sm = hash_ptr(data, ip + i, hlog_s, mls);
2086            let lg = hash_ptr(data, ip + i, hlog_l, 8);
2087            if i == 0 {
2088                ctx.hash_small[sm] = curr + i as u32;
2089            }
2090            if i == 0 || ctx.hash_long[lg] == 0 {
2091                ctx.hash_long[lg] = curr + i as u32;
2092            }
2093        }
2094        ip += 3;
2095    }
2096}
2097
2098/// `ZSTD_compressBlock_doubleFast_dictMatchState_generic`: the dfast CDict
2099/// **attach** match finder — the working context's own long+short tables plus
2100/// the attached CDict's tagged long+short tables, in the `content ++ src` buffer
2101/// (`dictIndexDelta == 0`). `data`/`block_start`/`block_end`/`content_len` as in
2102/// [`compress_block_fast_dict_match_state`]; `dict_hash_long`/`dict_hash_small`
2103/// are the CDict's tagged tables with base logs `dict_hlog_l`/`dict_hlog_s`.
2104#[allow(clippy::too_many_arguments)]
2105fn compress_block_dfast_dict_match_state(
2106    ctx: &mut DfastCtx,
2107    store: &mut SeqStore,
2108    rep: &mut [u32; 3],
2109    data: &[u8],
2110    block_start: usize,
2111    block_end: usize,
2112    content_len: usize,
2113    dict_hash_long: &[u32],
2114    dict_hash_small: &[u32],
2115    dict_hlog_l: u32,
2116    dict_hlog_s: u32,
2117) -> usize {
2118    let bias = WINDOW_START_INDEX;
2119    let hlog_l = ctx.hlog_l;
2120    let hlog_s = ctx.hlog_s;
2121    let mls = ctx.mls;
2122    let dict_h_bits_l = dict_hlog_l + SHORT_CACHE_TAG_BITS;
2123    let dict_h_bits_s = dict_hlog_s + SHORT_CACHE_TAG_BITS;
2124    let tag_mask = (1u32 << SHORT_CACHE_TAG_BITS) - 1;
2125
2126    let prefix_lowest_index = (bias + content_len) as u32;
2127    let dict_start_index = bias as u32;
2128    let prefix_start_pos = content_len;
2129    let dict_end_pos = content_len;
2130    let iend_pos = block_end;
2131
2132    let iend = block_end + bias;
2133    if block_end - block_start < HASH_READ_SIZE {
2134        return block_end - block_start;
2135    }
2136    let ilimit = iend - HASH_READ_SIZE;
2137    let istart = block_start + bias;
2138    let dict_and_prefix_length = (istart - prefix_lowest_index as usize) + content_len;
2139
2140    let mut offset_1 = rep[0];
2141    let mut offset_2 = rep[1];
2142
2143    let mut anchor = istart;
2144    let mut ip = istart + (dict_and_prefix_length == 0) as usize;
2145
2146    'outer: while ip < ilimit {
2147        let curr = ip as u32;
2148        let h2 = hash_ptr(data, ip - bias, hlog_l, 8);
2149        let h = hash_ptr(data, ip - bias, hlog_s, mls);
2150        let dict_hat_l = hash_ptr(data, ip - bias, dict_h_bits_l, 8);
2151        let dict_hat_s = hash_ptr(data, ip - bias, dict_h_bits_s, mls);
2152        let dict_idx_tag_l = dict_hash_long[dict_hat_l >> SHORT_CACHE_TAG_BITS];
2153        let dict_idx_tag_s = dict_hash_small[dict_hat_s >> SHORT_CACHE_TAG_BITS];
2154        let dict_tags_match_l = (dict_idx_tag_l & tag_mask) == (dict_hat_l as u32 & tag_mask);
2155        let dict_tags_match_s = (dict_idx_tag_s & tag_mask) == (dict_hat_s as u32 & tag_mask);
2156        let match_index_l = ctx.hash_long[h2];
2157        let mut match_index_s = ctx.hash_small[h];
2158        let rep_index = curr + 1 - offset_1;
2159        ctx.hash_long[h2] = curr;
2160        ctx.hash_small[h] = curr;
2161
2162        let m_length: usize;
2163
2164        if index_overlap_check(prefix_lowest_index, rep_index)
2165            && read32(data, rep_index as usize - bias) == read32(data, (ip + 1) - bias)
2166        {
2167            // repcode
2168            let mend = if rep_index < prefix_lowest_index {
2169                dict_end_pos
2170            } else {
2171                iend_pos
2172            };
2173            m_length = 4 + count_2segments(
2174                data,
2175                (ip + 1 - bias) + 4,
2176                (rep_index as usize - bias) + 4,
2177                iend_pos,
2178                mend,
2179                prefix_start_pos,
2180            );
2181            ip += 1;
2182            store.store_seq(&data[anchor - bias..ip - bias], 1, m_length as u32);
2183        } else {
2184            // long / short match cascade. The labeled block yields
2185            // (offset, matchLength) with `ip` already rewound by the catch-up,
2186            // or `continue`s the outer loop when nothing is found.
2187            let (offset, ml) = 'found: {
2188                // prefix long match
2189                if match_index_l >= prefix_lowest_index
2190                    && read64(data, match_index_l as usize - bias) == read64(data, ip - bias)
2191                {
2192                    let mut ml = 8 + count_eq(
2193                        data,
2194                        (ip - bias) + 8,
2195                        (match_index_l as usize - bias) + 8,
2196                        iend_pos,
2197                    );
2198                    let off = curr - match_index_l;
2199                    let mut m = match_index_l as usize;
2200                    while ip > anchor
2201                        && m > prefix_lowest_index as usize
2202                        && data[(ip - bias) - 1] == data[(m - bias) - 1]
2203                    {
2204                        ip -= 1;
2205                        m -= 1;
2206                        ml += 1;
2207                    }
2208                    break 'found (off, ml);
2209                }
2210                // dict long match
2211                if dict_tags_match_l {
2212                    let dml = dict_idx_tag_l >> SHORT_CACHE_TAG_BITS;
2213                    if dml > dict_start_index
2214                        && read64(data, dml as usize - bias) == read64(data, ip - bias)
2215                    {
2216                        let mut ml = 8 + count_2segments(
2217                            data,
2218                            (ip - bias) + 8,
2219                            (dml as usize - bias) + 8,
2220                            iend_pos,
2221                            dict_end_pos,
2222                            prefix_start_pos,
2223                        );
2224                        let off = curr - dml;
2225                        let mut dm = dml as usize;
2226                        while ip > anchor
2227                            && dm > dict_start_index as usize
2228                            && data[(ip - bias) - 1] == data[(dm - bias) - 1]
2229                        {
2230                            ip -= 1;
2231                            dm -= 1;
2232                            ml += 1;
2233                        }
2234                        break 'found (off, ml);
2235                    }
2236                }
2237
2238                // short candidate -> search a long match at ip+1; else give up.
2239                let short_found = if match_index_s > prefix_lowest_index {
2240                    read32(data, match_index_s as usize - bias) == read32(data, ip - bias)
2241                } else if dict_tags_match_s {
2242                    let dms = dict_idx_tag_s >> SHORT_CACHE_TAG_BITS;
2243                    match_index_s = dms;
2244                    dms > dict_start_index
2245                        && read32(data, dms as usize - bias) == read32(data, ip - bias)
2246                } else {
2247                    false
2248                };
2249                if !short_found {
2250                    ip += ((ip - anchor) >> K_SEARCH_STRENGTH) + 1;
2251                    continue 'outer;
2252                }
2253
2254                // _search_next_long
2255                let hl3 = hash_ptr(data, (ip + 1) - bias, hlog_l, 8);
2256                let dict_hat_l3 = hash_ptr(data, (ip + 1) - bias, dict_h_bits_l, 8);
2257                let match_index_l3 = ctx.hash_long[hl3];
2258                let dict_idx_tag_l3 = dict_hash_long[dict_hat_l3 >> SHORT_CACHE_TAG_BITS];
2259                let dict_tags_match_l3 =
2260                    (dict_idx_tag_l3 & tag_mask) == (dict_hat_l3 as u32 & tag_mask);
2261                ctx.hash_long[hl3] = curr + 1;
2262
2263                // prefix long +1 match
2264                if match_index_l3 >= prefix_lowest_index
2265                    && read64(data, match_index_l3 as usize - bias) == read64(data, (ip + 1) - bias)
2266                {
2267                    let mut ml = 8 + count_eq(
2268                        data,
2269                        (ip + 1 - bias) + 8,
2270                        (match_index_l3 as usize - bias) + 8,
2271                        iend_pos,
2272                    );
2273                    ip += 1;
2274                    let off = (curr + 1) - match_index_l3;
2275                    let mut m = match_index_l3 as usize;
2276                    while ip > anchor
2277                        && m > prefix_lowest_index as usize
2278                        && data[(ip - bias) - 1] == data[(m - bias) - 1]
2279                    {
2280                        ip -= 1;
2281                        m -= 1;
2282                        ml += 1;
2283                    }
2284                    break 'found (off, ml);
2285                }
2286                // dict long +1 match
2287                if dict_tags_match_l3 {
2288                    let dml3 = dict_idx_tag_l3 >> SHORT_CACHE_TAG_BITS;
2289                    if dml3 > dict_start_index
2290                        && read64(data, dml3 as usize - bias) == read64(data, (ip + 1) - bias)
2291                    {
2292                        let mut ml = 8 + count_2segments(
2293                            data,
2294                            (ip + 1 - bias) + 8,
2295                            (dml3 as usize - bias) + 8,
2296                            iend_pos,
2297                            dict_end_pos,
2298                            prefix_start_pos,
2299                        );
2300                        ip += 1;
2301                        let off = (curr + 1) - dml3;
2302                        let mut dm = dml3 as usize;
2303                        while ip > anchor
2304                            && dm > dict_start_index as usize
2305                            && data[(ip - bias) - 1] == data[(dm - bias) - 1]
2306                        {
2307                            ip -= 1;
2308                            dm -= 1;
2309                            ml += 1;
2310                        }
2311                        break 'found (off, ml);
2312                    }
2313                }
2314
2315                // No long +1 match: take the short match found earlier.
2316                if match_index_s < prefix_lowest_index {
2317                    let mut ml = 4 + count_2segments(
2318                        data,
2319                        (ip - bias) + 4,
2320                        (match_index_s as usize - bias) + 4,
2321                        iend_pos,
2322                        dict_end_pos,
2323                        prefix_start_pos,
2324                    );
2325                    let off = curr - match_index_s;
2326                    let mut m = match_index_s as usize;
2327                    while ip > anchor
2328                        && m > dict_start_index as usize
2329                        && data[(ip - bias) - 1] == data[(m - bias) - 1]
2330                    {
2331                        ip -= 1;
2332                        m -= 1;
2333                        ml += 1;
2334                    }
2335                    (off, ml)
2336                } else {
2337                    let mut ml = 4 + count_eq(
2338                        data,
2339                        (ip - bias) + 4,
2340                        (match_index_s as usize - bias) + 4,
2341                        iend_pos,
2342                    );
2343                    let off = curr - match_index_s;
2344                    let mut m = match_index_s as usize;
2345                    while ip > anchor
2346                        && m > prefix_lowest_index as usize
2347                        && data[(ip - bias) - 1] == data[(m - bias) - 1]
2348                    {
2349                        ip -= 1;
2350                        m -= 1;
2351                        ml += 1;
2352                    }
2353                    (off, ml)
2354                }
2355            };
2356
2357            // _match_found
2358            offset_2 = offset_1;
2359            offset_1 = offset;
2360            store.store_seq(&data[anchor - bias..ip - bias], offset + 3, ml as u32);
2361            m_length = ml;
2362        }
2363
2364        // _match_stored
2365        ip += m_length;
2366        anchor = ip;
2367        if ip <= ilimit {
2368            // Complementary insertion (candidates could be > iend-8).
2369            let index_to_insert = curr + 2;
2370            ctx.hash_long[hash_ptr(data, index_to_insert as usize - bias, hlog_l, 8)] =
2371                index_to_insert;
2372            ctx.hash_long[hash_ptr(data, (ip - 2) - bias, hlog_l, 8)] = (ip - 2) as u32;
2373            ctx.hash_small[hash_ptr(data, index_to_insert as usize - bias, hlog_s, mls)] =
2374                index_to_insert;
2375            ctx.hash_small[hash_ptr(data, (ip - 1) - bias, hlog_s, mls)] = (ip - 1) as u32;
2376
2377            while ip <= ilimit {
2378                let current2 = ip as u32;
2379                let rep_index2 = current2.wrapping_sub(offset_2);
2380                if index_overlap_check(prefix_lowest_index, rep_index2)
2381                    && read32(data, rep_index2 as usize - bias) == read32(data, ip - bias)
2382                {
2383                    let rep_end2 = if rep_index2 < prefix_lowest_index {
2384                        dict_end_pos
2385                    } else {
2386                        iend_pos
2387                    };
2388                    let rep_length2 = 4 + count_2segments(
2389                        data,
2390                        (ip - bias) + 4,
2391                        (rep_index2 as usize - bias) + 4,
2392                        iend_pos,
2393                        rep_end2,
2394                        prefix_start_pos,
2395                    );
2396                    std::mem::swap(&mut offset_1, &mut offset_2);
2397                    store.store_seq(&data[anchor - bias..ip - bias], 1, rep_length2 as u32);
2398                    ctx.hash_small[hash_ptr(data, ip - bias, hlog_s, mls)] = current2;
2399                    ctx.hash_long[hash_ptr(data, ip - bias, hlog_l, 8)] = current2;
2400                    ip += rep_length2;
2401                    anchor = ip;
2402                } else {
2403                    break;
2404                }
2405            }
2406        }
2407    }
2408
2409    rep[0] = offset_1;
2410    rep[1] = offset_2;
2411    iend_pos - (anchor - bias)
2412}
2413
2414/// `ZSTD_compressBlock_doubleFast` (noDict path). Same conventions as
2415/// [`compress_block_fast`]: biased indices, sequences into `store`, returns the
2416/// trailing-literals size, and `seg_bias` / `lowest_valid` map positions to
2417/// window indices after a streaming wrap (both are 2 for a frame's first
2418/// segment). The `ZSTD_selectAddr`/dummy-pointer constructs in C
2419/// are branchless forms of plain `idx >= prefixLowestIndex` validity tests
2420/// (the `+1` long probe is strictly `>`), which is how they appear here.
2421#[allow(clippy::too_many_arguments)]
2422fn compress_block_dfast(
2423    ctx: &mut DfastCtx,
2424    store: &mut SeqStore,
2425    rep: &mut [u32; 3],
2426    data: &[u8],
2427    block_start: usize,
2428    block_end: usize,
2429    seg_bias: usize,
2430    lowest_valid: usize,
2431) -> usize {
2432    let src_size = block_end - block_start;
2433    let hlog_l = ctx.hlog_l;
2434    let hlog_s = ctx.hlog_s;
2435    let mls = ctx.mls;
2436
2437    // `ZSTD_getLowestPrefixIndex(ms, endIndex, windowLog)`.
2438    let max_distance = 1usize << ctx.window_log;
2439    let end_index = block_end + seg_bias;
2440    let prefix_start_index = if end_index - lowest_valid > max_distance {
2441        end_index - max_distance
2442    } else {
2443        lowest_valid
2444    };
2445    // kStepIncr = 1 << kSearchStrength (256) — twice the fast matcher's.
2446    let k_step_incr: usize = 1 << K_SEARCH_STRENGTH;
2447
2448    let bias = seg_bias;
2449    let to_pos = |idx: usize| idx - bias;
2450    let istart = block_start + bias;
2451    let iend = block_end + bias;
2452    if src_size < HASH_READ_SIZE {
2453        return src_size;
2454    }
2455    let ilimit = iend - HASH_READ_SIZE;
2456
2457    let mut anchor = istart;
2458    let mut ip = istart;
2459    let mut ip1: usize;
2460
2461    let mut offset_1 = rep[0];
2462    let mut offset_2 = rep[1];
2463    let mut offset_saved1 = 0u32;
2464    let mut offset_saved2 = 0u32;
2465
2466    // init: skip the very first window position.
2467    ip += (ip == prefix_start_index) as usize;
2468    {
2469        // `ZSTD_getLowestPrefixIndex(ms, current, windowLog)` at the *block
2470        // start* (not prefix_start_index, which derives from the block end):
2471        // when the window slides, maxRep spans the full window.
2472        let window_low = if ip - lowest_valid > max_distance {
2473            ip - max_distance
2474        } else {
2475            lowest_valid
2476        };
2477        let max_rep = (ip - window_low) as u32;
2478        if offset_2 > max_rep {
2479            offset_saved2 = offset_2;
2480            offset_2 = 0;
2481        }
2482        if offset_1 > max_rep {
2483            offset_saved1 = offset_1;
2484            offset_1 = 0;
2485        }
2486    }
2487
2488    'outer: loop {
2489        let mut step = 1usize;
2490        let mut next_step = ip + k_step_incr;
2491        ip1 = ip + step;
2492
2493        if ip1 > ilimit {
2494            break 'outer;
2495        }
2496
2497        let mut hl0 = hash_ptr(data, to_pos(ip), hlog_l, 8);
2498        let mut idxl0 = ctx.hash_long[hl0];
2499
2500        loop {
2501            let hs0 = hash_ptr(data, to_pos(ip), hlog_s, mls);
2502            let idxs0 = ctx.hash_small[hs0];
2503            let curr = ip;
2504
2505            ctx.hash_long[hl0] = curr as u32;
2506            ctx.hash_small[hs0] = curr as u32;
2507
2508            // Sequence bookkeeping shared by every match exit.
2509            let m_length: usize;
2510            let m_ip: usize;
2511            let hl1_for_writeback: Option<(usize, usize)>; // (hl1, ip1) when step < 4
2512
2513            // Check noDict repcode at ip+1.
2514            if offset_1 > 0
2515                && read32(data, to_pos(ip + 1 - offset_1 as usize)) == read32(data, to_pos(ip + 1))
2516            {
2517                let len = count_eq(
2518                    data,
2519                    to_pos(ip + 1) + 4,
2520                    to_pos(ip + 1 - offset_1 as usize) + 4,
2521                    to_pos(iend),
2522                ) + 4;
2523                let seq_ip = ip + 1;
2524                store.store_seq(&data[to_pos(anchor)..to_pos(seq_ip)], 1, len as u32);
2525                m_length = len;
2526                m_ip = seq_ip;
2527                // (no hl1 write on the repcode path)
2528                ip = m_ip + m_length;
2529                anchor = ip;
2530                dfast_post_match(
2531                    ctx,
2532                    store,
2533                    data,
2534                    &mut ip,
2535                    &mut anchor,
2536                    curr,
2537                    &mut offset_1,
2538                    &mut offset_2,
2539                    ilimit,
2540                    iend,
2541                    bias,
2542                );
2543                continue 'outer;
2544            }
2545
2546            let hl1 = hash_ptr(data, to_pos(ip1), hlog_l, 8);
2547
2548            // Check prefix long match at ip (validity is `>=`, via selectAddr).
2549            if idxl0 as usize >= prefix_start_index
2550                && read64(data, to_pos(idxl0 as usize)) == read64(data, to_pos(ip))
2551            {
2552                let mut matchl0 = idxl0 as usize;
2553                let mut len = count_eq(data, to_pos(ip) + 8, to_pos(matchl0) + 8, to_pos(iend)) + 8;
2554                let offset = (ip - matchl0) as u32;
2555                while ip > anchor
2556                    && matchl0 > prefix_start_index
2557                    && data[to_pos(ip) - 1] == data[to_pos(matchl0) - 1]
2558                {
2559                    ip -= 1;
2560                    matchl0 -= 1;
2561                    len += 1;
2562                }
2563                m_length = len;
2564                m_ip = ip;
2565                hl1_for_writeback = if step < 4 { Some((hl1, ip1)) } else { None };
2566                dfast_match_found(
2567                    ctx,
2568                    store,
2569                    data,
2570                    &mut ip,
2571                    &mut anchor,
2572                    curr,
2573                    &mut offset_1,
2574                    &mut offset_2,
2575                    offset,
2576                    m_ip,
2577                    m_length,
2578                    hl1_for_writeback,
2579                    ilimit,
2580                    iend,
2581                    bias,
2582                );
2583                continue 'outer;
2584            }
2585
2586            let idxl1 = ctx.hash_long[hl1];
2587
2588            // Check prefix short match at ip (validity `>=`).
2589            if idxs0 as usize >= prefix_start_index
2590                && read32(data, to_pos(idxs0 as usize)) == read32(data, to_pos(ip))
2591            {
2592                // _search_next_long: extend the short match, then probe the
2593                // long table at ip1 for something better (validity strictly `>`).
2594                let mut matchs0 = idxs0 as usize;
2595                let mut len = count_eq(data, to_pos(ip) + 4, to_pos(matchs0) + 4, to_pos(iend)) + 4;
2596                let mut offset = (ip - matchs0) as u32;
2597
2598                if idxl1 as usize > prefix_start_index
2599                    && read64(data, to_pos(idxl1 as usize)) == read64(data, to_pos(ip1))
2600                {
2601                    let l1len = count_eq(
2602                        data,
2603                        to_pos(ip1) + 8,
2604                        to_pos(idxl1 as usize) + 8,
2605                        to_pos(iend),
2606                    ) + 8;
2607                    if l1len > len {
2608                        ip = ip1;
2609                        len = l1len;
2610                        offset = (ip - idxl1 as usize) as u32;
2611                        matchs0 = idxl1 as usize;
2612                    }
2613                }
2614
2615                while ip > anchor
2616                    && matchs0 > prefix_start_index
2617                    && data[to_pos(ip) - 1] == data[to_pos(matchs0) - 1]
2618                {
2619                    ip -= 1;
2620                    matchs0 -= 1;
2621                    len += 1;
2622                }
2623
2624                m_length = len;
2625                m_ip = ip;
2626                hl1_for_writeback = if step < 4 { Some((hl1, ip1)) } else { None };
2627                dfast_match_found(
2628                    ctx,
2629                    store,
2630                    data,
2631                    &mut ip,
2632                    &mut anchor,
2633                    curr,
2634                    &mut offset_1,
2635                    &mut offset_2,
2636                    offset,
2637                    m_ip,
2638                    m_length,
2639                    hl1_for_writeback,
2640                    ilimit,
2641                    iend,
2642                    bias,
2643                );
2644                continue 'outer;
2645            }
2646
2647            if ip1 >= next_step {
2648                step += 1;
2649                next_step += k_step_incr;
2650            }
2651            ip = ip1;
2652            ip1 += step;
2653
2654            hl0 = hl1;
2655            idxl0 = idxl1;
2656
2657            if ip1 > ilimit {
2658                break 'outer;
2659            }
2660        }
2661    }
2662
2663    // _cleanup: rotate restored offsets exactly as the fast matcher does.
2664    offset_saved2 = if offset_saved1 != 0 && offset_1 != 0 {
2665        offset_saved1
2666    } else {
2667        offset_saved2
2668    };
2669    rep[0] = if offset_1 != 0 {
2670        offset_1
2671    } else {
2672        offset_saved1
2673    };
2674    rep[1] = if offset_2 != 0 {
2675        offset_2
2676    } else {
2677        offset_saved2
2678    };
2679
2680    to_pos(iend) - to_pos(anchor)
2681}
2682
2683/// `_match_found` + `_match_stored` for an ordinary double-fast match: update
2684/// the offset history, optionally write back the ip1 long hash, store the
2685/// sequence, then run the shared post-match tail.
2686#[allow(clippy::too_many_arguments)]
2687fn dfast_match_found(
2688    ctx: &mut DfastCtx,
2689    store: &mut SeqStore,
2690    data: &[u8],
2691    ip: &mut usize,
2692    anchor: &mut usize,
2693    curr: usize,
2694    offset_1: &mut u32,
2695    offset_2: &mut u32,
2696    offset: u32,
2697    m_ip: usize,
2698    m_length: usize,
2699    hl1_writeback: Option<(usize, usize)>,
2700    ilimit: usize,
2701    iend: usize,
2702    bias: usize,
2703) {
2704    let to_pos = |idx: usize| idx - bias;
2705    *offset_2 = *offset_1;
2706    *offset_1 = offset;
2707
2708    if let Some((hl1, ip1)) = hl1_writeback {
2709        ctx.hash_long[hl1] = ip1 as u32;
2710    }
2711
2712    store.store_seq(
2713        &data[to_pos(*anchor)..to_pos(m_ip)],
2714        offset + 3, // OFFSET_TO_OFFBASE
2715        m_length as u32,
2716    );
2717    *ip = m_ip + m_length;
2718    *anchor = *ip;
2719
2720    dfast_post_match(
2721        ctx, store, data, ip, anchor, curr, offset_1, offset_2, ilimit, iend, bias,
2722    );
2723}
2724
2725/// The `_match_stored` tail: complementary insertion into both tables, then
2726/// the greedy immediate-repcode loop.
2727#[allow(clippy::too_many_arguments)]
2728fn dfast_post_match(
2729    ctx: &mut DfastCtx,
2730    store: &mut SeqStore,
2731    data: &[u8],
2732    ip: &mut usize,
2733    anchor: &mut usize,
2734    curr: usize,
2735    offset_1: &mut u32,
2736    offset_2: &mut u32,
2737    ilimit: usize,
2738    iend: usize,
2739    bias: usize,
2740) {
2741    let to_pos = |idx: usize| idx - bias;
2742    let hlog_l = ctx.hlog_l;
2743    let hlog_s = ctx.hlog_s;
2744    let mls = ctx.mls;
2745    if *ip <= ilimit {
2746        // Complementary insertion: candidates could be > iend-8 before this.
2747        let index_to_insert = curr + 2;
2748        let h = hash_ptr(data, to_pos(index_to_insert), hlog_l, 8);
2749        ctx.hash_long[h] = index_to_insert as u32;
2750        let h = hash_ptr(data, to_pos(*ip - 2), hlog_l, 8);
2751        ctx.hash_long[h] = (*ip - 2) as u32;
2752        let h = hash_ptr(data, to_pos(index_to_insert), hlog_s, mls);
2753        ctx.hash_small[h] = index_to_insert as u32;
2754        let h = hash_ptr(data, to_pos(*ip - 1), hlog_s, mls);
2755        ctx.hash_small[h] = (*ip - 1) as u32;
2756
2757        // Immediate repcode loop.
2758        while *ip <= ilimit
2759            && *offset_2 > 0
2760            && read32(data, to_pos(*ip)) == read32(data, to_pos(*ip - *offset_2 as usize))
2761        {
2762            let r_length = count_eq(
2763                data,
2764                to_pos(*ip) + 4,
2765                to_pos(*ip - *offset_2 as usize) + 4,
2766                to_pos(iend),
2767            ) + 4;
2768            std::mem::swap(offset_1, offset_2);
2769            let h = hash_ptr(data, to_pos(*ip), hlog_s, mls);
2770            ctx.hash_small[h] = *ip as u32;
2771            let h = hash_ptr(data, to_pos(*ip), hlog_l, 8);
2772            ctx.hash_long[h] = *ip as u32;
2773            store.store_seq(&[], 1, r_length as u32);
2774            *ip += r_length;
2775            *anchor = *ip;
2776        }
2777    }
2778}
2779
2780// --- The ZSTD_dfast extDict match finder ---------------------------------------------
2781
2782/// `ZSTD_compressBlock_doubleFast_extDict_generic`: double-fast over a
2783/// two-segment window. Unlike the rewritten noDict variant this is still the
2784/// classic single-cursor loop with `ip += (ip-anchor >> kSearchStrength) + 1`
2785/// advancement, and there is no `offsetSaved` rescue at the block start —
2786/// repcode validity is tested per use against `dictStartIndex` instead.
2787fn compress_block_dfast_extdict(
2788    ctx: &mut DfastCtx,
2789    store: &mut SeqStore,
2790    rep: &mut [u32; 3],
2791    data: &[u8],
2792    block_start: usize,
2793    block_end: usize,
2794    win: &Window,
2795) -> usize {
2796    let src_size = block_end - block_start;
2797    let hlog_l = ctx.hlog_l;
2798    let hlog_s = ctx.hlog_s;
2799    let mls = ctx.mls;
2800
2801    let seg_bias = win.seg_bias as usize;
2802    let istart = block_start + seg_bias;
2803    let iend = block_end + seg_bias;
2804    let end_index = iend;
2805
2806    // ZSTD_getLowestMatchIndex(ms, endIndex, windowLog). A loaded dictionary
2807    // (`loadedDictEnd != 0`) stays referenceable down to `lowLimit`; the windowed
2808    // floor applies only without one.
2809    let max_distance = 1usize << ctx.window_log;
2810    let lowest_valid = win.low_limit as usize;
2811    let dict_start_index = if win.loaded_dict_end != 0 {
2812        lowest_valid
2813    } else if end_index - lowest_valid > max_distance {
2814        end_index - max_distance
2815    } else {
2816        lowest_valid
2817    };
2818    let dict_limit = win.dict_limit as usize;
2819    let prefix_start_index = dict_start_index.max(dict_limit);
2820
2821    // If extDict is invalidated by maxDistance, switch to the "regular" variant.
2822    if prefix_start_index == dict_start_index {
2823        return compress_block_dfast(
2824            ctx,
2825            store,
2826            rep,
2827            data,
2828            block_start,
2829            block_end,
2830            seg_bias,
2831            dict_limit,
2832        );
2833    }
2834    if src_size < HASH_READ_SIZE {
2835        return src_size;
2836    }
2837    let ilimit = iend - HASH_READ_SIZE;
2838
2839    let w = ExtDictView {
2840        seg_bias,
2841        dict_bias: win.dict_bias as usize,
2842        prefix_start_index,
2843        dict_start_pos: dict_start_index - win.dict_bias as usize,
2844        dict_end_pos: prefix_start_index - win.dict_bias as usize,
2845        prefix_start_pos: prefix_start_index - seg_bias,
2846        iend_pos: block_end,
2847        ilimit,
2848    };
2849
2850    let mut anchor = istart;
2851    let mut ip = istart;
2852    let mut offset_1 = rep[0];
2853    let mut offset_2 = rep[1];
2854
2855    // `ZSTD_index_overlap_check`: repIndex must be fully prefix-side, or far
2856    // enough below the seam that the 4-byte read stays inside the dict.
2857    let overlap_ok = |rep_index: u32| {
2858        (prefix_start_index as u32)
2859            .wrapping_sub(1)
2860            .wrapping_sub(rep_index)
2861            >= 3
2862    };
2863
2864    // Search loop: `<` instead of `<=` because of the repcode check at ip+1.
2865    while ip < ilimit {
2866        let h_small = hash_ptr(data, w.pos_p(ip), hlog_s, mls);
2867        let match_index = ctx.hash_small[h_small] as usize;
2868        let h_long = hash_ptr(data, w.pos_p(ip), hlog_l, 8);
2869        let match_long_index = ctx.hash_long[h_long] as usize;
2870
2871        let curr = ip;
2872        // offset_1 expected <= curr+1.
2873        let rep_index = (curr as u32 + 1).wrapping_sub(offset_1);
2874        ctx.hash_small[h_small] = curr as u32;
2875        ctx.hash_long[h_long] = curr as u32;
2876
2877        let m_length: usize;
2878
2879        // Check repcode at ip+1 (hence the validity bound at curr+1).
2880        if overlap_ok(rep_index)
2881            && offset_1 <= (curr + 1 - dict_start_index) as u32
2882            && read32(data, w.pos_seg(rep_index as usize)) == read32(data, w.pos_p(ip + 1))
2883        {
2884            let rep_idx = rep_index as usize;
2885            m_length = count_2segments(
2886                data,
2887                w.pos_p(ip + 1) + 4,
2888                w.pos_seg(rep_idx) + 4,
2889                w.iend_pos,
2890                w.match_end_pos(rep_idx),
2891                w.prefix_start_pos,
2892            ) + 4;
2893            ip += 1;
2894            store.store_seq(&data[w.pos_p(anchor)..w.pos_p(ip)], 1, m_length as u32);
2895        } else if match_long_index > dict_start_index
2896            && read64(data, w.pos_seg(match_long_index)) == read64(data, w.pos_p(ip))
2897        {
2898            // Long match, in either segment.
2899            let mut match_pos = w.pos_seg(match_long_index);
2900            let low_match_pos = if match_long_index < prefix_start_index {
2901                w.dict_start_pos
2902            } else {
2903                w.prefix_start_pos
2904            };
2905            let mut len = count_2segments(
2906                data,
2907                w.pos_p(ip) + 8,
2908                match_pos + 8,
2909                w.iend_pos,
2910                w.match_end_pos(match_long_index),
2911                w.prefix_start_pos,
2912            ) + 8;
2913            let offset = (curr - match_long_index) as u32;
2914            // Catch up, bounded by the match's segment.
2915            while ip > anchor
2916                && match_pos > low_match_pos
2917                && data[w.pos_p(ip) - 1] == data[match_pos - 1]
2918            {
2919                ip -= 1;
2920                match_pos -= 1;
2921                len += 1;
2922            }
2923            offset_2 = offset_1;
2924            offset_1 = offset;
2925            store.store_seq(
2926                &data[w.pos_p(anchor)..w.pos_p(ip)],
2927                offset + 3, // OFFSET_TO_OFFBASE
2928                len as u32,
2929            );
2930            m_length = len;
2931        } else if match_index > dict_start_index
2932            && read32(data, w.pos_seg(match_index)) == read32(data, w.pos_p(ip))
2933        {
2934            // Short match: probe the long table at ip+1 for something better.
2935            let h3 = hash_ptr(data, w.pos_p(ip + 1), hlog_l, 8);
2936            let match_index3 = ctx.hash_long[h3] as usize;
2937            ctx.hash_long[h3] = (curr + 1) as u32;
2938
2939            let offset: u32;
2940            let mut len: usize;
2941            if match_index3 > dict_start_index
2942                && read64(data, w.pos_seg(match_index3)) == read64(data, w.pos_p(ip + 1))
2943            {
2944                let mut match_pos = w.pos_seg(match_index3);
2945                let low_match_pos = if match_index3 < prefix_start_index {
2946                    w.dict_start_pos
2947                } else {
2948                    w.prefix_start_pos
2949                };
2950                len = count_2segments(
2951                    data,
2952                    w.pos_p(ip + 1) + 8,
2953                    match_pos + 8,
2954                    w.iend_pos,
2955                    w.match_end_pos(match_index3),
2956                    w.prefix_start_pos,
2957                ) + 8;
2958                ip += 1;
2959                offset = (curr + 1 - match_index3) as u32;
2960                while ip > anchor
2961                    && match_pos > low_match_pos
2962                    && data[w.pos_p(ip) - 1] == data[match_pos - 1]
2963                {
2964                    ip -= 1;
2965                    match_pos -= 1;
2966                    len += 1;
2967                }
2968            } else {
2969                let mut match_pos = w.pos_seg(match_index);
2970                let low_match_pos = if match_index < prefix_start_index {
2971                    w.dict_start_pos
2972                } else {
2973                    w.prefix_start_pos
2974                };
2975                len = count_2segments(
2976                    data,
2977                    w.pos_p(ip) + 4,
2978                    match_pos + 4,
2979                    w.iend_pos,
2980                    w.match_end_pos(match_index),
2981                    w.prefix_start_pos,
2982                ) + 4;
2983                offset = (curr - match_index) as u32;
2984                while ip > anchor
2985                    && match_pos > low_match_pos
2986                    && data[w.pos_p(ip) - 1] == data[match_pos - 1]
2987                {
2988                    ip -= 1;
2989                    match_pos -= 1;
2990                    len += 1;
2991                }
2992            }
2993            offset_2 = offset_1;
2994            offset_1 = offset;
2995            store.store_seq(
2996                &data[w.pos_p(anchor)..w.pos_p(ip)],
2997                offset + 3, // OFFSET_TO_OFFBASE
2998                len as u32,
2999            );
3000            m_length = len;
3001        } else {
3002            ip += ((ip - anchor) >> K_SEARCH_STRENGTH) + 1;
3003            continue;
3004        }
3005
3006        // Move to next sequence start.
3007        ip += m_length;
3008        anchor = ip;
3009
3010        if ip <= ilimit {
3011            // Complementary insertion: candidates could be > iend-8 before this.
3012            let index_to_insert = curr + 2;
3013            let h = hash_ptr(data, w.pos_p(index_to_insert), hlog_l, 8);
3014            ctx.hash_long[h] = index_to_insert as u32;
3015            let h = hash_ptr(data, w.pos_p(ip - 2), hlog_l, 8);
3016            ctx.hash_long[h] = (ip - 2) as u32;
3017            let h = hash_ptr(data, w.pos_p(index_to_insert), hlog_s, mls);
3018            ctx.hash_small[h] = index_to_insert as u32;
3019            let h = hash_ptr(data, w.pos_p(ip - 1), hlog_s, mls);
3020            ctx.hash_small[h] = (ip - 1) as u32;
3021
3022            // Check immediate repcode (offset_2), with two-segment reads.
3023            while ip <= ilimit {
3024                let current2 = ip as u32;
3025                let rep_index2 = current2.wrapping_sub(offset_2);
3026                if !(overlap_ok(rep_index2) && offset_2 <= current2 - dict_start_index as u32) {
3027                    break;
3028                }
3029                let rep2 = rep_index2 as usize;
3030                if read32(data, w.pos_seg(rep2)) != read32(data, w.pos_p(ip)) {
3031                    break;
3032                }
3033                let rep_length2 = count_2segments(
3034                    data,
3035                    w.pos_p(ip) + 4,
3036                    w.pos_seg(rep2) + 4,
3037                    w.iend_pos,
3038                    w.match_end_pos(rep2),
3039                    w.prefix_start_pos,
3040                ) + 4;
3041                std::mem::swap(&mut offset_1, &mut offset_2);
3042                store.store_seq(&[], 1, rep_length2 as u32); // REPCODE1, no literals
3043                let h = hash_ptr(data, w.pos_p(ip), hlog_s, mls);
3044                ctx.hash_small[h] = current2;
3045                let h = hash_ptr(data, w.pos_p(ip), hlog_l, 8);
3046                ctx.hash_long[h] = current2;
3047                ip += rep_length2;
3048                anchor = ip;
3049            }
3050        }
3051    }
3052
3053    // Save reps for the next block — no offsetSaved rotation in this variant.
3054    rep[0] = offset_1;
3055    rep[1] = offset_2;
3056
3057    iend - anchor
3058}
3059
3060// --- Frame assembly ----------------------------------------------------------------
3061
3062/// `ZSTD_isRLE`.
3063pub(crate) fn is_rle(src: &[u8]) -> bool {
3064    src.iter().all(|&b| b == src[0])
3065}
3066
3067/// `ZSTD_writeFrameHeader`, no dictionary (dictID 0). `pledged` of `None` is
3068/// `ZSTD_CONTENTSIZE_UNKNOWN`; per `ZSTD_resetCCtx_internal` an unknown
3069/// pledged size clears the content-size flag, which both omits the FCS field
3070/// and disables the single-segment format.
3071fn write_frame_header(
3072    out: &mut Vec<u8>,
3073    cparams: &CParams,
3074    pledged: Option<u64>,
3075    checksum: bool,
3076    dict_id: u32,
3077) {
3078    let window_size = 1u64 << cparams.window_log;
3079    let content_size_flag = pledged.is_some();
3080    let pledged_src_size = pledged.unwrap_or(0);
3081    let single_segment = content_size_flag && window_size >= pledged_src_size;
3082    let fcs_code = if content_size_flag {
3083        (pledged_src_size >= 256) as u32
3084            + (pledged_src_size >= 65536 + 256) as u32
3085            + (pledged_src_size >= 0xFFFF_FFFF) as u32
3086    } else {
3087        0
3088    };
3089    // `ZSTD_writeFrameHeader`: the `Dictionary_ID` field is 0/1/2/4 bytes, and
3090    // its size code occupies the low two bits of the descriptor. The default
3091    // frame params keep `noDictIDFlag == 0`, so a nonzero dict ID is always
3092    // written (a zero dict ID — no dict, or a raw-content dict — emits no field).
3093    let did_size_code = (dict_id > 0) as u32 + (dict_id >= 256) as u32 + (dict_id >= 65536) as u32;
3094    let descriptor = (fcs_code << 6) as u8
3095        | ((single_segment as u8) << 5)
3096        | ((checksum as u8) << 2)
3097        | did_size_code as u8;
3098
3099    out.extend_from_slice(&ZSTD_MAGIC.to_le_bytes());
3100    out.push(descriptor);
3101    if !single_segment {
3102        out.push(((cparams.window_log - WINDOWLOG_ABSOLUTEMIN) << 3) as u8);
3103    }
3104    match did_size_code {
3105        0 => {}
3106        1 => out.push(dict_id as u8),
3107        2 => out.extend_from_slice(&(dict_id as u16).to_le_bytes()),
3108        _ => out.extend_from_slice(&dict_id.to_le_bytes()),
3109    }
3110    match fcs_code {
3111        0 => {
3112            if single_segment {
3113                out.push(pledged_src_size as u8);
3114            }
3115        }
3116        1 => out.extend_from_slice(&((pledged_src_size - 256) as u16).to_le_bytes()),
3117        2 => out.extend_from_slice(&(pledged_src_size as u32).to_le_bytes()),
3118        _ => out.extend_from_slice(&pledged_src_size.to_le_bytes()),
3119    }
3120}
3121
3122/// Append a block header (3 bytes LE: last flag, type, size).
3123pub(crate) fn push_block_header(out: &mut Vec<u8>, last: bool, block_type: u32, size: usize) {
3124    let v = (last as u32) | (block_type << 1) | ((size as u32) << 3);
3125    out.extend_from_slice(&v.to_le_bytes()[..3]);
3126}
3127
3128/// `ZSTDcs_*`: the frame-level stage of a [`FrameCompressor`].
3129#[derive(PartialEq, Eq, Clone, Copy)]
3130enum Stage {
3131    /// Frame header not written yet (`ZSTDcs_init`).
3132    Init,
3133    /// Header written, no block flagged "last" yet (`ZSTDcs_ongoing`).
3134    Ongoing,
3135    /// A chunk was compressed with the last-block flag (`ZSTDcs_ending`).
3136    Ending,
3137}
3138
3139/// A ZSTDMT job's pre-generated LDM sequences (the serial state's per-segment
3140/// `rawSeqStore`) plus the persistent cursor consumed across the job's blocks
3141/// (`zc->externSeqStore`, applied via `ZSTD_referenceExternalSequences`).
3142struct MtExtSeqs {
3143    seqs: Vec<crate::ldm::RawSeq>,
3144    cursor: crate::opt::LdmCursor,
3145}
3146
3147/// `ZSTD_ldm_blockCompress`'s `ZSTD_ldm_skipRawSeqStoreBytes(rawSeqStore,
3148/// srcSize)`: advance the externSeqStore cursor past one block (run after every
3149/// block of a ZSTDMT LDM job — including tiny/raw blocks, which C skips the same
3150/// way in `ZSTD_buildSeqStore`).
3151fn advance_ext_seqs(ext: &mut Option<MtExtSeqs>, block_size: usize) {
3152    if let Some(ext) = ext {
3153        ext.cursor.skip_bytes(&ext.seqs, block_size);
3154    }
3155}
3156
3157/// The frame-compression half of `ZSTD_CCtx`: every piece of state that
3158/// persists across `ZSTD_compressContinue` calls. The one-shot [`compress`]
3159/// feeds a single chunk; the streaming encoder feeds successive chunks of its
3160/// input buffer.
3161pub(crate) struct FrameCompressor {
3162    cparams: CParams,
3163    matcher: Matcher,
3164    rep: [u32; 3],
3165    entropy: FseEntropyState,
3166    is_first_block: bool,
3167    /// `cctx->consumedSrcSize` / `cctx->producedCSize`. Their difference seeds
3168    /// the pre-splitter `savings` at each frame-chunk call; `produced`
3169    /// includes the frame header, exactly as in C.
3170    consumed: u64,
3171    produced: u64,
3172    /// `windowSize` per `ZSTD_resetCCtx_internal`:
3173    /// `max(1, min(1 << windowLog, pledgedSrcSize))`.
3174    window_size: usize,
3175    /// `cctx->blockSizeMax = min(ZSTD_BLOCKSIZE_MAX, windowSize)`.
3176    block_size_max: usize,
3177    /// `None` is `ZSTD_CONTENTSIZE_UNKNOWN`.
3178    pledged: Option<u64>,
3179    checksum: bool,
3180    xxh: crate::xxhash::Xxh64,
3181    stage: Stage,
3182    disable_literal_compression: bool,
3183    window: Window,
3184    /// `cctx->ldmState`, present when `ZSTD_resolveEnableLdm` (auto) turns
3185    /// long-distance matching on: `strategy >= btopt && windowLog >= 27`,
3186    /// i.e. level 22 at unknown or > 64 MiB content sizes.
3187    ldm: Option<crate::ldm::LdmState>,
3188    /// A ZSTDMT job's pre-generated LDM sequences + persistent cursor (the serial
3189    /// state's per-segment `rawSeqStore`, applied as the job's `externSeqStore`).
3190    /// Present only on a ZSTDMT LDM job; mutually exclusive with `ldm` (the job
3191    /// itself has LDM disabled — the candidates come from here).
3192    ext_seqs: Option<MtExtSeqs>,
3193    /// Set by `compress_with_dict`: the window has been arranged directly in
3194    /// the post-flip extDict state, so the first `compress_continue` must NOT
3195    /// run `ZSTD_window_update` (which would recompute the wrong segment
3196    /// boundary for the concatenated `dict ++ src` buffer). One-shot only.
3197    window_preloaded: bool,
3198    /// `cctx->dictID`: the `Dictionary_ID` written into the frame header. Zero
3199    /// for no dictionary or a raw-content dictionary (no `Dictionary_ID` field);
3200    /// nonzero only for a trained (`ZDICT`) dictionary loaded by
3201    /// [`compress_with_dict`].
3202    dict_id: u32,
3203    /// `ms->dictMatchState` for the CDict (Path B) *attach* path: the
3204    /// dictionary's own tagged match table(s), consulted by the dictMatchState
3205    /// match finders. `None` on every other path (no dict, extDict, or copy).
3206    dict_match_state: Option<DictMatchState>,
3207    /// `cctx->appliedParams.postBlockSplitter`: whether the post-block splitter
3208    /// runs. C resolves this once from the **frame** cParams
3209    /// (`ZSTD_resolveBlockSplitterMode`: `strategy >= btopt && windowLog >= 17`)
3210    /// *before* a CDict copy/attach overwrites `cParams` with the CDict's own
3211    /// (`ZSTD_resetCCtx_byCopyingCDict` keeps the already-resolved switch). For
3212    /// every non-dict path the frame cParams *are* `cparams`, so this defaults to
3213    /// `block_splitter_enabled(&cparams)`; the CDict paths override it from the
3214    /// frame cParams, whose strategy can differ from the copied CDict strategy
3215    /// (the CDict uses createCDict params, sized for a 513-byte hint).
3216    post_block_splitter: bool,
3217}
3218
3219/// The dictionary's own (tagged) match table(s) referenced by an attached CDict
3220/// (`ms->dictMatchState`), per strategy. Held by [`FrameCompressor`] only on the
3221/// CDict attach path; the working context's own table(s) fill as `src` is parsed.
3222enum DictMatchState {
3223    Fast(FastDictMatchState),
3224    Dfast(DfastDictMatchState),
3225    Lazy(LazyDictMatchState),
3226    Opt(OptDictMatchState),
3227}
3228
3229struct FastDictMatchState {
3230    /// Tagged hash table (`ZSTD_writeTaggedIndex`), sized `1 << hlog`.
3231    hash_table: Vec<u32>,
3232    hlog: u32,
3233    /// The dictionary content length (the prefix of the `content ++ src` buffer).
3234    content_len: usize,
3235}
3236
3237/// The dfast variant: a long table (`hashLog + 8` bits, mls 8) and a short table
3238/// (`chainLog + 8` bits, mls), both tagged.
3239struct DfastDictMatchState {
3240    hash_long: Vec<u32>,
3241    hash_small: Vec<u32>,
3242    hlog_l: u32,
3243    hlog_s: u32,
3244    content_len: usize,
3245}
3246
3247/// The greedy/lazy/lazy2 variant: the CDict's own (untagged) lazy match state —
3248/// hash chain or row table — built over the dict content with the CDict's
3249/// parameters and salt 0. Read-only during the attach search.
3250struct LazyDictMatchState {
3251    ms: Box<crate::lazy::LazyCtx>,
3252    content_len: usize,
3253}
3254
3255/// The btopt/btultra/btultra2 variant: the CDict's own (untagged) optimal-parser
3256/// match state — the fully-sorted binary tree built over the dict content with
3257/// the CDict's parameters. Read-only during the attach search.
3258struct OptDictMatchState {
3259    ms: Box<crate::opt::OptCtx>,
3260    content_len: usize,
3261}
3262
3263impl FrameCompressor {
3264    pub(crate) fn new(level: i32, pledged: Option<u64>, checksum: bool) -> Self {
3265        // Unknown content size selects the "default" srcSize class and skips
3266        // the window resize (`ZSTD_getCParamRowSize` returns
3267        // ZSTD_CONTENTSIZE_UNKNOWN for unknown srcSize without a dictionary).
3268        let cparams = get_cparams(level, pledged.unwrap_or(CONTENTSIZE_UNKNOWN), 0);
3269        Self::from_cparams(cparams, pledged, checksum)
3270    }
3271
3272    /// Build a frame compressor from already-derived compression parameters.
3273    /// `ZSTD_compress` reaches this through [`new`](Self::new) (no-dict
3274    /// cParams); `compress_with_dict` derives dict-aware cParams and calls it
3275    /// directly.
3276    pub(crate) fn from_cparams(cparams: CParams, pledged: Option<u64>, checksum: bool) -> Self {
3277        let window_size_u64 = match pledged {
3278            Some(n) => (1u64 << cparams.window_log).min(n).max(1),
3279            None => 1u64 << cparams.window_log,
3280        };
3281        let block_size_max = (BLOCK_SIZE_MAX as u64).min(window_size_u64) as usize;
3282        let matcher = match cparams.strategy {
3283            Strategy::Dfast => Matcher::Dfast(DfastCtx::new(&cparams)),
3284            Strategy::Greedy | Strategy::Lazy | Strategy::Lazy2 | Strategy::Btlazy2 => {
3285                Matcher::Lazy(crate::lazy::LazyCtx::new(&cparams))
3286            }
3287            Strategy::Btopt | Strategy::Btultra | Strategy::Btultra2 => {
3288                Matcher::Opt(Box::new(crate::opt::OptCtx::new(&cparams)))
3289            }
3290            _ => Matcher::Fast(FastCtx::new(&cparams)),
3291        };
3292        // `ZSTD_literalsCompressionIsDisabled` in the default `ZSTD_ps_auto`
3293        // mode: the fast strategy with a nonzero target length (the negative
3294        // / acceleration levels) skips literal compression entirely.
3295        let disable_literal_compression =
3296            cparams.strategy == Strategy::Fast && cparams.target_length > 0;
3297        FrameCompressor {
3298            cparams,
3299            matcher,
3300            rep: [1, 4, 8],
3301            entropy: FseEntropyState::new(),
3302            is_first_block: true,
3303            consumed: 0,
3304            produced: 0,
3305            window_size: window_size_u64 as usize,
3306            block_size_max,
3307            pledged,
3308            checksum,
3309            xxh: crate::xxhash::Xxh64::new(0),
3310            stage: Stage::Init,
3311            disable_literal_compression,
3312            window: Window::new(),
3313            ldm: crate::ldm::LdmParams::auto(&cparams).map(crate::ldm::LdmState::new),
3314            ext_seqs: None,
3315            window_preloaded: false,
3316            dict_id: 0,
3317            dict_match_state: None,
3318            post_block_splitter: crate::post_split::block_splitter_enabled(&cparams),
3319        }
3320    }
3321
3322    pub(crate) fn window_size(&self) -> usize {
3323        self.window_size
3324    }
3325
3326    pub(crate) fn block_size_max(&self) -> usize {
3327        self.block_size_max
3328    }
3329
3330    /// For the streaming CDict-attach path: whether compressing a block ending at
3331    /// staging-buffer position `block_end` would carry the source past the window
3332    /// (`block_end + seg_bias > maxDist`), where C's `ZSTD_checkDictValidity` /
3333    /// `ZSTD_window_enforceMaxDist` would drop the attached dict or slide the
3334    /// window. That loadedDictEnd-aware machinery isn't ported, so the streaming
3335    /// encoder must stop with a clean error before this point. Always false when
3336    /// no dict is attached.
3337    pub(crate) fn cdict_attach_overflow(&self, block_end: usize) -> bool {
3338        self.dict_match_state.is_some()
3339            && (block_end + self.window.seg_bias as usize) as u64
3340                > (1u64 << self.cparams.window_log)
3341    }
3342
3343    /// `ZSTD_compressContinue_internal` (frame mode): write the frame header
3344    /// on first use, then compress `data[chunk_start..chunk_end]` into one or
3345    /// more terminated blocks (`ZSTD_compress_frameChunk`).
3346    ///
3347    /// `data` is the frame's contiguous history buffer: every chunk must
3348    /// directly follow the previous one in the same buffer, so the match
3349    /// finders can reach back across chunk boundaries.
3350    pub(crate) fn compress_continue(
3351        &mut self,
3352        out: &mut Vec<u8>,
3353        data: &[u8],
3354        chunk_start: usize,
3355        chunk_end: usize,
3356        last_frame_chunk: bool,
3357    ) -> Result<(), Error> {
3358        let out_start = out.len();
3359        if self.stage == Stage::Init {
3360            write_frame_header(
3361                out,
3362                &self.cparams,
3363                self.pledged,
3364                self.checksum,
3365                self.dict_id,
3366            );
3367            self.stage = Stage::Ongoing;
3368        }
3369        if chunk_start == chunk_end {
3370            // Do not generate an empty block, but do count the header.
3371            self.produced += (out.len() - out_start) as u64;
3372            return Ok(());
3373        }
3374
3375        // `ZSTD_window_update`: a non-contiguous chunk (the streaming input
3376        // buffer wrapped) turns the live window into the extDict.
3377        //
3378        // `compress_with_dict` pre-arranges the window in the post-flip extDict
3379        // state for the first (and, one-shot, only) chunk, so the first update
3380        // is skipped: running it on the concatenated `dict ++ src` buffer would
3381        // see `src` as contiguous with `dict` and miss the flip.
3382        if self.window_preloaded {
3383            self.window_preloaded = false;
3384        } else if !self.window.update(chunk_start, chunk_end) {
3385            // `ZSTD_compressContinue_internal`: a non-contiguous update
3386            // restarts table insertion at the new segment
3387            // (`ms->nextToUpdate = ms->window.dictLimit`).
3388            match &mut self.matcher {
3389                Matcher::Lazy(ctx) => ctx.next_to_update = self.window.dict_limit as usize,
3390                Matcher::Opt(ctx) => ctx.next_to_update = self.window.dict_limit as usize,
3391                _ => {}
3392            }
3393        }
3394        // The LDM state keeps its own window, updated in lockstep.
3395        if let Some(ldm) = &mut self.ldm {
3396            ldm.window.update(chunk_start, chunk_end);
3397        }
3398        if self.checksum {
3399            self.xxh.update(&data[chunk_start..chunk_end]);
3400        }
3401
3402        let cparams = self.cparams;
3403        let mut savings: i64 = self.consumed as i64 - self.produced as i64;
3404        let mut pos = chunk_start;
3405        while pos < chunk_end {
3406            let remaining = chunk_end - pos;
3407            // ZSTD_optimalBlockSize: only full 128 KiB blocks are candidates
3408            // for pre-splitting, and only once at least 3 bytes of savings
3409            // are verified (so the first full block is never split). The
3410            // auto split level is `splitLevels[strategy]`.
3411            let block_size = if remaining < BLOCK_SIZE_MAX || self.block_size_max < BLOCK_SIZE_MAX {
3412                remaining.min(self.block_size_max)
3413            } else if savings < 3 {
3414                BLOCK_SIZE_MAX
3415            } else {
3416                const SPLIT_LEVELS: [usize; 10] = [0, 0, 1, 2, 2, 3, 3, 4, 4, 4];
3417                let split_level = SPLIT_LEVELS[cparams.strategy as usize];
3418                pre_split::split_block(&data[pos..pos + BLOCK_SIZE_MAX], split_level)
3419            };
3420            let last_block = last_frame_chunk && block_size == remaining;
3421            let block = &data[pos..pos + block_size];
3422
3423            // `ZSTD_overflowCorrectIfNeeded`: once the running index reaches
3424            // 3500 MiB, recycle the 32-bit index space — slide the window and
3425            // subtract the same `correction` from every matcher table. Runs
3426            // before enforceMaxDist, exactly as `ZSTD_compress_frameChunk`
3427            // does. Lifts the streaming length limit entirely.
3428            let max_dist = 1u32 << cparams.window_log;
3429            if self.window.needs_overflow_correction(pos + block_size) {
3430                let cycle_log = cycle_log(cparams.chain_log, cparams.strategy);
3431                let correction = self.window.correct_overflow(cycle_log, max_dist, pos);
3432                match &mut self.matcher {
3433                    Matcher::Fast(ctx) => ctx.reduce_indices(correction),
3434                    Matcher::Dfast(ctx) => ctx.reduce_indices(correction),
3435                    Matcher::Lazy(ctx) => ctx.reduce_indices(correction),
3436                    Matcher::Opt(ctx) => ctx.reduce_indices(correction),
3437                }
3438            }
3439
3440            // `ZSTD_checkDictValidity` runs first, anchored at the block *end*
3441            // (`ip + blockSize`): it clears a loaded dictionary once the window
3442            // size is reached within the next block. Then `ZSTD_window_enforceMaxDist`,
3443            // anchored at the block *start* (`ip`), slides `lowLimit`; the dict
3444            // mode below (extDict vs noDict) is decided on the result. The
3445            // matchers tighten their own validity bound from the block end.
3446            let block_end_idx = (pos + block_size) as u32 + self.window.seg_bias;
3447            self.window.check_dict_validity(block_end_idx, max_dist);
3448            let block_start_idx = pos as u32 + self.window.seg_bias;
3449            self.window.enforce_max_dist(block_start_idx, max_dist);
3450
3451            // `Ensure hash/chain table insertion resumes no sooner than
3452            // lowLimit` (`ZSTD_compress_frameChunk`): after a slide or an
3453            // overflow correction, nextToUpdate may trail the new lowLimit.
3454            match &mut self.matcher {
3455                Matcher::Lazy(ctx) => {
3456                    ctx.next_to_update = ctx.next_to_update.max(self.window.low_limit as usize)
3457                }
3458                Matcher::Opt(ctx) => {
3459                    ctx.next_to_update = ctx.next_to_update.max(self.window.low_limit as usize)
3460                }
3461                _ => {}
3462            }
3463
3464            // --- ZSTD_compressBlock_internal ---
3465            let mut c_size_kind: BlockKind;
3466            let mut body: Vec<u8> = Vec::new();
3467
3468            // ZSTD_buildSeqStore: tiny blocks are not even attempted.
3469            if block_size < MIN_CBLOCK_SIZE + BLOCK_HEADER_SIZE + 1 + 1 {
3470                c_size_kind = BlockKind::Raw;
3471            } else {
3472                // `ZSTD_buildSeqStore`: "limited update after a very long
3473                // match" — cap how far nextToUpdate trails the block start.
3474                // Only the matchers that track nextToUpdate are affected.
3475                match &mut self.matcher {
3476                    Matcher::Lazy(ctx) => ctx.limit_update(pos + self.window.seg_bias as usize),
3477                    Matcher::Opt(ctx) => ctx.limit_update(pos + self.window.seg_bias as usize),
3478                    _ => {}
3479                }
3480                let mut store = SeqStore::new();
3481                let mut next_rep = self.rep;
3482                let last_ll_size = match &mut self.matcher {
3483                    Matcher::Fast(ctx) => {
3484                        // `ZSTD_selectBlockCompressor(strategy, ..,
3485                        // ZSTD_matchState_dictMode(ms))`: dictMatchState (an
3486                        // attached CDict) > extDict > noDict.
3487                        if let Some(DictMatchState::Fast(dms)) = &self.dict_match_state {
3488                            compress_block_fast_dict_match_state(
3489                                ctx,
3490                                &mut store,
3491                                &mut next_rep,
3492                                data,
3493                                pos,
3494                                pos + block_size,
3495                                dms.content_len,
3496                                &dms.hash_table,
3497                                dms.hlog,
3498                            )
3499                        } else if self.window.has_ext_dict() {
3500                            compress_block_fast_extdict(
3501                                ctx,
3502                                &mut store,
3503                                &mut next_rep,
3504                                data,
3505                                pos,
3506                                pos + block_size,
3507                                &self.window,
3508                            )
3509                        } else {
3510                            compress_block_fast(
3511                                ctx,
3512                                &mut store,
3513                                &mut next_rep,
3514                                data,
3515                                pos,
3516                                pos + block_size,
3517                                self.window.seg_bias as usize,
3518                                self.window.dict_limit as usize,
3519                            )
3520                        }
3521                    }
3522                    Matcher::Dfast(ctx) => {
3523                        if let Some(DictMatchState::Dfast(dms)) = &self.dict_match_state {
3524                            compress_block_dfast_dict_match_state(
3525                                ctx,
3526                                &mut store,
3527                                &mut next_rep,
3528                                data,
3529                                pos,
3530                                pos + block_size,
3531                                dms.content_len,
3532                                &dms.hash_long,
3533                                &dms.hash_small,
3534                                dms.hlog_l,
3535                                dms.hlog_s,
3536                            )
3537                        } else if self.window.has_ext_dict() {
3538                            compress_block_dfast_extdict(
3539                                ctx,
3540                                &mut store,
3541                                &mut next_rep,
3542                                data,
3543                                pos,
3544                                pos + block_size,
3545                                &self.window,
3546                            )
3547                        } else {
3548                            compress_block_dfast(
3549                                ctx,
3550                                &mut store,
3551                                &mut next_rep,
3552                                data,
3553                                pos,
3554                                pos + block_size,
3555                                self.window.seg_bias as usize,
3556                                self.window.dict_limit as usize,
3557                            )
3558                        }
3559                    }
3560                    Matcher::Lazy(ctx) => {
3561                        if let Some(DictMatchState::Lazy(dms)) = &self.dict_match_state {
3562                            crate::lazy::compress_block_lazy_dict_match_state(
3563                                ctx,
3564                                &mut store,
3565                                &mut next_rep,
3566                                data,
3567                                pos,
3568                                pos + block_size,
3569                                &self.window,
3570                                &dms.ms,
3571                                dms.content_len,
3572                            )
3573                        } else if self.window.has_ext_dict() {
3574                            crate::lazy::compress_block_lazy_extdict(
3575                                ctx,
3576                                &mut store,
3577                                &mut next_rep,
3578                                data,
3579                                pos,
3580                                pos + block_size,
3581                                &self.window,
3582                            )
3583                        } else {
3584                            crate::lazy::compress_block_lazy(
3585                                ctx,
3586                                &mut store,
3587                                &mut next_rep,
3588                                data,
3589                                pos,
3590                                pos + block_size,
3591                                &self.window,
3592                            )
3593                        }
3594                    }
3595                    Matcher::Opt(ctx) => {
3596                        let ext_dict = self.window.has_ext_dict();
3597                        // `ZSTD_selectBlockCompressor`: an attached CDict
3598                        // (dictMatchState) outranks extDict/noDict.
3599                        let opt_dms = match &self.dict_match_state {
3600                            Some(DictMatchState::Opt(d)) => Some(crate::opt::OptDms {
3601                                ms: d.ms.as_ref(),
3602                                content_len: d.content_len,
3603                            }),
3604                            _ => None,
3605                        };
3606                        // `ZSTD_buildSeqStore`: with LDM enabled, the opt parser
3607                        // takes the block's raw sequences as extra candidates
3608                        // (`ZSTD_ldm_blockCompress`, btopt+ branch). Single-threaded
3609                        // generates the *block's* sequences here (`self.ldm`); a
3610                        // ZSTDMT job instead consumes its *segment-wide*
3611                        // pre-generated `rawSeqStore` (`self.ext_seqs`, the serial
3612                        // state's externSeqStore) with a cursor that persists across
3613                        // the job's blocks.
3614                        let mut ldm_seqs: Vec<crate::ldm::RawSeq> = Vec::new();
3615                        if let Some(ldm) = &mut self.ldm {
3616                            let capacity =
3617                                self.block_size_max / ldm.params.min_match_length as usize;
3618                            crate::ldm::generate_sequences(
3619                                ldm,
3620                                &mut ldm_seqs,
3621                                capacity,
3622                                data,
3623                                pos,
3624                                pos + block_size,
3625                            )?;
3626                        }
3627                        let (ldm_input, ldm_cursor): (
3628                            Option<&[crate::ldm::RawSeq]>,
3629                            crate::opt::LdmCursor,
3630                        ) = if self.ldm.is_some() {
3631                            (Some(&ldm_seqs), crate::opt::LdmCursor::default())
3632                        } else if let Some(ext) = &self.ext_seqs {
3633                            (Some(&ext.seqs), ext.cursor)
3634                        } else {
3635                            (None, crate::opt::LdmCursor::default())
3636                        };
3637                        crate::opt::compress_block_opt(
3638                            ctx,
3639                            &mut store,
3640                            &mut next_rep,
3641                            data,
3642                            pos,
3643                            pos + block_size,
3644                            &mut self.window,
3645                            ext_dict,
3646                            ldm_input,
3647                            ldm_cursor,
3648                            Some(&self.entropy),
3649                            opt_dms.as_ref(),
3650                        )
3651                    }
3652                };
3653                let lits_from = block_size - last_ll_size;
3654                store.store_last_literals(&block[lits_from..]);
3655
3656                // The post-block splitter (btopt+ with windowLog >= 17) takes
3657                // over block emission entirely: it may emit several blocks
3658                // from this one seqStore, with dRep/cRep reconciliation.
3659                if self.post_block_splitter {
3660                    let c_size = crate::post_split::compress_block_split(
3661                        out,
3662                        &mut store,
3663                        &mut self.entropy,
3664                        &mut self.rep,
3665                        next_rep,
3666                        cparams.strategy as i32,
3667                        block,
3668                        last_block,
3669                        self.is_first_block,
3670                    )?;
3671                    savings += block_size as i64 - c_size as i64;
3672                    advance_ext_seqs(&mut self.ext_seqs, block_size);
3673                    pos += block_size;
3674                    self.is_first_block = false;
3675                    continue;
3676                }
3677
3678                match sequences_encode::entropy_compress_seq_store(
3679                    &store,
3680                    &self.entropy,
3681                    cparams.strategy as i32,
3682                    self.disable_literal_compression,
3683                    block_size,
3684                )? {
3685                    None => c_size_kind = BlockKind::Raw,
3686                    Some((b, next_entropy)) => {
3687                        body = b;
3688                        c_size_kind = BlockKind::Compressed;
3689                        // RLE-block override (not for the first block;
3690                        // decoder compat for zstd <= 1.4.3).
3691                        if !self.is_first_block && body.len() < 25 && is_rle(block) {
3692                            c_size_kind = BlockKind::Rle;
3693                        }
3694                        // Confirm repcodes + entropy only when actually
3695                        // emitting a compressed block (cSize > 1).
3696                        if c_size_kind == BlockKind::Compressed {
3697                            self.rep = next_rep;
3698                            self.entropy = next_entropy;
3699                        }
3700                    }
3701                }
3702            }
3703
3704            let c_size = match c_size_kind {
3705                BlockKind::Raw => {
3706                    push_block_header(out, last_block, 0, block_size);
3707                    out.extend_from_slice(block);
3708                    BLOCK_HEADER_SIZE + block_size
3709                }
3710                BlockKind::Rle => {
3711                    push_block_header(out, last_block, 1, block_size);
3712                    out.push(block[0]);
3713                    BLOCK_HEADER_SIZE + 1
3714                }
3715                BlockKind::Compressed => {
3716                    push_block_header(out, last_block, 2, body.len());
3717                    out.extend_from_slice(&body);
3718                    BLOCK_HEADER_SIZE + body.len()
3719                }
3720            };
3721
3722            savings += block_size as i64 - c_size as i64;
3723            advance_ext_seqs(&mut self.ext_seqs, block_size);
3724            pos += block_size;
3725            self.is_first_block = false;
3726        }
3727
3728        // `if (lastFrameChunk && (op>ostart)) cctx->stage = ZSTDcs_ending;` —
3729        // a nonempty chunk always emits at least one block.
3730        if last_frame_chunk {
3731            self.stage = Stage::Ending;
3732        }
3733        self.consumed += (chunk_end - chunk_start) as u64;
3734        self.produced += (out.len() - out_start) as u64;
3735        if self.pledged.is_some_and(|n| self.consumed > n) {
3736            // `srcSize_wrong`: more input than pledged.
3737            return Err(Error::Encode("pledged source size exceeded"));
3738        }
3739        Ok(())
3740    }
3741
3742    /// `ZSTD_compressEnd_public`: compress the final chunk, then write the
3743    /// epilogue (`ZSTD_writeEpilogue`: an empty last block if no block carried
3744    /// the last-block flag, plus the optional content checksum).
3745    pub(crate) fn compress_end(
3746        &mut self,
3747        out: &mut Vec<u8>,
3748        data: &[u8],
3749        chunk_start: usize,
3750        chunk_end: usize,
3751    ) -> Result<(), Error> {
3752        self.compress_continue(out, data, chunk_start, chunk_end, true)?;
3753        debug_assert!(self.stage != Stage::Init, "header is written above");
3754        if self.stage != Stage::Ending {
3755            // One last empty raw block to carry the end-of-frame mark.
3756            push_block_header(out, true, 0, 0);
3757        }
3758        if self.checksum {
3759            out.extend_from_slice(&(self.xxh.digest() as u32).to_le_bytes());
3760        }
3761        if self.pledged.is_some_and(|n| self.consumed != n) {
3762            // `srcSize_wrong`: pledged size must match exactly at frame end.
3763            return Err(Error::Encode("pledged source size not honored"));
3764        }
3765        Ok(())
3766    }
3767}
3768
3769/// `ZSTD_compress`: one-shot frame compression with the simple-API defaults
3770/// (contentSize known and flagged, no checksum, no dictionary).
3771///
3772/// Bit-exact with C libzstd 1.5.7 for the supported scope (see module docs);
3773/// unsupported configurations return [`Error::Encode`] rather than diverging.
3774pub fn compress(src: &[u8], level: i32) -> Result<Vec<u8>, Error> {
3775    // Match indices are 32-bit; larger inputs need the C window-cycling
3776    // (overflow correction) machinery.
3777    compress_maybe_checksum(src, level, false)
3778}
3779
3780/// `ZSTD_compress_usingDict`: one-shot frame compression primed with a
3781/// dictionary, so the start of `src` can reference `dict`. Bit-exact with C
3782/// libzstd 1.5.7 for the supported scope; unsupported configurations return
3783/// [`Error::Encode`] rather than diverging.
3784///
3785/// Current scope: raw / content-only **and** trained (`ZDICT`) dictionaries at
3786/// **every strategy** (fast through btultra2). A trained dictionary seeds the
3787/// first block's entropy tables, repeat offsets, and the frame's
3788/// `Dictionary_ID`, exactly as `ZSTD_loadCEntropy` does. The only rejected
3789/// configuration is the rare large input where C would enable long-distance
3790/// matching with a dictionary (`windowLog >= 27` at btopt+). An empty `dict` is
3791/// equivalent to [`compress`]; a raw dict shorter than 8 bytes is ignored (as in
3792/// C), though it still influences the derived parameters. A malformed trained
3793/// dictionary yields [`Error::DictionaryCorrupted`].
3794pub fn compress_with_dict(src: &[u8], dict: &[u8], level: i32) -> Result<Vec<u8>, Error> {
3795    if dict.len() as u64 + src.len() as u64 >= u64::from(u32::MAX) - 2 {
3796        // Match indices are 32-bit; larger histories need overflow correction.
3797        return Err(Error::Encode("inputs >= 4 GiB are not supported yet"));
3798    }
3799
3800    // cParams use the *whole* dictionary size, exactly as
3801    // `ZSTD_compress_usingDict` ->
3802    // `ZSTD_getParams_internal(level, srcSize, dictSize, cpm_noAttachDict)`.
3803    let cparams = get_cparams(level, src.len() as u64, dict.len() as u64);
3804    let pledged = Some(src.len() as u64);
3805    let mut fc = FrameCompressor::from_cparams(cparams, pledged, false);
3806
3807    // Resolve the dictionary *content* — the bytes the match finder loads. For a
3808    // trained (`ZDICT`) dictionary (`ZSTD_loadZstdDictionary`), parse the entropy
3809    // section first and seed the first block's `prevCBlock` tables, repeat
3810    // offsets, and the frame's dict ID; the content is everything after that
3811    // header. For a raw-content dictionary the whole buffer is content.
3812    let content: &[u8] = if dict.len() >= 8 && read32(dict, 0) == MAGIC_DICTIONARY {
3813        let seed = crate::dict_encode::load_c_entropy(dict)?;
3814        fc.entropy = seed.entropy;
3815        fc.rep = seed.rep;
3816        fc.dict_id = seed.dict_id;
3817        &dict[seed.entropy_size..]
3818    } else if dict.len() < 8 {
3819        // `ZSTD_compress_insertDictionary` ignores a (raw) dict shorter than 8
3820        // bytes entirely — the cParams above still reflect its size; compress
3821        // `src` as a plain frame.
3822        let mut out = Vec::with_capacity(src.len() + (src.len() >> 8) + 64);
3823        fc.compress_end(&mut out, src, 0, src.len())?;
3824        return Ok(out);
3825    } else {
3826        dict
3827    };
3828
3829    // C's `ZSTD_loadDictionaryContent` also seeds the dictionary into the LDM
3830    // tables (`ZSTD_ldm_fillHashTable`); that path isn't ported, so reject the
3831    // (large-input, btopt+) configurations where `ZSTD_resolveEnableLdm` turns
3832    // long-distance matching on rather than diverge. Unreachable at typical
3833    // sizes — LDM needs `windowLog >= 27`.
3834    if fc.ldm.is_some() {
3835        return Err(Error::Encode(
3836            "long-distance matching with a dictionary is not supported yet",
3837        ));
3838    }
3839
3840    // `ZSTD_loadDictionaryContent` followed by the non-contiguous `src` append:
3841    // lay the history out as one buffer `content ++ src` and put the window
3842    // directly in the post-flip extDict state. For a trained dict `content` is
3843    // the post-entropy tail; for a raw dict it is the whole buffer.
3844    let content_len = content.len();
3845    let src_len = src.len();
3846    let mut data = Vec::with_capacity(content_len + src_len);
3847    data.extend_from_slice(content);
3848    data.extend_from_slice(src);
3849
3850    prime_raw_prefix(&mut fc, &data, content_len);
3851
3852    let mut out = Vec::with_capacity(src_len + (src_len >> 8) + 64);
3853    fc.compress_end(&mut out, &data, content_len, content_len + src_len)?;
3854    Ok(out)
3855}
3856
3857/// `ZSTD_loadDictionaryContent` (raw content, `ZSTD_dtlm_fast`) followed by the
3858/// non-contiguous append: lay the history as one buffer `prefix ++ input` (the
3859/// prefix occupying `buf[..prefix_len]`), seed the strategy's match table(s)
3860/// from the prefix, and put the window directly in the post-flip extDict state
3861/// so the bit-exact extDict matchers run against it. Shared by
3862/// [`compress_with_dict`] (Path A, the dict as prefix) and the ZSTDMT job driver
3863/// ([`compress_mt`], where each non-first job sees the previous job's overlap
3864/// tail as a raw-content prefix). The caller has already seeded `fc.entropy` /
3865/// `fc.rep` (default `[1,4,8]` for raw content).
3866fn prime_raw_prefix(fc: &mut FrameCompressor, buf: &[u8], prefix_len: usize) {
3867    // Seed the strategy's table(s). Skipped when the prefix is `<=
3868    // HASH_READ_SIZE`, where C's `ZSTD_loadDictionaryContent` returns before
3869    // filling — the extDict is still live, just with empty tables.
3870    if prefix_len > HASH_READ_SIZE {
3871        match &mut fc.matcher {
3872            Matcher::Fast(ctx) => fill_fast_hash_table_for_cctx(ctx, buf, prefix_len),
3873            Matcher::Dfast(ctx) => fill_dfast_hash_tables_for_cctx(ctx, buf, prefix_len),
3874            Matcher::Lazy(ctx) => ctx.load_dictionary(buf, prefix_len),
3875            Matcher::Opt(ctx) => ctx.load_dictionary(buf, prefix_len),
3876        }
3877    }
3878    fc.window = Window::preloaded_ext_dict(prefix_len, buf.len() - prefix_len);
3879    fc.window_preloaded = true;
3880    // The window-preloaded path skips compress_continue's non-contiguous reset
3881    // (`ms->nextToUpdate = window.dictLimit`); apply it here so the lazy/opt
3882    // matchers resume insertion at the start of the input and never re-insert
3883    // prefix positions. (For a filled prefix, load_dictionary already left
3884    // nextToUpdate at dictLimit; this is what covers the no-fill `prefix_len ==
3885    // HASH_READ_SIZE` case, where leaving it at the prefix start would fabricate
3886    // prefix matches that C — whose post-flip base makes those indices hash
3887    // unrelated bytes — never finds. Fast/dfast don't track nextToUpdate.)
3888    match &mut fc.matcher {
3889        Matcher::Lazy(ctx) => ctx.next_to_update = fc.window.dict_limit as usize,
3890        Matcher::Opt(ctx) => ctx.next_to_update = fc.window.dict_limit as usize,
3891        _ => {}
3892    }
3893}
3894
3895/// Like [`prime_raw_prefix`] but for a ZSTDMT job: the prefix is **contiguous**
3896/// with the segment (same round buffer in C), so the window stays **noDict**
3897/// (the prefix is in-window history, not extDict). Seeds the same match
3898/// table(s) from the prefix, then arranges the whole `prefix ++ segment` buffer
3899/// as one in-window segment so the noDict matchers reach back into the prefix.
3900fn prime_contiguous_prefix(fc: &mut FrameCompressor, buf: &[u8], prefix_len: usize) {
3901    if prefix_len > HASH_READ_SIZE {
3902        match &mut fc.matcher {
3903            Matcher::Fast(ctx) => fill_fast_hash_table_for_cctx(ctx, buf, prefix_len),
3904            Matcher::Dfast(ctx) => fill_dfast_hash_tables_for_cctx(ctx, buf, prefix_len),
3905            Matcher::Lazy(ctx) => ctx.load_dictionary(buf, prefix_len),
3906            Matcher::Opt(ctx) => ctx.load_dictionary(buf, prefix_len),
3907        }
3908    }
3909    fc.window = Window::preloaded_contiguous_prefix(buf.len());
3910    fc.window_preloaded = true;
3911    // Resume table insertion at the segment start (the prefix is already
3912    // filled); `ZSTD_loadDictionaryContent` leaves `nextToUpdate` here.
3913    let seg_start_idx = WINDOW_START_INDEX + prefix_len;
3914    match &mut fc.matcher {
3915        Matcher::Lazy(ctx) => ctx.next_to_update = seg_start_idx,
3916        Matcher::Opt(ctx) => ctx.next_to_update = seg_start_idx,
3917        _ => {}
3918    }
3919}
3920
3921// --- ZSTDMT: multithreaded (job-splitting) compression -----------------------
3922//
3923// `zstdmt_compress.c`. C's multithreaded compressor splits the input into
3924// fixed-size *jobs* and compresses each one largely independently, with the
3925// previous job's tail supplied as a raw-content prefix ("overlap"). The
3926// resulting bytes differ from single-threaded output but are **deterministic
3927// and independent of the worker count** (job boundaries and per-job prefixes
3928// are pure arithmetic, not thread timing — see `ZSTDMT_compressStream_generic`
3929// / `ZSTDMT_createCompressionJob`). So we reproduce them by running the jobs
3930// *sequentially*: the library stays single-threaded and dependency-free.
3931
3932/// `ZSTDMT_JOBSIZE_MIN` (`zstdmt_compress.h:33`): inputs at or below this skip
3933/// multithreading entirely (`ZSTD_CCtx_init_compressStream2`, zstd_compress.c).
3934pub(crate) const ZSTDMT_JOBSIZE_MIN: u64 = 512 * 1024;
3935/// `ZSTDMT_JOBSIZE_MAX` on a 64-bit target (`zstdmt_compress.h:36`).
3936const ZSTDMT_JOBSIZE_MAX: u64 = 1024 * 1024 * 1024;
3937/// `ZSTDMT_JOBLOG_MAX` on a 64-bit target (`zstdmt_compress.h:35`).
3938const ZSTDMT_JOBLOG_MAX: u32 = 30;
3939
3940/// `ZSTDMT_computeTargetJobLog` (`zstdmt_compress.c:1184`): the log of the
3941/// computed per-job input size. With LDM the windowLog is oversized, so it sizes
3942/// jobs from `cycleLog` instead (`MAX(21, cycleLog(chainLog, strat) + 3)`).
3943fn mt_target_job_log(cp: &CParams, ldm: bool) -> u32 {
3944    let job_log = if ldm {
3945        21.max(cycle_log(cp.chain_log, cp.strategy) + 3)
3946    } else {
3947        20.max(cp.window_log + 2)
3948    };
3949    job_log.min(ZSTDMT_JOBLOG_MAX)
3950}
3951
3952/// `ZSTDMT_computeOverlapSize` (`zstdmt_compress.c:1226`): how many bytes of the
3953/// previous job's tail each job loads as a raw-content prefix
3954/// (`mtctx->targetPrefixSize`). With LDM the overlap is a fraction of the
3955/// (cycleLog-based) job size rather than the oversized window — and at the
3956/// btultra2 default it is the **whole window**, so cross-job LDM offsets stay
3957/// inside `prefix ++ segment`.
3958fn mt_overlap_size(cp: &CParams, overlap_log: i32, ldm: bool) -> usize {
3959    // `ZSTDMT_overlapLog_default` (`:1198`) then `ZSTDMT_overlapLog` (`:1219`).
3960    let default_overlap_log = match cp.strategy {
3961        Strategy::Btultra2 => 9,
3962        Strategy::Btultra | Strategy::Btopt => 8,
3963        Strategy::Btlazy2 | Strategy::Lazy2 => 7,
3964        _ => 6,
3965    };
3966    let eff = if overlap_log == 0 {
3967        default_overlap_log
3968    } else {
3969        overlap_log
3970    };
3971    let overlap_rlog = 9 - eff;
3972    let ov_log = if ldm {
3973        // The LDM branch overwrites ovLog (`:1236`).
3974        (cp.window_log as i32).min(mt_target_job_log(cp, true) as i32 - 2) - overlap_rlog
3975    } else if overlap_rlog >= 8 {
3976        0
3977    } else {
3978        cp.window_log as i32 - overlap_rlog
3979    };
3980    if ov_log <= 0 { 0 } else { 1usize << ov_log }
3981}
3982
3983/// `ZSTDMT_computeTargetJobLog` + the clamps in `ZSTDMT_initCStream_internal`
3984/// (`zstdmt_compress.c:1184` / `:1266-1309`): the per-job input size
3985/// (`mtctx->targetSectionSize`).
3986fn mt_target_section_size(cp: &CParams, job_size: u64, overlap_size: usize, ldm: bool) -> usize {
3987    let mut tss = if job_size != 0 {
3988        // Explicit `ZSTD_c_jobSize`: clamped to `[MIN, MAX]` (not rounded to a
3989        // power of two, unlike the computed path).
3990        job_size.clamp(ZSTDMT_JOBSIZE_MIN, ZSTDMT_JOBSIZE_MAX) as usize
3991    } else {
3992        1usize << mt_target_job_log(cp, ldm)
3993    };
3994    // "job size must be >= overlap size" (`:1309`).
3995    if tss < overlap_size {
3996        tss = overlap_size;
3997    }
3998    tss
3999}
4000
4001/// Compress one ZSTDMT job: a fresh frame-compressor (reset entropy + repcodes,
4002/// matching C's per-job `ZSTD_compressBegin_advanced_internal`) over the
4003/// `prefix ++ segment` buffer. The first job writes the frame header (its
4004/// `pledgedSrcSize` is the whole frame: a known size one-shot, `None` for
4005/// unknown-size streaming → a windowed header); later jobs emit blocks only
4006/// (their would-be header is suppressed). Only the last job writes the epilogue.
4007///
4008/// `checksum` is the *per-job* flag: C keeps it on only for job 0 (zstdmt
4009/// `jobID != 0` clears it), so the frame header carries the checksum bit and a
4010/// **single** job appends the digest itself; multi-job frames clear it on every
4011/// job and append the whole-input digest externally (see the callers).
4012#[allow(clippy::too_many_arguments)]
4013fn compress_mt_job(
4014    out: &mut Vec<u8>,
4015    cparams: CParams,
4016    buf: &[u8],
4017    prefix_len: usize,
4018    pledged: Option<u64>,
4019    write_header: bool,
4020    is_last: bool,
4021    checksum: bool,
4022    ext_seqs: Option<Vec<crate::ldm::RawSeq>>,
4023) -> Result<(), Error> {
4024    let mut fc = FrameCompressor::from_cparams(cparams, pledged, checksum);
4025    // C disables LDM on the job itself (`jobParams.ldmParams.enableLdm =
4026    // ps_disable`, zstdmt:718); LDM candidates arrive as the pre-generated
4027    // `externSeqStore` (the serial state's per-segment `rawSeqStore`).
4028    fc.ldm = None;
4029    fc.ext_seqs = ext_seqs.map(|seqs| MtExtSeqs {
4030        seqs,
4031        cursor: crate::opt::LdmCursor::default(),
4032    });
4033    if prefix_len > 0 {
4034        prime_contiguous_prefix(&mut fc, buf, prefix_len);
4035    }
4036    if !write_header {
4037        // Non-first jobs emit blocks only — C overwrites their frame header.
4038        fc.stage = Stage::Ongoing;
4039    }
4040    let seg_start = prefix_len;
4041    let seg_end = buf.len();
4042    if is_last {
4043        fc.compress_end(out, buf, seg_start, seg_end)
4044    } else {
4045        fc.compress_continue(out, buf, seg_start, seg_end, false)
4046    }
4047}
4048
4049/// `ZSTD_compress2` with `nbWorkers >= 1` (multithreaded / job-splitting mode).
4050/// Bit-exact with C libzstd 1.5.7's MT output, reproduced **single-threaded**:
4051/// the input is split into `jobSize`-byte jobs, each compressed with the
4052/// previous job's overlap tail as a raw-content prefix and reset repcodes, then
4053/// the per-job block streams are concatenated into one frame.
4054///
4055/// Because C's MT output is deterministic and independent of the actual worker
4056/// count, `nb_workers` only selects MT-vs-single-threaded: `0` (and any input
4057/// at or below `ZSTDMT_JOBSIZE_MIN` = 512 KiB, or any input that fits a single
4058/// job) produces exactly the single-threaded [`compress`] frame. `job_size` and
4059/// `overlap_log` of `0` mean "use C's defaults".
4060///
4061/// With `checksum`, the frame carries a content checksum exactly as C's MT path
4062/// does: the digest is over the whole input (computed by the serial state), the
4063/// frame header's checksum bit is set by job 0, and for a multi-job frame the
4064/// 4-byte digest is appended once after the last job (a single job appends it
4065/// itself).
4066///
4067/// Long-distance matching is handled: one continuous `LdmState` (C's
4068/// `ZSTDMT_serialState`) over the whole input generates each job's sequences per
4069/// segment, which the job consumes as its externSeqStore. The only configuration
4070/// that returns a clean [`Error::Encode`] rather than diverging is an explicit
4071/// `overlap_log` whose overlap exceeds the indexable dictionary size
4072/// (`maxDictSize`) — the default `overlap_log` never does. No dictionary yet.
4073pub fn compress_mt(
4074    src: &[u8],
4075    level: i32,
4076    nb_workers: u32,
4077    job_size: u64,
4078    overlap_log: i32,
4079    checksum: bool,
4080) -> Result<Vec<u8>, Error> {
4081    if src.len() as u64 >= u64::from(u32::MAX) - 2 {
4082        return Err(Error::Encode("inputs >= 4 GiB are not supported yet"));
4083    }
4084    let src_len = src.len();
4085
4086    // `ZSTD_CCtx_init_compressStream2`: multithreading is not invoked when the
4087    // source is small (`pledged <= ZSTDMT_JOBSIZE_MIN`), and `nbWorkers == 0` is
4088    // plain single-threaded. Both produce the single-threaded frame.
4089    if nb_workers == 0 || src_len as u64 <= ZSTDMT_JOBSIZE_MIN {
4090        return compress_maybe_checksum(src, level, checksum);
4091    }
4092
4093    // cParams are resolved exactly as single-threaded (nbWorkers is not an
4094    // input): the whole-frame `pledgedSrcSize` is the known input size.
4095    let cparams = get_cparams(level, src_len as u64, 0);
4096
4097    // C's MT path shares one LDM state across all jobs (`ZSTDMT_serialState`):
4098    // `ZSTD_resolveEnableLdm` turns it on for btopt+ with `windowLog >= 27`
4099    // (level 22 above ~64 MiB). The job decomposition + overlap use the LDM-aware
4100    // branches, and one continuous `LdmState` generates each job's `rawSeqStore`
4101    // per segment (in the loop below), consumed by the job as its externSeqStore.
4102    let ldm_params = crate::ldm::LdmParams::auto(&cparams);
4103    let ldm_enabled = ldm_params.is_some();
4104
4105    let overlap_size = mt_overlap_size(&cparams, overlap_log, ldm_enabled);
4106    let section_size = mt_target_section_size(&cparams, job_size, overlap_size, ldm_enabled);
4107
4108    // `ZSTD_loadDictionaryContent` only indexes the *suffix* of a prefix larger
4109    // than `maxDictSize` (zstd_compress.c:4962); that truncation isn't ported.
4110    // With the default `overlapLog` the overlap is always smaller than
4111    // `maxDictSize` for every strategy, so this never fires — but an explicit
4112    // large `overlapLog` (e.g. 9 at a fast level) can exceed it, so bail cleanly
4113    // there rather than diverge.
4114    let max_dict_size = 1u64 << ((cparams.hash_log + 3).max(cparams.chain_log + 1)).min(31);
4115    if overlap_size as u64 > max_dict_size {
4116        return Err(Error::Encode(
4117            "multithreaded overlap larger than the indexable dictionary size \
4118             is not supported yet",
4119        ));
4120    }
4121
4122    // A single job (the whole input fits one section) is byte-identical to
4123    // single-threaded — including the `pledged <= 512 KiB` case handled above.
4124    if src_len < section_size {
4125        return compress_maybe_checksum(src, level, checksum);
4126    }
4127
4128    // Multi-job: split into `section_size` segments; each non-first job sees the
4129    // last `overlap_size` bytes of the previous (full) segment as a raw prefix.
4130    let mut out = Vec::with_capacity(src_len + (src_len >> 8) + 64);
4131    // One continuous LDM state over the whole input (`ZSTDMT_serialState`),
4132    // reset once; advanced + queried per segment, in job order.
4133    let mut ldm_state = ldm_params.map(crate::ldm::LdmState::new);
4134    let mut seg_start = 0usize;
4135    let mut first = true;
4136    let mut job_count = 0usize;
4137    loop {
4138        let seg_end = (seg_start + section_size).min(src_len);
4139        let seg_len = seg_end - seg_start;
4140        let more_after = seg_end < src_len;
4141        // The whole input is available at once (one-shot `e_end`), so the final
4142        // segment — full or partial — is itself the last job: C marks it
4143        // `endFrame` and writes the epilogue inline. (The separate trailing
4144        // empty-block job only arises in *streaming* mode, when an exactly
4145        // section-aligned input ends in a later call; that's a later increment.)
4146        let is_last = !more_after;
4147
4148        let prefix_len = if first {
4149            0
4150        } else {
4151            overlap_size.min(seg_start)
4152        };
4153        let buf = &src[seg_start - prefix_len..seg_end];
4154        // One-shot: the whole frame's content size is known, so job 0's pledged
4155        // is the real `src_len` (FCS header); later jobs pledge their segment.
4156        let pledged = Some(if first {
4157            src_len as u64
4158        } else {
4159            seg_len as u64
4160        });
4161        // The serial LDM state advances over this segment (the new content only,
4162        // not the carried prefix) and writes the job's `rawSeqStore`
4163        // (`ZSTDMT_serialState_genSequences`); the job consumes it as its
4164        // externSeqStore. The shared window/table persist for the next segment.
4165        let ext_seqs = if let Some(ldm) = &mut ldm_state {
4166            ldm.window.update(seg_start, seg_end);
4167            let cap = section_size / ldm.params.min_match_length as usize;
4168            let mut seqs = Vec::new();
4169            crate::ldm::generate_sequences(ldm, &mut seqs, cap, src, seg_start, seg_end)?;
4170            Some(seqs)
4171        } else {
4172            None
4173        };
4174        // C keeps the checksum flag on for job 0 only (header bit; a single job
4175        // appends the digest itself), and clears it on later jobs.
4176        compress_mt_job(
4177            &mut out,
4178            cparams,
4179            buf,
4180            prefix_len,
4181            pledged,
4182            first,
4183            is_last,
4184            checksum && first,
4185            ext_seqs,
4186        )?;
4187        job_count += 1;
4188
4189        if !more_after {
4190            break;
4191        }
4192        seg_start = seg_end;
4193        first = false;
4194    }
4195    // `ZSTDMT_flushProduced` writes the whole-input checksum after the last job
4196    // of a *multi-job* frame (a single job already appended its own).
4197    if checksum && job_count > 1 {
4198        let mut xxh = crate::xxhash::Xxh64::new(0);
4199        xxh.update(src);
4200        out.extend_from_slice(&(xxh.digest() as u32).to_le_bytes());
4201    }
4202    Ok(out)
4203}
4204
4205/// Single-threaded one-shot compression, optionally with a content checksum —
4206/// the small / single-job fallback of [`compress_mt`].
4207fn compress_maybe_checksum(src: &[u8], level: i32, checksum: bool) -> Result<Vec<u8>, Error> {
4208    if src.len() as u64 >= u64::from(u32::MAX) - 2 {
4209        return Err(Error::Encode("inputs >= 4 GiB are not supported yet"));
4210    }
4211    let mut fc = FrameCompressor::new(level, Some(src.len() as u64), checksum);
4212    let mut out = Vec::with_capacity(src.len() + (src.len() >> 8) + 64);
4213    fc.compress_end(&mut out, src, 0, src.len())?;
4214    Ok(out)
4215}
4216
4217/// `ZSTD_compress2` with `nbWorkers >= 1` **and a loaded dictionary** — the one-shot
4218/// multithreaded dict frame (`ZSTD_compress2` after `ZSTD_CCtx_loadDictionary` with
4219/// `NbWorkers` set). Like [`compress_mt`] but the dictionary is applied to **job 0
4220/// only** (zstdmt `jobs[0].cdict`); later jobs are plain overlap jobs. A one-shot
4221/// frame pledges the known `srcSize`, which — once MT engages (`> 512 KiB`) — always
4222/// exceeds every attach cutoff, so job 0 takes the CDict **copy** path
4223/// (`ZSTD_resetCCtx_byCopyingCDict`), byte-identical to the streaming copy path.
4224///
4225/// Engagement follows C's `ZSTD_CCtx_init_compressStream2`: `nbWorkers == 0` or an
4226/// input at or below `ZSTDMT_JOBSIZE_MIN` runs single-threaded with the CDict
4227/// ([`compress_with_cdict`], checksum-aware). Raw and trained (ZDICT) dictionaries are
4228/// both bit-exact on the attach and copy paths. LDM composes with the dictionary
4229/// (level 22 above 64 MiB): the serial LDM state runs over the segments without the
4230/// dict (it reaches the serial reset as `dictSize == 0`), while job 0 copies the CDict.
4231#[allow(clippy::too_many_arguments)]
4232pub fn compress_mt_with_dict(
4233    src: &[u8],
4234    dict: &[u8],
4235    level: i32,
4236    nb_workers: u32,
4237    job_size: u64,
4238    overlap_log: i32,
4239    checksum: bool,
4240) -> Result<Vec<u8>, Error> {
4241    if dict.len() as u64 + src.len() as u64 >= u64::from(u32::MAX) - 2 {
4242        return Err(Error::Encode("inputs >= 4 GiB are not supported yet"));
4243    }
4244    // `ZSTD_CCtx_init_compressStream2`: multithreading is not invoked without workers
4245    // or for an input at or below the floor — C runs single-threaded with the CDict
4246    // (attach for a tiny input, copy otherwise; both honour the checksum flag, and
4247    // both raw and trained dicts are bit-exact now that the copy-path post-block
4248    // splitter resolves from the frame cParams).
4249    if nb_workers == 0 || src.len() as u64 <= ZSTDMT_JOBSIZE_MIN {
4250        return compress_with_cdict_checksum(src, dict, level, checksum);
4251    }
4252    // MT engaged: drive the streaming job-splitter with the whole input as a single
4253    // `e_end` — exactly what one-shot `ZSTD_compress2` (= `compressStream2` + `e_end`)
4254    // does. `MtStreamState` resolves the dict-aware copy-mode frame cParams, the
4255    // section/overlap framing, job 0's CDict copy, and (when LDM auto-enables) the
4256    // shared serial LDM state feeding each job's external sequences.
4257    let mut state = MtStreamState::new(
4258        level,
4259        job_size,
4260        overlap_log,
4261        checksum,
4262        Some(src.len() as u64),
4263        Some(dict),
4264    )?;
4265    let mut out = Vec::with_capacity(src.len() + (src.len() >> 8) + 64);
4266    state.end(src, &mut out)?;
4267    Ok(out)
4268}
4269
4270/// Streaming-mode ZSTDMT (`ZSTD_compressStream2` with `nbWorkers >= 1`,
4271/// unknown content size): the input arrives in chunks, is buffered into
4272/// `section_size` jobs, and each job after the first sees the previous job's
4273/// overlap tail as a contiguous prefix — same per-job machinery as
4274/// [`compress_mt`], driven by the [`ZSTDMT_compressStream_generic`] loop.
4275///
4276/// Bounded memory: one `overlap_size + section_size` staging buffer holding the
4277/// carried prefix and the section currently filling. Job 0 writes a windowed
4278/// (unknown-size) header; later jobs emit blocks only; the last job (or, when
4279/// the input ended exactly on a section boundary, a separate empty block)
4280/// closes the frame.
4281pub(crate) struct MtStreamState {
4282    cparams: CParams,
4283    section_size: usize,
4284    overlap_size: usize,
4285    /// `[carried prefix (prefix_len)] [section being filled (filled)]`.
4286    buf: Vec<u8>,
4287    prefix_len: usize,
4288    filled: usize,
4289    first: bool,
4290    /// `ZSTD_c_checksumFlag`: the whole-input digest (C's serial-state xxh,
4291    /// updated per segment) appended after the last job of a multi-job frame.
4292    checksum: bool,
4293    xxh: crate::xxhash::Xxh64,
4294    /// Number of jobs emitted (incl. the trailing empty block); a single-job
4295    /// frame appends its own checksum so the external append is multi-job only.
4296    job_count: usize,
4297    /// The frame's `pledgedSrcSize`: `Some` for a known content size (job 0
4298    /// writes an FCS header and cParams resize to it), `None` for unknown.
4299    frame_pledged: Option<u64>,
4300    /// A dictionary applied to **job 0 only** (zstdmt `jobs[0].cdict`): the
4301    /// pre-built CDict-attach compressor and the dict content (job 0's prefix).
4302    /// `None` for no dictionary; consumed when job 0 is emitted.
4303    dict: Option<MtDictJob0>,
4304    /// Cross-job long-distance matching: one continuous `LdmState`
4305    /// (C's `ZSTDMT_serialState.ldmState`) shared across every job, generating
4306    /// each segment's raw sequences for the job to consume as its externSeqStore.
4307    /// `None` when LDM is not enabled. Composes with `dict`: the serial state is
4308    /// never seeded with the dictionary (a CDict-loaded dict reaches the serial
4309    /// reset as `dictSize == 0` — see `new`), so it runs over the segments
4310    /// dict-obliviously while job 0 separately attaches/copies the CDict.
4311    ldm_state: Option<crate::ldm::LdmState>,
4312    /// The accumulated input the serial `LdmState` reads from — C's serial
4313    /// round buffer. Holds back to `maxDist` (= the window); older bytes are
4314    /// dropped from the front (they can never be matched: `enforceMaxDist`
4315    /// filters them out). For an input that never exceeds `maxDist` this is the
4316    /// whole input so far, so `ldm_base == 0` and the LDM behaves exactly like
4317    /// the one-shot `compress_mt` (whole input in memory).
4318    ldm_history: Vec<u8>,
4319    /// Absolute whole-input position of `ldm_history[0]` (nonzero only after a
4320    /// front-drop): the serial window's `seg_bias` is `ldm_base + WINDOW_START_INDEX`
4321    /// so hash-table offsets stay in whole-input index space across drops.
4322    ldm_base: usize,
4323    /// Absolute whole-input position of the current segment's start (total bytes
4324    /// emitted in segments so far) — where the serial LDM update/generate begins.
4325    input_pos: usize,
4326}
4327
4328/// The job-0 dictionary state for [`MtStreamState`]: C applies the dictionary
4329/// (an internally-built CDict) to the first job only, so job 0 is a Path-B
4330/// **attach** over `content ++ segment0` and later jobs are plain overlap jobs.
4331struct MtDictJob0 {
4332    /// The attach compressor (frame cParams, seeded entropy/rep/dictID, attach
4333    /// window) — writes the frame header with the dictID and compresses job 0.
4334    fc0: FrameCompressor,
4335    /// The dictionary content: job 0's concat-buffer prefix.
4336    content: Vec<u8>,
4337}
4338
4339impl MtStreamState {
4340    /// Set up MT streaming for a known (`Some`) or unknown (`None`) content size,
4341    /// or a clean [`Error::Encode`] for the configurations not yet supported (the
4342    /// same LDM / `maxDictSize` gates as the one-shot [`compress_mt`]).
4343    pub(crate) fn new(
4344        level: i32,
4345        job_size: u64,
4346        overlap_log: i32,
4347        checksum: bool,
4348        frame_pledged: Option<u64>,
4349        dict: Option<&[u8]>,
4350    ) -> Result<Self, Error> {
4351        // With a loaded dictionary, C resolves the frame cParams in the mode
4352        // `ZSTD_getCParamMode` picks: `ZSTD_cpm_attachDict` when the CDict will be
4353        // **attached** (an unknown or small frame, `ZSTD_shouldAttachDict`), which
4354        // **zeroes the dict size** so the frame / section / overlap cParams — and
4355        // every job's base cParams — are the plain no-dict ones; otherwise
4356        // `ZSTD_cpm_noAttachDict` (the **copy** path: a known frame above the
4357        // strategy cutoff), which keeps the dict size, so the frame cParams account
4358        // for `srcSize + dictSize`. Job 0 attaches or copies the CDict accordingly.
4359        let attach = match dict {
4360            Some(d) => {
4361                let cdict_strategy = get_cparams_create_cdict(level, d.len() as u64).strategy;
4362                let cutoff = cdict_attach_cutoff(cdict_strategy);
4363                match frame_pledged {
4364                    Some(p) => p as usize <= cutoff,
4365                    None => true, // unknown size attaches
4366                }
4367            }
4368            None => true, // no dictionary: dict size is zero regardless
4369        };
4370        // Copy keeps the dict size (`cpm_noAttachDict`); attach / no-dict zero it.
4371        let frame_dict_size = match dict {
4372            Some(d) if !attach => d.len() as u64,
4373            _ => 0,
4374        };
4375        let cparams = get_cparams(
4376            level,
4377            frame_pledged.unwrap_or(CONTENTSIZE_UNKNOWN),
4378            frame_dict_size,
4379        );
4380        // Cross-job LDM: one continuous `LdmState` (C's `ZSTDMT_serialState`)
4381        // shared across all jobs generates each segment's sequences for the job to
4382        // consume as its externSeqStore — exactly as the one-shot `compress_mt`.
4383        // The job decomposition + overlap use the LDM-aware sizing branches.
4384        //
4385        // MT + dict + LDM is supported: a dictionary loaded via
4386        // `ZSTD_CCtx_loadDictionary` reaches ZSTDMT as a prebuilt CDict
4387        // (`mtctx->cdict`), so `ZSTDMT_initCStream_internal` passes `dict == NULL,
4388        // dictSize == 0` to `ZSTDMT_serialState_reset` (zstd_compress.c:6410). Its
4389        // dict-fill (`ZSTD_window_update` + `ZSTD_ldm_fillHashTable`, zstdmt:538) is
4390        // gated on `dictSize > 0`, so the serial LDM state is **not** seeded with the
4391        // dict — it runs over the segments exactly as the no-dict LDM case. The
4392        // dictionary affects job 0 only (the CDict attach/copy), which also consumes
4393        // its segment's external LDM sequences like any other job (zstdmt:726+751).
4394        // (Only a `refPrefix` raw dict — which we don't expose — would seed the
4395        // serial state; that path isn't ported.)
4396        let ldm_params = crate::ldm::LdmParams::auto(&cparams);
4397        let ldm_enabled = ldm_params.is_some();
4398        let overlap_size = mt_overlap_size(&cparams, overlap_log, ldm_enabled);
4399        let section_size = mt_target_section_size(&cparams, job_size, overlap_size, ldm_enabled);
4400        let max_dict_size = 1u64 << ((cparams.hash_log + 3).max(cparams.chain_log + 1)).min(31);
4401        if overlap_size as u64 > max_dict_size {
4402            return Err(Error::Encode(
4403                "multithreaded overlap larger than the indexable dictionary size \
4404                 is not supported yet",
4405            ));
4406        }
4407
4408        let dict_job0 = match dict {
4409            None => None,
4410            Some(dict) => Some(Self::build_dict_job0(
4411                dict,
4412                level,
4413                cparams.window_log,
4414                frame_pledged,
4415                checksum,
4416                attach,
4417                // `self.cparams` is the frame cParams (attach zeroes the dict size,
4418                // copy keeps it), so this is C's frame-resolved `postBlockSplitter`.
4419                crate::post_split::block_splitter_enabled(&cparams),
4420            )?),
4421        };
4422
4423        Ok(MtStreamState {
4424            cparams,
4425            section_size,
4426            overlap_size,
4427            buf: vec![0u8; overlap_size + section_size],
4428            prefix_len: 0,
4429            filled: 0,
4430            first: true,
4431            checksum,
4432            xxh: crate::xxhash::Xxh64::new(0),
4433            job_count: 0,
4434            frame_pledged,
4435            dict: dict_job0,
4436            ldm_state: ldm_params.map(crate::ldm::LdmState::new),
4437            ldm_history: Vec::new(),
4438            ldm_base: 0,
4439            input_pos: 0,
4440        })
4441    }
4442
4443    /// Prepare job 0's CDict compressor (`jobs[0].cdict`). C builds the
4444    /// dictionary's CDict with the **createCDict** cParams (`ZSTD_initLocalDict`
4445    /// → `createCDict_advanced2`); job 0 then either **attaches** it (`attach`,
4446    /// `ZSTD_resetCCtx_byAttachingCDict`: an unknown / small frame — exactly
4447    /// [`streaming_cdict_init`]'s compressor) or **copies** the de-tagged CDict
4448    /// tables into the working context (`ZSTD_resetCCtx_byCopyingCDict`: a known
4449    /// frame above the strategy cutoff — exactly [`compress_with_cdict`]'s copy
4450    /// branch, with the dict as a contiguous extDict prefix). In both cases the
4451    /// working windowLog is the frame cParams' windowLog (no-dict for attach,
4452    /// dict-aware for copy); only the section/overlap framing around it is
4453    /// MT-specific. Tiny dicts are not supported yet.
4454    fn build_dict_job0(
4455        dict: &[u8],
4456        level: i32,
4457        frame_window_log: u32,
4458        frame_pledged: Option<u64>,
4459        checksum: bool,
4460        attach: bool,
4461        post_block_splitter: bool,
4462    ) -> Result<MtDictJob0, Error> {
4463        let (content, entropy, rep, dict_id) = parse_cdict(dict)?;
4464        if content.len() <= HASH_READ_SIZE {
4465            return Err(Error::Encode(
4466                "multithreaded streaming with a <= 8-byte dictionary is not supported yet",
4467            ));
4468        }
4469        let content_len = content.len();
4470        let cdict_cparams = get_cparams_create_cdict(level, dict.len() as u64);
4471        let mut fc0 = if attach {
4472            // Job 0 attaches the CDict (createCDict cParams for its tables); the
4473            // working windowLog is the frame's (no-dict) windowLog.
4474            let mut fc0 = attach_cdict_compressor(
4475                content,
4476                cdict_cparams,
4477                frame_pledged,
4478                checksum,
4479                frame_window_log,
4480            );
4481            fc0.window = Window::streaming_attached_dict(content_len);
4482            fc0
4483        } else {
4484            // `ZSTD_resetCCtx_byCopyingCDict`: the working tables become the
4485            // de-tagged CDict tables (= a Path-A `load_dictionary` / `dtlm_full`
4486            // fill with the CDict's own cParams), the dict a contiguous extDict
4487            // prefix, and the windowLog overridden to the frame's (dict-aware)
4488            // one. Mirrors the copy branch of [`compress_with_cdict`] — trained and
4489            // raw dicts both supported (the post-block-splitter is resolved from
4490            // the frame cParams below, which was the trained-copy divergence).
4491            let mut working = cdict_cparams;
4492            working.window_log = frame_window_log;
4493            let mut fc0 = FrameCompressor::from_cparams(working, frame_pledged, checksum);
4494            match cdict_cparams.strategy {
4495                Strategy::Greedy | Strategy::Lazy | Strategy::Lazy2 | Strategy::Btlazy2 => {
4496                    let cdict_uses_row = crate::lazy::use_row_match_finder(&cdict_cparams);
4497                    let mut ctx =
4498                        crate::lazy::LazyCtx::with_row_match_finder(&working, cdict_uses_row);
4499                    ctx.use_cdict_hash_salt();
4500                    ctx.load_dictionary(content, content_len);
4501                    fc0.matcher = Matcher::Lazy(ctx);
4502                }
4503                Strategy::Btopt | Strategy::Btultra | Strategy::Btultra2 => {
4504                    if let Matcher::Opt(ctx) = &mut fc0.matcher {
4505                        ctx.load_dictionary(content, content_len);
4506                    }
4507                }
4508                _ => match &mut fc0.matcher {
4509                    Matcher::Fast(ctx) => {
4510                        fill_fast_hash_table_for_cctx_full(ctx, content, content_len)
4511                    }
4512                    Matcher::Dfast(ctx) => {
4513                        fill_dfast_hash_tables_for_cctx_full(ctx, content, content_len)
4514                    }
4515                    _ => {}
4516                },
4517            }
4518            fc0.window = Window::streaming_ext_dict(content_len);
4519            fc0
4520        };
4521        // `prevCBlock = cdict.cBlockState` on both paths.
4522        fc0.entropy = entropy;
4523        fc0.rep = rep;
4524        fc0.dict_id = dict_id;
4525        // The post-block splitter is resolved from the **frame** cParams (the
4526        // caller's `self.cparams`), not the copied CDict strategy — see the field
4527        // doc on [`FrameCompressor::post_block_splitter`] and the copy branch of
4528        // [`compress_with_cdict`].
4529        fc0.post_block_splitter = post_block_splitter;
4530        // C disables LDM on every job, including job 0 (`jobParams.ldmParams.enableLdm
4531        // = ps_disable`, zstdmt:718): when the frame enables LDM, the createCDict
4532        // cParams (≥ btopt at windowLog ≥ 27) would otherwise auto-enable it on `fc0`.
4533        // The serial `LdmState` generates job 0's sequences instead; the caller feeds
4534        // them to `fc0.ext_seqs` (the externSeqStore) in `emit`. (A no-op when the
4535        // frame has no LDM — `fc0.ldm` would already be `None`.)
4536        fc0.ldm = None;
4537        // Lazy/opt resume table insertion at the src start (attach: src begins at
4538        // `cdictEnd`; copy: at the dict/src seam — both == `dictLimit`).
4539        match &mut fc0.matcher {
4540            Matcher::Lazy(ctx) => ctx.next_to_update = fc0.window.dict_limit as usize,
4541            Matcher::Opt(ctx) => ctx.next_to_update = fc0.window.dict_limit as usize,
4542            _ => {}
4543        }
4544        Ok(MtDictJob0 {
4545            fc0,
4546            content: content.to_vec(),
4547        })
4548    }
4549
4550    /// `ZSTD_compressStream2(.., ZSTD_e_continue)`: buffer `input`, emitting a
4551    /// (non-last) job each time a full section accumulates.
4552    pub(crate) fn push(&mut self, mut input: &[u8], out: &mut Vec<u8>) -> Result<(), Error> {
4553        while !input.is_empty() {
4554            let n = (self.section_size - self.filled).min(input.len());
4555            let dst = self.prefix_len + self.filled;
4556            self.buf[dst..dst + n].copy_from_slice(&input[..n]);
4557            self.filled += n;
4558            input = &input[n..];
4559            if self.filled == self.section_size {
4560                self.emit(out, false)?;
4561            }
4562        }
4563        Ok(())
4564    }
4565
4566    /// `ZSTD_compressStream2(.., ZSTD_e_end)`: buffer `input`, emit full sections
4567    /// (non-last) while input remains, then close the frame with the final
4568    /// remainder as the last job — or, if the input ended exactly on a section
4569    /// boundary (nothing buffered), a single trailing empty block.
4570    pub(crate) fn end(&mut self, mut input: &[u8], out: &mut Vec<u8>) -> Result<(), Error> {
4571        loop {
4572            let n = (self.section_size - self.filled).min(input.len());
4573            let dst = self.prefix_len + self.filled;
4574            self.buf[dst..dst + n].copy_from_slice(&input[..n]);
4575            self.filled += n;
4576            input = &input[n..];
4577            if input.is_empty() {
4578                self.emit(out, true)?;
4579                break;
4580            }
4581            // Input still remaining ⇒ this section is exactly full: a non-last job
4582            // (C downgrades `e_end` to `e_flush` while input is left).
4583            self.emit(out, false)?;
4584        }
4585        // `ZSTDMT_flushProduced` appends the whole-input checksum after the last
4586        // job of a multi-job frame (a single job appended its own).
4587        if self.checksum && self.job_count > 1 {
4588            out.extend_from_slice(&(self.xxh.digest() as u32).to_le_bytes());
4589        }
4590        Ok(())
4591    }
4592
4593    /// `ZSTD_compressStream2(.., ZSTD_e_flush)`: emit whatever is buffered as a
4594    /// (non-last) job, leaving the frame open. C's loop creates a job for
4595    /// `endOp != continue && filled > 0`; a flush with nothing buffered is a
4596    /// no-op. The flushed segment's overlap tail still primes the next job.
4597    pub(crate) fn flush(&mut self, out: &mut Vec<u8>) -> Result<(), Error> {
4598        if self.filled > 0 {
4599            self.emit(out, false)?;
4600        }
4601        Ok(())
4602    }
4603
4604    /// Compress the buffered section as one job (or close the frame with an empty
4605    /// block), then carry its overlap tail forward as the next job's prefix.
4606    fn emit(&mut self, out: &mut Vec<u8>, is_last: bool) -> Result<(), Error> {
4607        // C's serial state accumulates the checksum over each job's segment (the
4608        // new content, not the carried prefix), in order.
4609        if self.checksum {
4610            let seg = &self.buf[self.prefix_len..self.prefix_len + self.filled];
4611            self.xxh.update(seg);
4612        }
4613        if self.filled == 0 && !self.first {
4614            // `ZSTDMT_writeLastEmptyBlock`: the input ended on a section boundary.
4615            debug_assert!(is_last);
4616            push_block_header(out, true, 0, 0);
4617            self.job_count += 1;
4618            return Ok(());
4619        }
4620        let job_filled = self.filled;
4621        // Cross-job LDM (`ZSTDMT_serialState_genSequences`, run for *every* job in
4622        // order — zstdmt:726, including a dict job 0): advance the shared serial
4623        // `LdmState` over this segment and produce the job's rawSeqStore, which the
4624        // job consumes as its externSeqStore. The LDM reads from `ldm_history` (C's
4625        // serial round buffer); `seg_bias` remaps absolute window indices to history
4626        // positions, so hash-table offsets stay in whole-input space across jobs and
4627        // front-drops. The serial state is never seeded with the dictionary (see
4628        // `new`), so a dict job 0 generates over its segment exactly like any job.
4629        let ext_seqs = if let Some(ldm) = &mut self.ldm_state {
4630            // Drop history older than `maxDist` before this segment: those bytes can
4631            // never be matched (`enforceMaxDist` filters offsets below `idx -
4632            // maxDist`), so the serial buffer stays bounded. (`ldm_history`/
4633            // `ldm_base`/`buf` are disjoint fields from the borrowed `ldm_state`, so
4634            // these stay borrow-checker clean.)
4635            let max_dist = 1usize << self.cparams.window_log;
4636            let keep_from = self.input_pos.saturating_sub(max_dist);
4637            if keep_from > self.ldm_base {
4638                self.ldm_history.drain(..keep_from - self.ldm_base);
4639                self.ldm_base = keep_from;
4640            }
4641            self.ldm_history
4642                .extend_from_slice(&self.buf[self.prefix_len..self.prefix_len + job_filled]);
4643            let seg_bias = (self.ldm_base + WINDOW_START_INDEX) as u32;
4644            ldm.window.seg_bias = seg_bias;
4645            ldm.window.dict_bias = seg_bias;
4646            let chunk_start = self.input_pos - self.ldm_base;
4647            let chunk_end = chunk_start + job_filled;
4648            let cap = self.section_size / ldm.params.min_match_length as usize;
4649            let mut seqs = Vec::new();
4650            crate::ldm::generate_sequences(
4651                ldm,
4652                &mut seqs,
4653                cap,
4654                &self.ldm_history,
4655                chunk_start,
4656                chunk_end,
4657            )?;
4658            Some(seqs)
4659        } else {
4660            None
4661        };
4662        if self.first && self.dict.is_some() {
4663            // Job 0 with a dictionary: the CDict attach/copy path over the
4664            // `content ++ segment0` concat buffer. The pre-built `fc0` writes the
4665            // frame header (with the dictID + its checksum flag) and compresses
4666            // job 0; later jobs are plain overlap jobs over `self.cparams`. With LDM
4667            // on, `fc0` consumes job 0's external sequences exactly as a regular job
4668            // (`ZSTDMT_serialState_applySequences`, zstdmt:751 — applied for job 0
4669            // too); `fc0.ldm` was disabled in `build_dict_job0`.
4670            let d = self.dict.take().expect("dict present");
4671            let cl = d.content.len();
4672            let mut data = Vec::with_capacity(cl + job_filled);
4673            data.extend_from_slice(&d.content);
4674            data.extend_from_slice(&self.buf[..job_filled]);
4675            let mut fc0 = d.fc0;
4676            fc0.ext_seqs = ext_seqs.map(|seqs| MtExtSeqs {
4677                seqs,
4678                cursor: crate::opt::LdmCursor::default(),
4679            });
4680            if is_last {
4681                fc0.compress_end(out, &data, cl, cl + job_filled)?;
4682            } else {
4683                fc0.compress_continue(out, &data, cl, cl + job_filled, false)?;
4684            }
4685        } else {
4686            let total = self.prefix_len + job_filled;
4687            // Job 0 carries the whole-frame size (a known size → FCS header sized
4688            // for the whole frame; `None` → windowed header); later jobs pledge
4689            // their own segment size (which clamps that job's window).
4690            let pledged = if self.first {
4691                self.frame_pledged
4692            } else {
4693                Some(job_filled as u64)
4694            };
4695            compress_mt_job(
4696                out,
4697                self.cparams,
4698                &self.buf[..total],
4699                self.prefix_len,
4700                pledged,
4701                self.first,
4702                is_last,
4703                // C keeps the checksum flag on for job 0 only (header bit; a single
4704                // job appends its own digest), clearing it on later jobs.
4705                self.checksum && self.first,
4706                ext_seqs,
4707            )?;
4708        }
4709        self.input_pos += job_filled;
4710        self.job_count += 1;
4711        if !is_last {
4712            // Next job's prefix = this segment's overlap tail.
4713            let new_prefix = self.overlap_size.min(job_filled);
4714            let seg_end = self.prefix_len + job_filled;
4715            self.buf.copy_within(seg_end - new_prefix..seg_end, 0);
4716            self.prefix_len = new_prefix;
4717        }
4718        self.filled = 0;
4719        self.first = false;
4720        Ok(())
4721    }
4722}
4723
4724/// Parse a CDict's dictionary buffer (`ZSTD_compress_insertDictionary` for the
4725/// CDict): a trained (`ZDICT`) dict seeds the first block's entropy tables,
4726/// repeat offsets and ID; a raw dict uses the default block state and the whole
4727/// buffer as content. Returns `(content, entropy, rep, dictID)`.
4728#[allow(clippy::type_complexity)]
4729pub(crate) fn parse_cdict(dict: &[u8]) -> Result<(&[u8], FseEntropyState, [u32; 3], u32), Error> {
4730    if dict.len() >= 8 && read32(dict, 0) == MAGIC_DICTIONARY {
4731        let seed = crate::dict_encode::load_c_entropy(dict)?;
4732        Ok((
4733            &dict[seed.entropy_size..],
4734            seed.entropy,
4735            seed.rep,
4736            seed.dict_id,
4737        ))
4738    } else {
4739        Ok((dict, FseEntropyState::new(), [1, 4, 8], 0))
4740    }
4741}
4742
4743/// Build the working [`FrameCompressor`] for the CDict ATTACH path
4744/// (`ZSTD_resetCCtx_byAttachingCDict`): working tables sized from the CDict
4745/// cParams adjusted for the source (dict zeroed by `cpm_attachDict`), windowLog
4746/// taken from the CCtx's resolved cParams (`working_window_log`), and the CDict's
4747/// own match state (`ms->dictMatchState`) built over `content`. Shared by the
4748/// one-shot [`compress_with_cdict`], the streaming attach path, and the ZSTDMT
4749/// job-0 attach (which passes the dict-aware *frame* windowLog rather than the
4750/// no-dict one). The caller arranges the history buffer with `content` as its
4751/// prefix and sets the window; `entropy`/`rep`/`dictID`
4752/// (`prevCBlock = cdict.cBlockState`) are set by the caller.
4753pub(crate) fn attach_cdict_compressor(
4754    content: &[u8],
4755    cdict_cparams: CParams,
4756    pledged: Option<u64>,
4757    checksum: bool,
4758    working_window_log: u32,
4759) -> FrameCompressor {
4760    let content_len = content.len();
4761    let src_size = pledged.unwrap_or(CONTENTSIZE_UNKNOWN);
4762    let mut working = adjust_cparams_internal(cdict_cparams, src_size, 0, CParamMode::NoAttachDict);
4763    working.window_log = working_window_log;
4764    let mut fc = FrameCompressor::from_cparams(working, pledged, checksum);
4765
4766    let mls = cdict_cparams.min_match.clamp(4, 7);
4767    fc.dict_match_state = Some(match cdict_cparams.strategy {
4768        Strategy::Greedy | Strategy::Lazy | Strategy::Lazy2 | Strategy::Btlazy2 => {
4769            // The CDict's own lazy match state (its params, salt 0), filled over
4770            // the content; consulted read-only by the dictMatchState search. The
4771            // working context keeps its own empty tables, but its backend (hash
4772            // chain / row / binary tree) must match the CDict's
4773            // (`ZSTD_resetCCtx_byAttachingCDict` overrides useRowMatchFinder;
4774            // btlazy2 is always the binary tree).
4775            let cdict_uses_row = crate::lazy::use_row_match_finder(&cdict_cparams);
4776            let mut dms =
4777                crate::lazy::LazyCtx::with_row_match_finder(&cdict_cparams, cdict_uses_row);
4778            dms.use_cdict_hash_salt();
4779            dms.load_dictionary(content, content_len);
4780            fc.matcher = Matcher::Lazy(crate::lazy::LazyCtx::with_row_match_finder(
4781                &working,
4782                cdict_uses_row,
4783            ));
4784            DictMatchState::Lazy(LazyDictMatchState {
4785                ms: Box::new(dms),
4786                content_len,
4787            })
4788        }
4789        Strategy::Btopt | Strategy::Btultra | Strategy::Btultra2 => {
4790            // The CDict's own optimal-parser binary tree (its params); the
4791            // working OptCtx keeps its own empty tree (no backend to override).
4792            let mut dms = crate::opt::OptCtx::new(&cdict_cparams);
4793            dms.load_dictionary(content, content_len);
4794            DictMatchState::Opt(OptDictMatchState {
4795                ms: Box::new(dms),
4796                content_len,
4797            })
4798        }
4799        Strategy::Dfast => {
4800            let mut hash_long = vec![0u32; 1usize << cdict_cparams.hash_log];
4801            let mut hash_small = vec![0u32; 1usize << cdict_cparams.chain_log];
4802            fill_dfast_hash_tables_for_cdict(
4803                &mut hash_long,
4804                &mut hash_small,
4805                content,
4806                content_len,
4807                cdict_cparams.hash_log,
4808                cdict_cparams.chain_log,
4809                mls,
4810            );
4811            DictMatchState::Dfast(DfastDictMatchState {
4812                hash_long,
4813                hash_small,
4814                hlog_l: cdict_cparams.hash_log,
4815                hlog_s: cdict_cparams.chain_log,
4816                content_len,
4817            })
4818        }
4819        _ => {
4820            let mut hash_table = vec![0u32; 1usize << cdict_cparams.hash_log];
4821            fill_fast_hash_table_for_cdict(
4822                &mut hash_table,
4823                content,
4824                content_len,
4825                cdict_cparams.hash_log,
4826                mls,
4827            );
4828            DictMatchState::Fast(FastDictMatchState {
4829                hash_table,
4830                hlog: cdict_cparams.hash_log,
4831                content_len,
4832            })
4833        }
4834    });
4835    fc
4836}
4837
4838/// `ZSTD_shouldAttachDict` / `attachDictSizeCutoffs`: the per-strategy
4839/// pledged-size threshold below which the CDict is attached rather than copied.
4840pub(crate) fn cdict_attach_cutoff(strategy: Strategy) -> usize {
4841    match strategy {
4842        Strategy::Greedy
4843        | Strategy::Lazy
4844        | Strategy::Lazy2
4845        | Strategy::Btlazy2
4846        | Strategy::Btopt => 32 * 1024,
4847        Strategy::Dfast => 16 * 1024,
4848        // fast, btultra, btultra2
4849        _ => 8 * 1024,
4850    }
4851}
4852
4853/// What the streaming encoder needs to drive a CDict-attach frame: the
4854/// configured compressor and the staging buffer with the dict content as its
4855/// permanent prefix (`in_buff[..content_len]`), input staged from `content_len`.
4856pub(crate) struct StreamCdictInit {
4857    pub(crate) fc: FrameCompressor,
4858    pub(crate) in_buff: Vec<u8>,
4859    pub(crate) content_len: usize,
4860}
4861
4862/// Prepare a streaming CDict-attach frame (`ZSTD_CCtx_loadDictionary` →
4863/// internal CDict → `ZSTD_resetCCtx_byAttachingCDict`, with the unknown / small
4864/// pledged size that attaches rather than copies). The dict content becomes a
4865/// permanent prefix of the staging buffer (the concat model, `dictIndexDelta
4866/// == 0`), and the window starts in the streaming-attach state so each chunk is
4867/// registered contiguously. The copy path (a large pledged size) is rejected
4868/// for now, as is a dictionary with <= 8 bytes of content.
4869pub(crate) fn streaming_cdict_init(
4870    dict: &[u8],
4871    level: i32,
4872    pledged: Option<u64>,
4873    checksum: bool,
4874) -> Result<StreamCdictInit, Error> {
4875    let (content, entropy, rep, dict_id) = parse_cdict(dict)?;
4876    let content_len = content.len();
4877    if content_len <= HASH_READ_SIZE {
4878        return Err(Error::Encode(
4879            "CDict (Path B): dictionaries with <= 8 bytes of content are not supported yet",
4880        ));
4881    }
4882    let cdict_cparams = get_cparams_create_cdict(level, dict.len() as u64);
4883    // Streaming defaults to an unknown size, which attaches. A pledged size above
4884    // the strategy cutoff would copy the dict (extDict) — not ported for streams.
4885    if pledged.is_some_and(|p| p as usize > cdict_attach_cutoff(cdict_cparams.strategy)) {
4886        return Err(Error::Encode(
4887            "streaming with a CDict above the attach cutoff (copy path) is not supported yet",
4888        ));
4889    }
4890
4891    // Standalone (CDict not loaded on the CCtx): the working windowLog is the
4892    // no-dict source cParams' (the CCtx has no dictionary of its own).
4893    let working_window_log =
4894        get_cparams(level, pledged.unwrap_or(CONTENTSIZE_UNKNOWN), 0).window_log;
4895    let mut fc = attach_cdict_compressor(
4896        content,
4897        cdict_cparams,
4898        pledged,
4899        checksum,
4900        working_window_log,
4901    );
4902    fc.window = Window::streaming_attached_dict(content_len);
4903    fc.entropy = entropy;
4904    fc.rep = rep;
4905    fc.dict_id = dict_id;
4906    // Post-block splitter resolved from the **frame** cParams (attach zeroes the
4907    // dict size), not the copied CDict strategy — see the field doc on
4908    // [`FrameCompressor::post_block_splitter`].
4909    fc.post_block_splitter = crate::post_split::block_splitter_enabled(&get_cparams(
4910        level,
4911        pledged.unwrap_or(CONTENTSIZE_UNKNOWN),
4912        0,
4913    ));
4914    // Lazy/opt resume table insertion at the src start (the first contiguous
4915    // chunk's nextToUpdate floor also does this, but set it explicitly).
4916    match &mut fc.matcher {
4917        Matcher::Lazy(ctx) => ctx.next_to_update = fc.window.dict_limit as usize,
4918        Matcher::Opt(ctx) => ctx.next_to_update = fc.window.dict_limit as usize,
4919        _ => {}
4920    }
4921
4922    let block_size = fc.block_size_max();
4923    let window_size = fc.window_size();
4924    let mut in_buff = vec![0u8; content_len + window_size + block_size];
4925    in_buff[..content_len].copy_from_slice(content);
4926    Ok(StreamCdictInit {
4927        fc,
4928        in_buff,
4929        content_len,
4930    })
4931}
4932
4933/// `ZSTD_compress_usingCDict` (what `zstd::bulk::Compressor::with_dictionary`
4934/// uses): one-shot compression with the dictionary loaded as a **CDict**
4935/// (Path B). Produces **different bytes** than [`compress_with_dict`] (Path A):
4936/// the CDict tables are filled `dtlm_full` (tagged short cache), and the working
4937/// context either **attaches** the CDict (small inputs ≤ the strategy cutoff) or
4938/// **copies** its de-tagged tables (larger inputs).
4939///
4940/// Current scope: all nine strategies, both raw and trained dictionaries, on
4941/// both the attach and copy sides of the cutoff. The only rejected
4942/// configurations are a dictionary with <= 8 bytes of content and the rare
4943/// large-window case where C would enable long-distance matching with the dict
4944/// (`windowLog >= 27` at btopt+) — both return a clean [`Error::Encode`].
4945pub fn compress_with_cdict(src: &[u8], dict: &[u8], level: i32) -> Result<Vec<u8>, Error> {
4946    compress_with_cdict_checksum(src, dict, level, false)
4947}
4948
4949/// [`compress_with_cdict`] with an optional content checksum — the single-threaded
4950/// fallback of [`compress_mt_with_dict`] (`nbWorkers == 0` or an input at or below
4951/// the MT floor, where C runs `ZSTD_compress2` + the CDict single-threaded).
4952fn compress_with_cdict_checksum(
4953    src: &[u8],
4954    dict: &[u8],
4955    level: i32,
4956    checksum: bool,
4957) -> Result<Vec<u8>, Error> {
4958    if dict.len() as u64 + src.len() as u64 >= u64::from(u32::MAX) - 2 {
4959        return Err(Error::Encode("inputs >= 4 GiB are not supported yet"));
4960    }
4961
4962    // The CDict's own cParams (`cpm_createCDict`, the *whole* dict buffer size).
4963    let cdict_cparams = get_cparams_create_cdict(level, dict.len() as u64);
4964    // All nine strategies are supported; the only unsupported configurations are
4965    // tiny dictionaries (gated below) and the rare large-window LDM-with-dict
4966    // case (gated after the context is built).
4967
4968    // Parse the dictionary (`ZSTD_compress_insertDictionary` for the CDict).
4969    let (content, entropy, rep, dict_id) = parse_cdict(dict)?;
4970
4971    let content_len = content.len();
4972    let src_len = src.len();
4973    let src_size = src_len as u64;
4974
4975    // A CDict with no usable content attaches nothing (C: "don't attach empty
4976    // dictionary"); that degenerate case isn't ported yet.
4977    if content_len <= HASH_READ_SIZE {
4978        return Err(Error::Encode(
4979            "CDict (Path B): dictionaries with <= 8 bytes of content are not supported yet",
4980        ));
4981    }
4982
4983    // `ZSTD_shouldAttachDict`: attach iff srcSize <= the strategy cutoff,
4984    // otherwise copy the dict into the context.
4985    let attach = src_len <= cdict_attach_cutoff(cdict_cparams.strategy);
4986
4987    let pledged = Some(src_size);
4988    let mut data = Vec::with_capacity(content_len + src_len);
4989    data.extend_from_slice(content);
4990    data.extend_from_slice(src);
4991
4992    let mut fc = if attach {
4993        // `ZSTD_resetCCtx_byAttachingCDict` (the dms over `content`); arrange the
4994        // window over the concatenated `content ++ src` buffer in the post-reset
4995        // attach state (src begins at `cdictEnd`, no extDict).
4996        let working_window_log = get_cparams(level, src_size, 0).window_log;
4997        let mut fc = attach_cdict_compressor(
4998            content,
4999            cdict_cparams,
5000            pledged,
5001            checksum,
5002            working_window_log,
5003        );
5004        fc.window = Window::preloaded_attached_dict(content_len, src_len);
5005        fc.window_preloaded = true;
5006        fc
5007    } else {
5008        // `ZSTD_resetCCtx_byCopyingCDict`: the window holds the dict as a prefix
5009        // (extDict on the `src` append) — i.e. the Path A flow with the CDict's
5010        // own cParams and windowLog overridden to the working context's.
5011        let mut working = cdict_cparams;
5012        working.window_log = get_cparams(level, src_size, dict.len() as u64).window_log;
5013        let mut fc = FrameCompressor::from_cparams(working, pledged, checksum);
5014        match cdict_cparams.strategy {
5015            Strategy::Greedy | Strategy::Lazy | Strategy::Lazy2 | Strategy::Btlazy2 => {
5016                // Lazy/btlazy2 tables aren't tagged (`ZSTD_CDictIndicesAreTagged`
5017                // is fast/dfast only), so the copied CDict tables equal a plain
5018                // Path A (`load_dictionary`) fill — same params, the CDict's
5019                // backend, and the CDict's salt (0, copied for the row finder).
5020                let cdict_uses_row = crate::lazy::use_row_match_finder(&cdict_cparams);
5021                let mut ctx = crate::lazy::LazyCtx::with_row_match_finder(&working, cdict_uses_row);
5022                ctx.use_cdict_hash_salt();
5023                ctx.load_dictionary(&data, content_len);
5024                fc.matcher = Matcher::Lazy(ctx);
5025            }
5026            Strategy::Btopt | Strategy::Btultra | Strategy::Btultra2 => {
5027                // btopt+ tables aren't tagged either: the copied CDict tree equals
5028                // a plain Path A `load_dictionary` fill over the working OptCtx.
5029                if let Matcher::Opt(ctx) = &mut fc.matcher {
5030                    ctx.load_dictionary(&data, content_len);
5031                }
5032            }
5033            // The de-tagged CDict fast/dfast table equals an untagged `dtlm_full`
5034            // fill, reproduced directly in the working tables.
5035            _ => match &mut fc.matcher {
5036                Matcher::Fast(ctx) => fill_fast_hash_table_for_cctx_full(ctx, &data, content_len),
5037                Matcher::Dfast(ctx) => {
5038                    fill_dfast_hash_tables_for_cctx_full(ctx, &data, content_len)
5039                }
5040                _ => {}
5041            },
5042        }
5043        fc.window = Window::preloaded_ext_dict(content_len, src_len);
5044        fc.window_preloaded = true;
5045        fc
5046    };
5047
5048    // C's `ZSTD_loadDictionaryContent` seeds the dict into the LDM tables too;
5049    // that path isn't ported, so reject the (large-window) configurations where
5050    // `ZSTD_resolveEnableLdm` turns long-distance matching on (btopt+ with
5051    // windowLog >= 27) rather than diverge. Unreachable at typical sizes.
5052    if fc.ldm.is_some() {
5053        return Err(Error::Encode(
5054            "long-distance matching with a CDict is not supported yet",
5055        ));
5056    }
5057
5058    // `prevCBlock = cdict.cBlockState` on both paths.
5059    fc.entropy = entropy;
5060    fc.rep = rep;
5061    fc.dict_id = dict_id;
5062
5063    // Lazy/opt resume table insertion at the src start; the window-preloaded path
5064    // skips compress_continue's non-contiguous `nextToUpdate = dictLimit` reset.
5065    // (Fast/dfast don't track nextToUpdate; the attach window's dictLimit is the
5066    // src start, the copy window's is the dict/src seam — both the right resume
5067    // point.)
5068    match &mut fc.matcher {
5069        Matcher::Lazy(ctx) => ctx.next_to_update = fc.window.dict_limit as usize,
5070        Matcher::Opt(ctx) => ctx.next_to_update = fc.window.dict_limit as usize,
5071        _ => {}
5072    }
5073
5074    // `ZSTD_resolveBlockSplitterMode` runs on the **frame** cParams (resolved
5075    // before the CDict copy overwrites `cParams` with the CDict's own strategy):
5076    // the copied CDict strategy — createCDict params, sized for a 513-byte hint —
5077    // can sit at/above btopt while the frame strategy (sized for the real src)
5078    // is below it, so gating on the working (CDict) strategy splits where C does
5079    // not. Resolve from the frame cParams instead (attach zeroes the dict size;
5080    // copy keeps it — exactly `ZSTD_getCParamMode`).
5081    let frame_dict_size = if attach { 0 } else { dict.len() as u64 };
5082    fc.post_block_splitter =
5083        crate::post_split::block_splitter_enabled(&get_cparams(level, src_size, frame_dict_size));
5084
5085    let mut out = Vec::with_capacity(src_len + (src_len >> 8) + 64);
5086    fc.compress_end(&mut out, &data, content_len, content_len + src_len)?;
5087    Ok(out)
5088}
5089
5090#[derive(PartialEq, Eq, Clone, Copy)]
5091enum BlockKind {
5092    Raw,
5093    Rle,
5094    Compressed,
5095}
5096
5097/// The per-strategy match-finder state held across a frame's blocks.
5098enum Matcher {
5099    Fast(FastCtx),
5100    Dfast(DfastCtx),
5101    Lazy(crate::lazy::LazyCtx),
5102    Opt(Box<crate::opt::OptCtx>),
5103}