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/// A parsed dictionary held by however many users need it at once.
55///
56/// Both sides prime frame after frame from one dictionary, so what they hold is
57/// shared rather than copied: `Arc` where atomics exist, `Rc` where they do not.
58#[cfg(target_has_atomic = "ptr")]
59pub(crate) type SharedDictionary = Arc<Dictionary>;
60#[cfg(not(target_has_atomic = "ptr"))]
61pub(crate) type SharedDictionary = Rc<Dictionary>;
62
63/// Shared pre-parsed dictionary handle for repeated decoding.
64///
65/// Uses `Arc` on targets with atomics and falls back to `Rc` otherwise.
66#[derive(Clone)]
67pub struct DictionaryHandle {
68    inner: SharedDictionary,
69}
70
71/// This 4 byte (little endian) magic number refers to the start of a dictionary
72pub const MAGIC_NUM: [u8; 4] = [0x37, 0xA4, 0x30, 0xEC];
73
74impl Dictionary {
75    /// Heap bytes owned by this dictionary: the content plus the parsed
76    /// entropy tables' heap. `fse` is a `Box`, so the fixed-size FSE decode
77    /// arrays are heap bytes rather than inline in `size_of::<Dictionary>()`;
78    /// charge that `size_of` here so a caller budgeting against the reported
79    /// figure sees them.
80    pub fn heap_bytes(&self) -> usize {
81        self.dict_content.capacity()
82            + core::mem::size_of::<FSEScratch>()
83            + self.fse.heap_bytes()
84            + self.huf.heap_bytes()
85    }
86
87    /// Most [`heap_bytes`](Self::heap_bytes) a dictionary holding
88    /// `content_len` bytes of content can report, so a caller can budget for
89    /// one before parsing it.
90    ///
91    /// # Examples
92    ///
93    /// ```
94    /// use structured_zstd::decoding::Dictionary;
95    ///
96    /// let dict = Dictionary::from_raw_content(0, vec![7u8; 1024]).unwrap();
97    /// assert!(dict.heap_bytes() <= Dictionary::max_heap_bytes(1024));
98    /// ```
99    pub const fn max_heap_bytes(content_len: usize) -> usize {
100        content_len
101            + core::mem::size_of::<FSEScratch>()
102            + FSEScratch::max_heap_bytes()
103            + HuffmanScratch::max_heap_bytes()
104    }
105
106    /// Build a dictionary from raw content bytes (without entropy table sections).
107    ///
108    /// This is primarily intended for dictionaries produced by the `dict-builder`
109    /// module, which currently emits raw-content dictionaries.
110    ///
111    /// An `id` of 0 means the dictionary is unidentified, which is what any
112    /// plain file used as a dictionary is: frames built with it record no
113    /// dictionary ID, so it can only ever be supplied explicitly, never
114    /// resolved from a frame header. Registration by ID
115    /// ([`FrameDecoder::add_dict`](crate::decoding::FrameDecoder::add_dict))
116    /// still requires a non-zero one, since the ID is the key it is stored
117    /// under.
118    pub fn from_raw_content(
119        id: u32,
120        dict_content: Vec<u8>,
121    ) -> Result<Dictionary, DictionaryDecodeError> {
122        if dict_content.is_empty() {
123            return Err(DictionaryDecodeError::DictionaryTooSmall { got: 0, need: 1 });
124        }
125
126        Ok(Dictionary {
127            id,
128            fse: Box::new(FSEScratch::new()),
129            huf: HuffmanScratch::new(),
130            dict_content,
131            offset_hist: [1, 4, 8],
132        })
133    }
134
135    /// Parses the dictionary from `raw`, initializes its tables,
136    /// and returns a fully constructed [`Dictionary`] whose `id` can be
137    /// checked against the frame's `dict_id`.
138    pub fn decode_dict(raw: &[u8]) -> Result<Dictionary, DictionaryDecodeError> {
139        Self::decode_dict_inner(raw, true)
140    }
141
142    /// Whether `raw` is a serialized dictionary rather than raw content: it
143    /// starts with [`MAGIC_NUM`] and has room for the ID after it. A shorter
144    /// blob is raw content whatever it starts with, as upstream zstd reads it
145    /// (zstd_ddict.c:97, zstd_compress.c:5206).
146    ///
147    /// ```
148    /// use structured_zstd::decoding::{Dictionary, DICTIONARY_MAGIC};
149    /// assert!(!Dictionary::is_serialized(&DICTIONARY_MAGIC));
150    /// assert!(Dictionary::is_serialized(&[0x37, 0xA4, 0x30, 0xEC, 7, 0, 0, 0]));
151    /// ```
152    pub fn is_serialized(raw: &[u8]) -> bool {
153        raw.len() >= 8 && raw.starts_with(&MAGIC_NUM)
154    }
155
156    /// Loads whichever kind of dictionary `raw` holds, the way `zstd -D` does:
157    /// a [serialized](Self::is_serialized) dictionary with entropy tables and
158    /// an ID, and anything else is taken as raw content,
159    /// which is why any file can be handed to `-D`. A raw-content dictionary
160    /// has no ID, so it must be supplied explicitly on both sides — see
161    /// [`Self::from_raw_content`].
162    pub fn from_serialized_or_raw_content(raw: &[u8]) -> Result<Dictionary, DictionaryDecodeError> {
163        if Self::is_serialized(raw) {
164            Self::decode_dict(raw)
165        } else if raw.is_empty() {
166            // A zero-sized buffer is a dictionary with nothing in it rather
167            // than a malformed one: `ZSTD_createDDict(NULL, 0)` builds a
168            // usable `DDict` referencing no content, and
169            // `ZSTD_CCtx_loadDictionary` with an empty buffer is how a caller
170            // says "no dictionary". [`Self::from_raw_content`] still refuses
171            // it, because naming raw content and handing over none is the
172            // caller asking for a dictionary that cannot exist.
173            Ok(Dictionary {
174                id: 0,
175                fse: Box::new(FSEScratch::new()),
176                huf: HuffmanScratch::new(),
177                dict_content: Vec::new(),
178                offset_hist: [1, 4, 8],
179            })
180        } else {
181            Self::from_raw_content(0, raw.to_vec())
182        }
183    }
184
185    /// Parse a dictionary for ENCODER use: builds the entropy
186    /// probabilities/weights needed by `to_encoder_table` but skips the
187    /// decode-only work the encoder never reads — the FSE *decoding*
188    /// tables + their `enrich_*` post-passes, and the HUF decode lookup
189    /// table (`packed_decode`). Produces a [`Dictionary`] whose FSE
190    /// `symbol_probabilities` / `accuracy_log` and HUF `bits` /
191    /// `max_num_bits` match `decode_dict` exactly, so the encoder entropy
192    /// tables — and thus the emitted frame — are byte-identical; only the
193    /// wasted decode-table builds are dropped. Offset history + content
194    /// are parsed the same way.
195    /// Crate-internal: the returned [`Dictionary`] deliberately has no
196    /// decode lookup tables (`packed_decode` / FSE `decode`), so it is
197    /// NOT safe to feed into a [`FrameDecoder`](crate::decoding::FrameDecoder)
198    /// — Huffman decode would index an empty `packed_decode`. The only caller
199    /// is `EncoderDictionary::from_bytes`, which wraps the result in the
200    /// encoder-only `EncoderDictionary` type (no decode path), so this
201    /// incomplete dictionary can never escape to the decode side. Keeping
202    /// this `pub(crate)` keeps it off the public `Dictionary` API entirely.
203    pub(crate) fn decode_dict_for_encoding(
204        raw: &[u8],
205    ) -> Result<Dictionary, DictionaryDecodeError> {
206        Self::decode_dict_inner(raw, false)
207    }
208
209    /// Shared dictionary parser. `build_decode_tables` selects whether the
210    /// FSE/HUF tables get their full decoding tables (FSE decode table +
211    /// `enrich_*`, HUF `packed_decode`; decoder path) or only the
212    /// probability/weight parse (encoder path — see
213    /// [`Self::decode_dict_for_encoding`]).
214    fn decode_dict_inner(
215        raw: &[u8],
216        build_decode_tables: bool,
217    ) -> Result<Dictionary, DictionaryDecodeError> {
218        const MIN_MAGIC_AND_ID_LEN: usize = 8;
219        const OFFSET_HISTORY_LEN: usize = 12;
220
221        if raw.len() < MIN_MAGIC_AND_ID_LEN {
222            return Err(DictionaryDecodeError::DictionaryTooSmall {
223                got: raw.len(),
224                need: MIN_MAGIC_AND_ID_LEN,
225            });
226        }
227
228        let mut new_dict = Dictionary {
229            id: 0,
230            fse: Box::new(FSEScratch::new()),
231            huf: HuffmanScratch::new(),
232            dict_content: Vec::new(),
233            offset_hist: [1, 4, 8],
234        };
235
236        let magic_num: [u8; 4] = raw[..4].try_into().expect("optimized away");
237        if magic_num != MAGIC_NUM {
238            return Err(DictionaryDecodeError::BadMagicNum { got: magic_num });
239        }
240
241        let dict_id = raw[4..8].try_into().expect("optimized away");
242        let dict_id = u32::from_le_bytes(dict_id);
243        if dict_id == 0 {
244            return Err(DictionaryDecodeError::ZeroDictionaryId);
245        }
246        new_dict.id = dict_id;
247
248        let raw_tables = &raw[8..];
249
250        let huf_size = if build_decode_tables {
251            new_dict.huf.table.build_decoder(raw_tables)?
252        } else {
253            new_dict.huf.table.build_weights_only(raw_tables)?
254        };
255        let raw_tables = &raw_tables[huf_size as usize..];
256
257        let of_size = if build_decode_tables {
258            let n = new_dict.fse.offsets.build_decoder(
259                raw_tables,
260                crate::decoding::sequence_section_decoder::OF_MAX_LOG,
261            )?;
262            new_dict.fse.offsets.enrich_for_offsets();
263            // Compute the pipeline-gate long-offset share ONCE here, while the
264            // dictionary handle is built, so the per-decode `init_from_dict`
265            // path can COPY it instead of re-walking the offsets table on every
266            // `decode_*_with_dict_handle` call (the dict is immutable, so the
267            // share never changes after this).
268            new_dict.fse.offsets_long_share =
269                crate::decoding::sequence_section_decoder::compute_offsets_long_share(
270                    &new_dict.fse.offsets,
271                );
272            n
273        } else {
274            new_dict.fse.offsets.read_table_probabilities(
275                raw_tables,
276                crate::decoding::sequence_section_decoder::OF_MAX_LOG,
277            )?
278        };
279        let raw_tables = &raw_tables[of_size..];
280
281        let ml_size = if build_decode_tables {
282            let n = new_dict.fse.match_lengths.build_decoder(
283                raw_tables,
284                crate::decoding::sequence_section_decoder::ML_MAX_LOG,
285            )?;
286            new_dict
287                .fse
288                .match_lengths
289                .enrich_with_packed_seq_meta(&crate::decoding::sequence_section_decoder::ML_META);
290            n
291        } else {
292            new_dict.fse.match_lengths.read_table_probabilities(
293                raw_tables,
294                crate::decoding::sequence_section_decoder::ML_MAX_LOG,
295            )?
296        };
297        let raw_tables = &raw_tables[ml_size..];
298
299        let ll_size = if build_decode_tables {
300            let n = new_dict.fse.literal_lengths.build_decoder(
301                raw_tables,
302                crate::decoding::sequence_section_decoder::LL_MAX_LOG,
303            )?;
304            new_dict
305                .fse
306                .literal_lengths
307                .enrich_with_packed_seq_meta(&crate::decoding::sequence_section_decoder::LL_META);
308            n
309        } else {
310            new_dict.fse.literal_lengths.read_table_probabilities(
311                raw_tables,
312                crate::decoding::sequence_section_decoder::LL_MAX_LOG,
313            )?
314        };
315        let raw_tables = &raw_tables[ll_size..];
316
317        if raw_tables.len() < OFFSET_HISTORY_LEN {
318            return Err(DictionaryDecodeError::DictionaryTooSmall {
319                got: raw_tables.len(),
320                need: OFFSET_HISTORY_LEN,
321            });
322        }
323
324        let offset1 = raw_tables[0..4].try_into().expect("optimized away");
325        let offset1 = u32::from_le_bytes(offset1);
326
327        let offset2 = raw_tables[4..8].try_into().expect("optimized away");
328        let offset2 = u32::from_le_bytes(offset2);
329
330        let offset3 = raw_tables[8..12].try_into().expect("optimized away");
331        let offset3 = u32::from_le_bytes(offset3);
332
333        if offset1 == 0 {
334            return Err(DictionaryDecodeError::ZeroRepeatOffsetInDictionary { index: 0 });
335        }
336        if offset2 == 0 {
337            return Err(DictionaryDecodeError::ZeroRepeatOffsetInDictionary { index: 1 });
338        }
339        if offset3 == 0 {
340            return Err(DictionaryDecodeError::ZeroRepeatOffsetInDictionary { index: 2 });
341        }
342
343        new_dict.offset_hist[0] = offset1;
344        new_dict.offset_hist[1] = offset2;
345        new_dict.offset_hist[2] = offset3;
346
347        let raw_content = &raw_tables[12..];
348        new_dict.dict_content.extend(raw_content);
349
350        Ok(new_dict)
351    }
352
353    /// Convert this parsed dictionary into a reusable shared handle.
354    pub fn into_handle(self) -> DictionaryHandle {
355        DictionaryHandle::from_dictionary(self)
356    }
357}
358
359impl DictionaryHandle {
360    /// Wrap an already-parsed dictionary in a shared handle.
361    pub fn from_dictionary(dict: Dictionary) -> Self {
362        Self {
363            inner: SharedDictionary::new(dict),
364        }
365    }
366
367    /// Heap bytes the handle keeps alive: the shared allocation the
368    /// dictionary lives in and the dictionary's own content and tables.
369    /// Clones share all of it.
370    ///
371    /// # Examples
372    ///
373    /// ```
374    /// use structured_zstd::decoding::{Dictionary, DictionaryHandle};
375    ///
376    /// let handle = DictionaryHandle::from_dictionary(
377    ///     Dictionary::from_raw_content(0, vec![7u8; 1024]).unwrap(),
378    /// );
379    /// assert!(handle.heap_size() > handle.as_dict().heap_bytes());
380    /// assert!(handle.heap_size() <= DictionaryHandle::max_heap_size(1024));
381    /// ```
382    pub fn heap_size(&self) -> usize {
383        Self::shared_allocation_size() + self.inner.heap_bytes()
384    }
385
386    /// Most [`Self::heap_size`] a handle over `content_len` bytes of content
387    /// can report, for budgeting before the dictionary is parsed.
388    pub fn max_heap_size(content_len: usize) -> usize {
389        Self::shared_allocation_size() + Dictionary::max_heap_bytes(content_len)
390    }
391
392    /// Heap bytes that `handles` alone keep alive: each distinct dictionary
393    /// among them once, and only when no handle outside them shares it. For an
394    /// owner holding several handles, such as a decoding context that keeps a
395    /// dictionary attached and its decoder keeps the last frame's.
396    ///
397    /// # Examples
398    ///
399    /// ```
400    /// use structured_zstd::decoding::{Dictionary, DictionaryHandle};
401    ///
402    /// let handle = DictionaryHandle::from_dictionary(
403    ///     Dictionary::from_raw_content(0, vec![7u8; 64]).unwrap(),
404    /// );
405    /// let copy = handle.clone();
406    /// // Both handles are in the set: counted once.
407    /// assert_eq!(
408    ///     DictionaryHandle::exclusive_heap_size([&handle, &copy]),
409    ///     handle.heap_size()
410    /// );
411    /// // `copy` is held elsewhere: nothing is this set's alone.
412    /// assert_eq!(DictionaryHandle::exclusive_heap_size([&handle]), 0);
413    /// ```
414    pub fn exclusive_heap_size<'a, I>(handles: I) -> usize
415    where
416        I: IntoIterator<Item = &'a DictionaryHandle>,
417        I::IntoIter: Clone,
418    {
419        let handles = handles.into_iter();
420        let mut total = 0;
421        for (position, handle) in handles.clone().enumerate() {
422            let same =
423                |other: &&DictionaryHandle| SharedDictionary::ptr_eq(&handle.inner, &other.inner);
424            // The first handle to an allocation speaks for all of them.
425            if handles.clone().take(position).any(|other| same(&other)) {
426                continue;
427            }
428            // Holders, not references: one handle listed twice is one count.
429            let held_here = handles
430                .clone()
431                .enumerate()
432                .filter(|(at, other)| {
433                    same(other)
434                        && !handles
435                            .clone()
436                            .take(*at)
437                            .any(|earlier| core::ptr::eq(earlier, *other))
438                })
439                .count();
440            if SharedDictionary::strong_count(&handle.inner) == held_here {
441                total += handle.heap_size();
442            }
443        }
444        total
445    }
446
447    /// Bytes of the shared allocation itself: `Arc` and `Rc` both place two
448    /// word-sized counts ahead of the value, padded to its alignment.
449    fn shared_allocation_size() -> usize {
450        core::alloc::Layout::new::<[usize; 2]>()
451            .extend(core::alloc::Layout::new::<Dictionary>())
452            .expect("a fixed-size layout extends")
453            .0
454            .pad_to_align()
455            .size()
456    }
457
458    /// Parse a serialized dictionary and return a reusable shared handle.
459    pub fn decode_dict(raw: &[u8]) -> Result<Self, DictionaryDecodeError> {
460        Dictionary::decode_dict(raw).map(Self::from_dictionary)
461    }
462
463    /// Load whichever kind of dictionary `raw` holds, as `ZSTD_createDDict`
464    /// does: a [serialized](Dictionary::is_serialized) dictionary, or raw
465    /// content. See
466    /// [`Dictionary::from_serialized_or_raw_content`].
467    pub fn from_serialized_or_raw_content(raw: &[u8]) -> Result<Self, DictionaryDecodeError> {
468        Dictionary::from_serialized_or_raw_content(raw).map(Self::from_dictionary)
469    }
470
471    pub fn id(&self) -> u32 {
472        self.inner.id
473    }
474
475    pub fn as_dict(&self) -> &Dictionary {
476        &self.inner
477    }
478}
479
480impl AsRef<Dictionary> for DictionaryHandle {
481    fn as_ref(&self) -> &Dictionary {
482        self.as_dict()
483    }
484}
485
486impl From<Dictionary> for DictionaryHandle {
487    fn from(dict: Dictionary) -> Self {
488        DictionaryHandle::from_dictionary(dict)
489    }
490}
491
492#[cfg(test)]
493mod tests;