Skip to main content

zstd/
dict.rs

1//! Train a dictionary from various sources.
2//!
3//! A dictionary can help improve the compression of small files.
4//! The dictionary must be present during decompression,
5//! but can be shared across multiple "similar" files.
6//!
7//! Creating a dictionary using the `zstd` C library,
8//! using the `zstd` command-line interface, using this library,
9//! or using the `train` binary provided, should give the same result,
10//! and are therefore completely compatible.
11//!
12//! To use, see [`Encoder::with_dictionary`] or [`Decoder::with_dictionary`].
13//!
14//! [`Encoder::with_dictionary`]: ../struct.Encoder.html#method.with_dictionary
15//! [`Decoder::with_dictionary`]: ../struct.Decoder.html#method.with_dictionary
16
17#[cfg(feature = "zdict_builder")]
18use std::io::{self, Read};
19
20pub use zstd_safe::{CDict, DDict};
21
22/// Prepared dictionary for compression
23///
24/// A dictionary can include its own copy of the data (if it is `'static`), or it can merely point
25/// to a separate buffer (if it has another lifetime).
26pub struct EncoderDictionary<'a> {
27    cdict: CDict<'a>,
28}
29
30/// The error for a dictionary zstd could not load.
31fn invalid_dictionary() -> std::io::Error {
32    std::io::Error::new(
33        std::io::ErrorKind::InvalidData,
34        "zstd could not load the dictionary",
35    )
36}
37
38impl EncoderDictionary<'static> {
39    /// Creates a prepared dictionary for compression.
40    ///
41    /// This will copy the dictionary internally.
42    ///
43    /// # Panics
44    ///
45    /// If zstd cannot load the dictionary - for instance, it starts with the
46    /// dictionary magic number but its header is corrupt. Use
47    /// [`EncoderDictionary::try_copy`] for a dictionary you did not make
48    /// yourself.
49    pub fn copy(dictionary: &[u8], level: i32) -> Self {
50        Self::try_copy(dictionary, level)
51            .expect("zstd returned null pointer when creating dict")
52    }
53
54    /// Creates a prepared dictionary for compression, or an error if zstd
55    /// cannot load it.
56    ///
57    /// This will copy the dictionary internally.
58    pub fn try_copy(dictionary: &[u8], level: i32) -> std::io::Result<Self> {
59        let cdict = zstd_safe::CDict::try_create(dictionary, level)
60            .ok_or_else(invalid_dictionary)?;
61        Ok(Self { cdict })
62    }
63}
64
65impl<'a> EncoderDictionary<'a> {
66    #[cfg(feature = "experimental")]
67    #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "experimental")))]
68    /// Create prepared dictionary for compression
69    ///
70    /// A level of `0` uses zstd's default (currently `3`).
71    ///
72    /// Only available with the `experimental` feature. Use `EncoderDictionary::copy` otherwise.
73    ///
74    /// # Panics
75    ///
76    /// If zstd cannot load the dictionary. Use [`EncoderDictionary::try_new`]
77    /// for a dictionary you did not make yourself.
78    pub fn new(dictionary: &'a [u8], level: i32) -> Self {
79        Self::try_new(dictionary, level).expect("zstd returned null pointer")
80    }
81
82    #[cfg(feature = "experimental")]
83    #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "experimental")))]
84    /// Create prepared dictionary for compression, or an error if zstd cannot
85    /// load it.
86    ///
87    /// Only available with the `experimental` feature. Use
88    /// `EncoderDictionary::try_copy` otherwise.
89    pub fn try_new(dictionary: &'a [u8], level: i32) -> std::io::Result<Self> {
90        let cdict =
91            zstd_safe::CDict::try_create_by_reference(dictionary, level)
92                .ok_or_else(invalid_dictionary)?;
93        Ok(Self { cdict })
94    }
95
96    /// Returns reference to `CDict` inner object
97    pub fn as_cdict(&self) -> &CDict<'a> {
98        &self.cdict
99    }
100}
101
102/// Prepared dictionary for decompression
103pub struct DecoderDictionary<'a> {
104    ddict: DDict<'a>,
105}
106
107impl DecoderDictionary<'static> {
108    /// Create a prepared dictionary for decompression.
109    ///
110    /// This will copy the dictionary internally.
111    ///
112    /// # Panics
113    ///
114    /// If zstd cannot load the dictionary - for instance, it starts with the
115    /// dictionary magic number but its header is corrupt. Use
116    /// [`DecoderDictionary::try_copy`] for a dictionary you did not make
117    /// yourself.
118    pub fn copy(dictionary: &[u8]) -> Self {
119        Self::try_copy(dictionary)
120            .expect("zstd returned null pointer when creating dict")
121    }
122
123    /// Create a prepared dictionary for decompression, or an error if zstd
124    /// cannot load it.
125    ///
126    /// This will copy the dictionary internally.
127    pub fn try_copy(dictionary: &[u8]) -> std::io::Result<Self> {
128        let ddict = zstd_safe::DDict::try_create(dictionary)
129            .ok_or_else(invalid_dictionary)?;
130        Ok(Self { ddict })
131    }
132}
133
134impl<'a> DecoderDictionary<'a> {
135    #[cfg(feature = "experimental")]
136    #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "experimental")))]
137    /// Create prepared dictionary for decompression
138    ///
139    /// Only available with the `experimental` feature. Use `DecoderDictionary::copy` otherwise.
140    ///
141    /// # Panics
142    ///
143    /// If zstd cannot load the dictionary. Use [`DecoderDictionary::try_new`]
144    /// for a dictionary you did not make yourself.
145    pub fn new(dict: &'a [u8]) -> Self {
146        Self::try_new(dict).expect("zstd returned null pointer")
147    }
148
149    #[cfg(feature = "experimental")]
150    #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "experimental")))]
151    /// Create prepared dictionary for decompression, or an error if zstd
152    /// cannot load it.
153    ///
154    /// Only available with the `experimental` feature. Use
155    /// `DecoderDictionary::try_copy` otherwise.
156    pub fn try_new(dict: &'a [u8]) -> std::io::Result<Self> {
157        let ddict = zstd_safe::DDict::try_create_by_reference(dict)
158            .ok_or_else(invalid_dictionary)?;
159        Ok(Self { ddict })
160    }
161
162    /// Returns reference to `DDict` inner object
163    pub fn as_ddict(&self) -> &DDict<'a> {
164        &self.ddict
165    }
166}
167
168/// Train a dictionary from a big continuous chunk of data, with all samples
169/// contiguous in memory.
170///
171/// This is the most efficient way to train a dictionary,
172/// since this is directly fed into `zstd`.
173///
174/// * `sample_data` is the concatenation of all sample data.
175/// * `sample_sizes` is the size of each sample in `sample_data`.
176///   The sum of all `sample_sizes` should equal the length of `sample_data`.
177/// * `max_size` is the maximum size of the dictionary to generate.
178///
179/// The result is the dictionary data. You can, for example, feed it to [`CDict::create`].
180#[cfg(feature = "zdict_builder")]
181#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "zdict_builder")))]
182pub fn from_continuous(
183    sample_data: &[u8],
184    sample_sizes: &[usize],
185    max_size: usize,
186) -> io::Result<Vec<u8>> {
187    use crate::map_error_code;
188
189    // Complain if the lengths don't add up to the entire data.
190    // Add them with overflow detection: a wrapping sum would accept sizes
191    // reaching far outside of `sample_data`.
192    let total_size = sample_sizes
193        .iter()
194        .try_fold(0usize, |total, &size| total.checked_add(size));
195    if total_size != Some(sample_data.len()) {
196        return Err(io::Error::new(
197            io::ErrorKind::Other,
198            "sample sizes don't add up".to_string(),
199        ));
200    }
201
202    let mut result = Vec::with_capacity(max_size);
203    zstd_safe::train_from_buffer(&mut result, sample_data, sample_sizes)
204        .map_err(map_error_code)?;
205    Ok(result)
206}
207
208/// Train a dictionary from multiple samples.
209///
210/// The samples will internally be copied to a single continuous buffer,
211/// so make sure you have enough memory available.
212///
213/// If you need to stretch your system's limits,
214/// [`from_continuous`] directly uses the given slice.
215///
216/// [`from_continuous`]: ./fn.from_continuous.html
217///
218/// * `samples` is a list of individual samples to train on.
219/// * `max_size` is the maximum size of the dictionary to generate.
220///
221/// The result is the dictionary data. You can, for example, feed it to [`CDict::create`].
222#[cfg(feature = "zdict_builder")]
223#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "zdict_builder")))]
224pub fn from_samples<S: AsRef<[u8]>>(
225    samples: &[S],
226    max_size: usize,
227) -> io::Result<Vec<u8>> {
228    // Pre-allocate the entire required size.
229    let total_length: usize =
230        samples.iter().map(|sample| sample.as_ref().len()).sum();
231
232    let mut data = Vec::with_capacity(total_length);
233
234    // Copy every sample to a big chunk of memory
235    data.extend(samples.iter().flat_map(|s| s.as_ref()).cloned());
236
237    let sizes: Vec<_> = samples.iter().map(|s| s.as_ref().len()).collect();
238
239    from_continuous(&data, &sizes, max_size)
240}
241
242/// Train a dictionary from multiple samples.
243///
244/// Unlike [`from_samples`], this does not require having a list of all samples.
245/// It also allows running into an error when iterating through the samples.
246///
247/// They will still be copied to a continuous array and fed to [`from_continuous`].
248///
249/// * `samples` is an iterator of individual samples to train on.
250/// * `max_size` is the maximum size of the dictionary to generate.
251///
252/// The result is the dictionary data. You can, for example, feed it to [`CDict::create`].
253///
254/// # Examples
255///
256/// ```rust,no_run
257/// // Train from a couple of json files.
258/// let dict_buffer = zstd::dict::from_sample_iterator(
259///     ["file_a.json", "file_b.json"]
260///         .into_iter()
261///         .map(|filename| std::fs::File::open(filename)),
262///     10_000,  // 10kB dictionary
263/// ).unwrap();
264/// ```
265///
266/// ```rust,no_run
267/// use std::io::BufRead as _;
268/// // Treat each line from stdin as a separate sample.
269/// let dict_buffer = zstd::dict::from_sample_iterator(
270///     std::io::stdin().lock().lines().map(|line: std::io::Result<String>| {
271///         // Transform each line into a `Cursor<Vec<u8>>` so they implement Read.
272///         line.map(String::into_bytes)
273///             .map(std::io::Cursor::new)
274///     }),
275///     10_000,  // 10kB dictionary
276/// ).unwrap();
277/// ```
278#[cfg(feature = "zdict_builder")]
279#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "zdict_builder")))]
280pub fn from_sample_iterator<I, R>(
281    samples: I,
282    max_size: usize,
283) -> io::Result<Vec<u8>>
284where
285    I: IntoIterator<Item = io::Result<R>>,
286    R: Read,
287{
288    let mut data = Vec::new();
289    let mut sizes = Vec::new();
290
291    for sample in samples {
292        let mut sample = sample?;
293        let len = sample.read_to_end(&mut data)?;
294        sizes.push(len);
295    }
296
297    from_continuous(&data, &sizes, max_size)
298}
299
300/// Train a dict from a list of files.
301///
302/// * `filenames` is an iterator of files to load. Each file will be treated as an individual
303///   sample.
304/// * `max_size` is the maximum size of the dictionary to generate.
305///
306/// The result is the dictionary data. You can, for example, feed it to [`CDict::create`].
307#[cfg(feature = "zdict_builder")]
308#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "zdict_builder")))]
309pub fn from_files<I, P>(filenames: I, max_size: usize) -> io::Result<Vec<u8>>
310where
311    P: AsRef<std::path::Path>,
312    I: IntoIterator<Item = P>,
313{
314    from_sample_iterator(
315        filenames
316            .into_iter()
317            .map(|filename| std::fs::File::open(filename)),
318        max_size,
319    )
320}
321
322#[cfg(test)]
323#[cfg(feature = "zdict_builder")]
324mod tests {
325    use std::fs;
326    use std::io;
327    use std::io::Read;
328
329    use walkdir;
330
331    #[test]
332    fn test_dict_training() {
333        // Train a dictionary
334        let paths: Vec<_> = walkdir::WalkDir::new("src")
335            .into_iter()
336            .map(|entry| entry.unwrap())
337            .map(|entry| entry.into_path())
338            .filter(|path| path.to_str().unwrap().ends_with(".rs"))
339            .collect();
340
341        let dict = super::from_files(&paths, 4000).unwrap();
342
343        for path in paths {
344            let mut buffer = Vec::new();
345            let mut file = fs::File::open(path).unwrap();
346            let mut content = Vec::new();
347            file.read_to_end(&mut content).unwrap();
348            io::copy(
349                &mut &content[..],
350                &mut crate::stream::Encoder::with_dictionary(
351                    &mut buffer,
352                    1,
353                    &dict,
354                )
355                .unwrap()
356                .auto_finish(),
357            )
358            .unwrap();
359
360            let mut result = Vec::new();
361            io::copy(
362                &mut crate::stream::Decoder::with_dictionary(
363                    &buffer[..],
364                    &dict[..],
365                )
366                .unwrap(),
367                &mut result,
368            )
369            .unwrap();
370
371            assert_eq!(&content, &result);
372        }
373    }
374
375    #[test]
376    fn test_from_continuous_overflowing_sizes() {
377        // These sizes only add up to the data length by wrapping around.
378        let data = [0u8; 100];
379        let sizes = [usize::MAX - 99, 200];
380        assert!(super::from_continuous(&data, &sizes, 4000).is_err());
381    }
382}