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}