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