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//! When enabled, the LDM producer attaches to the optimal (`btopt` / `btultra`
44//! / `btultra2`) match-finder; pair it with an optimal [`Strategy`] (or a level
45//! ≥ 16) for it to take effect.
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/// One tunable compression parameter — the analogue of a C zstd
158/// `ZSTD_cParameter`. Used to query bounds via [`CParameter::bounds`].
159#[derive(Copy, Clone, Debug, PartialEq, Eq)]
160#[non_exhaustive]
161pub enum CParameter {
162 /// Maximum back-reference distance, `log2`. C `ZSTD_c_windowLog`.
163 WindowLog,
164 /// Match-finder hash table size, `log2`. C `ZSTD_c_hashLog`.
165 HashLog,
166 /// Match-finder chain table size, `log2`. C `ZSTD_c_chainLog`.
167 ChainLog,
168 /// Number of search attempts, `log2`. C `ZSTD_c_searchLog`.
169 SearchLog,
170 /// Minimum match length in bytes. C `ZSTD_c_minMatch`.
171 MinMatch,
172 /// "Good enough" match length that ends the search. C `ZSTD_c_targetLength`.
173 TargetLength,
174 /// Match-finder [`Strategy`] (1..=9). C `ZSTD_c_strategy`.
175 Strategy,
176 /// LDM enable flag (0/1). C `ZSTD_c_enableLongDistanceMatching`.
177 EnableLongDistanceMatching,
178 /// LDM hash table size, `log2`. C `ZSTD_c_ldmHashLog`.
179 LdmHashLog,
180 /// LDM minimum match length in bytes. C `ZSTD_c_ldmMinMatch`.
181 LdmMinMatch,
182 /// LDM bucket size, `log2`. C `ZSTD_c_ldmBucketSizeLog`.
183 LdmBucketSizeLog,
184 /// LDM hash-insertion rate, `log2`. C `ZSTD_c_ldmHashRateLog`.
185 LdmHashRateLog,
186}
187
188/// Inclusive `[lower_bound, upper_bound]` range for a [`CParameter`],
189/// the drop-in equivalent of C zstd's `ZSTD_bounds`.
190#[derive(Copy, Clone, Debug, PartialEq, Eq)]
191pub struct Bounds {
192 /// Smallest accepted value (inclusive).
193 pub lower_bound: i64,
194 /// Largest accepted value (inclusive).
195 pub upper_bound: i64,
196}
197
198impl Bounds {
199 /// Whether `value` falls within `[lower_bound, upper_bound]`.
200 pub const fn contains(&self, value: i64) -> bool {
201 value >= self.lower_bound && value <= self.upper_bound
202 }
203}
204
205impl CParameter {
206 /// Inclusive value bounds for this parameter, mirroring
207 /// `ZSTD_cParam_getBounds`. Window/hash/chain logs cap at 30 (the
208 /// encoder's match-finder ceiling) rather than the 31 C allows on
209 /// 64-bit, because the back-reference history is indexed with `u32`
210 /// positions over a `2 * window` eviction band.
211 pub const fn bounds(self) -> Bounds {
212 let (lower_bound, upper_bound) = match self {
213 // ZSTD_WINDOWLOG_MIN .. encoder ceiling.
214 Self::WindowLog => (10, 30),
215 // ZSTD_HASHLOG_MIN .. ZSTD_HASHLOG_MAX.
216 Self::HashLog => (6, 30),
217 // ZSTD_CHAINLOG_MIN .. ZSTD_CHAINLOG_MAX (64-bit).
218 Self::ChainLog => (6, 30),
219 // ZSTD_SEARCHLOG_MIN .. ZSTD_SEARCHLOG_MAX (64-bit).
220 Self::SearchLog => (1, 30),
221 // ZSTD_MINMATCH_MIN .. ZSTD_MINMATCH_MAX.
222 Self::MinMatch => (3, 7),
223 // ZSTD_TARGETLENGTH_MIN .. ZSTD_TARGETLENGTH_MAX.
224 Self::TargetLength => (0, 131_072),
225 // ZSTD_fast .. ZSTD_btultra2.
226 Self::Strategy => (1, 9),
227 // Boolean flag.
228 Self::EnableLongDistanceMatching => (0, 1),
229 // ZSTD_LDM_HASHLOG_MIN .. ZSTD_LDM_HASHLOG_MAX.
230 Self::LdmHashLog => (6, 30),
231 // ZSTD_LDM_MINMATCH_MIN .. ZSTD_LDM_MINMATCH_MAX.
232 Self::LdmMinMatch => (4, 4096),
233 // ZSTD_LDM_BUCKETSIZELOG_MIN .. ZSTD_LDM_BUCKETSIZELOG_MAX.
234 Self::LdmBucketSizeLog => (1, 8),
235 // ZSTD_LDM_HASHRATELOG_MIN .. ZSTD_WINDOWLOG_MAX - ZSTD_HASHLOG_MIN.
236 Self::LdmHashRateLog => (0, 24),
237 };
238 Bounds {
239 lower_bound,
240 upper_bound,
241 }
242 }
243}
244
245/// The match-finder parameters a numeric level selects for a source of a given
246/// size, the drop-in equivalent of C zstd's `ZSTD_getCParams`.
247///
248/// The size matters: the reference's level table has a row per source-size
249/// tier, and the tier changes the strategy as well as the table widths, so one
250/// level can run a different match-finder on a small input than on a large
251/// one. This is the selection the encoder itself makes for a frame without a
252/// dictionary; knobs set through [`CompressionParameters`] override it.
253///
254/// # Examples
255///
256/// ```
257/// use structured_zstd::encoding::{LevelParameters, Strategy};
258///
259/// // Level 11 is a lazy2 level on a large or unknown-size source...
260/// let large = LevelParameters::for_level(11, None, 0);
261/// assert_eq!(large.strategy, Strategy::Lazy2);
262///
263/// // ...and the optimal parser on one of 16 KiB or less, whose window is
264/// // also cut down to the source.
265/// let small = LevelParameters::for_level(11, Some(4096), 0);
266/// assert_eq!(small.strategy, Strategy::Btopt);
267/// assert_eq!(small.window_log, 12);
268/// ```
269#[derive(Copy, Clone, Debug, PartialEq, Eq)]
270pub struct LevelParameters {
271 /// Back-reference window, `log2`. C `windowLog`.
272 pub window_log: u32,
273 /// Chain / binary-tree table size, `log2`. C `chainLog`.
274 pub chain_log: u32,
275 /// Hash table size, `log2`. C `hashLog`.
276 pub hash_log: u32,
277 /// Search attempts per position, `log2`. C `searchLog`.
278 pub search_log: u32,
279 /// Match-finder hash width, in bytes. C `minMatch`.
280 pub min_match: u32,
281 /// Length that ends a search early; on a negative level, the step size
282 /// minus one. C `targetLength`.
283 pub target_length: u32,
284 /// The match-finder strategy. C `strategy`.
285 pub strategy: Strategy,
286}
287
288impl LevelParameters {
289 /// Parameters for `level` compressing a source of `source_size` bytes with
290 /// a `dictionary_size`-byte dictionary (`0` for none).
291 ///
292 /// `level` is on the reference's scale: `0` is the default level, levels
293 /// above [`CompressionLevel::MAX_LEVEL`] clamp to it, and a negative level
294 /// is an acceleration factor. `None` is a source of unknown size, which is
295 /// sized as a large one; `Some(0)` is a source that is really empty.
296 /// C `ZSTD_getCParams` spells unknown as `0`, so a caller porting from it
297 /// maps `0` to `None`.
298 pub fn for_level(level: i32, source_size: Option<u64>, dictionary_size: usize) -> Self {
299 let size = source_size.unwrap_or(crate::encoding::cparams::CONTENTSIZE_UNKNOWN);
300 let cp = crate::encoding::cparams::get_cparams(level, size, dictionary_size);
301 Self {
302 window_log: cp.window_log,
303 chain_log: cp.chain_log,
304 hash_log: cp.hash_log,
305 search_log: cp.search_log,
306 min_match: cp.min_match,
307 target_length: cp.target_length,
308 // Every row of the level table names one of the nine strategies.
309 strategy: Strategy::from_ordinal(cp.strategy)
310 .expect("the level table only holds strategies 1..=9"),
311 }
312 }
313}
314
315/// Error returned by [`CompressionParametersBuilder::build`] when a knob
316/// is set outside its [`CParameter::bounds`].
317#[derive(Copy, Clone, Debug, PartialEq, Eq)]
318#[non_exhaustive]
319pub enum ParameterError {
320 /// A parameter was set to a value outside its inclusive bounds.
321 OutOfBounds {
322 /// Which parameter violated its range.
323 parameter: CParameter,
324 /// The rejected value.
325 value: i64,
326 /// The inclusive `[lower, upper]` range it had to fall within.
327 bounds: Bounds,
328 },
329}
330
331impl core::fmt::Display for ParameterError {
332 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
333 match self {
334 Self::OutOfBounds {
335 parameter,
336 value,
337 bounds,
338 } => write!(
339 f,
340 "compression parameter {parameter:?} = {value} out of bounds \
341 [{}, {}]",
342 bounds.lower_bound, bounds.upper_bound
343 ),
344 }
345 }
346}
347
348#[cfg(feature = "std")]
349impl std::error::Error for ParameterError {}
350
351/// LDM tuning overrides — every knob is `Option`, falling back to the
352/// strategy-derived upstream zstd default (`LdmParams::adjust_for`) when unset.
353#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
354pub(crate) struct LdmOverride {
355 pub(crate) hash_log: Option<u32>,
356 pub(crate) min_match: Option<u32>,
357 pub(crate) bucket_size_log: Option<u32>,
358 pub(crate) hash_rate_log: Option<u32>,
359}
360
361/// Internal per-knob override set consumed by the match-generator's
362/// `reset` path. Every field left `None` inherits the base level's
363/// resolved value, so the default path is byte-identical to level-based
364/// compression.
365#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
366pub(crate) struct ParamOverrides {
367 pub(crate) window_log: Option<u8>,
368 pub(crate) hash_log: Option<u32>,
369 pub(crate) chain_log: Option<u32>,
370 pub(crate) search_log: Option<u32>,
371 pub(crate) min_match: Option<u32>,
372 pub(crate) target_length: Option<u32>,
373 pub(crate) strategy: Option<Strategy>,
374 /// `Some` when `enable_long_distance_matching(true)` was set; carries
375 /// the (possibly empty) LDM knob overrides.
376 pub(crate) ldm: Option<LdmOverride>,
377 /// Whether literals are entropy-coded; `Auto` leaves it to the level.
378 pub(crate) literal_compression: LiteralCompressionMode,
379}
380
381impl ParamOverrides {
382 /// Whether any knob overrides the base level. An all-`None`
383 /// override is a no-op the `reset` path can skip entirely, keeping
384 /// the default level-based geometry byte-identical.
385 pub(crate) fn is_empty(&self) -> bool {
386 self.window_log.is_none()
387 && self.hash_log.is_none()
388 && self.chain_log.is_none()
389 && self.search_log.is_none()
390 && self.min_match.is_none()
391 && self.target_length.is_none()
392 && self.strategy.is_none()
393 && self.ldm.is_none()
394 && self.literal_compression == LiteralCompressionMode::Auto
395 }
396}
397
398/// Fully-resolved fine-grained compression parameters. Build through
399/// [`CompressionParameters::builder`]; pass to
400/// [`FrameCompressor::set_parameters`](crate::encoding::FrameCompressor::set_parameters)
401/// or [`compress_with_parameters`](crate::encoding::compress_with_parameters).
402///
403/// Wraps a base [`CompressionLevel`] plus the set of knobs that override it. A parameter set that
404/// overrides nothing is equivalent to compressing at its base level.
405#[derive(Copy, Clone, Debug, PartialEq, Eq)]
406pub struct CompressionParameters {
407 level: CompressionLevel,
408 overrides: ParamOverrides,
409}
410
411impl CompressionParameters {
412 /// Start a builder from a base compression level. Knobs left unset
413 /// inherit that level's resolved defaults.
414 pub fn builder(level: CompressionLevel) -> CompressionParametersBuilder {
415 CompressionParametersBuilder {
416 level,
417 window_log: None,
418 hash_log: None,
419 chain_log: None,
420 search_log: None,
421 min_match: None,
422 target_length: None,
423 strategy: None,
424 enable_ldm: false,
425 ldm: LdmOverride::default(),
426 literal_compression: LiteralCompressionMode::Auto,
427 }
428 }
429
430 /// The base compression level these parameters override.
431 pub fn level(&self) -> CompressionLevel {
432 self.level
433 }
434
435 /// Whether long-distance matching is enabled.
436 pub fn long_distance_matching_enabled(&self) -> bool {
437 self.overrides.ldm.is_some()
438 }
439
440 /// How literals are entropy-coded (see [`LiteralCompressionMode`]).
441 pub fn literal_compression_mode(&self) -> LiteralCompressionMode {
442 self.overrides.literal_compression
443 }
444
445 pub(crate) fn overrides(&self) -> ParamOverrides {
446 self.overrides
447 }
448}
449
450/// Builder for [`CompressionParameters`]. Each setter records one knob;
451/// [`Self::build`] validates them against [`CParameter::bounds`].
452#[derive(Copy, Clone, Debug)]
453pub struct CompressionParametersBuilder {
454 level: CompressionLevel,
455 window_log: Option<u32>,
456 hash_log: Option<u32>,
457 chain_log: Option<u32>,
458 search_log: Option<u32>,
459 min_match: Option<u32>,
460 target_length: Option<u32>,
461 strategy: Option<Strategy>,
462 enable_ldm: bool,
463 ldm: LdmOverride,
464 literal_compression: LiteralCompressionMode,
465}
466
467impl CompressionParametersBuilder {
468 /// Decide whether literals are entropy-coded, overriding the level's own
469 /// choice. C `ZSTD_c_literalCompressionMode`.
470 ///
471 /// ```rust
472 /// use structured_zstd::encoding::{
473 /// compress_with_parameters, CompressionLevel, CompressionParameters,
474 /// LiteralCompressionMode,
475 /// };
476 ///
477 /// // Literal-heavy input: 32 distinct symbols and nothing for the match
478 /// // finder to repeat, so only entropy-coding the literals shrinks it.
479 /// let text: Vec<u8> = (0..8192u32)
480 /// .map(|i| b'a' + (i.wrapping_mul(2_654_435_761) >> 27) as u8)
481 /// .collect();
482 /// // Level -3 stores literals raw by default; asking for compression
483 /// // makes the frame smaller on input like this.
484 /// let compressed_literals = CompressionParameters::builder(CompressionLevel::Level(-3))
485 /// .literal_compression(LiteralCompressionMode::Enable)
486 /// .build()
487 /// .unwrap();
488 /// let plain = compress_with_parameters(
489 /// &text[..],
490 /// &CompressionParameters::builder(CompressionLevel::Level(-3)).build().unwrap(),
491 /// );
492 /// let coded = compress_with_parameters(&text[..], &compressed_literals);
493 /// assert!(coded.len() < plain.len());
494 /// ```
495 pub fn literal_compression(mut self, mode: LiteralCompressionMode) -> Self {
496 self.literal_compression = mode;
497 self
498 }
499
500 /// Override the maximum back-reference distance (`log2`). C
501 /// `ZSTD_c_windowLog`.
502 pub fn window_log(mut self, value: u32) -> Self {
503 self.window_log = Some(value);
504 self
505 }
506
507 /// Override the match-finder hash table size (`log2`). C `ZSTD_c_hashLog`.
508 pub fn hash_log(mut self, value: u32) -> Self {
509 self.hash_log = Some(value);
510 self
511 }
512
513 /// Override the match-finder chain table size (`log2`). C `ZSTD_c_chainLog`.
514 pub fn chain_log(mut self, value: u32) -> Self {
515 self.chain_log = Some(value);
516 self
517 }
518
519 /// Override the search-attempts count (`log2`). C `ZSTD_c_searchLog`.
520 pub fn search_log(mut self, value: u32) -> Self {
521 self.search_log = Some(value);
522 self
523 }
524
525 /// Override the minimum match length in bytes. C `ZSTD_c_minMatch`.
526 pub fn min_match(mut self, value: u32) -> Self {
527 self.min_match = Some(value);
528 self
529 }
530
531 /// Override the "good enough" target match length. C `ZSTD_c_targetLength`.
532 pub fn target_length(mut self, value: u32) -> Self {
533 self.target_length = Some(value);
534 self
535 }
536
537 /// Override the match-finder [`Strategy`]. C `ZSTD_c_strategy`.
538 pub fn strategy(mut self, value: Strategy) -> Self {
539 self.strategy = Some(value);
540 self
541 }
542
543 /// Enable or disable long-distance matching. C
544 /// `ZSTD_c_enableLongDistanceMatching`. Off at every level preset.
545 /// This is the explicit activation toggle; the `ldm_*` knob setters
546 /// also enable LDM implicitly. The flag is plain last-write-wins, so
547 /// a trailing `enable_long_distance_matching(false)` disables LDM even
548 /// if an earlier `ldm_*` call set a knob (the knob is then ignored at
549 /// [`build`](Self::build)).
550 pub fn enable_long_distance_matching(mut self, enable: bool) -> Self {
551 self.enable_ldm = enable;
552 self
553 }
554
555 /// Override the LDM hash table size (`log2`). C `ZSTD_c_ldmHashLog`.
556 /// Implies [`Self::enable_long_distance_matching(true)`](Self::enable_long_distance_matching).
557 pub fn ldm_hash_log(mut self, value: u32) -> Self {
558 self.enable_ldm = true;
559 self.ldm.hash_log = Some(value);
560 self
561 }
562
563 /// Override the LDM minimum match length. C `ZSTD_c_ldmMinMatch`.
564 /// Implies [`Self::enable_long_distance_matching(true)`](Self::enable_long_distance_matching).
565 pub fn ldm_min_match(mut self, value: u32) -> Self {
566 self.enable_ldm = true;
567 self.ldm.min_match = Some(value);
568 self
569 }
570
571 /// Override the LDM bucket size (`log2`). C `ZSTD_c_ldmBucketSizeLog`.
572 /// Implies [`Self::enable_long_distance_matching(true)`](Self::enable_long_distance_matching).
573 pub fn ldm_bucket_size_log(mut self, value: u32) -> Self {
574 self.enable_ldm = true;
575 self.ldm.bucket_size_log = Some(value);
576 self
577 }
578
579 /// Override the LDM hash-insertion rate (`log2`). C `ZSTD_c_ldmHashRateLog`.
580 /// Implies [`Self::enable_long_distance_matching(true)`](Self::enable_long_distance_matching).
581 pub fn ldm_hash_rate_log(mut self, value: u32) -> Self {
582 self.enable_ldm = true;
583 self.ldm.hash_rate_log = Some(value);
584 self
585 }
586
587 /// Validate every set knob against [`CParameter::bounds`] and
588 /// produce the resolved [`CompressionParameters`].
589 ///
590 /// # Errors
591 ///
592 /// Returns [`ParameterError::OutOfBounds`] for the first knob whose
593 /// value falls outside its inclusive range.
594 pub fn build(self) -> Result<CompressionParameters, ParameterError> {
595 check(CParameter::WindowLog, self.window_log)?;
596 check(CParameter::HashLog, self.hash_log)?;
597 check(CParameter::ChainLog, self.chain_log)?;
598 check(CParameter::SearchLog, self.search_log)?;
599 check(CParameter::MinMatch, self.min_match)?;
600 check(CParameter::TargetLength, self.target_length)?;
601 if let Some(s) = self.strategy {
602 check(CParameter::Strategy, Some(s.ordinal()))?;
603 }
604 let ldm = if self.enable_ldm {
605 check(CParameter::LdmHashLog, self.ldm.hash_log)?;
606 check(CParameter::LdmMinMatch, self.ldm.min_match)?;
607 check(CParameter::LdmBucketSizeLog, self.ldm.bucket_size_log)?;
608 check(CParameter::LdmHashRateLog, self.ldm.hash_rate_log)?;
609 Some(self.ldm)
610 } else {
611 None
612 };
613 Ok(CompressionParameters {
614 level: self.level,
615 overrides: ParamOverrides {
616 // `window_log` is bounds-checked at <= 30, so the cast is lossless.
617 window_log: self.window_log.map(|v| v as u8),
618 hash_log: self.hash_log,
619 chain_log: self.chain_log,
620 search_log: self.search_log,
621 min_match: self.min_match,
622 target_length: self.target_length,
623 strategy: self.strategy,
624 ldm,
625 literal_compression: self.literal_compression,
626 },
627 })
628 }
629}
630
631/// Validate one optional knob against its bounds.
632fn check(parameter: CParameter, value: Option<u32>) -> Result<(), ParameterError> {
633 if let Some(value) = value {
634 let bounds = parameter.bounds();
635 let value = i64::from(value);
636 if !bounds.contains(value) {
637 return Err(ParameterError::OutOfBounds {
638 parameter,
639 value,
640 bounds,
641 });
642 }
643 }
644 Ok(())
645}
646
647#[cfg(test)]
648mod tests;