Skip to main content

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}