Skip to main content

structured_zstd/decoding/
dictionary.rs

1use alloc::boxed::Box;
2#[cfg(not(target_has_atomic = "ptr"))]
3use alloc::rc::Rc;
4#[cfg(target_has_atomic = "ptr")]
5use alloc::sync::Arc;
6use alloc::vec::Vec;
7use core::convert::TryInto;
8
9use crate::decoding::errors::DictionaryDecodeError;
10use crate::decoding::scratch::FSEScratch;
11use crate::decoding::scratch::HuffmanScratch;
12
13/// Zstandard includes support for "raw content" dictionaries, that store bytes optionally used
14/// during sequence execution.
15///
16/// <https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md#dictionary-format>
17#[derive(Clone)]
18pub struct Dictionary {
19    /// A 4 byte value used by decoders to check if they can use
20    /// the correct dictionary.
21    ///
22    /// Zero means unidentified: a raw-content dictionary has no header to
23    /// carry an ID, and the frames built from it record none, so it can only
24    /// be supplied explicitly and never resolved from a frame header.
25    /// Registration by ID
26    /// ([`FrameDecoder::add_dict`](crate::decoding::FrameDecoder::add_dict))
27    /// therefore still requires a non-zero one.
28    pub id: u32,
29    /// A dictionary can contain an entropy table, either FSE or
30    /// Huffman. Boxed for the same reason as `DecoderScratch::fse`: the
31    /// ~12 KiB of inline tables must not ride the stack through
32    /// construction.
33    pub fse: Box<FSEScratch>,
34    /// A dictionary can contain an entropy table, either FSE or
35    /// Huffman.
36    pub huf: HuffmanScratch,
37    /// The content of a dictionary acts as a "past" in front of data
38    /// to compress or decompress,
39    /// so it can be referenced in sequence commands.
40    /// As long as the amount of data decoded from this frame is less than or
41    /// equal to Window_Size, sequence commands may specify offsets longer than
42    /// the total length of decoded output so far to reference back to the
43    /// dictionary, even parts of the dictionary with offsets larger than Window_Size.
44    /// After the total output has surpassed Window_Size however,
45    /// this is no longer allowed and the dictionary is no longer accessible
46    pub dict_content: Vec<u8>,
47    /// The 3 most recent offsets are stored so that they can be used
48    /// during sequence execution, see
49    /// <https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md#repeat-offsets>
50    /// for more.
51    pub offset_hist: [u32; 3],
52}
53
54/// What a [`DictionaryHandle`] shares: the parsed dictionary and, for one
55/// parsed from serialized bytes, the bytes in front of its content (magic
56/// number, ID, tables, repeat offsets). A v0.5 or v0.6 frame takes a
57/// dictionary of this layout whole as history, header too.
58pub(crate) struct HeldDictionary {
59    pub(crate) dict: Dictionary,
60    #[cfg(feature = "legacy")]
61    pub(crate) serialized_header: Vec<u8>,
62}
63
64/// A parsed dictionary held by however many users need it at once.
65///
66/// Both sides prime frame after frame from one dictionary, so what they hold is
67/// shared rather than copied: `Arc` where atomics exist, `Rc` where they do not.
68#[cfg(target_has_atomic = "ptr")]
69pub(crate) type SharedDictionary = Arc<HeldDictionary>;
70#[cfg(not(target_has_atomic = "ptr"))]
71pub(crate) type SharedDictionary = Rc<HeldDictionary>;
72
73/// Shared pre-parsed dictionary handle for repeated decoding.
74///
75/// Uses `Arc` on targets with atomics and falls back to `Rc` otherwise.
76#[derive(Clone)]
77pub struct DictionaryHandle {
78    inner: SharedDictionary,
79}
80
81/// This 4 byte (little endian) magic number refers to the start of a dictionary
82pub const MAGIC_NUM: [u8; 4] = [0x37, 0xA4, 0x30, 0xEC];
83
84/// Most bytes [`Dictionary::decode_dict`] accepts in front of a dictionary's
85/// content: magic number and ID, a Huffman description (its size byte and at
86/// most 127 more), the three FSE descriptions at their largest, and the
87/// repeat offsets.
88#[cfg(feature = "legacy")]
89const MAX_SERIALIZED_HEADER_LEN: usize = 8
90    + 128
91    + max_fse_description_len(
92        crate::blocks::sequence_section::MAX_OFFSET_CODE,
93        crate::decoding::sequence_section_decoder::OF_MAX_LOG,
94    )
95    + max_fse_description_len(
96        crate::blocks::sequence_section::MAX_MATCH_LENGTH_CODE,
97        crate::decoding::sequence_section_decoder::ML_MAX_LOG,
98    )
99    + max_fse_description_len(
100        crate::blocks::sequence_section::MAX_LITERAL_LENGTH_CODE,
101        crate::decoding::sequence_section_decoder::LL_MAX_LOG,
102    )
103    + 12;
104
105/// Most bytes an FSE description of symbols `0..=max_symbol` at accuracy log
106/// `max_log` takes: the 4-bit log, then per symbol a count of at most
107/// `max_log + 1` bits and, amortized, at most 2 bits of zero-run flags.
108#[cfg(feature = "legacy")]
109const fn max_fse_description_len(max_symbol: u8, max_log: u8) -> usize {
110    (4 + (max_symbol as usize + 1) * (max_log as usize + 3)).div_ceil(8)
111}
112
113impl Dictionary {
114    /// Heap bytes owned by this dictionary: the content plus the parsed
115    /// entropy tables' heap. `fse` is a `Box`, so the fixed-size FSE decode
116    /// arrays are heap bytes rather than inline in `size_of::<Dictionary>()`;
117    /// charge that `size_of` here so a caller budgeting against the reported
118    /// figure sees them.
119    pub fn heap_bytes(&self) -> usize {
120        self.dict_content.capacity()
121            + core::mem::size_of::<FSEScratch>()
122            + self.fse.heap_bytes()
123            + self.huf.heap_bytes()
124    }
125
126    /// Most [`heap_bytes`](Self::heap_bytes) a dictionary holding
127    /// `content_len` bytes of content can report, so a caller can budget for
128    /// one before parsing it.
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// use structured_zstd::decoding::Dictionary;
134    ///
135    /// let dict = Dictionary::from_raw_content(0, vec![7u8; 1024]).unwrap();
136    /// assert!(dict.heap_bytes() <= Dictionary::max_heap_bytes(1024));
137    /// ```
138    pub const fn max_heap_bytes(content_len: usize) -> usize {
139        content_len
140            + core::mem::size_of::<FSEScratch>()
141            + FSEScratch::max_heap_bytes()
142            + HuffmanScratch::max_heap_bytes()
143    }
144
145    /// Build a dictionary from raw content bytes (without entropy table sections).
146    ///
147    /// This is primarily intended for dictionaries produced by the `dict-builder`
148    /// module, which currently emits raw-content dictionaries.
149    ///
150    /// An `id` of 0 means the dictionary is unidentified, which is what any
151    /// plain file used as a dictionary is: frames built with it record no
152    /// dictionary ID, so it can only ever be supplied explicitly, never
153    /// resolved from a frame header. Registration by ID
154    /// ([`FrameDecoder::add_dict`](crate::decoding::FrameDecoder::add_dict))
155    /// still requires a non-zero one, since the ID is the key it is stored
156    /// under.
157    pub fn from_raw_content(
158        id: u32,
159        dict_content: Vec<u8>,
160    ) -> Result<Dictionary, DictionaryDecodeError> {
161        if dict_content.is_empty() {
162            return Err(DictionaryDecodeError::DictionaryTooSmall { got: 0, need: 1 });
163        }
164
165        Ok(Dictionary {
166            id,
167            fse: Box::new(FSEScratch::new()),
168            huf: HuffmanScratch::new(),
169            dict_content,
170            offset_hist: [1, 4, 8],
171        })
172    }
173
174    /// Parses the dictionary from `raw`, initializes its tables,
175    /// and returns a fully constructed [`Dictionary`] whose `id` can be
176    /// checked against the frame's `dict_id`.
177    pub fn decode_dict(raw: &[u8]) -> Result<Dictionary, DictionaryDecodeError> {
178        Self::decode_dict_inner(raw, true)
179    }
180
181    /// Whether `raw` is a serialized dictionary rather than raw content: it
182    /// starts with [`MAGIC_NUM`] and has room for the ID after it. A shorter
183    /// blob is raw content whatever it starts with, as upstream zstd reads it
184    /// (zstd_ddict.c:97, zstd_compress.c:5206).
185    ///
186    /// ```
187    /// use structured_zstd::decoding::{Dictionary, DICTIONARY_MAGIC};
188    /// assert!(!Dictionary::is_serialized(&DICTIONARY_MAGIC));
189    /// assert!(Dictionary::is_serialized(&[0x37, 0xA4, 0x30, 0xEC, 7, 0, 0, 0]));
190    /// ```
191    pub fn is_serialized(raw: &[u8]) -> bool {
192        raw.len() >= 8 && raw.starts_with(&MAGIC_NUM)
193    }
194
195    /// Loads whichever kind of dictionary `raw` holds, the way `zstd -D` does:
196    /// a [serialized](Self::is_serialized) dictionary with entropy tables and
197    /// an ID, and anything else is taken as raw content,
198    /// which is why any file can be handed to `-D`. A raw-content dictionary
199    /// has no ID, so it must be supplied explicitly on both sides — see
200    /// [`Self::from_raw_content`].
201    pub fn from_serialized_or_raw_content(raw: &[u8]) -> Result<Dictionary, DictionaryDecodeError> {
202        if Self::is_serialized(raw) {
203            Self::decode_dict(raw)
204        } else if raw.is_empty() {
205            // A zero-sized buffer is a dictionary with nothing in it rather
206            // than a malformed one: `ZSTD_createDDict(NULL, 0)` builds a
207            // usable `DDict` referencing no content, and
208            // `ZSTD_CCtx_loadDictionary` with an empty buffer is how a caller
209            // says "no dictionary". [`Self::from_raw_content`] still refuses
210            // it, because naming raw content and handing over none is the
211            // caller asking for a dictionary that cannot exist.
212            Ok(Dictionary {
213                id: 0,
214                fse: Box::new(FSEScratch::new()),
215                huf: HuffmanScratch::new(),
216                dict_content: Vec::new(),
217                offset_hist: [1, 4, 8],
218            })
219        } else {
220            Self::from_raw_content(0, raw.to_vec())
221        }
222    }
223
224    /// Parse a dictionary for ENCODER use: builds the entropy
225    /// probabilities/weights needed by `to_encoder_table` but skips the
226    /// decode-only work the encoder never reads — the FSE *decoding*
227    /// tables + their `enrich_*` post-passes, and the HUF decode lookup
228    /// table (`packed_decode`). Produces a [`Dictionary`] whose FSE
229    /// `symbol_probabilities` / `accuracy_log` and HUF `bits` /
230    /// `max_num_bits` match `decode_dict` exactly, so the encoder entropy
231    /// tables — and thus the emitted frame — are byte-identical; only the
232    /// wasted decode-table builds are dropped. Offset history + content
233    /// are parsed the same way.
234    /// Crate-internal: the returned [`Dictionary`] deliberately has no
235    /// decode lookup tables (`packed_decode` / FSE `decode`), so it is
236    /// NOT safe to feed into a [`FrameDecoder`](crate::decoding::FrameDecoder)
237    /// — Huffman decode would index an empty `packed_decode`. The only caller
238    /// is `EncoderDictionary::from_bytes`, which wraps the result in the
239    /// encoder-only `EncoderDictionary` type (no decode path), so this
240    /// incomplete dictionary can never escape to the decode side. Keeping
241    /// this `pub(crate)` keeps it off the public `Dictionary` API entirely.
242    pub(crate) fn decode_dict_for_encoding(
243        raw: &[u8],
244    ) -> Result<Dictionary, DictionaryDecodeError> {
245        Self::decode_dict_inner(raw, false)
246    }
247
248    /// Shared dictionary parser. `build_decode_tables` selects whether the
249    /// FSE/HUF tables get their full decoding tables (FSE decode table +
250    /// `enrich_*`, HUF `packed_decode`; decoder path) or only the
251    /// probability/weight parse (encoder path — see
252    /// [`Self::decode_dict_for_encoding`]).
253    fn decode_dict_inner(
254        raw: &[u8],
255        build_decode_tables: bool,
256    ) -> Result<Dictionary, DictionaryDecodeError> {
257        const MIN_MAGIC_AND_ID_LEN: usize = 8;
258        const OFFSET_HISTORY_LEN: usize = 12;
259
260        if raw.len() < MIN_MAGIC_AND_ID_LEN {
261            return Err(DictionaryDecodeError::DictionaryTooSmall {
262                got: raw.len(),
263                need: MIN_MAGIC_AND_ID_LEN,
264            });
265        }
266
267        let mut new_dict = Dictionary {
268            id: 0,
269            fse: Box::new(FSEScratch::new()),
270            huf: HuffmanScratch::new(),
271            dict_content: Vec::new(),
272            offset_hist: [1, 4, 8],
273        };
274
275        let magic_num: [u8; 4] = raw[..4].try_into().expect("optimized away");
276        if magic_num != MAGIC_NUM {
277            return Err(DictionaryDecodeError::BadMagicNum { got: magic_num });
278        }
279
280        let dict_id = raw[4..8].try_into().expect("optimized away");
281        let dict_id = u32::from_le_bytes(dict_id);
282        if dict_id == 0 {
283            return Err(DictionaryDecodeError::ZeroDictionaryId);
284        }
285        new_dict.id = dict_id;
286
287        let raw_tables = &raw[8..];
288
289        let huf_size = if build_decode_tables {
290            new_dict.huf.table.build_decoder(raw_tables)?
291        } else {
292            new_dict.huf.table.build_weights_only(raw_tables)?
293        };
294        let raw_tables = &raw_tables[huf_size as usize..];
295
296        let of_size = if build_decode_tables {
297            let n = new_dict.fse.offsets.build_decoder(
298                raw_tables,
299                crate::decoding::sequence_section_decoder::OF_MAX_LOG,
300            )?;
301            new_dict.fse.offsets.enrich_for_offsets();
302            // Compute the pipeline-gate long-offset share ONCE here, while the
303            // dictionary handle is built, so the per-decode `init_from_dict`
304            // path can COPY it instead of re-walking the offsets table on every
305            // `decode_*_with_dict_handle` call (the dict is immutable, so the
306            // share never changes after this).
307            new_dict.fse.offsets_long_share =
308                crate::decoding::sequence_section_decoder::compute_offsets_long_share(
309                    &new_dict.fse.offsets,
310                );
311            n
312        } else {
313            new_dict.fse.offsets.read_table_probabilities(
314                raw_tables,
315                crate::decoding::sequence_section_decoder::OF_MAX_LOG,
316            )?
317        };
318        let raw_tables = &raw_tables[of_size..];
319
320        let ml_size = if build_decode_tables {
321            let n = new_dict.fse.match_lengths.build_decoder(
322                raw_tables,
323                crate::decoding::sequence_section_decoder::ML_MAX_LOG,
324            )?;
325            new_dict
326                .fse
327                .match_lengths
328                .enrich_with_packed_seq_meta(&crate::decoding::sequence_section_decoder::ML_META);
329            n
330        } else {
331            new_dict.fse.match_lengths.read_table_probabilities(
332                raw_tables,
333                crate::decoding::sequence_section_decoder::ML_MAX_LOG,
334            )?
335        };
336        let raw_tables = &raw_tables[ml_size..];
337
338        let ll_size = if build_decode_tables {
339            let n = new_dict.fse.literal_lengths.build_decoder(
340                raw_tables,
341                crate::decoding::sequence_section_decoder::LL_MAX_LOG,
342            )?;
343            new_dict
344                .fse
345                .literal_lengths
346                .enrich_with_packed_seq_meta(&crate::decoding::sequence_section_decoder::LL_META);
347            n
348        } else {
349            new_dict.fse.literal_lengths.read_table_probabilities(
350                raw_tables,
351                crate::decoding::sequence_section_decoder::LL_MAX_LOG,
352            )?
353        };
354        let raw_tables = &raw_tables[ll_size..];
355
356        if raw_tables.len() < OFFSET_HISTORY_LEN {
357            return Err(DictionaryDecodeError::DictionaryTooSmall {
358                got: raw_tables.len(),
359                need: OFFSET_HISTORY_LEN,
360            });
361        }
362
363        let offset1 = raw_tables[0..4].try_into().expect("optimized away");
364        let offset1 = u32::from_le_bytes(offset1);
365
366        let offset2 = raw_tables[4..8].try_into().expect("optimized away");
367        let offset2 = u32::from_le_bytes(offset2);
368
369        let offset3 = raw_tables[8..12].try_into().expect("optimized away");
370        let offset3 = u32::from_le_bytes(offset3);
371
372        if offset1 == 0 {
373            return Err(DictionaryDecodeError::ZeroRepeatOffsetInDictionary { index: 0 });
374        }
375        if offset2 == 0 {
376            return Err(DictionaryDecodeError::ZeroRepeatOffsetInDictionary { index: 1 });
377        }
378        if offset3 == 0 {
379            return Err(DictionaryDecodeError::ZeroRepeatOffsetInDictionary { index: 2 });
380        }
381
382        new_dict.offset_hist[0] = offset1;
383        new_dict.offset_hist[1] = offset2;
384        new_dict.offset_hist[2] = offset3;
385
386        let raw_content = &raw_tables[12..];
387        new_dict.dict_content.extend(raw_content);
388
389        Ok(new_dict)
390    }
391
392    /// Convert this parsed dictionary into a reusable shared handle.
393    pub fn into_handle(self) -> DictionaryHandle {
394        DictionaryHandle::from_dictionary(self)
395    }
396}
397
398impl DictionaryHandle {
399    /// Wrap an already-parsed dictionary in a shared handle.
400    pub fn from_dictionary(dict: Dictionary) -> Self {
401        Self {
402            inner: SharedDictionary::new(HeldDictionary {
403                dict,
404                #[cfg(feature = "legacy")]
405                serialized_header: Vec::new(),
406            }),
407        }
408    }
409
410    /// The bytes of the serialized dictionary in front of its content, empty
411    /// for one built from raw content or from a parsed [`Dictionary`].
412    #[cfg(feature = "legacy")]
413    pub(crate) fn serialized_header(&self) -> &[u8] {
414        &self.inner.serialized_header
415    }
416
417    /// Heap bytes the handle keeps alive: the shared allocation the
418    /// dictionary lives in and the dictionary's own content and tables.
419    /// Clones share all of it.
420    ///
421    /// # Examples
422    ///
423    /// ```
424    /// use structured_zstd::decoding::{Dictionary, DictionaryHandle};
425    ///
426    /// let handle = DictionaryHandle::from_dictionary(
427    ///     Dictionary::from_raw_content(0, vec![7u8; 1024]).unwrap(),
428    /// );
429    /// assert!(handle.heap_size() > handle.as_dict().heap_bytes());
430    /// assert!(handle.heap_size() <= DictionaryHandle::max_heap_size(1024));
431    /// ```
432    pub fn heap_size(&self) -> usize {
433        let bytes = Self::shared_allocation_size() + self.inner.dict.heap_bytes();
434        #[cfg(feature = "legacy")]
435        let bytes = bytes + self.inner.serialized_header.capacity();
436        bytes
437    }
438
439    /// Most [`Self::heap_size`] a handle over a dictionary of `content_len`
440    /// bytes can report, serialized header included, for budgeting before
441    /// the dictionary is parsed.
442    pub fn max_heap_size(content_len: usize) -> usize {
443        let bytes = Self::shared_allocation_size() + Dictionary::max_heap_bytes(content_len);
444        #[cfg(feature = "legacy")]
445        let bytes = bytes + MAX_SERIALIZED_HEADER_LEN;
446        bytes
447    }
448
449    /// Heap bytes that `handles` alone keep alive: each distinct dictionary
450    /// among them once, and only when no handle outside them shares it. For an
451    /// owner holding several handles, such as a decoding context that keeps a
452    /// dictionary attached and its decoder keeps the last frame's.
453    ///
454    /// # Examples
455    ///
456    /// ```
457    /// use structured_zstd::decoding::{Dictionary, DictionaryHandle};
458    ///
459    /// let handle = DictionaryHandle::from_dictionary(
460    ///     Dictionary::from_raw_content(0, vec![7u8; 64]).unwrap(),
461    /// );
462    /// let copy = handle.clone();
463    /// // Both handles are in the set: counted once.
464    /// assert_eq!(
465    ///     DictionaryHandle::exclusive_heap_size([&handle, &copy]),
466    ///     handle.heap_size()
467    /// );
468    /// // `copy` is held elsewhere: nothing is this set's alone.
469    /// assert_eq!(DictionaryHandle::exclusive_heap_size([&handle]), 0);
470    /// ```
471    pub fn exclusive_heap_size<'a, I>(handles: I) -> usize
472    where
473        I: IntoIterator<Item = &'a DictionaryHandle>,
474        I::IntoIter: Clone,
475    {
476        let handles = handles.into_iter();
477        let mut total = 0;
478        for (position, handle) in handles.clone().enumerate() {
479            let same =
480                |other: &&DictionaryHandle| SharedDictionary::ptr_eq(&handle.inner, &other.inner);
481            // The first handle to an allocation speaks for all of them.
482            if handles.clone().take(position).any(|other| same(&other)) {
483                continue;
484            }
485            // Holders, not references: one handle listed twice is one count.
486            let held_here = handles
487                .clone()
488                .enumerate()
489                .filter(|(at, other)| {
490                    same(other)
491                        && !handles
492                            .clone()
493                            .take(*at)
494                            .any(|earlier| core::ptr::eq(earlier, *other))
495                })
496                .count();
497            if SharedDictionary::strong_count(&handle.inner) == held_here {
498                total += handle.heap_size();
499            }
500        }
501        total
502    }
503
504    /// Bytes of the shared allocation itself: `Arc` and `Rc` both place two
505    /// word-sized counts ahead of the value, padded to its alignment.
506    fn shared_allocation_size() -> usize {
507        core::alloc::Layout::new::<[usize; 2]>()
508            .extend(core::alloc::Layout::new::<HeldDictionary>())
509            .expect("a fixed-size layout extends")
510            .0
511            .pad_to_align()
512            .size()
513    }
514
515    /// Parse a serialized dictionary and return a reusable shared handle.
516    ///
517    /// With the `legacy` feature a dictionary in the v0.5 or v0.6 layout is
518    /// taken too, whole as content with no ID: frames of its format parse
519    /// its tables from it, as libzstd's `ZSTD_decompress_usingDict` does.
520    pub fn decode_dict(raw: &[u8]) -> Result<Self, DictionaryDecodeError> {
521        #[cfg(feature = "legacy")]
522        if crate::decoding::legacy::is_older_dictionary(raw) {
523            return Dictionary::from_raw_content(0, raw.to_vec()).map(Self::from_dictionary);
524        }
525        let dict = Dictionary::decode_dict(raw)?;
526        // The content is the tail of the serialized bytes.
527        #[cfg(feature = "legacy")]
528        let serialized_header = raw[..raw.len() - dict.dict_content.len()].to_vec();
529        #[cfg(feature = "legacy")]
530        debug_assert!(serialized_header.capacity() <= MAX_SERIALIZED_HEADER_LEN);
531        Ok(Self {
532            inner: SharedDictionary::new(HeldDictionary {
533                dict,
534                #[cfg(feature = "legacy")]
535                serialized_header,
536            }),
537        })
538    }
539
540    /// Load whichever kind of dictionary `raw` holds, as `ZSTD_createDDict`
541    /// does: a [serialized](Dictionary::is_serialized) dictionary, or raw
542    /// content. See
543    /// [`Dictionary::from_serialized_or_raw_content`].
544    pub fn from_serialized_or_raw_content(raw: &[u8]) -> Result<Self, DictionaryDecodeError> {
545        if Dictionary::is_serialized(raw) {
546            Self::decode_dict(raw)
547        } else {
548            Dictionary::from_serialized_or_raw_content(raw).map(Self::from_dictionary)
549        }
550    }
551
552    pub fn id(&self) -> u32 {
553        self.inner.dict.id
554    }
555
556    pub fn as_dict(&self) -> &Dictionary {
557        &self.inner.dict
558    }
559}
560
561impl AsRef<Dictionary> for DictionaryHandle {
562    fn as_ref(&self) -> &Dictionary {
563        self.as_dict()
564    }
565}
566
567impl From<Dictionary> for DictionaryHandle {
568    fn from(dict: Dictionary) -> Self {
569        DictionaryHandle::from_dictionary(dict)
570    }
571}
572
573#[cfg(test)]
574mod tests;