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, ©]),
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;