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.
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/// 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    /// `value` moved to the nearest end of the range when it falls outside.
205    ///
206    /// ```
207    /// use structured_zstd::encoding::CParameter;
208    /// assert_eq!(CParameter::MinMatch.bounds().clamp(9), 7);
209    /// assert_eq!(CParameter::MinMatch.bounds().clamp(5), 5);
210    /// ```
211    pub const fn clamp(&self, value: i64) -> i64 {
212        if value < self.lower_bound {
213            self.lower_bound
214        } else if value > self.upper_bound {
215            self.upper_bound
216        } else {
217            value
218        }
219    }
220}
221
222impl CParameter {
223    /// Inclusive value bounds for this parameter, mirroring
224    /// `ZSTD_cParam_getBounds`. Window/hash/chain logs cap at 30 (the
225    /// encoder's match-finder ceiling) rather than the 31 C allows on
226    /// 64-bit, because the back-reference history is indexed with `u32`
227    /// positions over a `2 * window` eviction band.
228    pub const fn bounds(self) -> Bounds {
229        let (lower_bound, upper_bound) = match self {
230            // ZSTD_WINDOWLOG_MIN .. encoder ceiling.
231            Self::WindowLog => (10, 30),
232            // ZSTD_HASHLOG_MIN .. ZSTD_HASHLOG_MAX.
233            Self::HashLog => (6, 30),
234            // ZSTD_CHAINLOG_MIN .. ZSTD_CHAINLOG_MAX (64-bit).
235            Self::ChainLog => (6, 30),
236            // ZSTD_SEARCHLOG_MIN .. ZSTD_SEARCHLOG_MAX (64-bit).
237            Self::SearchLog => (1, 30),
238            // ZSTD_MINMATCH_MIN .. ZSTD_MINMATCH_MAX.
239            Self::MinMatch => (3, 7),
240            // ZSTD_TARGETLENGTH_MIN .. ZSTD_TARGETLENGTH_MAX.
241            Self::TargetLength => (0, 131_072),
242            // ZSTD_fast .. ZSTD_btultra2.
243            Self::Strategy => (1, 9),
244            // Boolean flag.
245            Self::EnableLongDistanceMatching => (0, 1),
246            // ZSTD_LDM_HASHLOG_MIN .. ZSTD_LDM_HASHLOG_MAX.
247            Self::LdmHashLog => (6, 30),
248            // ZSTD_LDM_MINMATCH_MIN .. ZSTD_LDM_MINMATCH_MAX.
249            Self::LdmMinMatch => (4, 4096),
250            // ZSTD_LDM_BUCKETSIZELOG_MIN .. ZSTD_LDM_BUCKETSIZELOG_MAX.
251            Self::LdmBucketSizeLog => (1, 8),
252            // ZSTD_LDM_HASHRATELOG_MIN .. ZSTD_WINDOWLOG_MAX - ZSTD_HASHLOG_MIN.
253            Self::LdmHashRateLog => (0, 24),
254        };
255        Bounds {
256            lower_bound,
257            upper_bound,
258        }
259    }
260}
261
262/// The match-finder parameters a numeric level selects for a source of a given
263/// size, the drop-in equivalent of C zstd's `ZSTD_getCParams`.
264///
265/// The size matters: the reference's level table has a row per source-size
266/// tier, and the tier changes the strategy as well as the table widths, so one
267/// level can run a different match-finder on a small input than on a large
268/// one. This is the selection the encoder itself makes for a frame without a
269/// dictionary; knobs set through [`CompressionParameters`] override it.
270///
271/// # Examples
272///
273/// ```
274/// use structured_zstd::encoding::{LevelParameters, Strategy};
275///
276/// // Level 11 is a lazy2 level on a large or unknown-size source...
277/// let large = LevelParameters::for_level(11, None, 0);
278/// assert_eq!(large.strategy, Strategy::Lazy2);
279///
280/// // ...and the optimal parser on one of 16 KiB or less, whose window is
281/// // also cut down to the source.
282/// let small = LevelParameters::for_level(11, Some(4096), 0);
283/// assert_eq!(small.strategy, Strategy::Btopt);
284/// assert_eq!(small.window_log, 12);
285/// ```
286#[derive(Copy, Clone, Debug, PartialEq, Eq)]
287pub struct LevelParameters {
288    /// Back-reference window, `log2`. C `windowLog`.
289    pub window_log: u32,
290    /// Chain / binary-tree table size, `log2`. C `chainLog`.
291    pub chain_log: u32,
292    /// Hash table size, `log2`. C `hashLog`.
293    pub hash_log: u32,
294    /// Search attempts per position, `log2`. C `searchLog`.
295    pub search_log: u32,
296    /// Match-finder hash width, in bytes. C `minMatch`.
297    pub min_match: u32,
298    /// Length that ends a search early; on a negative level, the step size
299    /// minus one. C `targetLength`.
300    pub target_length: u32,
301    /// The match-finder strategy. C `strategy`.
302    pub strategy: Strategy,
303}
304
305impl LevelParameters {
306    /// Parameters for `level` compressing a source of `source_size` bytes with
307    /// a `dictionary_size`-byte dictionary (`0` for none).
308    ///
309    /// `level` is on the reference's scale: `0` is the default level, levels
310    /// above [`CompressionLevel::MAX_LEVEL`] clamp to it, and a negative level
311    /// is an acceleration factor. `None` is a source of unknown size, which is
312    /// sized as a large one; `Some(0)` is a source that is really empty.
313    /// C `ZSTD_getCParams` spells unknown as `0`, so a caller porting from it
314    /// maps `0` to `None`.
315    pub fn for_level(level: i32, source_size: Option<u64>, dictionary_size: usize) -> Self {
316        let size = source_size.unwrap_or(crate::encoding::cparams::CONTENTSIZE_UNKNOWN);
317        let cp = crate::encoding::cparams::get_cparams(level, size, dictionary_size);
318        Self {
319            window_log: cp.window_log,
320            chain_log: cp.chain_log,
321            hash_log: cp.hash_log,
322            search_log: cp.search_log,
323            min_match: cp.min_match,
324            target_length: cp.target_length,
325            // Every row of the level table names one of the nine strategies.
326            strategy: Strategy::from_ordinal(cp.strategy)
327                .expect("the level table only holds strategies 1..=9"),
328        }
329    }
330
331    /// These parameters fitted to a source of `source_size` bytes and a
332    /// `dictionary_size`-byte dictionary, the drop-in equivalent of C zstd's
333    /// `ZSTD_adjustCParams`: a window wider than the data comes down to it, and
334    /// the tables shrink with the window. `None` is a source of unknown size,
335    /// which leaves the tables alone.
336    ///
337    /// Every field must already lie within its [`CParameter::bounds`].
338    ///
339    /// # Examples
340    ///
341    /// ```
342    /// use structured_zstd::encoding::LevelParameters;
343    ///
344    /// let level_19 = LevelParameters::for_level(19, None, 0);
345    /// let small = level_19.adjusted(Some(1000), 0);
346    /// assert_eq!(small.window_log, 10);
347    /// assert!(small.hash_log <= 11);
348    /// assert_eq!(level_19.adjusted(None, 0), level_19);
349    /// ```
350    pub fn adjusted(self, source_size: Option<u64>, dictionary_size: usize) -> Self {
351        let cp = crate::encoding::cparams::CParams {
352            window_log: self.window_log,
353            chain_log: self.chain_log,
354            hash_log: self.hash_log,
355            search_log: self.search_log,
356            min_match: self.min_match,
357            target_length: self.target_length,
358            strategy: self.strategy.ordinal(),
359        };
360        let size = source_size.unwrap_or(crate::encoding::cparams::CONTENTSIZE_UNKNOWN);
361        let cp = crate::encoding::cparams::adjust_cparams(cp, size, dictionary_size, false);
362        Self {
363            window_log: cp.window_log,
364            chain_log: cp.chain_log,
365            hash_log: cp.hash_log,
366            search_log: cp.search_log,
367            min_match: cp.min_match,
368            target_length: cp.target_length,
369            strategy: self.strategy,
370        }
371    }
372}
373
374/// Error returned by [`CompressionParametersBuilder::build`] when a knob
375/// is set outside its [`CParameter::bounds`].
376#[derive(Copy, Clone, Debug, PartialEq, Eq)]
377#[non_exhaustive]
378pub enum ParameterError {
379    /// A parameter was set to a value outside its inclusive bounds.
380    OutOfBounds {
381        /// Which parameter violated its range.
382        parameter: CParameter,
383        /// The rejected value.
384        value: i64,
385        /// The inclusive `[lower, upper]` range it had to fall within.
386        bounds: Bounds,
387    },
388}
389
390impl core::fmt::Display for ParameterError {
391    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
392        match self {
393            Self::OutOfBounds {
394                parameter,
395                value,
396                bounds,
397            } => write!(
398                f,
399                "compression parameter {parameter:?} = {value} out of bounds \
400                 [{}, {}]",
401                bounds.lower_bound, bounds.upper_bound
402            ),
403        }
404    }
405}
406
407#[cfg(feature = "std")]
408impl std::error::Error for ParameterError {}
409
410/// LDM tuning overrides — every knob is `Option`, falling back to the
411/// strategy-derived upstream zstd default (`LdmParams::adjust_for`) when unset.
412#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
413pub(crate) struct LdmOverride {
414    pub(crate) hash_log: Option<u32>,
415    pub(crate) min_match: Option<u32>,
416    pub(crate) bucket_size_log: Option<u32>,
417    pub(crate) hash_rate_log: Option<u32>,
418}
419
420/// Internal per-knob override set consumed by the match-generator's
421/// `reset` path. Every field left `None` inherits the base level's
422/// resolved value, so the default path is byte-identical to level-based
423/// compression.
424#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
425pub(crate) struct ParamOverrides {
426    pub(crate) window_log: Option<u8>,
427    pub(crate) hash_log: Option<u32>,
428    pub(crate) chain_log: Option<u32>,
429    pub(crate) search_log: Option<u32>,
430    pub(crate) min_match: Option<u32>,
431    pub(crate) target_length: Option<u32>,
432    pub(crate) strategy: Option<Strategy>,
433    /// `Some` when `enable_long_distance_matching(true)` was set; carries
434    /// the (possibly empty) LDM knob overrides.
435    pub(crate) ldm: Option<LdmOverride>,
436    /// Whether literals are entropy-coded; `Auto` leaves it to the level.
437    pub(crate) literal_compression: LiteralCompressionMode,
438}
439
440impl ParamOverrides {
441    /// Whether any knob overrides the base level. An all-`None`
442    /// override is a no-op the `reset` path can skip entirely, keeping
443    /// the default level-based geometry byte-identical.
444    pub(crate) fn is_empty(&self) -> bool {
445        self.window_log.is_none()
446            && self.hash_log.is_none()
447            && self.chain_log.is_none()
448            && self.search_log.is_none()
449            && self.min_match.is_none()
450            && self.target_length.is_none()
451            && self.strategy.is_none()
452            && self.ldm.is_none()
453            && self.literal_compression == LiteralCompressionMode::Auto
454    }
455}
456
457/// Fully-resolved fine-grained compression parameters. Build through
458/// [`CompressionParameters::builder`]; pass to
459/// [`FrameCompressor::set_parameters`](crate::encoding::FrameCompressor::set_parameters)
460/// or [`compress_with_parameters`](crate::encoding::compress_with_parameters).
461///
462/// Wraps a base [`CompressionLevel`] plus the set of knobs that override it. A parameter set that
463/// overrides nothing is equivalent to compressing at its base level.
464#[derive(Copy, Clone, Debug, PartialEq, Eq)]
465pub struct CompressionParameters {
466    level: CompressionLevel,
467    overrides: ParamOverrides,
468}
469
470impl CompressionParameters {
471    /// Start a builder from a base compression level. Knobs left unset
472    /// inherit that level's resolved defaults.
473    pub fn builder(level: CompressionLevel) -> CompressionParametersBuilder {
474        CompressionParametersBuilder {
475            level,
476            window_log: None,
477            hash_log: None,
478            chain_log: None,
479            search_log: None,
480            min_match: None,
481            target_length: None,
482            strategy: None,
483            enable_ldm: false,
484            ldm: LdmOverride::default(),
485            literal_compression: LiteralCompressionMode::Auto,
486        }
487    }
488
489    /// The base compression level these parameters override.
490    pub fn level(&self) -> CompressionLevel {
491        self.level
492    }
493
494    /// Whether long-distance matching is enabled.
495    pub fn long_distance_matching_enabled(&self) -> bool {
496        self.overrides.ldm.is_some()
497    }
498
499    /// How literals are entropy-coded (see [`LiteralCompressionMode`]).
500    pub fn literal_compression_mode(&self) -> LiteralCompressionMode {
501        self.overrides.literal_compression
502    }
503
504    /// The window, `log2`, the encoder gives a frame of `source_size` bytes
505    /// (`None` = unknown) with `dictionary_size` bytes of dictionary content
506    /// under these parameters: resolved exactly as the encoder resolves it.
507    ///
508    /// ```
509    /// use structured_zstd::encoding::{CompressionLevel, CompressionParameters};
510    ///
511    /// let params = CompressionParameters::builder(CompressionLevel::Level(19))
512    ///     .window_log(23)
513    ///     .build()
514    ///     .unwrap();
515    /// assert_eq!(params.window_log_for(None, 0), 23);
516    /// assert!(params.window_log_for(Some(1000), 0) < 23);
517    /// ```
518    pub fn window_log_for(&self, source_size: Option<u64>, dictionary_size: usize) -> u32 {
519        let sizes = crate::encoding::DictionarySizes {
520            content: dictionary_size,
521            serialized: dictionary_size,
522        };
523        let params =
524            crate::encoding::levels::config::frame_level_params(self, source_size, Some(sizes));
525        u32::from(params.window_log)
526    }
527
528    /// These parameters as a dictionary's own, driving a frame of
529    /// `source_size` bytes (`None` = unknown): an explicit window is raised to
530    /// cover the source, as far as 512 KiB, as upstream raises a prepared
531    /// dictionary's window for a frame of known size
532    /// (`ZSTD_compressBegin_usingCDict_internal`, zstd_compress.c:5844-5852).
533    /// A window left to the level is resolved for the source and covers it.
534    ///
535    /// ```
536    /// use structured_zstd::encoding::{CompressionLevel, CompressionParameters};
537    ///
538    /// let params = CompressionParameters::builder(CompressionLevel::Level(3))
539    ///     .window_log(10)
540    ///     .build()
541    ///     .unwrap();
542    /// let source = Some(1 << 20);
543    /// assert_eq!(params.for_dictionary_frame(source).window_log_for(source, 0), 19);
544    /// assert_eq!(params.for_dictionary_frame(None).window_log_for(None, 0), 10);
545    /// ```
546    pub fn for_dictionary_frame(mut self, source_size: Option<u64>) -> Self {
547        const RAISE_LIMIT: u64 = 1 << 19;
548        let known =
549            source_size.filter(|size| *size != crate::encoding::cparams::CONTENTSIZE_UNKNOWN);
550        if let (Some(window), Some(source)) = (self.overrides.window_log, known) {
551            // Bounded by `RAISE_LIMIT`, so it fits and its log is at most 19.
552            let limited = source.min(RAISE_LIMIT) as u32;
553            let source_log = if limited > 1 {
554                (u32::BITS - (limited - 1).leading_zeros()) as u8
555            } else {
556                1
557            };
558            self.overrides.window_log = Some(window.max(source_log));
559        }
560        self
561    }
562
563    pub(crate) fn overrides(&self) -> ParamOverrides {
564        self.overrides
565    }
566}
567
568/// Builder for [`CompressionParameters`]. Each setter records one knob;
569/// [`Self::build`] validates them against [`CParameter::bounds`].
570#[derive(Copy, Clone, Debug)]
571pub struct CompressionParametersBuilder {
572    level: CompressionLevel,
573    window_log: Option<u32>,
574    hash_log: Option<u32>,
575    chain_log: Option<u32>,
576    search_log: Option<u32>,
577    min_match: Option<u32>,
578    target_length: Option<u32>,
579    strategy: Option<Strategy>,
580    enable_ldm: bool,
581    ldm: LdmOverride,
582    literal_compression: LiteralCompressionMode,
583}
584
585impl CompressionParametersBuilder {
586    /// Decide whether literals are entropy-coded, overriding the level's own
587    /// choice. C `ZSTD_c_literalCompressionMode`.
588    ///
589    /// ```rust
590    /// use structured_zstd::encoding::{
591    ///     compress_with_parameters, CompressionLevel, CompressionParameters,
592    ///     LiteralCompressionMode,
593    /// };
594    ///
595    /// // Literal-heavy input: 32 distinct symbols and nothing for the match
596    /// // finder to repeat, so only entropy-coding the literals shrinks it.
597    /// let text: Vec<u8> = (0..8192u32)
598    ///     .map(|i| b'a' + (i.wrapping_mul(2_654_435_761) >> 27) as u8)
599    ///     .collect();
600    /// // Level -3 stores literals raw by default; asking for compression
601    /// // makes the frame smaller on input like this.
602    /// let compressed_literals = CompressionParameters::builder(CompressionLevel::Level(-3))
603    ///     .literal_compression(LiteralCompressionMode::Enable)
604    ///     .build()
605    ///     .unwrap();
606    /// let plain = compress_with_parameters(
607    ///     &text[..],
608    ///     &CompressionParameters::builder(CompressionLevel::Level(-3)).build().unwrap(),
609    /// );
610    /// let coded = compress_with_parameters(&text[..], &compressed_literals);
611    /// assert!(coded.len() < plain.len());
612    /// ```
613    pub fn literal_compression(mut self, mode: LiteralCompressionMode) -> Self {
614        self.literal_compression = mode;
615        self
616    }
617
618    /// Override the maximum back-reference distance (`log2`). C
619    /// `ZSTD_c_windowLog`.
620    pub fn window_log(mut self, value: u32) -> Self {
621        self.window_log = Some(value);
622        self
623    }
624
625    /// Override the match-finder hash table size (`log2`). C `ZSTD_c_hashLog`.
626    pub fn hash_log(mut self, value: u32) -> Self {
627        self.hash_log = Some(value);
628        self
629    }
630
631    /// Override the match-finder chain table size (`log2`). C `ZSTD_c_chainLog`.
632    pub fn chain_log(mut self, value: u32) -> Self {
633        self.chain_log = Some(value);
634        self
635    }
636
637    /// Override the search-attempts count (`log2`). C `ZSTD_c_searchLog`.
638    pub fn search_log(mut self, value: u32) -> Self {
639        self.search_log = Some(value);
640        self
641    }
642
643    /// Override the minimum match length in bytes. C `ZSTD_c_minMatch`.
644    pub fn min_match(mut self, value: u32) -> Self {
645        self.min_match = Some(value);
646        self
647    }
648
649    /// Override the "good enough" target match length. C `ZSTD_c_targetLength`.
650    pub fn target_length(mut self, value: u32) -> Self {
651        self.target_length = Some(value);
652        self
653    }
654
655    /// Override the match-finder [`Strategy`]. C `ZSTD_c_strategy`.
656    pub fn strategy(mut self, value: Strategy) -> Self {
657        self.strategy = Some(value);
658        self
659    }
660
661    /// Enable or disable long-distance matching. C
662    /// `ZSTD_c_enableLongDistanceMatching`. Off at every level preset.
663    /// This is the explicit activation toggle; the `ldm_*` knob setters
664    /// also enable LDM implicitly. The flag is plain last-write-wins, so
665    /// a trailing `enable_long_distance_matching(false)` disables LDM even
666    /// if an earlier `ldm_*` call set a knob (the knob is then ignored at
667    /// [`build`](Self::build)).
668    pub fn enable_long_distance_matching(mut self, enable: bool) -> Self {
669        self.enable_ldm = enable;
670        self
671    }
672
673    /// Override the LDM hash table size (`log2`). C `ZSTD_c_ldmHashLog`.
674    /// Implies [`Self::enable_long_distance_matching(true)`](Self::enable_long_distance_matching).
675    pub fn ldm_hash_log(mut self, value: u32) -> Self {
676        self.enable_ldm = true;
677        self.ldm.hash_log = Some(value);
678        self
679    }
680
681    /// Override the LDM minimum match length. C `ZSTD_c_ldmMinMatch`.
682    /// Implies [`Self::enable_long_distance_matching(true)`](Self::enable_long_distance_matching).
683    pub fn ldm_min_match(mut self, value: u32) -> Self {
684        self.enable_ldm = true;
685        self.ldm.min_match = Some(value);
686        self
687    }
688
689    /// Override the LDM bucket size (`log2`). C `ZSTD_c_ldmBucketSizeLog`.
690    /// Implies [`Self::enable_long_distance_matching(true)`](Self::enable_long_distance_matching).
691    pub fn ldm_bucket_size_log(mut self, value: u32) -> Self {
692        self.enable_ldm = true;
693        self.ldm.bucket_size_log = Some(value);
694        self
695    }
696
697    /// Override the LDM hash-insertion rate (`log2`). C `ZSTD_c_ldmHashRateLog`.
698    /// Implies [`Self::enable_long_distance_matching(true)`](Self::enable_long_distance_matching).
699    pub fn ldm_hash_rate_log(mut self, value: u32) -> Self {
700        self.enable_ldm = true;
701        self.ldm.hash_rate_log = Some(value);
702        self
703    }
704
705    /// Validate every set knob against [`CParameter::bounds`] and
706    /// produce the resolved [`CompressionParameters`].
707    ///
708    /// # Errors
709    ///
710    /// Returns [`ParameterError::OutOfBounds`] for the first knob whose
711    /// value falls outside its inclusive range.
712    pub fn build(self) -> Result<CompressionParameters, ParameterError> {
713        check(CParameter::WindowLog, self.window_log)?;
714        check(CParameter::HashLog, self.hash_log)?;
715        check(CParameter::ChainLog, self.chain_log)?;
716        check(CParameter::SearchLog, self.search_log)?;
717        check(CParameter::MinMatch, self.min_match)?;
718        check(CParameter::TargetLength, self.target_length)?;
719        if let Some(s) = self.strategy {
720            check(CParameter::Strategy, Some(s.ordinal()))?;
721        }
722        let ldm = if self.enable_ldm {
723            check(CParameter::LdmHashLog, self.ldm.hash_log)?;
724            check(CParameter::LdmMinMatch, self.ldm.min_match)?;
725            check(CParameter::LdmBucketSizeLog, self.ldm.bucket_size_log)?;
726            check(CParameter::LdmHashRateLog, self.ldm.hash_rate_log)?;
727            Some(self.ldm)
728        } else {
729            None
730        };
731        Ok(CompressionParameters {
732            level: self.level,
733            overrides: ParamOverrides {
734                // `window_log` is bounds-checked at <= 30, so the cast is lossless.
735                window_log: self.window_log.map(|v| v as u8),
736                hash_log: self.hash_log,
737                chain_log: self.chain_log,
738                search_log: self.search_log,
739                min_match: self.min_match,
740                target_length: self.target_length,
741                strategy: self.strategy,
742                ldm,
743                literal_compression: self.literal_compression,
744            },
745        })
746    }
747}
748
749/// Validate one optional knob against its bounds.
750fn check(parameter: CParameter, value: Option<u32>) -> Result<(), ParameterError> {
751    if let Some(value) = value {
752        let bounds = parameter.bounds();
753        let value = i64::from(value);
754        if !bounds.contains(value) {
755            return Err(ParameterError::OutOfBounds {
756                parameter,
757                value,
758                bounds,
759            });
760        }
761    }
762    Ok(())
763}
764
765#[cfg(test)]
766mod tests;