Skip to main content

zstd_complete/encoding/
mod.rs

1//! Structures and utilities used for compressing/encoding data into the Zstd format.
2
3pub(crate) mod block_header;
4pub(crate) mod blocks;
5mod file_profile;
6pub(crate) mod frame_header;
7pub(crate) mod match_generator;
8pub(crate) mod util;
9
10mod compression_level;
11mod compression_parameters;
12mod frame_compressor;
13mod levels;
14#[cfg(all(feature = "std", feature = "multithreading"))]
15mod parallel_encoder;
16#[cfg(feature = "std")]
17mod streaming_encoder;
18mod workspace;
19pub use compression_level::{CompressionLevel, InvalidCompressionLevel};
20pub use compression_parameters::{
21    CompressionStrategy, CompressionTuning, ContentSizePolicy, LongDistanceMatching,
22};
23pub(crate) use file_profile::CompressionFileProfile;
24pub use file_profile::CompressionFileType;
25#[cfg(feature = "std")]
26pub(crate) use file_profile::{compression_file_profile_for_path_and_data, read_file_type_sample};
27#[cfg(feature = "std")]
28pub use file_profile::{compression_file_type_for_path, compression_file_type_for_path_and_data};
29pub use frame_compressor::FrameCompressor;
30pub use match_generator::MatchGeneratorDriver;
31#[cfg(all(feature = "std", feature = "multithreading"))]
32pub use parallel_encoder::{encode_all_parallel, encode_parallel, ParallelEncoder};
33#[cfg(feature = "std")]
34pub use streaming_encoder::{
35    encode, encode_all, DictionaryError, EncodeError, Encoder, EncoderDictionary, EncoderOptions,
36    DEFAULT_FRAME_CHUNK_SIZE, DEFAULT_MEMORY_LIMIT,
37};
38pub use workspace::{EncoderWorkspace, EncoderWorkspaceError, StaticEncoderWorkspace};
39
40use crate::io::{Read, Write};
41use alloc::vec::Vec;
42#[cfg(feature = "std")]
43use std::path::Path;
44
45/// Convenience function to compress some source into a target without reusing any resources of the compressor
46/// ```rust
47/// use zstd_complete::encoding::{compress, CompressionLevel};
48/// let data: &[u8] = &[0,0,0,0,0,0,0,0,0,0,0,0];
49/// let mut target = Vec::new();
50/// compress(data, &mut target, CompressionLevel::Fastest);
51/// ```
52pub fn compress<R: Read, W: Write>(source: R, target: W, level: CompressionLevel) {
53    let mut frame_enc = FrameCompressor::new(level);
54    frame_enc.set_source(source);
55    frame_enc.set_drain(target);
56    frame_enc.compress();
57}
58
59/// Convenience function to compress some source into a target using a coarse file-type hint.
60///
61/// The public API stays narrow: callers provide only the requested compression level and
62/// the file family. The encoder decides the internal starting point from there.
63pub fn compress_with_file_type<R: Read, W: Write>(
64    source: R,
65    target: W,
66    file_type: CompressionFileType,
67    level: CompressionLevel,
68) {
69    let mut frame_enc =
70        FrameCompressor::new_with_hints(level, file_type, CompressionFileProfile::None);
71    frame_enc.set_source(source);
72    frame_enc.set_drain(target);
73    frame_enc.compress();
74}
75
76/// Convenience function to compress some source into a target using a path-based file-type hint.
77#[cfg(feature = "std")]
78pub fn compress_with_path<R: Read, W: Write>(
79    mut source: R,
80    target: W,
81    path: &Path,
82    level: CompressionLevel,
83) {
84    let sample = read_file_type_sample(&mut source);
85    let file_type = compression_file_type_for_path_and_data(path, &sample);
86    let file_profile = compression_file_profile_for_path_and_data(path, &sample);
87    let mut frame_enc = FrameCompressor::new_with_hints(level, file_type, file_profile);
88    frame_enc.set_source(sample.as_slice().chain(source));
89    frame_enc.set_drain(target);
90    frame_enc.compress();
91}
92
93/// Convenience function to compress some source into a Vec without reusing any resources of the compressor
94/// ```rust
95/// use zstd_complete::encoding::{compress_to_vec, CompressionLevel};
96/// let data: &[u8] = &[0,0,0,0,0,0,0,0,0,0,0,0];
97/// let compressed = compress_to_vec(data, CompressionLevel::Fastest);
98/// ```
99pub fn compress_to_vec<R: Read>(source: R, level: CompressionLevel) -> Vec<u8> {
100    let mut vec = Vec::new();
101    compress(source, &mut vec, level);
102    vec
103}
104
105/// Compress a full source into a target using the faithful C no-dictionary level table.
106///
107/// This entry point accepts the same numeric level range as upstream zstd. It currently
108/// targets the no-dictionary path and buffers the complete source so that the C-port
109/// frame encoder can choose block strategies from the full content size.
110#[cfg(any(test, feature = "c-port-validation"))]
111pub fn compress_c_level<R: Read, W: Write>(mut source: R, mut target: W, level: i32) {
112    let mut input = Vec::new();
113    source.read_to_end(&mut input).unwrap();
114    let compressed = levels::c_port::encode_frame_no_dict(&input, level);
115    target.write_all(&compressed).unwrap();
116}
117
118/// Compress a full source into a Vec using the faithful C no-dictionary level table.
119#[cfg(any(test, feature = "c-port-validation"))]
120pub fn compress_to_vec_c_level<R: Read>(mut source: R, level: i32) -> Vec<u8> {
121    let mut input = Vec::new();
122    source.read_to_end(&mut input).unwrap();
123    compress_slice_c_level(&input, level)
124}
125
126/// Compress a complete in-memory source into a Vec using the faithful C
127/// no-dictionary level table.
128///
129/// This avoids the extra staging copy required by the generic `Read` entry
130/// point when the caller already has the full source in memory.
131#[cfg(any(test, feature = "c-port-validation"))]
132pub fn compress_slice_c_level(source: &[u8], level: i32) -> Vec<u8> {
133    levels::c_port::encode_frame_no_dict(source, level)
134}
135
136/// Compress a complete in-memory source with an explicit C `targetCBlockSize`.
137///
138/// This is a narrow validation hook for the faithful C-port path. It returns
139/// `None` when the requested target size is outside C's accepted range.
140#[cfg(any(test, feature = "c-port-validation"))]
141pub fn compress_slice_c_level_with_target_c_block_size(
142    source: &[u8],
143    level: i32,
144    target_c_block_size: usize,
145) -> Option<Vec<u8>> {
146    levels::c_port::encode_frame_no_dict_with_target_c_block_size(
147        source,
148        level,
149        target_c_block_size,
150    )
151}
152
153/// Errors returned while preparing a C-level dictionary compression.
154#[derive(Copy, Clone, Debug, Eq, PartialEq)]
155#[cfg(any(test, feature = "c-port-validation"))]
156pub enum CLevelDictionaryError {
157    WrongDictionary,
158    CorruptedDictionary,
159}
160
161/// A dictionary parsed and owned for repeated C-level compression.
162///
163/// Like upstream `ZSTD_CDict`, this binds dictionary parsing and compression
164/// parameters to one numeric compression level. Reuse the value when
165/// compressing multiple independent sources with the same dictionary.
166#[derive(Clone)]
167#[cfg(any(test, feature = "c-port-validation"))]
168pub struct CLevelEncoderDictionary {
169    level: i32,
170    dictionary: Option<levels::c_port::PreparedDictionary>,
171}
172
173#[cfg(any(test, feature = "c-port-validation"))]
174impl CLevelEncoderDictionary {
175    /// Copies and prepares a dictionary for the supplied numeric C level.
176    pub fn copy(dictionary: &[u8], level: i32) -> Result<Self, CLevelDictionaryError> {
177        let dictionary = levels::c_port::PreparedDictionary::from_bytes(dictionary)
178            .map_err(map_c_level_dictionary_error)?;
179        Ok(Self { level, dictionary })
180    }
181
182    /// Returns the numeric compression level fixed at preparation time.
183    pub fn level(&self) -> i32 {
184        self.level
185    }
186}
187
188/// Compress a complete in-memory source using a prepared C-level dictionary.
189#[cfg(any(test, feature = "c-port-validation"))]
190pub fn compress_slice_c_level_with_prepared_dictionary(
191    source: &[u8],
192    dictionary: &CLevelEncoderDictionary,
193) -> Vec<u8> {
194    match dictionary.dictionary.as_ref() {
195        Some(prepared) => levels::c_port::encode_frame_with_prepared_dictionary(
196            source,
197            dictionary.level,
198            prepared,
199        ),
200        None => levels::c_port::encode_frame_no_dict(source, dictionary.level),
201    }
202}
203
204/// Compress a full source into a Vec using a prepared C-level dictionary.
205#[cfg(any(test, feature = "c-port-validation"))]
206pub fn compress_to_vec_c_level_with_prepared_dictionary<R: Read>(
207    mut source: R,
208    dictionary: &CLevelEncoderDictionary,
209) -> Vec<u8> {
210    let mut input = Vec::new();
211    source.read_to_end(&mut input).unwrap();
212    compress_slice_c_level_with_prepared_dictionary(&input, dictionary)
213}
214
215/// Compress a full source into a target using the faithful C level table and
216/// C-style automatic dictionary parsing.
217#[cfg(any(test, feature = "c-port-validation"))]
218pub fn compress_c_level_with_dictionary<R: Read, W: Write>(
219    mut source: R,
220    mut target: W,
221    level: i32,
222    dictionary: &[u8],
223) -> Result<(), CLevelDictionaryError> {
224    let mut input = Vec::new();
225    source.read_to_end(&mut input).unwrap();
226    let compressed = levels::c_port::encode_frame_with_dictionary(&input, level, dictionary)
227        .map_err(map_c_level_dictionary_error)?;
228    target.write_all(&compressed).unwrap();
229    Ok(())
230}
231
232/// Compress a full source into a Vec using the faithful C level table and
233/// C-style automatic dictionary parsing.
234#[cfg(any(test, feature = "c-port-validation"))]
235pub fn compress_to_vec_c_level_with_dictionary<R: Read>(
236    mut source: R,
237    level: i32,
238    dictionary: &[u8],
239) -> Result<Vec<u8>, CLevelDictionaryError> {
240    let mut input = Vec::new();
241    source.read_to_end(&mut input).unwrap();
242    compress_slice_c_level_with_dictionary(&input, level, dictionary)
243}
244
245/// Compress a complete in-memory source into a Vec using the faithful C level
246/// table and C-style automatic dictionary parsing.
247#[cfg(any(test, feature = "c-port-validation"))]
248pub fn compress_slice_c_level_with_dictionary(
249    source: &[u8],
250    level: i32,
251    dictionary: &[u8],
252) -> Result<Vec<u8>, CLevelDictionaryError> {
253    levels::c_port::encode_frame_with_dictionary(source, level, dictionary)
254        .map_err(map_c_level_dictionary_error)
255}
256
257/// Compress a complete in-memory source with both a C-style dictionary and
258/// explicit C `targetCBlockSize`.
259///
260/// This is a validation hook for the combined dictionary+target surface. It
261/// returns `Ok(None)` when the target size is outside C's accepted range.
262#[cfg(any(test, feature = "c-port-validation"))]
263pub fn compress_slice_c_level_with_dictionary_and_target_c_block_size(
264    source: &[u8],
265    level: i32,
266    dictionary: &[u8],
267    target_c_block_size: usize,
268) -> Result<Option<Vec<u8>>, CLevelDictionaryError> {
269    levels::c_port::encode_frame_with_dictionary_and_target_c_block_size(
270        source,
271        level,
272        dictionary,
273        target_c_block_size,
274    )
275    .map_err(map_c_level_dictionary_error)
276}
277
278#[cfg(any(test, feature = "c-port-validation"))]
279fn map_c_level_dictionary_error(
280    error: levels::c_port::DictionaryParseError,
281) -> CLevelDictionaryError {
282    match error {
283        levels::c_port::DictionaryParseError::WrongDictionary => {
284            CLevelDictionaryError::WrongDictionary
285        }
286        levels::c_port::DictionaryParseError::CorruptedDictionary => {
287            CLevelDictionaryError::CorruptedDictionary
288        }
289    }
290}
291
292/// Convenience function to compress some source into a Vec using a coarse file-type hint.
293pub fn compress_to_vec_with_file_type<R: Read>(
294    source: R,
295    file_type: CompressionFileType,
296    level: CompressionLevel,
297) -> Vec<u8> {
298    let mut vec = Vec::new();
299    compress_with_file_type(source, &mut vec, file_type, level);
300    vec
301}
302
303/// Convenience function to compress some source into a Vec using a path-based file-type hint.
304#[cfg(feature = "std")]
305pub fn compress_to_vec_with_path<R: Read>(
306    source: R,
307    path: &Path,
308    level: CompressionLevel,
309) -> Vec<u8> {
310    let mut vec = Vec::new();
311    compress_with_path(source, &mut vec, path, level);
312    vec
313}
314
315/// Trait used by the encoder that users can use to extend the matching facilities with their own algorithm
316/// making their own tradeoffs between runtime, memory usage and compression ratio
317///
318/// This trait operates on buffers that represent the chunks of data the matching algorithm wants to work on.
319/// Each one of these buffers is referred to as a *space*. One or more of these buffers represent the window
320/// the decoder will need to decode the data again.
321///
322/// This library asks the Matcher for a new buffer using `get_next_space` to allow reusing of allocated buffers when they are no longer part of the
323/// window of data that is being used for matching.
324///
325/// The library fills the buffer with data that is to be compressed and commits them back to the matcher using `commit_space`.
326///
327/// Then it will either call `start_matching` or, if the space is deemed not worth compressing, `skip_matching` is called.
328///
329/// This is repeated until no more data is left to be compressed.
330pub trait Matcher {
331    /// Get a space where we can put data to be matched on. Will be encoded as one block. The maximum allowed size is 128 kB.
332    fn get_next_space(&mut self) -> alloc::vec::Vec<u8>;
333    /// Get a reference to the last commited space
334    fn get_last_space(&self) -> &[u8];
335    /// Commit a space to the matcher so it can be matched against
336    fn commit_space(&mut self, space: alloc::vec::Vec<u8>);
337    /// Just process the data in the last commited space for future matching
338    fn skip_matching(&mut self);
339    /// Process the data in the last commited space for future matching AND generate matches for the data
340    fn start_matching(&mut self, handle_sequence: impl for<'a> FnMut(Sequence<'a>));
341    /// Reset this matcher so it can be used for the next new frame
342    fn reset(&mut self, level: CompressionLevel);
343    /// Provide a coarse file-type hint so the matcher can choose an internal starting point.
344    ///
345    /// Implementations that do not care about path/extension hints can ignore this hook.
346    fn set_file_type_hint(&mut self, _file_type: CompressionFileType) {}
347    /// Provide a narrower internal file profile when one is known.
348    ///
349    /// This is encoded as a small integer so the public matcher trait does not expose the
350    /// encoder's private profile enum.
351    fn set_internal_file_profile_hint(&mut self, _file_profile_code: u8) {}
352    /// Synchronize the matcher with the encoder's current repeat-offset history.
353    ///
354    /// Matchers that do not use repeat-offset history can ignore this hook.
355    fn set_repeat_offsets(&mut self, _newest: u32, _second: u32, _third: u32) {}
356    /// Mark the last committed space as processed without indexing it for future matches.
357    ///
358    /// This is intended for data that has already been classified as very unlikely to
359    /// be useful match history, such as incompressible raw blocks.
360    fn skip_matching_for_incompressible(&mut self) {
361        self.skip_matching();
362    }
363    /// Mark the last committed space as processed after it was emitted as an RLE block.
364    ///
365    /// The default behavior preserves the existing matcher contract by indexing the block
366    /// normally. Matchers can specialize this because every minimum-match suffix in an RLE
367    /// block has the same key.
368    fn skip_matching_for_rle(&mut self) {
369        self.skip_matching();
370    }
371    /// The size of the window the decoder will need to execute all sequences produced by this matcher
372    ///
373    /// May change after a call to reset with a different compression level
374    fn window_size(&self) -> u64;
375}
376
377#[derive(PartialEq, Eq, Debug)]
378/// Sequences that a [`Matcher`] can produce
379pub enum Sequence<'data> {
380    /// Is encoded as a sequence for the decoder sequence execution.
381    ///
382    /// First the literals will be copied to the decoded data,
383    /// then `match_len` bytes are copied from `offset` bytes back in the decoded data
384    Triple {
385        literals: &'data [u8],
386        offset: usize,
387        match_len: usize,
388    },
389    /// This is returned as the last sequence in a block
390    ///
391    /// These literals will just be copied at the end of the sequence execution by the decoder
392    Literals { literals: &'data [u8] },
393}
394
395#[cfg(test)]
396mod tests {
397    use super::{
398        compress_c_level, compress_c_level_with_dictionary, compress_slice_c_level,
399        compress_slice_c_level_with_dictionary,
400        compress_slice_c_level_with_dictionary_and_target_c_block_size, compress_to_vec_c_level,
401        compress_to_vec_c_level_with_dictionary, CLevelDictionaryError, CompressionLevel,
402    };
403    use crate::decoding::{dictionary::Dictionary, FrameDecoder};
404    use alloc::vec::Vec;
405
406    #[test]
407    fn compression_level_equality_is_available_for_api_comparisons() {
408        assert_eq!(CompressionLevel::Fastest, CompressionLevel::Fastest);
409    }
410
411    #[test]
412    fn c_level_compression_round_trips_representative_strategies() {
413        let mut data = Vec::new();
414        while data.len() < (crate::common::MAX_BLOCK_SIZE as usize * 2) + 2048 {
415            data.extend_from_slice(b"public-c-level route=/archive status=200 bytes=1874\n");
416        }
417
418        for level in [1, 3, 5, 8, 13, 16, 18, 19, 22] {
419            let encoded = compress_to_vec_c_level(data.as_slice(), level);
420            assert_round_trips(&encoded, &data);
421            let encoded_from_slice = compress_slice_c_level(&data, level);
422            assert_eq!(encoded_from_slice, encoded);
423        }
424    }
425
426    #[test]
427    fn c_level_compression_writes_to_target() {
428        let data = b"public-c-level-writer public-c-level-writer";
429        let mut encoded = Vec::new();
430
431        compress_c_level(data.as_slice(), &mut encoded, 16);
432
433        assert_round_trips(&encoded, data);
434    }
435
436    #[test]
437    fn c_level_dictionary_compression_round_trips_representative_strategies() {
438        let dict = full_dictionary_fixture();
439        let data = dictionary_payload();
440
441        for level in [1, 3, 5, 8, 13, 16, 18, 19] {
442            let encoded =
443                compress_to_vec_c_level_with_dictionary(data.as_slice(), level, &dict).unwrap();
444            assert_round_trips_with_dictionary(&encoded, &data, &dict);
445            let encoded_from_slice =
446                compress_slice_c_level_with_dictionary(&data, level, &dict).unwrap();
447            assert_eq!(encoded_from_slice, encoded);
448        }
449    }
450
451    #[test]
452    fn c_level_dictionary_compression_writes_to_target() {
453        let dict = full_dictionary_fixture();
454        let data = dictionary_payload();
455        let mut encoded = Vec::new();
456
457        compress_c_level_with_dictionary(data.as_slice(), &mut encoded, 13, &dict).unwrap();
458
459        assert_round_trips_with_dictionary(&encoded, &data, &dict);
460    }
461
462    #[test]
463    fn c_level_dictionary_target_c_block_size_round_trips_optimal_strategy() {
464        let dict = full_dictionary_fixture();
465        let data = dictionary_payload();
466
467        let encoded =
468            compress_slice_c_level_with_dictionary_and_target_c_block_size(&data, 16, &dict, 2048)
469                .unwrap()
470                .expect("level 16 dictionary target mode is target-wired");
471
472        assert_round_trips_with_dictionary(&encoded, &data, &dict);
473    }
474
475    #[test]
476    fn c_level_dictionary_target_c_block_size_round_trips_hash_chain_strategy() {
477        let dict = full_dictionary_fixture();
478        let data = dictionary_payload();
479
480        let encoded =
481            compress_slice_c_level_with_dictionary_and_target_c_block_size(&data, 5, &dict, 2048)
482                .unwrap()
483                .expect("level 5 dictionary target mode is target-wired");
484
485        assert_round_trips_with_dictionary(&encoded, &data, &dict);
486    }
487
488    #[test]
489    fn c_level_dictionary_target_c_block_size_round_trips_fast_strategies() {
490        let dict = full_dictionary_fixture();
491        let data = dictionary_payload();
492
493        for level in [1, 3] {
494            let encoded = compress_slice_c_level_with_dictionary_and_target_c_block_size(
495                &data, level, &dict, 2048,
496            )
497            .unwrap();
498
499            assert_round_trips_with_dictionary(
500                &encoded.expect("fast dictionary target mode is target-wired"),
501                &data,
502                &dict,
503            );
504        }
505    }
506
507    #[test]
508    fn c_level_dictionary_target_c_block_size_rejects_invalid_sizes() {
509        let dict = full_dictionary_fixture();
510        let data = dictionary_payload();
511
512        let encoded = compress_slice_c_level_with_dictionary_and_target_c_block_size(
513            &data,
514            1,
515            &dict,
516            128 * 1024 + 1,
517        )
518        .unwrap();
519
520        assert_eq!(encoded, None);
521    }
522
523    #[test]
524    fn c_level_dictionary_compression_reports_corrupt_full_dictionary() {
525        let mut dict = full_dictionary_fixture();
526        let repeat_offset_start = dict.len() - dictionary_content().len() - 12;
527        dict[repeat_offset_start..repeat_offset_start + 4]
528            .copy_from_slice(&0x00AB_CDEF_u32.to_le_bytes());
529
530        let error = compress_to_vec_c_level_with_dictionary(b"data".as_slice(), 3, &dict)
531            .expect_err("invalid full dictionary offsets must be rejected");
532
533        assert_eq!(error, CLevelDictionaryError::CorruptedDictionary);
534    }
535
536    fn assert_round_trips(encoded: &[u8], expected: &[u8]) {
537        let mut decoded = Vec::with_capacity(expected.len());
538        FrameDecoder::new()
539            .decode_all_to_vec(encoded, &mut decoded)
540            .unwrap();
541
542        assert_eq!(decoded, expected);
543    }
544
545    fn assert_round_trips_with_dictionary(encoded: &[u8], expected: &[u8], dict: &[u8]) {
546        let mut decoded = Vec::with_capacity(expected.len());
547        let mut decoder = FrameDecoder::new();
548        decoder
549            .add_dict(Dictionary::decode_dict(dict).unwrap())
550            .unwrap();
551        decoder.decode_all_to_vec(encoded, &mut decoded).unwrap();
552
553        assert_eq!(decoded, expected);
554    }
555
556    fn dictionary_payload() -> Vec<u8> {
557        let mut data = Vec::new();
558        for _ in 0..10 {
559            data.extend_from_slice(dictionary_content());
560        }
561        data
562    }
563
564    const DICT_ID: u32 = 0x4723_2101;
565
566    fn full_dictionary_fixture() -> Vec<u8> {
567        let mut raw = Vec::new();
568        raw.extend_from_slice(&crate::decoding::dictionary::MAGIC_NUM);
569        raw.extend_from_slice(&DICT_ID.to_le_bytes());
570        raw.extend_from_slice(dictionary_tables());
571        for offset in [3_u32, 10, 25] {
572            raw.extend_from_slice(&offset.to_le_bytes());
573        }
574        raw.extend_from_slice(dictionary_content());
575        raw
576    }
577
578    fn dictionary_tables() -> &'static [u8] {
579        &[
580            54, 16, 192, 155, 4, 0, 207, 59, 239, 121, 158, 116, 220, 93, 114, 229, 110, 41, 249,
581            95, 165, 255, 83, 202, 254, 68, 74, 159, 63, 161, 100, 151, 137, 21, 184, 183, 189,
582            100, 235, 209, 251, 174, 91, 75, 91, 185, 19, 39, 75, 146, 98, 177, 249, 14, 4, 35, 0,
583            0, 0, 40, 40, 20, 10, 12, 204, 37, 196, 1, 173, 122, 0, 4, 0, 128, 1, 2, 2, 25, 32, 27,
584            27, 22, 24, 26, 18, 12, 12, 15, 16, 11, 69, 37, 225, 48, 20, 12, 6, 2, 161, 80, 40, 20,
585            44, 137, 145, 204, 46, 0, 0, 0, 0, 0, 116, 253, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
586            0, 0, 0,
587        ]
588    }
589
590    fn dictionary_content() -> &'static [u8] {
591        b"method=GET path=/v1/projects/beta status=200 bytes=1847\n\
592          method=POST path=/v1/projects/beta status=202 bytes=932\n"
593    }
594}