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 {
125 Self::from_raw_content(0, raw.to_vec())
126 }
127 }
128
129 /// Parse a dictionary for ENCODER use: builds the entropy
130 /// probabilities/weights needed by `to_encoder_table` but skips the
131 /// decode-only work the encoder never reads — the FSE *decoding*
132 /// tables + their `enrich_*` post-passes, and the HUF decode lookup
133 /// table (`packed_decode`). Produces a [`Dictionary`] whose FSE
134 /// `symbol_probabilities` / `accuracy_log` and HUF `bits` /
135 /// `max_num_bits` match `decode_dict` exactly, so the encoder entropy
136 /// tables — and thus the emitted frame — are byte-identical; only the
137 /// wasted decode-table builds are dropped. Offset history + content
138 /// are parsed the same way.
139 /// Crate-internal: the returned [`Dictionary`] deliberately has no
140 /// decode lookup tables (`packed_decode` / FSE `decode`), so it is
141 /// NOT safe to feed into a [`FrameDecoder`](crate::decoding::FrameDecoder)
142 /// — Huffman decode would index an empty `packed_decode`. The only caller
143 /// is `EncoderDictionary::from_bytes`, which wraps the result in the
144 /// encoder-only `EncoderDictionary` type (no decode path), so this
145 /// incomplete dictionary can never escape to the decode side. Keeping
146 /// this `pub(crate)` keeps it off the public `Dictionary` API entirely.
147 pub(crate) fn decode_dict_for_encoding(
148 raw: &[u8],
149 ) -> Result<Dictionary, DictionaryDecodeError> {
150 Self::decode_dict_inner(raw, false)
151 }
152
153 /// Shared dictionary parser. `build_decode_tables` selects whether the
154 /// FSE/HUF tables get their full decoding tables (FSE decode table +
155 /// `enrich_*`, HUF `packed_decode`; decoder path) or only the
156 /// probability/weight parse (encoder path — see
157 /// [`Self::decode_dict_for_encoding`]).
158 fn decode_dict_inner(
159 raw: &[u8],
160 build_decode_tables: bool,
161 ) -> Result<Dictionary, DictionaryDecodeError> {
162 const MIN_MAGIC_AND_ID_LEN: usize = 8;
163 const OFFSET_HISTORY_LEN: usize = 12;
164
165 if raw.len() < MIN_MAGIC_AND_ID_LEN {
166 return Err(DictionaryDecodeError::DictionaryTooSmall {
167 got: raw.len(),
168 need: MIN_MAGIC_AND_ID_LEN,
169 });
170 }
171
172 let mut new_dict = Dictionary {
173 id: 0,
174 fse: FSEScratch::new(),
175 huf: HuffmanScratch::new(),
176 dict_content: Vec::new(),
177 offset_hist: [1, 4, 8],
178 };
179
180 let magic_num: [u8; 4] = raw[..4].try_into().expect("optimized away");
181 if magic_num != MAGIC_NUM {
182 return Err(DictionaryDecodeError::BadMagicNum { got: magic_num });
183 }
184
185 let dict_id = raw[4..8].try_into().expect("optimized away");
186 let dict_id = u32::from_le_bytes(dict_id);
187 if dict_id == 0 {
188 return Err(DictionaryDecodeError::ZeroDictionaryId);
189 }
190 new_dict.id = dict_id;
191
192 let raw_tables = &raw[8..];
193
194 let huf_size = if build_decode_tables {
195 new_dict.huf.table.build_decoder(raw_tables)?
196 } else {
197 new_dict.huf.table.build_weights_only(raw_tables)?
198 };
199 let raw_tables = &raw_tables[huf_size as usize..];
200
201 let of_size = if build_decode_tables {
202 let n = new_dict.fse.offsets.build_decoder(
203 raw_tables,
204 crate::decoding::sequence_section_decoder::OF_MAX_LOG,
205 )?;
206 new_dict.fse.offsets.enrich_for_offsets();
207 // Compute the pipeline-gate long-offset share ONCE here, while the
208 // dictionary handle is built, so the per-decode `init_from_dict`
209 // path can COPY it instead of re-walking the offsets table on every
210 // `decode_*_with_dict_handle` call (the dict is immutable, so the
211 // share never changes after this).
212 new_dict.fse.offsets_long_share =
213 crate::decoding::sequence_section_decoder::compute_offsets_long_share(
214 &new_dict.fse.offsets,
215 );
216 n
217 } else {
218 new_dict.fse.offsets.read_table_probabilities(
219 raw_tables,
220 crate::decoding::sequence_section_decoder::OF_MAX_LOG,
221 )?
222 };
223 let raw_tables = &raw_tables[of_size..];
224
225 let ml_size = if build_decode_tables {
226 let n = new_dict.fse.match_lengths.build_decoder(
227 raw_tables,
228 crate::decoding::sequence_section_decoder::ML_MAX_LOG,
229 )?;
230 new_dict
231 .fse
232 .match_lengths
233 .enrich_with_packed_seq_meta(&crate::decoding::sequence_section_decoder::ML_META);
234 n
235 } else {
236 new_dict.fse.match_lengths.read_table_probabilities(
237 raw_tables,
238 crate::decoding::sequence_section_decoder::ML_MAX_LOG,
239 )?
240 };
241 let raw_tables = &raw_tables[ml_size..];
242
243 let ll_size = if build_decode_tables {
244 let n = new_dict.fse.literal_lengths.build_decoder(
245 raw_tables,
246 crate::decoding::sequence_section_decoder::LL_MAX_LOG,
247 )?;
248 new_dict
249 .fse
250 .literal_lengths
251 .enrich_with_packed_seq_meta(&crate::decoding::sequence_section_decoder::LL_META);
252 n
253 } else {
254 new_dict.fse.literal_lengths.read_table_probabilities(
255 raw_tables,
256 crate::decoding::sequence_section_decoder::LL_MAX_LOG,
257 )?
258 };
259 let raw_tables = &raw_tables[ll_size..];
260
261 if raw_tables.len() < OFFSET_HISTORY_LEN {
262 return Err(DictionaryDecodeError::DictionaryTooSmall {
263 got: raw_tables.len(),
264 need: OFFSET_HISTORY_LEN,
265 });
266 }
267
268 let offset1 = raw_tables[0..4].try_into().expect("optimized away");
269 let offset1 = u32::from_le_bytes(offset1);
270
271 let offset2 = raw_tables[4..8].try_into().expect("optimized away");
272 let offset2 = u32::from_le_bytes(offset2);
273
274 let offset3 = raw_tables[8..12].try_into().expect("optimized away");
275 let offset3 = u32::from_le_bytes(offset3);
276
277 if offset1 == 0 {
278 return Err(DictionaryDecodeError::ZeroRepeatOffsetInDictionary { index: 0 });
279 }
280 if offset2 == 0 {
281 return Err(DictionaryDecodeError::ZeroRepeatOffsetInDictionary { index: 1 });
282 }
283 if offset3 == 0 {
284 return Err(DictionaryDecodeError::ZeroRepeatOffsetInDictionary { index: 2 });
285 }
286
287 new_dict.offset_hist[0] = offset1;
288 new_dict.offset_hist[1] = offset2;
289 new_dict.offset_hist[2] = offset3;
290
291 let raw_content = &raw_tables[12..];
292 new_dict.dict_content.extend(raw_content);
293
294 Ok(new_dict)
295 }
296
297 /// Convert this parsed dictionary into a reusable shared handle.
298 pub fn into_handle(self) -> DictionaryHandle {
299 DictionaryHandle::from_dictionary(self)
300 }
301}
302
303impl DictionaryHandle {
304 /// Wrap an already-parsed dictionary in a shared handle.
305 pub fn from_dictionary(dict: Dictionary) -> Self {
306 Self {
307 inner: SharedDictionary::new(dict),
308 }
309 }
310
311 /// Parse a serialized dictionary and return a reusable shared handle.
312 pub fn decode_dict(raw: &[u8]) -> Result<Self, DictionaryDecodeError> {
313 Dictionary::decode_dict(raw).map(Self::from_dictionary)
314 }
315
316 pub fn id(&self) -> u32 {
317 self.inner.id
318 }
319
320 pub fn as_dict(&self) -> &Dictionary {
321 &self.inner
322 }
323}
324
325impl AsRef<Dictionary> for DictionaryHandle {
326 fn as_ref(&self) -> &Dictionary {
327 self.as_dict()
328 }
329}
330
331impl From<Dictionary> for DictionaryHandle {
332 fn from(dict: Dictionary) -> Self {
333 DictionaryHandle::from_dictionary(dict)
334 }
335}
336
337#[cfg(test)]
338mod tests;