Skip to main content

structured_zstd/decoding/
dictionary.rs

1#[cfg(not(target_has_atomic = "ptr"))]
2use alloc::rc::Rc;
3#[cfg(target_has_atomic = "ptr")]
4use alloc::sync::Arc;
5use alloc::vec::Vec;
6use core::convert::TryInto;
7
8use crate::decoding::errors::DictionaryDecodeError;
9use crate::decoding::scratch::FSEScratch;
10use crate::decoding::scratch::HuffmanScratch;
11
12/// Zstandard includes support for "raw content" dictionaries, that store bytes optionally used
13/// during sequence execution.
14///
15/// <https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md#dictionary-format>
16#[derive(Clone)]
17pub struct Dictionary {
18    /// A 4 byte value used by decoders to check if they can use
19    /// the correct dictionary.
20    ///
21    /// Zero means unidentified: a raw-content dictionary has no header to
22    /// carry an ID, and the frames built from it record none, so it can only
23    /// be supplied explicitly and never resolved from a frame header.
24    /// Registration by ID
25    /// ([`FrameDecoder::add_dict`](crate::decoding::FrameDecoder::add_dict))
26    /// therefore still requires a non-zero one.
27    pub id: u32,
28    /// A dictionary can contain an entropy table, either FSE or
29    /// Huffman.
30    pub fse: FSEScratch,
31    /// A dictionary can contain an entropy table, either FSE or
32    /// Huffman.
33    pub huf: HuffmanScratch,
34    /// The content of a dictionary acts as a "past" in front of data
35    /// to compress or decompress,
36    /// so it can be referenced in sequence commands.
37    /// As long as the amount of data decoded from this frame is less than or
38    /// equal to Window_Size, sequence commands may specify offsets longer than
39    /// the total length of decoded output so far to reference back to the
40    /// dictionary, even parts of the dictionary with offsets larger than Window_Size.
41    /// After the total output has surpassed Window_Size however,
42    /// this is no longer allowed and the dictionary is no longer accessible
43    pub dict_content: Vec<u8>,
44    /// The 3 most recent offsets are stored so that they can be used
45    /// during sequence execution, see
46    /// <https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md#repeat-offsets>
47    /// for more.
48    pub offset_hist: [u32; 3],
49}
50
51/// A parsed dictionary held by however many users need it at once.
52///
53/// Both sides prime frame after frame from one dictionary, so what they hold is
54/// shared rather than copied: `Arc` where atomics exist, `Rc` where they do not.
55#[cfg(target_has_atomic = "ptr")]
56pub(crate) type SharedDictionary = Arc<Dictionary>;
57#[cfg(not(target_has_atomic = "ptr"))]
58pub(crate) type SharedDictionary = Rc<Dictionary>;
59
60/// Shared pre-parsed dictionary handle for repeated decoding.
61///
62/// Uses `Arc` on targets with atomics and falls back to `Rc` otherwise.
63#[derive(Clone)]
64pub struct DictionaryHandle {
65    inner: SharedDictionary,
66}
67
68/// This 4 byte (little endian) magic number refers to the start of a dictionary
69pub const MAGIC_NUM: [u8; 4] = [0x37, 0xA4, 0x30, 0xEC];
70
71impl Dictionary {
72    /// Heap bytes owned by this dictionary: the content plus the parsed
73    /// entropy tables' heap (the fixed-size FSE decode arrays are inline,
74    /// counted by `size_of::<Dictionary>()`).
75    pub fn heap_bytes(&self) -> usize {
76        self.dict_content.capacity() + self.fse.heap_bytes() + self.huf.heap_bytes()
77    }
78
79    /// Build a dictionary from raw content bytes (without entropy table sections).
80    ///
81    /// This is primarily intended for dictionaries produced by the `dict-builder`
82    /// module, which currently emits raw-content dictionaries.
83    ///
84    /// An `id` of 0 means the dictionary is unidentified, which is what any
85    /// plain file used as a dictionary is: frames built with it record no
86    /// dictionary ID, so it can only ever be supplied explicitly, never
87    /// resolved from a frame header. Registration by ID
88    /// ([`FrameDecoder::add_dict`](crate::decoding::FrameDecoder::add_dict))
89    /// still requires a non-zero one, since the ID is the key it is stored
90    /// under.
91    pub fn from_raw_content(
92        id: u32,
93        dict_content: Vec<u8>,
94    ) -> Result<Dictionary, DictionaryDecodeError> {
95        if dict_content.is_empty() {
96            return Err(DictionaryDecodeError::DictionaryTooSmall { got: 0, need: 1 });
97        }
98
99        Ok(Dictionary {
100            id,
101            fse: FSEScratch::new(),
102            huf: HuffmanScratch::new(),
103            dict_content,
104            offset_hist: [1, 4, 8],
105        })
106    }
107
108    /// Parses the dictionary from `raw`, initializes its tables,
109    /// and returns a fully constructed [`Dictionary`] whose `id` can be
110    /// checked against the frame's `dict_id`.
111    pub fn decode_dict(raw: &[u8]) -> Result<Dictionary, DictionaryDecodeError> {
112        Self::decode_dict_inner(raw, true)
113    }
114
115    /// Loads whichever kind of dictionary `raw` holds, the way `zstd -D` does:
116    /// a blob starting with [`MAGIC_NUM`] is a serialized dictionary with
117    /// entropy tables and an ID, and anything else is taken as raw content,
118    /// which is why any file can be handed to `-D`. A raw-content dictionary
119    /// has no ID, so it must be supplied explicitly on both sides — see
120    /// [`Self::from_raw_content`].
121    pub fn from_serialized_or_raw_content(raw: &[u8]) -> Result<Dictionary, DictionaryDecodeError> {
122        if raw.starts_with(&MAGIC_NUM) {
123            Self::decode_dict(raw)
124        } else if raw.is_empty() {
125            // A zero-sized buffer is a dictionary with nothing in it rather
126            // than a malformed one: `ZSTD_createDDict(NULL, 0)` builds a
127            // usable `DDict` referencing no content, and
128            // `ZSTD_CCtx_loadDictionary` with an empty buffer is how a caller
129            // says "no dictionary". [`Self::from_raw_content`] still refuses
130            // it, because naming raw content and handing over none is the
131            // caller asking for a dictionary that cannot exist.
132            Ok(Dictionary {
133                id: 0,
134                fse: FSEScratch::new(),
135                huf: HuffmanScratch::new(),
136                dict_content: Vec::new(),
137                offset_hist: [1, 4, 8],
138            })
139        } else {
140            Self::from_raw_content(0, raw.to_vec())
141        }
142    }
143
144    /// Parse a dictionary for ENCODER use: builds the entropy
145    /// probabilities/weights needed by `to_encoder_table` but skips the
146    /// decode-only work the encoder never reads — the FSE *decoding*
147    /// tables + their `enrich_*` post-passes, and the HUF decode lookup
148    /// table (`packed_decode`). Produces a [`Dictionary`] whose FSE
149    /// `symbol_probabilities` / `accuracy_log` and HUF `bits` /
150    /// `max_num_bits` match `decode_dict` exactly, so the encoder entropy
151    /// tables — and thus the emitted frame — are byte-identical; only the
152    /// wasted decode-table builds are dropped. Offset history + content
153    /// are parsed the same way.
154    /// Crate-internal: the returned [`Dictionary`] deliberately has no
155    /// decode lookup tables (`packed_decode` / FSE `decode`), so it is
156    /// NOT safe to feed into a [`FrameDecoder`](crate::decoding::FrameDecoder)
157    /// — Huffman decode would index an empty `packed_decode`. The only caller
158    /// is `EncoderDictionary::from_bytes`, which wraps the result in the
159    /// encoder-only `EncoderDictionary` type (no decode path), so this
160    /// incomplete dictionary can never escape to the decode side. Keeping
161    /// this `pub(crate)` keeps it off the public `Dictionary` API entirely.
162    pub(crate) fn decode_dict_for_encoding(
163        raw: &[u8],
164    ) -> Result<Dictionary, DictionaryDecodeError> {
165        Self::decode_dict_inner(raw, false)
166    }
167
168    /// Shared dictionary parser. `build_decode_tables` selects whether the
169    /// FSE/HUF tables get their full decoding tables (FSE decode table +
170    /// `enrich_*`, HUF `packed_decode`; decoder path) or only the
171    /// probability/weight parse (encoder path — see
172    /// [`Self::decode_dict_for_encoding`]).
173    fn decode_dict_inner(
174        raw: &[u8],
175        build_decode_tables: bool,
176    ) -> Result<Dictionary, DictionaryDecodeError> {
177        const MIN_MAGIC_AND_ID_LEN: usize = 8;
178        const OFFSET_HISTORY_LEN: usize = 12;
179
180        if raw.len() < MIN_MAGIC_AND_ID_LEN {
181            return Err(DictionaryDecodeError::DictionaryTooSmall {
182                got: raw.len(),
183                need: MIN_MAGIC_AND_ID_LEN,
184            });
185        }
186
187        let mut new_dict = Dictionary {
188            id: 0,
189            fse: FSEScratch::new(),
190            huf: HuffmanScratch::new(),
191            dict_content: Vec::new(),
192            offset_hist: [1, 4, 8],
193        };
194
195        let magic_num: [u8; 4] = raw[..4].try_into().expect("optimized away");
196        if magic_num != MAGIC_NUM {
197            return Err(DictionaryDecodeError::BadMagicNum { got: magic_num });
198        }
199
200        let dict_id = raw[4..8].try_into().expect("optimized away");
201        let dict_id = u32::from_le_bytes(dict_id);
202        if dict_id == 0 {
203            return Err(DictionaryDecodeError::ZeroDictionaryId);
204        }
205        new_dict.id = dict_id;
206
207        let raw_tables = &raw[8..];
208
209        let huf_size = if build_decode_tables {
210            new_dict.huf.table.build_decoder(raw_tables)?
211        } else {
212            new_dict.huf.table.build_weights_only(raw_tables)?
213        };
214        let raw_tables = &raw_tables[huf_size as usize..];
215
216        let of_size = if build_decode_tables {
217            let n = new_dict.fse.offsets.build_decoder(
218                raw_tables,
219                crate::decoding::sequence_section_decoder::OF_MAX_LOG,
220            )?;
221            new_dict.fse.offsets.enrich_for_offsets();
222            // Compute the pipeline-gate long-offset share ONCE here, while the
223            // dictionary handle is built, so the per-decode `init_from_dict`
224            // path can COPY it instead of re-walking the offsets table on every
225            // `decode_*_with_dict_handle` call (the dict is immutable, so the
226            // share never changes after this).
227            new_dict.fse.offsets_long_share =
228                crate::decoding::sequence_section_decoder::compute_offsets_long_share(
229                    &new_dict.fse.offsets,
230                );
231            n
232        } else {
233            new_dict.fse.offsets.read_table_probabilities(
234                raw_tables,
235                crate::decoding::sequence_section_decoder::OF_MAX_LOG,
236            )?
237        };
238        let raw_tables = &raw_tables[of_size..];
239
240        let ml_size = if build_decode_tables {
241            let n = new_dict.fse.match_lengths.build_decoder(
242                raw_tables,
243                crate::decoding::sequence_section_decoder::ML_MAX_LOG,
244            )?;
245            new_dict
246                .fse
247                .match_lengths
248                .enrich_with_packed_seq_meta(&crate::decoding::sequence_section_decoder::ML_META);
249            n
250        } else {
251            new_dict.fse.match_lengths.read_table_probabilities(
252                raw_tables,
253                crate::decoding::sequence_section_decoder::ML_MAX_LOG,
254            )?
255        };
256        let raw_tables = &raw_tables[ml_size..];
257
258        let ll_size = if build_decode_tables {
259            let n = new_dict.fse.literal_lengths.build_decoder(
260                raw_tables,
261                crate::decoding::sequence_section_decoder::LL_MAX_LOG,
262            )?;
263            new_dict
264                .fse
265                .literal_lengths
266                .enrich_with_packed_seq_meta(&crate::decoding::sequence_section_decoder::LL_META);
267            n
268        } else {
269            new_dict.fse.literal_lengths.read_table_probabilities(
270                raw_tables,
271                crate::decoding::sequence_section_decoder::LL_MAX_LOG,
272            )?
273        };
274        let raw_tables = &raw_tables[ll_size..];
275
276        if raw_tables.len() < OFFSET_HISTORY_LEN {
277            return Err(DictionaryDecodeError::DictionaryTooSmall {
278                got: raw_tables.len(),
279                need: OFFSET_HISTORY_LEN,
280            });
281        }
282
283        let offset1 = raw_tables[0..4].try_into().expect("optimized away");
284        let offset1 = u32::from_le_bytes(offset1);
285
286        let offset2 = raw_tables[4..8].try_into().expect("optimized away");
287        let offset2 = u32::from_le_bytes(offset2);
288
289        let offset3 = raw_tables[8..12].try_into().expect("optimized away");
290        let offset3 = u32::from_le_bytes(offset3);
291
292        if offset1 == 0 {
293            return Err(DictionaryDecodeError::ZeroRepeatOffsetInDictionary { index: 0 });
294        }
295        if offset2 == 0 {
296            return Err(DictionaryDecodeError::ZeroRepeatOffsetInDictionary { index: 1 });
297        }
298        if offset3 == 0 {
299            return Err(DictionaryDecodeError::ZeroRepeatOffsetInDictionary { index: 2 });
300        }
301
302        new_dict.offset_hist[0] = offset1;
303        new_dict.offset_hist[1] = offset2;
304        new_dict.offset_hist[2] = offset3;
305
306        let raw_content = &raw_tables[12..];
307        new_dict.dict_content.extend(raw_content);
308
309        Ok(new_dict)
310    }
311
312    /// Convert this parsed dictionary into a reusable shared handle.
313    pub fn into_handle(self) -> DictionaryHandle {
314        DictionaryHandle::from_dictionary(self)
315    }
316}
317
318impl DictionaryHandle {
319    /// Wrap an already-parsed dictionary in a shared handle.
320    pub fn from_dictionary(dict: Dictionary) -> Self {
321        Self {
322            inner: SharedDictionary::new(dict),
323        }
324    }
325
326    /// Parse a serialized dictionary and return a reusable shared handle.
327    pub fn decode_dict(raw: &[u8]) -> Result<Self, DictionaryDecodeError> {
328        Dictionary::decode_dict(raw).map(Self::from_dictionary)
329    }
330
331    /// Load whichever kind of dictionary `raw` holds, as `ZSTD_createDDict`
332    /// does: a blob starting with [`MAGIC_NUM`] is a serialized dictionary,
333    /// anything else is raw content. See
334    /// [`Dictionary::from_serialized_or_raw_content`].
335    pub fn from_serialized_or_raw_content(raw: &[u8]) -> Result<Self, DictionaryDecodeError> {
336        Dictionary::from_serialized_or_raw_content(raw).map(Self::from_dictionary)
337    }
338
339    pub fn id(&self) -> u32 {
340        self.inner.id
341    }
342
343    pub fn as_dict(&self) -> &Dictionary {
344        &self.inner
345    }
346}
347
348impl AsRef<Dictionary> for DictionaryHandle {
349    fn as_ref(&self) -> &Dictionary {
350        self.as_dict()
351    }
352}
353
354impl From<Dictionary> for DictionaryHandle {
355    fn from(dict: Dictionary) -> Self {
356        DictionaryHandle::from_dictionary(dict)
357    }
358}
359
360#[cfg(test)]
361mod tests;