structured_zstd/encoding/parameters.rs
1//! Fine-grained compression parameters — the drop-in equivalent of C
2//! zstd's advanced `ZSTD_CCtx_setParameter` surface (#27).
3//!
4//! [`CompressionLevel`](crate::encoding::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`](crate::encoding::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`](crate::encoding::CompressionLevel)
40//! preset**, matching upstream `libzstd.so.1` where `ZSTD_compress(..., level)`
41//! never enables LDM — even at level 22. It is activated either by
42//! [`CompressionParametersBuilder::enable_long_distance_matching`] or by any of
43//! the `ldm_*` setters, which each imply `enable_long_distance_matching(true)`.
44//! When enabled, the LDM producer attaches to the optimal (`btopt` / `btultra`
45//! / `btultra2`) match-finder; pair it with an optimal [`Strategy`] (or a level
46//! ≥ 16) for it to take effect.
47
48use crate::encoding::CompressionLevel;
49
50/// Match-finder strategy — the drop-in equivalent of C zstd's
51/// `ZSTD_strategy` enum (`ZSTD_fast` … `ZSTD_btultra2`). The numeric
52/// ordinals match upstream (`fast = 1` … `btultra2 = 9`), so
53/// [`Strategy::ordinal`] / [`Strategy::from_ordinal`] round-trip with
54/// the C `ZSTD_c_strategy` parameter value.
55#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
56pub enum Strategy {
57 /// `ZSTD_fast` (1) — single-table fast finder.
58 Fast,
59 /// `ZSTD_dfast` (2) — two parallel hash tables.
60 Dfast,
61 /// `ZSTD_greedy` (3) — commit the first acceptable match, no lookahead.
62 Greedy,
63 /// `ZSTD_lazy` (4) — one-position lazy lookahead.
64 Lazy,
65 /// `ZSTD_lazy2` (5) — two-position lazy lookahead.
66 Lazy2,
67 /// `ZSTD_btlazy2` (6) — binary-tree-assisted lazy2.
68 Btlazy2,
69 /// `ZSTD_btopt` (7) — optimal parser, no ultra refinements.
70 Btopt,
71 /// `ZSTD_btultra` (8) — optimal parser with refined price tables.
72 Btultra,
73 /// `ZSTD_btultra2` (9) — optimal parser with two-pass dynamic stats.
74 Btultra2,
75}
76
77impl Strategy {
78 /// Upstream `ZSTD_strategy` ordinal (`fast = 1` … `btultra2 = 9`).
79 pub const fn ordinal(self) -> u32 {
80 match self {
81 Self::Fast => 1,
82 Self::Dfast => 2,
83 Self::Greedy => 3,
84 Self::Lazy => 4,
85 Self::Lazy2 => 5,
86 Self::Btlazy2 => 6,
87 Self::Btopt => 7,
88 Self::Btultra => 8,
89 Self::Btultra2 => 9,
90 }
91 }
92
93 /// Construct from an upstream `ZSTD_strategy` ordinal. Returns
94 /// `None` outside `1..=9`.
95 pub const fn from_ordinal(ordinal: u32) -> Option<Self> {
96 Some(match ordinal {
97 1 => Self::Fast,
98 2 => Self::Dfast,
99 3 => Self::Greedy,
100 4 => Self::Lazy,
101 5 => Self::Lazy2,
102 6 => Self::Btlazy2,
103 7 => Self::Btopt,
104 8 => Self::Btultra,
105 9 => Self::Btultra2,
106 _ => return None,
107 })
108 }
109
110 /// Internal runtime strategy tag.
111 pub(crate) const fn tag(self) -> crate::encoding::strategy::StrategyTag {
112 use crate::encoding::strategy::StrategyTag;
113 match self {
114 Self::Fast => StrategyTag::Fast,
115 Self::Dfast => StrategyTag::Dfast,
116 Self::Greedy => StrategyTag::Greedy,
117 // Lazy / Lazy2 ride the runtime `Lazy` tag (the lazy lookahead
118 // depth carries the variance, see `lazy_depth`). `Btlazy2`
119 // keeps its own tag: `Lazy` resolves to the Row finder, while
120 // btlazy2 is a binary-tree search and must stay on the
121 // HashChain/BT storage.
122 Self::Lazy | Self::Lazy2 => StrategyTag::Lazy,
123 Self::Btlazy2 => StrategyTag::Btlazy2,
124 Self::Btopt => StrategyTag::BtOpt,
125 Self::Btultra => StrategyTag::BtUltra,
126 Self::Btultra2 => StrategyTag::BtUltra2,
127 }
128 }
129
130 /// Lazy lookahead depth for the greedy/lazy band (0/1/2). `Optimal`
131 /// strategies report 2 (the depth their hash-chain seed walk runs at).
132 pub(crate) const fn lazy_depth(self) -> u8 {
133 match self {
134 Self::Fast | Self::Dfast | Self::Greedy => 0,
135 Self::Lazy => 1,
136 _ => 2,
137 }
138 }
139}
140
141/// One tunable compression parameter — the analogue of a C zstd
142/// `ZSTD_cParameter`. Used to query bounds via [`CParameter::bounds`].
143#[derive(Copy, Clone, Debug, PartialEq, Eq)]
144#[non_exhaustive]
145pub enum CParameter {
146 /// Maximum back-reference distance, `log2`. C `ZSTD_c_windowLog`.
147 WindowLog,
148 /// Match-finder hash table size, `log2`. C `ZSTD_c_hashLog`.
149 HashLog,
150 /// Match-finder chain table size, `log2`. C `ZSTD_c_chainLog`.
151 ChainLog,
152 /// Number of search attempts, `log2`. C `ZSTD_c_searchLog`.
153 SearchLog,
154 /// Minimum match length in bytes. C `ZSTD_c_minMatch`.
155 MinMatch,
156 /// "Good enough" match length that ends the search. C `ZSTD_c_targetLength`.
157 TargetLength,
158 /// Match-finder [`Strategy`] (1..=9). C `ZSTD_c_strategy`.
159 Strategy,
160 /// LDM enable flag (0/1). C `ZSTD_c_enableLongDistanceMatching`.
161 EnableLongDistanceMatching,
162 /// LDM hash table size, `log2`. C `ZSTD_c_ldmHashLog`.
163 LdmHashLog,
164 /// LDM minimum match length in bytes. C `ZSTD_c_ldmMinMatch`.
165 LdmMinMatch,
166 /// LDM bucket size, `log2`. C `ZSTD_c_ldmBucketSizeLog`.
167 LdmBucketSizeLog,
168 /// LDM hash-insertion rate, `log2`. C `ZSTD_c_ldmHashRateLog`.
169 LdmHashRateLog,
170}
171
172/// Inclusive `[lower_bound, upper_bound]` range for a [`CParameter`],
173/// the drop-in equivalent of C zstd's `ZSTD_bounds`.
174#[derive(Copy, Clone, Debug, PartialEq, Eq)]
175pub struct Bounds {
176 /// Smallest accepted value (inclusive).
177 pub lower_bound: i64,
178 /// Largest accepted value (inclusive).
179 pub upper_bound: i64,
180}
181
182impl Bounds {
183 /// Whether `value` falls within `[lower_bound, upper_bound]`.
184 pub const fn contains(&self, value: i64) -> bool {
185 value >= self.lower_bound && value <= self.upper_bound
186 }
187}
188
189impl CParameter {
190 /// Inclusive value bounds for this parameter, mirroring
191 /// `ZSTD_cParam_getBounds`. Window/hash/chain logs cap at 30 (the
192 /// encoder's match-finder ceiling) rather than the 31 C allows on
193 /// 64-bit, because the back-reference history is indexed with `u32`
194 /// positions over a `2 * window` eviction band.
195 pub const fn bounds(self) -> Bounds {
196 let (lower_bound, upper_bound) = match self {
197 // ZSTD_WINDOWLOG_MIN .. encoder ceiling.
198 Self::WindowLog => (10, 30),
199 // ZSTD_HASHLOG_MIN .. ZSTD_HASHLOG_MAX.
200 Self::HashLog => (6, 30),
201 // ZSTD_CHAINLOG_MIN .. ZSTD_CHAINLOG_MAX (64-bit).
202 Self::ChainLog => (6, 30),
203 // ZSTD_SEARCHLOG_MIN .. ZSTD_SEARCHLOG_MAX (64-bit).
204 Self::SearchLog => (1, 30),
205 // ZSTD_MINMATCH_MIN .. ZSTD_MINMATCH_MAX.
206 Self::MinMatch => (3, 7),
207 // ZSTD_TARGETLENGTH_MIN .. ZSTD_TARGETLENGTH_MAX.
208 Self::TargetLength => (0, 131_072),
209 // ZSTD_fast .. ZSTD_btultra2.
210 Self::Strategy => (1, 9),
211 // Boolean flag.
212 Self::EnableLongDistanceMatching => (0, 1),
213 // ZSTD_LDM_HASHLOG_MIN .. ZSTD_LDM_HASHLOG_MAX.
214 Self::LdmHashLog => (6, 30),
215 // ZSTD_LDM_MINMATCH_MIN .. ZSTD_LDM_MINMATCH_MAX.
216 Self::LdmMinMatch => (4, 4096),
217 // ZSTD_LDM_BUCKETSIZELOG_MIN .. ZSTD_LDM_BUCKETSIZELOG_MAX.
218 Self::LdmBucketSizeLog => (1, 8),
219 // ZSTD_LDM_HASHRATELOG_MIN .. ZSTD_WINDOWLOG_MAX - ZSTD_HASHLOG_MIN.
220 Self::LdmHashRateLog => (0, 24),
221 };
222 Bounds {
223 lower_bound,
224 upper_bound,
225 }
226 }
227}
228
229/// Error returned by [`CompressionParametersBuilder::build`] when a knob
230/// is set outside its [`CParameter::bounds`].
231#[derive(Copy, Clone, Debug, PartialEq, Eq)]
232#[non_exhaustive]
233pub enum ParameterError {
234 /// A parameter was set to a value outside its inclusive bounds.
235 OutOfBounds {
236 /// Which parameter violated its range.
237 parameter: CParameter,
238 /// The rejected value.
239 value: i64,
240 /// The inclusive `[lower, upper]` range it had to fall within.
241 bounds: Bounds,
242 },
243}
244
245impl core::fmt::Display for ParameterError {
246 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
247 match self {
248 Self::OutOfBounds {
249 parameter,
250 value,
251 bounds,
252 } => write!(
253 f,
254 "compression parameter {parameter:?} = {value} out of bounds \
255 [{}, {}]",
256 bounds.lower_bound, bounds.upper_bound
257 ),
258 }
259 }
260}
261
262#[cfg(feature = "std")]
263impl std::error::Error for ParameterError {}
264
265/// LDM tuning overrides — every knob is `Option`, falling back to the
266/// strategy-derived donor default (`LdmParams::adjust_for`) when unset.
267#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
268pub(crate) struct LdmOverride {
269 pub(crate) hash_log: Option<u32>,
270 pub(crate) min_match: Option<u32>,
271 pub(crate) bucket_size_log: Option<u32>,
272 pub(crate) hash_rate_log: Option<u32>,
273}
274
275/// Internal per-knob override set consumed by the match-generator's
276/// `reset` path. Every field left `None` inherits the base level's
277/// resolved value, so the default path is byte-identical to level-based
278/// compression.
279#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
280pub(crate) struct ParamOverrides {
281 pub(crate) window_log: Option<u8>,
282 pub(crate) hash_log: Option<u32>,
283 pub(crate) chain_log: Option<u32>,
284 pub(crate) search_log: Option<u32>,
285 pub(crate) min_match: Option<u32>,
286 pub(crate) target_length: Option<u32>,
287 pub(crate) strategy: Option<Strategy>,
288 /// `Some` when `enable_long_distance_matching(true)` was set; carries
289 /// the (possibly empty) LDM knob overrides.
290 pub(crate) ldm: Option<LdmOverride>,
291}
292
293impl ParamOverrides {
294 /// Whether any knob overrides the base level. An all-`None`
295 /// override is a no-op the `reset` path can skip entirely, keeping
296 /// the default level-based geometry byte-identical.
297 pub(crate) fn is_empty(&self) -> bool {
298 self.window_log.is_none()
299 && self.hash_log.is_none()
300 && self.chain_log.is_none()
301 && self.search_log.is_none()
302 && self.min_match.is_none()
303 && self.target_length.is_none()
304 && self.strategy.is_none()
305 && self.ldm.is_none()
306 }
307}
308
309/// Fully-resolved fine-grained compression parameters. Build through
310/// [`CompressionParameters::builder`]; pass to
311/// [`FrameCompressor::set_parameters`](crate::encoding::FrameCompressor::set_parameters)
312/// or [`compress_with_parameters`](crate::encoding::compress_with_parameters).
313///
314/// Wraps a base [`CompressionLevel`](crate::encoding::CompressionLevel)
315/// plus the set of knobs that override it. A parameter set that
316/// overrides nothing is equivalent to compressing at its base level.
317#[derive(Copy, Clone, Debug, PartialEq, Eq)]
318pub struct CompressionParameters {
319 level: CompressionLevel,
320 overrides: ParamOverrides,
321}
322
323impl CompressionParameters {
324 /// Start a builder from a base compression level. Knobs left unset
325 /// inherit that level's resolved defaults.
326 pub fn builder(level: CompressionLevel) -> CompressionParametersBuilder {
327 CompressionParametersBuilder {
328 level,
329 window_log: None,
330 hash_log: None,
331 chain_log: None,
332 search_log: None,
333 min_match: None,
334 target_length: None,
335 strategy: None,
336 enable_ldm: false,
337 ldm: LdmOverride::default(),
338 }
339 }
340
341 /// The base compression level these parameters override.
342 pub fn level(&self) -> CompressionLevel {
343 self.level
344 }
345
346 /// Whether long-distance matching is enabled.
347 pub fn long_distance_matching_enabled(&self) -> bool {
348 self.overrides.ldm.is_some()
349 }
350
351 pub(crate) fn overrides(&self) -> ParamOverrides {
352 self.overrides
353 }
354}
355
356/// Builder for [`CompressionParameters`]. Each setter records one knob;
357/// [`Self::build`] validates them against [`CParameter::bounds`].
358#[derive(Copy, Clone, Debug)]
359pub struct CompressionParametersBuilder {
360 level: CompressionLevel,
361 window_log: Option<u32>,
362 hash_log: Option<u32>,
363 chain_log: Option<u32>,
364 search_log: Option<u32>,
365 min_match: Option<u32>,
366 target_length: Option<u32>,
367 strategy: Option<Strategy>,
368 enable_ldm: bool,
369 ldm: LdmOverride,
370}
371
372impl CompressionParametersBuilder {
373 /// Override the maximum back-reference distance (`log2`). C
374 /// `ZSTD_c_windowLog`.
375 pub fn window_log(mut self, value: u32) -> Self {
376 self.window_log = Some(value);
377 self
378 }
379
380 /// Override the match-finder hash table size (`log2`). C `ZSTD_c_hashLog`.
381 pub fn hash_log(mut self, value: u32) -> Self {
382 self.hash_log = Some(value);
383 self
384 }
385
386 /// Override the match-finder chain table size (`log2`). C `ZSTD_c_chainLog`.
387 pub fn chain_log(mut self, value: u32) -> Self {
388 self.chain_log = Some(value);
389 self
390 }
391
392 /// Override the search-attempts count (`log2`). C `ZSTD_c_searchLog`.
393 pub fn search_log(mut self, value: u32) -> Self {
394 self.search_log = Some(value);
395 self
396 }
397
398 /// Override the minimum match length in bytes. C `ZSTD_c_minMatch`.
399 pub fn min_match(mut self, value: u32) -> Self {
400 self.min_match = Some(value);
401 self
402 }
403
404 /// Override the "good enough" target match length. C `ZSTD_c_targetLength`.
405 pub fn target_length(mut self, value: u32) -> Self {
406 self.target_length = Some(value);
407 self
408 }
409
410 /// Override the match-finder [`Strategy`]. C `ZSTD_c_strategy`.
411 pub fn strategy(mut self, value: Strategy) -> Self {
412 self.strategy = Some(value);
413 self
414 }
415
416 /// Enable or disable long-distance matching. C
417 /// `ZSTD_c_enableLongDistanceMatching`. Off at every level preset.
418 /// This is the explicit activation toggle; the `ldm_*` knob setters
419 /// also enable LDM implicitly. The flag is plain last-write-wins, so
420 /// a trailing `enable_long_distance_matching(false)` disables LDM even
421 /// if an earlier `ldm_*` call set a knob (the knob is then ignored at
422 /// [`build`](Self::build)).
423 pub fn enable_long_distance_matching(mut self, enable: bool) -> Self {
424 self.enable_ldm = enable;
425 self
426 }
427
428 /// Override the LDM hash table size (`log2`). C `ZSTD_c_ldmHashLog`.
429 /// Implies [`Self::enable_long_distance_matching(true)`](Self::enable_long_distance_matching).
430 pub fn ldm_hash_log(mut self, value: u32) -> Self {
431 self.enable_ldm = true;
432 self.ldm.hash_log = Some(value);
433 self
434 }
435
436 /// Override the LDM minimum match length. C `ZSTD_c_ldmMinMatch`.
437 /// Implies [`Self::enable_long_distance_matching(true)`](Self::enable_long_distance_matching).
438 pub fn ldm_min_match(mut self, value: u32) -> Self {
439 self.enable_ldm = true;
440 self.ldm.min_match = Some(value);
441 self
442 }
443
444 /// Override the LDM bucket size (`log2`). C `ZSTD_c_ldmBucketSizeLog`.
445 /// Implies [`Self::enable_long_distance_matching(true)`](Self::enable_long_distance_matching).
446 pub fn ldm_bucket_size_log(mut self, value: u32) -> Self {
447 self.enable_ldm = true;
448 self.ldm.bucket_size_log = Some(value);
449 self
450 }
451
452 /// Override the LDM hash-insertion rate (`log2`). C `ZSTD_c_ldmHashRateLog`.
453 /// Implies [`Self::enable_long_distance_matching(true)`](Self::enable_long_distance_matching).
454 pub fn ldm_hash_rate_log(mut self, value: u32) -> Self {
455 self.enable_ldm = true;
456 self.ldm.hash_rate_log = Some(value);
457 self
458 }
459
460 /// Validate every set knob against [`CParameter::bounds`] and
461 /// produce the resolved [`CompressionParameters`].
462 ///
463 /// # Errors
464 ///
465 /// Returns [`ParameterError::OutOfBounds`] for the first knob whose
466 /// value falls outside its inclusive range.
467 pub fn build(self) -> Result<CompressionParameters, ParameterError> {
468 check(CParameter::WindowLog, self.window_log)?;
469 check(CParameter::HashLog, self.hash_log)?;
470 check(CParameter::ChainLog, self.chain_log)?;
471 check(CParameter::SearchLog, self.search_log)?;
472 check(CParameter::MinMatch, self.min_match)?;
473 check(CParameter::TargetLength, self.target_length)?;
474 if let Some(s) = self.strategy {
475 check(CParameter::Strategy, Some(s.ordinal()))?;
476 }
477 let ldm = if self.enable_ldm {
478 check(CParameter::LdmHashLog, self.ldm.hash_log)?;
479 check(CParameter::LdmMinMatch, self.ldm.min_match)?;
480 check(CParameter::LdmBucketSizeLog, self.ldm.bucket_size_log)?;
481 check(CParameter::LdmHashRateLog, self.ldm.hash_rate_log)?;
482 Some(self.ldm)
483 } else {
484 None
485 };
486 Ok(CompressionParameters {
487 level: self.level,
488 overrides: ParamOverrides {
489 // `window_log` is bounds-checked at <= 30, so the cast is lossless.
490 window_log: self.window_log.map(|v| v as u8),
491 hash_log: self.hash_log,
492 chain_log: self.chain_log,
493 search_log: self.search_log,
494 min_match: self.min_match,
495 target_length: self.target_length,
496 strategy: self.strategy,
497 ldm,
498 },
499 })
500 }
501}
502
503/// Validate one optional knob against its bounds.
504fn check(parameter: CParameter, value: Option<u32>) -> Result<(), ParameterError> {
505 if let Some(value) = value {
506 let bounds = parameter.bounds();
507 let value = i64::from(value);
508 if !bounds.contains(value) {
509 return Err(ParameterError::OutOfBounds {
510 parameter,
511 value,
512 bounds,
513 });
514 }
515 }
516 Ok(())
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522
523 #[test]
524 fn strategy_ordinals_round_trip() {
525 for ordinal in 1..=9 {
526 let s = Strategy::from_ordinal(ordinal).expect("valid ordinal");
527 assert_eq!(s.ordinal(), ordinal);
528 }
529 assert_eq!(Strategy::from_ordinal(0), None);
530 assert_eq!(Strategy::from_ordinal(10), None);
531 }
532
533 #[test]
534 fn builder_default_overrides_nothing() {
535 let p = CompressionParameters::builder(CompressionLevel::Level(7))
536 .build()
537 .unwrap();
538 assert!(p.overrides().is_empty());
539 assert_eq!(p.level(), CompressionLevel::Level(7));
540 assert!(!p.long_distance_matching_enabled());
541 }
542
543 #[test]
544 fn builder_records_each_knob() {
545 let p = CompressionParameters::builder(CompressionLevel::Level(19))
546 .window_log(22)
547 .hash_log(23)
548 .chain_log(24)
549 .search_log(7)
550 .min_match(4)
551 .target_length(256)
552 .strategy(Strategy::Btultra2)
553 .build()
554 .unwrap();
555 let o = p.overrides();
556 assert_eq!(o.window_log, Some(22));
557 assert_eq!(o.hash_log, Some(23));
558 assert_eq!(o.chain_log, Some(24));
559 assert_eq!(o.search_log, Some(7));
560 assert_eq!(o.min_match, Some(4));
561 assert_eq!(o.target_length, Some(256));
562 assert_eq!(o.strategy, Some(Strategy::Btultra2));
563 assert!(!o.is_empty());
564 }
565
566 #[test]
567 fn enable_ldm_sets_override_block() {
568 let p = CompressionParameters::builder(CompressionLevel::Level(19))
569 .enable_long_distance_matching(true)
570 .build()
571 .unwrap();
572 assert!(p.long_distance_matching_enabled());
573 assert_eq!(p.overrides().ldm, Some(LdmOverride::default()));
574 }
575
576 #[test]
577 fn ldm_knob_implies_enable() {
578 let p = CompressionParameters::builder(CompressionLevel::Level(19))
579 .ldm_hash_log(24)
580 .ldm_min_match(64)
581 .ldm_bucket_size_log(4)
582 .ldm_hash_rate_log(7)
583 .build()
584 .unwrap();
585 assert!(p.long_distance_matching_enabled());
586 let ldm = p.overrides().ldm.unwrap();
587 assert_eq!(ldm.hash_log, Some(24));
588 assert_eq!(ldm.min_match, Some(64));
589 assert_eq!(ldm.bucket_size_log, Some(4));
590 assert_eq!(ldm.hash_rate_log, Some(7));
591 }
592
593 #[test]
594 fn out_of_bounds_window_log_rejected() {
595 let err = CompressionParameters::builder(CompressionLevel::Default)
596 .window_log(31)
597 .build()
598 .unwrap_err();
599 match err {
600 ParameterError::OutOfBounds {
601 parameter, value, ..
602 } => {
603 assert_eq!(parameter, CParameter::WindowLog);
604 assert_eq!(value, 31);
605 }
606 }
607 }
608
609 #[test]
610 fn out_of_bounds_min_match_rejected() {
611 let err = CompressionParameters::builder(CompressionLevel::Default)
612 .min_match(2)
613 .build()
614 .unwrap_err();
615 assert!(matches!(
616 err,
617 ParameterError::OutOfBounds {
618 parameter: CParameter::MinMatch,
619 ..
620 }
621 ));
622 }
623
624 #[test]
625 fn ldm_bounds_only_checked_when_enabled() {
626 // An out-of-range LDM knob is only rejected when LDM is on. A
627 // builder that never enables LDM ignores the (unreachable)
628 // values entirely.
629 let err = CompressionParameters::builder(CompressionLevel::Default)
630 .ldm_bucket_size_log(9)
631 .build()
632 .unwrap_err();
633 assert!(matches!(
634 err,
635 ParameterError::OutOfBounds {
636 parameter: CParameter::LdmBucketSizeLog,
637 ..
638 }
639 ));
640 }
641
642 #[test]
643 fn bounds_match_c_reference() {
644 assert_eq!(
645 CParameter::WindowLog.bounds(),
646 Bounds {
647 lower_bound: 10,
648 upper_bound: 30
649 }
650 );
651 assert_eq!(
652 CParameter::Strategy.bounds(),
653 Bounds {
654 lower_bound: 1,
655 upper_bound: 9
656 }
657 );
658 assert_eq!(
659 CParameter::TargetLength.bounds(),
660 Bounds {
661 lower_bound: 0,
662 upper_bound: 131_072
663 }
664 );
665 assert!(CParameter::MinMatch.bounds().contains(3));
666 assert!(CParameter::MinMatch.bounds().contains(7));
667 assert!(!CParameter::MinMatch.bounds().contains(8));
668 }
669}