structured_zstd/encoding/parameters.rs
1//! Fine-grained compression parameters — the drop-in equivalent of C
2//! zstd's advanced `ZSTD_CCtx_setParameter` surface.
3//!
4//! [`CompressionLevel`] selects a
5//! whole tuning preset in one knob. This module exposes the individual
6//! knobs underneath it — window/hash/chain/search logs, the match
7//! strategy, and the long-distance-matching (LDM) block — so callers
8//! can override a level's defaults for domain-specific tuning.
9//!
10//! # Builder
11//!
12//! [`CompressionParameters`] is built through
13//! [`CompressionParameters::builder`], which takes an explicit base
14//! [`CompressionLevel`] (there is no
15//! implicit default). Every knob left unset inherits that base level's
16//! resolved value, so a builder that overrides nothing reproduces plain
17//! level-based compression byte-for-byte.
18//!
19//! ```rust
20//! use structured_zstd::encoding::{CompressionLevel, CompressionParameters, Strategy};
21//!
22//! let params = CompressionParameters::builder(CompressionLevel::Level(19))
23//! .window_log(22)
24//! .strategy(Strategy::Btultra2)
25//! .enable_long_distance_matching(true)
26//! .build()
27//! .expect("parameters within bounds");
28//! ```
29//!
30//! # Bounds
31//!
32//! Every knob has an inclusive `[lower, upper]` range, queryable via
33//! [`CParameter::bounds`] (the analogue of `ZSTD_cParam_getBounds`).
34//! [`CompressionParametersBuilder::build`] validates each set knob and
35//! returns [`ParameterError::OutOfBounds`] for the first violation.
36//!
37//! # Long-distance matching (LDM)
38//!
39//! LDM is **off at every [`CompressionLevel`] preset**, matching upstream `libzstd.so.1` where `ZSTD_compress(..., level)`
40//! never enables LDM — even at level 22. It is activated either by
41//! [`CompressionParametersBuilder::enable_long_distance_matching`] or by any of
42//! the `ldm_*` setters, which each imply `enable_long_distance_matching(true)`.
43//! It works under every [`Strategy`]: the optimal parsers weigh its matches as
44//! candidates, the others take them outright and search only the bytes between
45//! them, as upstream does. A dictionary loaded under it is indexed for it too.
46
47use crate::encoding::CompressionLevel;
48
49/// Match-finder strategy — the drop-in equivalent of C zstd's
50/// `ZSTD_strategy` enum (`ZSTD_fast` … `ZSTD_btultra2`). The numeric
51/// ordinals match upstream (`fast = 1` … `btultra2 = 9`), so
52/// [`Strategy::ordinal`] / [`Strategy::from_ordinal`] round-trip with
53/// the C `ZSTD_c_strategy` parameter value.
54#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
55pub enum Strategy {
56 /// `ZSTD_fast` (1) — single-table fast finder.
57 Fast,
58 /// `ZSTD_dfast` (2) — two parallel hash tables.
59 Dfast,
60 /// `ZSTD_greedy` (3) — commit the first acceptable match, no lookahead.
61 Greedy,
62 /// `ZSTD_lazy` (4) — one-position lazy lookahead.
63 Lazy,
64 /// `ZSTD_lazy2` (5) — two-position lazy lookahead.
65 Lazy2,
66 /// `ZSTD_btlazy2` (6) — binary-tree-assisted lazy2.
67 Btlazy2,
68 /// `ZSTD_btopt` (7) — optimal parser, no ultra refinements.
69 Btopt,
70 /// `ZSTD_btultra` (8) — optimal parser with refined price tables.
71 Btultra,
72 /// `ZSTD_btultra2` (9) — optimal parser with two-pass dynamic stats.
73 Btultra2,
74}
75
76impl Strategy {
77 /// Upstream `ZSTD_strategy` ordinal (`fast = 1` … `btultra2 = 9`).
78 pub const fn ordinal(self) -> u32 {
79 match self {
80 Self::Fast => 1,
81 Self::Dfast => 2,
82 Self::Greedy => 3,
83 Self::Lazy => 4,
84 Self::Lazy2 => 5,
85 Self::Btlazy2 => 6,
86 Self::Btopt => 7,
87 Self::Btultra => 8,
88 Self::Btultra2 => 9,
89 }
90 }
91
92 /// Construct from an upstream `ZSTD_strategy` ordinal. Returns
93 /// `None` outside `1..=9`.
94 pub const fn from_ordinal(ordinal: u32) -> Option<Self> {
95 Some(match ordinal {
96 1 => Self::Fast,
97 2 => Self::Dfast,
98 3 => Self::Greedy,
99 4 => Self::Lazy,
100 5 => Self::Lazy2,
101 6 => Self::Btlazy2,
102 7 => Self::Btopt,
103 8 => Self::Btultra,
104 9 => Self::Btultra2,
105 _ => return None,
106 })
107 }
108
109 /// Internal runtime strategy tag.
110 pub(crate) const fn tag(self) -> crate::encoding::strategy::StrategyTag {
111 use crate::encoding::strategy::StrategyTag;
112 match self {
113 Self::Fast => StrategyTag::Fast,
114 Self::Dfast => StrategyTag::Dfast,
115 Self::Greedy => StrategyTag::Greedy,
116 // Lazy / Lazy2 ride the runtime `Lazy` tag (the lazy lookahead
117 // depth carries the variance, see `lazy_depth`). `Btlazy2`
118 // keeps its own tag: `Lazy` resolves to the Row finder, while
119 // btlazy2 is a binary-tree search and must stay on the
120 // HashChain/BT storage.
121 Self::Lazy | Self::Lazy2 => StrategyTag::Lazy,
122 Self::Btlazy2 => StrategyTag::Btlazy2,
123 Self::Btopt => StrategyTag::BtOpt,
124 Self::Btultra => StrategyTag::BtUltra,
125 Self::Btultra2 => StrategyTag::BtUltra2,
126 }
127 }
128
129 /// Lazy lookahead depth for the greedy/lazy band (0/1/2). `Optimal`
130 /// strategies report 2 (the depth their hash-chain seed walk runs at).
131 pub(crate) const fn lazy_depth(self) -> u8 {
132 match self {
133 Self::Fast | Self::Dfast | Self::Greedy => 0,
134 Self::Lazy => 1,
135 _ => 2,
136 }
137 }
138}
139
140/// Whether literals are entropy-coded, the drop-in equivalent of C zstd's
141/// `ZSTD_c_literalCompressionMode` (`ZSTD_ps_auto` / `ZSTD_ps_enable` /
142/// `ZSTD_ps_disable`).
143#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
144pub enum LiteralCompressionMode {
145 /// The level decides: literals are stored raw on the fast strategy's
146 /// acceleration (negative) levels, where the Huffman pass costs more
147 /// speed than it saves, and compressed everywhere else.
148 #[default]
149 Auto,
150 /// Compress literals on every level, the negative ones included. A
151 /// block whose literals do not shrink still stores them raw.
152 Enable,
153 /// Never compress literals: every literal section is stored raw.
154 Disable,
155}
156
157/// Which finder the greedy, lazy and lazy2 strategies search, the drop-in
158/// equivalent of C zstd's `ZSTD_c_useRowMatchFinder` (`ZSTD_ps_auto` /
159/// `ZSTD_ps_enable` / `ZSTD_ps_disable`). The other strategies have a finder
160/// of their own and ignore it.
161#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
162pub enum RowMatchFinderMode {
163 /// Rows above a 16 KiB window, a hash chain at or below it, where the rows
164 /// cost more to set up than they save (upstream
165 /// `ZSTD_resolveRowMatchFinderMode`, zstd_compress.c:246).
166 #[default]
167 Auto,
168 /// Search rows at every window size.
169 Enable,
170 /// Search a hash chain at every window size.
171 Disable,
172}
173
174impl RowMatchFinderMode {
175 /// Whether a greedy / lazy / lazy2 frame with a `window`-byte window
176 /// searches rows.
177 pub(crate) const fn searches_rows(self, window: usize) -> bool {
178 match self {
179 Self::Auto => window > (1 << 14),
180 Self::Enable => true,
181 Self::Disable => false,
182 }
183 }
184}
185
186/// One tunable compression parameter — the analogue of a C zstd
187/// `ZSTD_cParameter`. Used to query bounds via [`CParameter::bounds`].
188#[derive(Copy, Clone, Debug, PartialEq, Eq)]
189#[non_exhaustive]
190pub enum CParameter {
191 /// Maximum back-reference distance, `log2`. C `ZSTD_c_windowLog`.
192 WindowLog,
193 /// Match-finder hash table size, `log2`. C `ZSTD_c_hashLog`.
194 HashLog,
195 /// Match-finder chain table size, `log2`. C `ZSTD_c_chainLog`.
196 ChainLog,
197 /// Number of search attempts, `log2`. C `ZSTD_c_searchLog`.
198 SearchLog,
199 /// Minimum match length in bytes. C `ZSTD_c_minMatch`.
200 MinMatch,
201 /// "Good enough" match length that ends the search. C `ZSTD_c_targetLength`.
202 TargetLength,
203 /// Match-finder [`Strategy`] (1..=9). C `ZSTD_c_strategy`.
204 Strategy,
205 /// LDM enable flag (0/1). C `ZSTD_c_enableLongDistanceMatching`.
206 EnableLongDistanceMatching,
207 /// LDM hash table size, `log2`. C `ZSTD_c_ldmHashLog`.
208 LdmHashLog,
209 /// LDM minimum match length in bytes. C `ZSTD_c_ldmMinMatch`.
210 LdmMinMatch,
211 /// LDM bucket size, `log2`. C `ZSTD_c_ldmBucketSizeLog`.
212 LdmBucketSizeLog,
213 /// LDM hash-insertion rate, `log2`. C `ZSTD_c_ldmHashRateLog`.
214 LdmHashRateLog,
215}
216
217/// Inclusive `[lower_bound, upper_bound]` range for a [`CParameter`],
218/// the drop-in equivalent of C zstd's `ZSTD_bounds`.
219#[derive(Copy, Clone, Debug, PartialEq, Eq)]
220pub struct Bounds {
221 /// Smallest accepted value (inclusive).
222 pub lower_bound: i64,
223 /// Largest accepted value (inclusive).
224 pub upper_bound: i64,
225}
226
227impl Bounds {
228 /// Whether `value` falls within `[lower_bound, upper_bound]`.
229 pub const fn contains(&self, value: i64) -> bool {
230 value >= self.lower_bound && value <= self.upper_bound
231 }
232
233 /// `value` moved to the nearest end of the range when it falls outside.
234 ///
235 /// ```
236 /// use structured_zstd::encoding::CParameter;
237 /// assert_eq!(CParameter::MinMatch.bounds().clamp(9), 7);
238 /// assert_eq!(CParameter::MinMatch.bounds().clamp(5), 5);
239 /// ```
240 pub const fn clamp(&self, value: i64) -> i64 {
241 if value < self.lower_bound {
242 self.lower_bound
243 } else if value > self.upper_bound {
244 self.upper_bound
245 } else {
246 value
247 }
248 }
249}
250
251impl CParameter {
252 /// Inclusive value bounds for this parameter, mirroring
253 /// `ZSTD_cParam_getBounds`. Window/hash/chain logs cap at 30 (the
254 /// encoder's match-finder ceiling) rather than the 31 C allows on
255 /// 64-bit, because the back-reference history is indexed with `u32`
256 /// positions over a `2 * window` eviction band.
257 pub const fn bounds(self) -> Bounds {
258 let (lower_bound, upper_bound) = match self {
259 // ZSTD_WINDOWLOG_MIN .. encoder ceiling.
260 Self::WindowLog => (10, 30),
261 // ZSTD_HASHLOG_MIN .. ZSTD_HASHLOG_MAX.
262 Self::HashLog => (6, 30),
263 // ZSTD_CHAINLOG_MIN .. ZSTD_CHAINLOG_MAX (64-bit).
264 Self::ChainLog => (6, 30),
265 // ZSTD_SEARCHLOG_MIN .. ZSTD_SEARCHLOG_MAX (64-bit).
266 Self::SearchLog => (1, 30),
267 // ZSTD_MINMATCH_MIN .. ZSTD_MINMATCH_MAX.
268 Self::MinMatch => (3, 7),
269 // ZSTD_TARGETLENGTH_MIN .. ZSTD_TARGETLENGTH_MAX.
270 Self::TargetLength => (0, 131_072),
271 // ZSTD_fast .. ZSTD_btultra2.
272 Self::Strategy => (1, 9),
273 // Boolean flag.
274 Self::EnableLongDistanceMatching => (0, 1),
275 // ZSTD_LDM_HASHLOG_MIN .. ZSTD_LDM_HASHLOG_MAX.
276 Self::LdmHashLog => (6, 30),
277 // ZSTD_LDM_MINMATCH_MIN .. ZSTD_LDM_MINMATCH_MAX.
278 Self::LdmMinMatch => (4, 4096),
279 // ZSTD_LDM_BUCKETSIZELOG_MIN .. ZSTD_LDM_BUCKETSIZELOG_MAX.
280 Self::LdmBucketSizeLog => (1, 8),
281 // ZSTD_LDM_HASHRATELOG_MIN .. ZSTD_WINDOWLOG_MAX - ZSTD_HASHLOG_MIN.
282 Self::LdmHashRateLog => (0, 24),
283 };
284 Bounds {
285 lower_bound,
286 upper_bound,
287 }
288 }
289}
290
291/// The match-finder parameters a numeric level selects for a source of a given
292/// size, the drop-in equivalent of C zstd's `ZSTD_getCParams`.
293///
294/// The size matters: the reference's level table has a row per source-size
295/// tier, and the tier changes the strategy as well as the table widths, so one
296/// level can run a different match-finder on a small input than on a large
297/// one. This is the selection the encoder itself makes for a frame without a
298/// dictionary; knobs set through [`CompressionParameters`] override it.
299///
300/// # Examples
301///
302/// ```
303/// use structured_zstd::encoding::{LevelParameters, Strategy};
304///
305/// // Level 11 is a lazy2 level on a large or unknown-size source...
306/// let large = LevelParameters::for_level(11, None, 0);
307/// assert_eq!(large.strategy, Strategy::Lazy2);
308///
309/// // ...and the optimal parser on one of 16 KiB or less, whose window is
310/// // also cut down to the source.
311/// let small = LevelParameters::for_level(11, Some(4096), 0);
312/// assert_eq!(small.strategy, Strategy::Btopt);
313/// assert_eq!(small.window_log, 12);
314/// ```
315#[derive(Copy, Clone, Debug, PartialEq, Eq)]
316pub struct LevelParameters {
317 /// Back-reference window, `log2`. C `windowLog`.
318 pub window_log: u32,
319 /// Chain / binary-tree table size, `log2`. C `chainLog`.
320 pub chain_log: u32,
321 /// Hash table size, `log2`. C `hashLog`.
322 pub hash_log: u32,
323 /// Search attempts per position, `log2`. C `searchLog`.
324 pub search_log: u32,
325 /// Match-finder hash width, in bytes. C `minMatch`.
326 pub min_match: u32,
327 /// Length that ends a search early; on a negative level, the step size
328 /// minus one. C `targetLength`.
329 pub target_length: u32,
330 /// The match-finder strategy. C `strategy`.
331 pub strategy: Strategy,
332}
333
334impl LevelParameters {
335 /// Parameters for `level` compressing a source of `source_size` bytes with
336 /// a `dictionary_size`-byte dictionary (`0` for none).
337 ///
338 /// `level` is on the reference's scale: `0` is the default level, levels
339 /// above [`CompressionLevel::MAX_LEVEL`] clamp to it, and a negative level
340 /// is an acceleration factor. `None` is a source of unknown size, which is
341 /// sized as a large one; `Some(0)` is a source that is really empty.
342 /// C `ZSTD_getCParams` spells unknown as `0`, so a caller porting from it
343 /// maps `0` to `None`.
344 pub fn for_level(level: i32, source_size: Option<u64>, dictionary_size: usize) -> Self {
345 let size = source_size.unwrap_or(crate::encoding::cparams::CONTENTSIZE_UNKNOWN);
346 let cp = crate::encoding::cparams::get_cparams(level, size, dictionary_size);
347 Self {
348 window_log: cp.window_log,
349 chain_log: cp.chain_log,
350 hash_log: cp.hash_log,
351 search_log: cp.search_log,
352 min_match: cp.min_match,
353 target_length: cp.target_length,
354 // Every row of the level table names one of the nine strategies.
355 strategy: Strategy::from_ordinal(cp.strategy)
356 .expect("the level table only holds strategies 1..=9"),
357 }
358 }
359
360 /// These parameters fitted to a source of `source_size` bytes and a
361 /// `dictionary_size`-byte dictionary, the drop-in equivalent of C zstd's
362 /// `ZSTD_adjustCParams`: a window wider than the data comes down to it, and
363 /// the tables shrink with the window. `None` is a source of unknown size,
364 /// which leaves the tables alone.
365 ///
366 /// Every field must already lie within its [`CParameter::bounds`].
367 ///
368 /// # Examples
369 ///
370 /// ```
371 /// use structured_zstd::encoding::LevelParameters;
372 ///
373 /// let level_19 = LevelParameters::for_level(19, None, 0);
374 /// let small = level_19.adjusted(Some(1000), 0);
375 /// assert_eq!(small.window_log, 10);
376 /// assert!(small.hash_log <= 11);
377 /// assert_eq!(level_19.adjusted(None, 0), level_19);
378 /// ```
379 pub fn adjusted(self, source_size: Option<u64>, dictionary_size: usize) -> Self {
380 let cp = crate::encoding::cparams::CParams {
381 window_log: self.window_log,
382 chain_log: self.chain_log,
383 hash_log: self.hash_log,
384 search_log: self.search_log,
385 min_match: self.min_match,
386 target_length: self.target_length,
387 strategy: self.strategy.ordinal(),
388 };
389 let size = source_size.unwrap_or(crate::encoding::cparams::CONTENTSIZE_UNKNOWN);
390 let cp = crate::encoding::cparams::adjust_cparams(cp, size, dictionary_size, false);
391 Self {
392 window_log: cp.window_log,
393 chain_log: cp.chain_log,
394 hash_log: cp.hash_log,
395 search_log: cp.search_log,
396 min_match: cp.min_match,
397 target_length: cp.target_length,
398 strategy: self.strategy,
399 }
400 }
401}
402
403/// Error returned by [`CompressionParametersBuilder::build`] when a knob
404/// is set outside its [`CParameter::bounds`].
405#[derive(Copy, Clone, Debug, PartialEq, Eq)]
406#[non_exhaustive]
407pub enum ParameterError {
408 /// A parameter was set to a value outside its inclusive bounds.
409 OutOfBounds {
410 /// Which parameter violated its range.
411 parameter: CParameter,
412 /// The rejected value.
413 value: i64,
414 /// The inclusive `[lower, upper]` range it had to fall within.
415 bounds: Bounds,
416 },
417}
418
419impl core::fmt::Display for ParameterError {
420 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
421 match self {
422 Self::OutOfBounds {
423 parameter,
424 value,
425 bounds,
426 } => write!(
427 f,
428 "compression parameter {parameter:?} = {value} out of bounds \
429 [{}, {}]",
430 bounds.lower_bound, bounds.upper_bound
431 ),
432 }
433 }
434}
435
436#[cfg(feature = "std")]
437impl std::error::Error for ParameterError {}
438
439/// LDM tuning overrides — every knob is `Option`, falling back to the
440/// strategy-derived upstream zstd default (`LdmParams::adjust_for`) when unset.
441#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
442pub(crate) struct LdmOverride {
443 pub(crate) hash_log: Option<u32>,
444 pub(crate) min_match: Option<u32>,
445 pub(crate) bucket_size_log: Option<u32>,
446 pub(crate) hash_rate_log: Option<u32>,
447}
448
449/// Internal per-knob override set consumed by the match-generator's
450/// `reset` path. Every field left `None` inherits the base level's
451/// resolved value, so the default path is byte-identical to level-based
452/// compression.
453#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
454pub(crate) struct ParamOverrides {
455 pub(crate) window_log: Option<u8>,
456 pub(crate) hash_log: Option<u32>,
457 pub(crate) chain_log: Option<u32>,
458 pub(crate) search_log: Option<u32>,
459 pub(crate) min_match: Option<u32>,
460 pub(crate) target_length: Option<u32>,
461 pub(crate) strategy: Option<Strategy>,
462 /// `Some` when `enable_long_distance_matching(true)` was set; carries
463 /// the (possibly empty) LDM knob overrides.
464 pub(crate) ldm: Option<LdmOverride>,
465 /// Whether literals are entropy-coded; `Auto` leaves it to the level.
466 pub(crate) literal_compression: LiteralCompressionMode,
467 /// The greedy / lazy band's finder; `Auto` leaves it to the window.
468 pub(crate) row_match_finder: RowMatchFinderMode,
469}
470
471impl ParamOverrides {
472 /// Whether any knob overrides the base level. An all-`None`
473 /// override is a no-op the `reset` path can skip entirely, keeping
474 /// the default level-based geometry byte-identical.
475 pub(crate) fn is_empty(&self) -> bool {
476 self.window_log.is_none()
477 && self.hash_log.is_none()
478 && self.chain_log.is_none()
479 && self.search_log.is_none()
480 && self.min_match.is_none()
481 && self.target_length.is_none()
482 && self.strategy.is_none()
483 && self.ldm.is_none()
484 && self.literal_compression == LiteralCompressionMode::Auto
485 && self.row_match_finder == RowMatchFinderMode::Auto
486 }
487}
488
489/// Fully-resolved fine-grained compression parameters. Build through
490/// [`CompressionParameters::builder`]; pass to
491/// [`FrameCompressor::set_parameters`](crate::encoding::FrameCompressor::set_parameters)
492/// or [`compress_with_parameters`](crate::encoding::compress_with_parameters).
493///
494/// Wraps a base [`CompressionLevel`] plus the set of knobs that override it. A parameter set that
495/// overrides nothing is equivalent to compressing at its base level.
496#[derive(Copy, Clone, Debug, PartialEq, Eq)]
497pub struct CompressionParameters {
498 level: CompressionLevel,
499 overrides: ParamOverrides,
500}
501
502impl CompressionParameters {
503 /// Start a builder from a base compression level. Knobs left unset
504 /// inherit that level's resolved defaults.
505 pub fn builder(level: CompressionLevel) -> CompressionParametersBuilder {
506 CompressionParametersBuilder {
507 level,
508 window_log: None,
509 hash_log: None,
510 chain_log: None,
511 search_log: None,
512 min_match: None,
513 target_length: None,
514 strategy: None,
515 enable_ldm: false,
516 ldm: LdmOverride::default(),
517 literal_compression: LiteralCompressionMode::Auto,
518 row_match_finder: RowMatchFinderMode::Auto,
519 }
520 }
521
522 /// The base compression level these parameters override.
523 pub fn level(&self) -> CompressionLevel {
524 self.level
525 }
526
527 /// Whether long-distance matching is enabled.
528 pub fn long_distance_matching_enabled(&self) -> bool {
529 self.overrides.ldm.is_some()
530 }
531
532 /// How literals are entropy-coded (see [`LiteralCompressionMode`]).
533 pub fn literal_compression_mode(&self) -> LiteralCompressionMode {
534 self.overrides.literal_compression
535 }
536
537 /// Which finder the greedy / lazy band searches (see [`RowMatchFinderMode`]).
538 pub fn row_match_finder_mode(&self) -> RowMatchFinderMode {
539 self.overrides.row_match_finder
540 }
541
542 /// The window, `log2`, the encoder gives a frame of `source_size` bytes
543 /// (`None` = unknown) with `dictionary_size` bytes of dictionary content
544 /// under these parameters: resolved exactly as the encoder resolves it.
545 ///
546 /// ```
547 /// use structured_zstd::encoding::{CompressionLevel, CompressionParameters};
548 ///
549 /// let params = CompressionParameters::builder(CompressionLevel::Level(19))
550 /// .window_log(23)
551 /// .build()
552 /// .unwrap();
553 /// assert_eq!(params.window_log_for(None, 0), 23);
554 /// assert!(params.window_log_for(Some(1000), 0) < 23);
555 /// ```
556 pub fn window_log_for(&self, source_size: Option<u64>, dictionary_size: usize) -> u32 {
557 let sizes = crate::encoding::DictionarySizes {
558 content: dictionary_size,
559 serialized: dictionary_size,
560 };
561 let params =
562 crate::encoding::levels::config::frame_level_params(self, source_size, Some(sizes));
563 u32::from(params.window_log)
564 }
565
566 /// These parameters as a dictionary's own, driving a frame of
567 /// `source_size` bytes (`None` = unknown): an explicit window is raised to
568 /// cover the source, as far as 512 KiB, as upstream raises a prepared
569 /// dictionary's window for a frame of known size
570 /// (`ZSTD_compressBegin_usingCDict_internal`, zstd_compress.c:5844-5852).
571 /// A window left to the level is resolved for the source and covers it.
572 ///
573 /// ```
574 /// use structured_zstd::encoding::{CompressionLevel, CompressionParameters};
575 ///
576 /// let params = CompressionParameters::builder(CompressionLevel::Level(3))
577 /// .window_log(10)
578 /// .build()
579 /// .unwrap();
580 /// let source = Some(1 << 20);
581 /// assert_eq!(params.for_dictionary_frame(source).window_log_for(source, 0), 19);
582 /// assert_eq!(params.for_dictionary_frame(None).window_log_for(None, 0), 10);
583 /// ```
584 pub fn for_dictionary_frame(mut self, source_size: Option<u64>) -> Self {
585 const RAISE_LIMIT: u64 = 1 << 19;
586 let known =
587 source_size.filter(|size| *size != crate::encoding::cparams::CONTENTSIZE_UNKNOWN);
588 if let (Some(window), Some(source)) = (self.overrides.window_log, known) {
589 // Bounded by `RAISE_LIMIT`, so it fits and its log is at most 19.
590 let limited = source.min(RAISE_LIMIT) as u32;
591 let source_log = if limited > 1 {
592 (u32::BITS - (limited - 1).leading_zeros()) as u8
593 } else {
594 1
595 };
596 self.overrides.window_log = Some(window.max(source_log));
597 }
598 self
599 }
600
601 pub(crate) fn overrides(&self) -> ParamOverrides {
602 self.overrides
603 }
604}
605
606/// Builder for [`CompressionParameters`]. Each setter records one knob;
607/// [`Self::build`] validates them against [`CParameter::bounds`].
608#[derive(Copy, Clone, Debug)]
609pub struct CompressionParametersBuilder {
610 level: CompressionLevel,
611 window_log: Option<u32>,
612 hash_log: Option<u32>,
613 chain_log: Option<u32>,
614 search_log: Option<u32>,
615 min_match: Option<u32>,
616 target_length: Option<u32>,
617 strategy: Option<Strategy>,
618 enable_ldm: bool,
619 ldm: LdmOverride,
620 literal_compression: LiteralCompressionMode,
621 row_match_finder: RowMatchFinderMode,
622}
623
624impl CompressionParametersBuilder {
625 /// Choose the finder the greedy, lazy and lazy2 strategies search instead
626 /// of leaving it to the window size. C `ZSTD_c_useRowMatchFinder`.
627 ///
628 /// ```rust
629 /// use structured_zstd::encoding::{
630 /// compress_with_parameters, CompressionLevel, CompressionParameters,
631 /// RowMatchFinderMode,
632 /// };
633 /// use structured_zstd::decoding::FrameDecoder;
634 ///
635 /// let data = b"a row or a chain, the frame decodes the same ".repeat(2000);
636 /// let chain = CompressionParameters::builder(CompressionLevel::Level(7))
637 /// .row_match_finder(RowMatchFinderMode::Disable)
638 /// .build()
639 /// .unwrap();
640 /// let frame = compress_with_parameters(&data[..], &chain);
641 /// let mut decoded = Vec::with_capacity(data.len());
642 /// FrameDecoder::new()
643 /// .decode_all_to_vec(&frame, &mut decoded)
644 /// .unwrap();
645 /// assert_eq!(decoded, data);
646 /// ```
647 pub fn row_match_finder(mut self, mode: RowMatchFinderMode) -> Self {
648 self.row_match_finder = mode;
649 self
650 }
651
652 /// Decide whether literals are entropy-coded, overriding the level's own
653 /// choice. C `ZSTD_c_literalCompressionMode`.
654 ///
655 /// ```rust
656 /// use structured_zstd::encoding::{
657 /// compress_with_parameters, CompressionLevel, CompressionParameters,
658 /// LiteralCompressionMode,
659 /// };
660 ///
661 /// // Literal-heavy input: 32 distinct symbols and nothing for the match
662 /// // finder to repeat, so only entropy-coding the literals shrinks it.
663 /// let text: Vec<u8> = (0..8192u32)
664 /// .map(|i| b'a' + (i.wrapping_mul(2_654_435_761) >> 27) as u8)
665 /// .collect();
666 /// // Level -3 stores literals raw by default; asking for compression
667 /// // makes the frame smaller on input like this.
668 /// let compressed_literals = CompressionParameters::builder(CompressionLevel::Level(-3))
669 /// .literal_compression(LiteralCompressionMode::Enable)
670 /// .build()
671 /// .unwrap();
672 /// let plain = compress_with_parameters(
673 /// &text[..],
674 /// &CompressionParameters::builder(CompressionLevel::Level(-3)).build().unwrap(),
675 /// );
676 /// let coded = compress_with_parameters(&text[..], &compressed_literals);
677 /// assert!(coded.len() < plain.len());
678 /// ```
679 pub fn literal_compression(mut self, mode: LiteralCompressionMode) -> Self {
680 self.literal_compression = mode;
681 self
682 }
683
684 /// Override the maximum back-reference distance (`log2`). C
685 /// `ZSTD_c_windowLog`.
686 pub fn window_log(mut self, value: u32) -> Self {
687 self.window_log = Some(value);
688 self
689 }
690
691 /// Override the match-finder hash table size (`log2`). C `ZSTD_c_hashLog`.
692 pub fn hash_log(mut self, value: u32) -> Self {
693 self.hash_log = Some(value);
694 self
695 }
696
697 /// Override the match-finder chain table size (`log2`). C `ZSTD_c_chainLog`.
698 pub fn chain_log(mut self, value: u32) -> Self {
699 self.chain_log = Some(value);
700 self
701 }
702
703 /// Override the search-attempts count (`log2`). C `ZSTD_c_searchLog`.
704 pub fn search_log(mut self, value: u32) -> Self {
705 self.search_log = Some(value);
706 self
707 }
708
709 /// Override the minimum match length in bytes. C `ZSTD_c_minMatch`.
710 pub fn min_match(mut self, value: u32) -> Self {
711 self.min_match = Some(value);
712 self
713 }
714
715 /// Override the "good enough" target match length. C `ZSTD_c_targetLength`.
716 pub fn target_length(mut self, value: u32) -> Self {
717 self.target_length = Some(value);
718 self
719 }
720
721 /// Override the match-finder [`Strategy`]. C `ZSTD_c_strategy`.
722 pub fn strategy(mut self, value: Strategy) -> Self {
723 self.strategy = Some(value);
724 self
725 }
726
727 /// Enable or disable long-distance matching. C
728 /// `ZSTD_c_enableLongDistanceMatching`. Off at every level preset.
729 /// This is the explicit activation toggle; the `ldm_*` knob setters
730 /// also enable LDM implicitly. The flag is plain last-write-wins, so
731 /// a trailing `enable_long_distance_matching(false)` disables LDM even
732 /// if an earlier `ldm_*` call set a knob (the knob is then ignored at
733 /// [`build`](Self::build)).
734 pub fn enable_long_distance_matching(mut self, enable: bool) -> Self {
735 self.enable_ldm = enable;
736 self
737 }
738
739 /// Override the LDM hash table size (`log2`). C `ZSTD_c_ldmHashLog`.
740 /// Implies [`Self::enable_long_distance_matching(true)`](Self::enable_long_distance_matching).
741 pub fn ldm_hash_log(mut self, value: u32) -> Self {
742 self.enable_ldm = true;
743 self.ldm.hash_log = Some(value);
744 self
745 }
746
747 /// Override the LDM minimum match length. C `ZSTD_c_ldmMinMatch`.
748 /// Implies [`Self::enable_long_distance_matching(true)`](Self::enable_long_distance_matching).
749 pub fn ldm_min_match(mut self, value: u32) -> Self {
750 self.enable_ldm = true;
751 self.ldm.min_match = Some(value);
752 self
753 }
754
755 /// Override the LDM bucket size (`log2`). C `ZSTD_c_ldmBucketSizeLog`.
756 /// Implies [`Self::enable_long_distance_matching(true)`](Self::enable_long_distance_matching).
757 pub fn ldm_bucket_size_log(mut self, value: u32) -> Self {
758 self.enable_ldm = true;
759 self.ldm.bucket_size_log = Some(value);
760 self
761 }
762
763 /// Override the LDM hash-insertion rate (`log2`). C `ZSTD_c_ldmHashRateLog`.
764 /// Implies [`Self::enable_long_distance_matching(true)`](Self::enable_long_distance_matching).
765 pub fn ldm_hash_rate_log(mut self, value: u32) -> Self {
766 self.enable_ldm = true;
767 self.ldm.hash_rate_log = Some(value);
768 self
769 }
770
771 /// Validate every set knob against [`CParameter::bounds`] and
772 /// produce the resolved [`CompressionParameters`].
773 ///
774 /// # Errors
775 ///
776 /// Returns [`ParameterError::OutOfBounds`] for the first knob whose
777 /// value falls outside its inclusive range.
778 pub fn build(self) -> Result<CompressionParameters, ParameterError> {
779 check(CParameter::WindowLog, self.window_log)?;
780 check(CParameter::HashLog, self.hash_log)?;
781 check(CParameter::ChainLog, self.chain_log)?;
782 check(CParameter::SearchLog, self.search_log)?;
783 check(CParameter::MinMatch, self.min_match)?;
784 check(CParameter::TargetLength, self.target_length)?;
785 if let Some(s) = self.strategy {
786 check(CParameter::Strategy, Some(s.ordinal()))?;
787 }
788 let ldm = if self.enable_ldm {
789 check(CParameter::LdmHashLog, self.ldm.hash_log)?;
790 check(CParameter::LdmMinMatch, self.ldm.min_match)?;
791 check(CParameter::LdmBucketSizeLog, self.ldm.bucket_size_log)?;
792 check(CParameter::LdmHashRateLog, self.ldm.hash_rate_log)?;
793 Some(self.ldm)
794 } else {
795 None
796 };
797 Ok(CompressionParameters {
798 level: self.level,
799 overrides: ParamOverrides {
800 // `window_log` is bounds-checked at <= 30, so the cast is lossless.
801 window_log: self.window_log.map(|v| v as u8),
802 hash_log: self.hash_log,
803 chain_log: self.chain_log,
804 search_log: self.search_log,
805 min_match: self.min_match,
806 target_length: self.target_length,
807 strategy: self.strategy,
808 ldm,
809 literal_compression: self.literal_compression,
810 row_match_finder: self.row_match_finder,
811 },
812 })
813 }
814}
815
816/// Validate one optional knob against its bounds.
817fn check(parameter: CParameter, value: Option<u32>) -> Result<(), ParameterError> {
818 if let Some(value) = value {
819 let bounds = parameter.bounds();
820 let value = i64::from(value);
821 if !bounds.contains(value) {
822 return Err(ParameterError::OutOfBounds {
823 parameter,
824 value,
825 bounds,
826 });
827 }
828 }
829 Ok(())
830}
831
832#[cfg(test)]
833mod tests;