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    /// Build a dictionary from raw content bytes (without entropy table sections).
88    ///
89    /// This is primarily intended for dictionaries produced by the `dict-builder`
90    /// module, which currently emits raw-content dictionaries.
91    ///
92    /// An `id` of 0 means the dictionary is unidentified, which is what any
93    /// plain file used as a dictionary is: frames built with it record no
94    /// dictionary ID, so it can only ever be supplied explicitly, never
95    /// resolved from a frame header. Registration by ID
96    /// ([`FrameDecoder::add_dict`](crate::decoding::FrameDecoder::add_dict))
97    /// still requires a non-zero one, since the ID is the key it is stored
98    /// under.
99    pub fn from_raw_content(
100        id: u32,
101        dict_content: Vec<u8>,
102    ) -> Result<Dictionary, DictionaryDecodeError> {
103        if dict_content.is_empty() {
104            return Err(DictionaryDecodeError::DictionaryTooSmall { got: 0, need: 1 });
105        }
106
107        Ok(Dictionary {
108            id,
109            fse: Box::new(FSEScratch::new()),
110            huf: HuffmanScratch::new(),
111            dict_content,
112            offset_hist: [1, 4, 8],
113        })
114    }
115
116    /// Parses the dictionary from `raw`, initializes its tables,
117    /// and returns a fully constructed [`Dictionary`] whose `id` can be
118    /// checked against the frame's `dict_id`.
119    pub fn decode_dict(raw: &[u8]) -> Result<Dictionary, DictionaryDecodeError> {
120        Self::decode_dict_inner(raw, true)
121    }
122
123    /// Loads whichever kind of dictionary `raw` holds, the way `zstd -D` does:
124    /// a blob starting with [`MAGIC_NUM`] is a serialized dictionary with
125    /// entropy tables and an ID, and anything else is taken as raw content,
126    /// which is why any file can be handed to `-D`. A raw-content dictionary
127    /// has no ID, so it must be supplied explicitly on both sides — see
128    /// [`Self::from_raw_content`].
129    pub fn from_serialized_or_raw_content(raw: &[u8]) -> Result<Dictionary, DictionaryDecodeError> {
130        if raw.starts_with(&MAGIC_NUM) {
131            Self::decode_dict(raw)
132        } else if raw.is_empty() {
133            // A zero-sized buffer is a dictionary with nothing in it rather
134            // than a malformed one: `ZSTD_createDDict(NULL, 0)` builds a
135            // usable `DDict` referencing no content, and
136            // `ZSTD_CCtx_loadDictionary` with an empty buffer is how a caller
137            // says "no dictionary". [`Self::from_raw_content`] still refuses
138            // it, because naming raw content and handing over none is the
139            // caller asking for a dictionary that cannot exist.
140            Ok(Dictionary {
141                id: 0,
142                fse: Box::new(FSEScratch::new()),
143                huf: HuffmanScratch::new(),
144                dict_content: Vec::new(),
145                offset_hist: [1, 4, 8],
146            })
147        } else {
148            Self::from_raw_content(0, raw.to_vec())
149        }
150    }
151
152    /// Parse a dictionary for ENCODER use: builds the entropy
153    /// probabilities/weights needed by `to_encoder_table` but skips the
154    /// decode-only work the encoder never reads — the FSE *decoding*
155    /// tables + their `enrich_*` post-passes, and the HUF decode lookup
156    /// table (`packed_decode`). Produces a [`Dictionary`] whose FSE
157    /// `symbol_probabilities` / `accuracy_log` and HUF `bits` /
158    /// `max_num_bits` match `decode_dict` exactly, so the encoder entropy
159    /// tables — and thus the emitted frame — are byte-identical; only the
160    /// wasted decode-table builds are dropped. Offset history + content
161    /// are parsed the same way.
162    /// Crate-internal: the returned [`Dictionary`] deliberately has no
163    /// decode lookup tables (`packed_decode` / FSE `decode`), so it is
164    /// NOT safe to feed into a [`FrameDecoder`](crate::decoding::FrameDecoder)
165    /// — Huffman decode would index an empty `packed_decode`. The only caller
166    /// is `EncoderDictionary::from_bytes`, which wraps the result in the
167    /// encoder-only `EncoderDictionary` type (no decode path), so this
168    /// incomplete dictionary can never escape to the decode side. Keeping
169    /// this `pub(crate)` keeps it off the public `Dictionary` API entirely.
170    pub(crate) fn decode_dict_for_encoding(
171        raw: &[u8],
172    ) -> Result<Dictionary, DictionaryDecodeError> {
173        Self::decode_dict_inner(raw, false)
174    }
175
176    /// Shared dictionary parser. `build_decode_tables` selects whether the
177    /// FSE/HUF tables get their full decoding tables (FSE decode table +
178    /// `enrich_*`, HUF `packed_decode`; decoder path) or only the
179    /// probability/weight parse (encoder path — see
180    /// [`Self::decode_dict_for_encoding`]).
181    fn decode_dict_inner(
182        raw: &[u8],
183        build_decode_tables: bool,
184    ) -> Result<Dictionary, DictionaryDecodeError> {
185        const MIN_MAGIC_AND_ID_LEN: usize = 8;
186        const OFFSET_HISTORY_LEN: usize = 12;
187
188        if raw.len() < MIN_MAGIC_AND_ID_LEN {
189            return Err(DictionaryDecodeError::DictionaryTooSmall {
190                got: raw.len(),
191                need: MIN_MAGIC_AND_ID_LEN,
192            });
193        }
194
195        let mut new_dict = Dictionary {
196            id: 0,
197            fse: Box::new(FSEScratch::new()),
198            huf: HuffmanScratch::new(),
199            dict_content: Vec::new(),
200            offset_hist: [1, 4, 8],
201        };
202
203        let magic_num: [u8; 4] = raw[..4].try_into().expect("optimized away");
204        if magic_num != MAGIC_NUM {
205            return Err(DictionaryDecodeError::BadMagicNum { got: magic_num });
206        }
207
208        let dict_id = raw[4..8].try_into().expect("optimized away");
209        let dict_id = u32::from_le_bytes(dict_id);
210        if dict_id == 0 {
211            return Err(DictionaryDecodeError::ZeroDictionaryId);
212        }
213        new_dict.id = dict_id;
214
215        let raw_tables = &raw[8..];
216
217        let huf_size = if build_decode_tables {
218            new_dict.huf.table.build_decoder(raw_tables)?
219        } else {
220            new_dict.huf.table.build_weights_only(raw_tables)?
221        };
222        let raw_tables = &raw_tables[huf_size as usize..];
223
224        let of_size = if build_decode_tables {
225            let n = new_dict.fse.offsets.build_decoder(
226                raw_tables,
227                crate::decoding::sequence_section_decoder::OF_MAX_LOG,
228            )?;
229            new_dict.fse.offsets.enrich_for_offsets();
230            // Compute the pipeline-gate long-offset share ONCE here, while the
231            // dictionary handle is built, so the per-decode `init_from_dict`
232            // path can COPY it instead of re-walking the offsets table on every
233            // `decode_*_with_dict_handle` call (the dict is immutable, so the
234            // share never changes after this).
235            new_dict.fse.offsets_long_share =
236                crate::decoding::sequence_section_decoder::compute_offsets_long_share(
237                    &new_dict.fse.offsets,
238                );
239            n
240        } else {
241            new_dict.fse.offsets.read_table_probabilities(
242                raw_tables,
243                crate::decoding::sequence_section_decoder::OF_MAX_LOG,
244            )?
245        };
246        let raw_tables = &raw_tables[of_size..];
247
248        let ml_size = if build_decode_tables {
249            let n = new_dict.fse.match_lengths.build_decoder(
250                raw_tables,
251                crate::decoding::sequence_section_decoder::ML_MAX_LOG,
252            )?;
253            new_dict
254                .fse
255                .match_lengths
256                .enrich_with_packed_seq_meta(&crate::decoding::sequence_section_decoder::ML_META);
257            n
258        } else {
259            new_dict.fse.match_lengths.read_table_probabilities(
260                raw_tables,
261                crate::decoding::sequence_section_decoder::ML_MAX_LOG,
262            )?
263        };
264        let raw_tables = &raw_tables[ml_size..];
265
266        let ll_size = if build_decode_tables {
267            let n = new_dict.fse.literal_lengths.build_decoder(
268                raw_tables,
269                crate::decoding::sequence_section_decoder::LL_MAX_LOG,
270            )?;
271            new_dict
272                .fse
273                .literal_lengths
274                .enrich_with_packed_seq_meta(&crate::decoding::sequence_section_decoder::LL_META);
275            n
276        } else {
277            new_dict.fse.literal_lengths.read_table_probabilities(
278                raw_tables,
279                crate::decoding::sequence_section_decoder::LL_MAX_LOG,
280            )?
281        };
282        let raw_tables = &raw_tables[ll_size..];
283
284        if raw_tables.len() < OFFSET_HISTORY_LEN {
285            return Err(DictionaryDecodeError::DictionaryTooSmall {
286                got: raw_tables.len(),
287                need: OFFSET_HISTORY_LEN,
288            });
289        }
290
291        let offset1 = raw_tables[0..4].try_into().expect("optimized away");
292        let offset1 = u32::from_le_bytes(offset1);
293
294        let offset2 = raw_tables[4..8].try_into().expect("optimized away");
295        let offset2 = u32::from_le_bytes(offset2);
296
297        let offset3 = raw_tables[8..12].try_into().expect("optimized away");
298        let offset3 = u32::from_le_bytes(offset3);
299
300        if offset1 == 0 {
301            return Err(DictionaryDecodeError::ZeroRepeatOffsetInDictionary { index: 0 });
302        }
303        if offset2 == 0 {
304            return Err(DictionaryDecodeError::ZeroRepeatOffsetInDictionary { index: 1 });
305        }
306        if offset3 == 0 {
307            return Err(DictionaryDecodeError::ZeroRepeatOffsetInDictionary { index: 2 });
308        }
309
310        new_dict.offset_hist[0] = offset1;
311        new_dict.offset_hist[1] = offset2;
312        new_dict.offset_hist[2] = offset3;
313
314        let raw_content = &raw_tables[12..];
315        new_dict.dict_content.extend(raw_content);
316
317        Ok(new_dict)
318    }
319
320    /// Convert this parsed dictionary into a reusable shared handle.
321    pub fn into_handle(self) -> DictionaryHandle {
322        DictionaryHandle::from_dictionary(self)
323    }
324}
325
326impl DictionaryHandle {
327    /// Wrap an already-parsed dictionary in a shared handle.
328    pub fn from_dictionary(dict: Dictionary) -> Self {
329        Self {
330            inner: SharedDictionary::new(dict),
331        }
332    }
333
334    /// Parse a serialized dictionary and return a reusable shared handle.
335    pub fn decode_dict(raw: &[u8]) -> Result<Self, DictionaryDecodeError> {
336        Dictionary::decode_dict(raw).map(Self::from_dictionary)
337    }
338
339    /// Load whichever kind of dictionary `raw` holds, as `ZSTD_createDDict`
340    /// does: a blob starting with [`MAGIC_NUM`] is a serialized dictionary,
341    /// anything else is raw content. See
342    /// [`Dictionary::from_serialized_or_raw_content`].
343    pub fn from_serialized_or_raw_content(raw: &[u8]) -> Result<Self, DictionaryDecodeError> {
344        Dictionary::from_serialized_or_raw_content(raw).map(Self::from_dictionary)
345    }
346
347    pub fn id(&self) -> u32 {
348        self.inner.id
349    }
350
351    pub fn as_dict(&self) -> &Dictionary {
352        &self.inner
353    }
354}
355
356impl AsRef<Dictionary> for DictionaryHandle {
357    fn as_ref(&self) -> &Dictionary {
358        self.as_dict()
359    }
360}
361
362impl From<Dictionary> for DictionaryHandle {
363    fn from(dict: Dictionary) -> Self {
364        DictionaryHandle::from_dictionary(dict)
365    }
366}
367
368#[cfg(test)]
369mod tests;