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