Skip to main content

zstd_complete/encoding/
frame_compressor.rs

1//! Utilities and interfaces for encoding an entire frame. Allows reusing resources
2
3use alloc::vec::Vec;
4use core::convert::TryInto;
5#[cfg(feature = "hash")]
6use twox_hash::XxHash64;
7
8#[cfg(feature = "hash")]
9use core::hash::Hasher;
10
11mod adaptive;
12mod offset_history;
13
14use adaptive::best_block_segment_lengths;
15pub(crate) use offset_history::OffsetHistory;
16
17use super::{
18    block_header::BlockHeader, frame_header::FrameHeader, levels::*,
19    match_generator::MatchGeneratorDriver, CompressionFileProfile, CompressionFileType,
20    CompressionLevel, Matcher,
21};
22use crate::fse::fse_encoder::{
23    default_ll_table, default_ml_table, default_of_table, FSETable, SharedFSETable,
24};
25
26use crate::io::{Read, Write};
27
28/// An interface for compressing arbitrary data with the ZStandard compression algorithm.
29///
30/// `FrameCompressor` will generally be used by:
31/// 1. Initializing a compressor by providing a buffer of data using `FrameCompressor::new()`
32/// 2. Starting compression and writing that compression into a vec using `FrameCompressor::begin`
33///
34/// # Examples
35/// ```
36/// use zstd_complete::encoding::{FrameCompressor, CompressionLevel};
37/// let mock_data: &[_] = &[0x1, 0x2, 0x3, 0x4];
38/// let mut output = std::vec::Vec::new();
39/// // Initialize a compressor.
40/// let mut compressor = FrameCompressor::new(CompressionLevel::Uncompressed);
41/// compressor.set_source(mock_data);
42/// compressor.set_drain(&mut output);
43///
44/// // `compress` writes the compressed output into the provided buffer.
45/// compressor.compress();
46/// ```
47pub struct FrameCompressor<R: Read, W: Write, M: Matcher> {
48    uncompressed_data: Option<R>,
49    compressed_data: Option<W>,
50    compression_level: CompressionLevel,
51    file_type_hint: CompressionFileType,
52    file_profile_hint: CompressionFileProfile,
53    state: CompressState<M>,
54    #[cfg(feature = "hash")]
55    hasher: XxHash64,
56}
57
58#[derive(Clone)]
59pub(crate) struct FseTables {
60    pub(crate) ll_default: FSETable,
61    pub(crate) ll_previous: Option<SharedFSETable>,
62    pub(crate) ll_repeat_valid: bool,
63    pub(crate) ml_default: FSETable,
64    pub(crate) ml_previous: Option<SharedFSETable>,
65    pub(crate) ml_repeat_valid: bool,
66    pub(crate) of_default: FSETable,
67    pub(crate) of_previous: Option<SharedFSETable>,
68    pub(crate) of_repeat_valid: bool,
69}
70
71#[derive(Clone)]
72pub(crate) struct FseTableSnapshot {
73    ll_previous: Option<SharedFSETable>,
74    ll_repeat_valid: bool,
75    ml_previous: Option<SharedFSETable>,
76    ml_repeat_valid: bool,
77    of_previous: Option<SharedFSETable>,
78    of_repeat_valid: bool,
79}
80
81impl FseTables {
82    pub fn new() -> Self {
83        Self {
84            ll_default: default_ll_table(),
85            ll_previous: None,
86            ll_repeat_valid: false,
87            ml_default: default_ml_table(),
88            ml_previous: None,
89            ml_repeat_valid: false,
90            of_default: default_of_table(),
91            of_previous: None,
92            of_repeat_valid: false,
93        }
94    }
95
96    pub(crate) fn new_in(scratch: &mut crate::fse::fse_encoder::FSETableBuildScratch) -> Result<Self, crate::workspace::CapacityError> {
97        Ok(Self {
98            ll_default: crate::fse::fse_encoder::default_ll_table_with_scratch(scratch)?,
99            ll_previous: None,
100            ll_repeat_valid: false,
101            ml_default: crate::fse::fse_encoder::default_ml_table_with_scratch(scratch)?,
102            ml_previous: None,
103            ml_repeat_valid: false,
104            of_default: crate::fse::fse_encoder::default_of_table_with_scratch(scratch)?,
105            of_previous: None,
106            of_repeat_valid: false,
107        })
108    }
109
110    pub fn reset(&mut self) {
111        self.ll_previous = None;
112        self.ll_repeat_valid = false;
113        self.ml_previous = None;
114        self.ml_repeat_valid = false;
115        self.of_previous = None;
116        self.of_repeat_valid = false;
117    }
118
119    pub(crate) fn snapshot_previous(&self) -> FseTableSnapshot {
120        FseTableSnapshot {
121            ll_previous: self.ll_previous.clone(),
122            ll_repeat_valid: self.ll_repeat_valid,
123            ml_previous: self.ml_previous.clone(),
124            ml_repeat_valid: self.ml_repeat_valid,
125            of_previous: self.of_previous.clone(),
126            of_repeat_valid: self.of_repeat_valid,
127        }
128    }
129
130    pub(crate) fn restore_previous(&mut self, snapshot: FseTableSnapshot) {
131        self.ll_previous = snapshot.ll_previous;
132        self.ll_repeat_valid = snapshot.ll_repeat_valid;
133        self.ml_previous = snapshot.ml_previous;
134        self.ml_repeat_valid = snapshot.ml_repeat_valid;
135        self.of_previous = snapshot.of_previous;
136        self.of_repeat_valid = snapshot.of_repeat_valid;
137    }
138
139    /// C only treats a dictionary offset table as universally valid for the
140    /// first source block. Later blocks can require larger offset codes.
141    pub(crate) fn downgrade_offset_repeat_validity(&mut self) {
142        self.of_repeat_valid = false;
143    }
144}
145
146pub(crate) struct CompressState<M: Matcher> {
147    pub(crate) matcher: M,
148    pub(crate) last_huff_table: Option<crate::huff0::huff0_encoder::HuffmanTable>,
149    pub(crate) fse_tables: FseTables,
150    pub(crate) offset_history: OffsetHistory,
151    pub(crate) file_type_hint: CompressionFileType,
152    pub(crate) file_profile_hint: CompressionFileProfile,
153}
154
155impl<R: Read, W: Write> FrameCompressor<R, W, MatchGeneratorDriver> {
156    /// Create a new `FrameCompressor`
157    pub fn new(compression_level: CompressionLevel) -> Self {
158        Self {
159            uncompressed_data: None,
160            compressed_data: None,
161            compression_level,
162            file_type_hint: CompressionFileType::Unknown,
163            file_profile_hint: CompressionFileProfile::None,
164            state: CompressState {
165                matcher: MatchGeneratorDriver::new(1024 * 128, 4),
166                last_huff_table: None,
167                fse_tables: FseTables::new(),
168                offset_history: OffsetHistory::new(),
169                file_type_hint: CompressionFileType::Unknown,
170                file_profile_hint: CompressionFileProfile::None,
171            },
172            #[cfg(feature = "hash")]
173            hasher: XxHash64::with_seed(0),
174        }
175    }
176
177    /// Create a new `FrameCompressor` with a coarse file-type hint.
178    pub fn new_with_file_type(
179        compression_level: CompressionLevel,
180        file_type_hint: CompressionFileType,
181    ) -> Self {
182        Self::new_with_hints(
183            compression_level,
184            file_type_hint,
185            CompressionFileProfile::None,
186        )
187    }
188
189    pub(crate) fn new_with_hints(
190        compression_level: CompressionLevel,
191        file_type_hint: CompressionFileType,
192        file_profile_hint: CompressionFileProfile,
193    ) -> Self {
194        let mut compressor = Self::new(compression_level);
195        compressor.file_type_hint = file_type_hint;
196        compressor.file_profile_hint = file_profile_hint;
197        compressor
198    }
199}
200
201impl<R: Read, W: Write, M: Matcher> FrameCompressor<R, W, M> {
202    /// Create a new `FrameCompressor` with a custom matching algorithm implementation
203    pub fn new_with_matcher(matcher: M, compression_level: CompressionLevel) -> Self {
204        Self {
205            uncompressed_data: None,
206            compressed_data: None,
207            file_type_hint: CompressionFileType::Unknown,
208            file_profile_hint: CompressionFileProfile::None,
209            state: CompressState {
210                matcher,
211                last_huff_table: None,
212                fse_tables: FseTables::new(),
213                offset_history: OffsetHistory::new(),
214                file_type_hint: CompressionFileType::Unknown,
215                file_profile_hint: CompressionFileProfile::None,
216            },
217            compression_level,
218            #[cfg(feature = "hash")]
219            hasher: XxHash64::with_seed(0),
220        }
221    }
222
223    /// Before calling [FrameCompressor::compress] you need to set the source.
224    ///
225    /// This is the data that is compressed and written into the drain.
226    pub fn set_source(&mut self, uncompressed_data: R) -> Option<R> {
227        self.uncompressed_data.replace(uncompressed_data)
228    }
229
230    /// Before calling [FrameCompressor::compress] you need to set the drain.
231    ///
232    /// As the compressor compresses data, the drain serves as a place for the output to be writte.
233    pub fn set_drain(&mut self, compressed_data: W) -> Option<W> {
234        self.compressed_data.replace(compressed_data)
235    }
236
237    /// Compress the uncompressed data from the provided source as one Zstd frame and write it to the provided drain
238    ///
239    /// This will repeatedly call [Read::read] on the source to fill up blocks until the source returns 0 on the read call.
240    /// Also [Write::write_all] will be called on the drain after each block has been encoded.
241    ///
242    /// To avoid endlessly encoding from a potentially endless source (like a network socket) you can use the
243    /// [Read::take] function
244    pub fn compress(&mut self) {
245        // Clearing buffers to allow re-using of the compressor
246        self.state.file_type_hint = self.file_type_hint;
247        self.state.file_profile_hint = self.file_profile_hint;
248        self.state.matcher.set_file_type_hint(self.file_type_hint);
249        self.state
250            .matcher
251            .set_internal_file_profile_hint(self.file_profile_hint.internal_hint_code());
252        self.state.matcher.reset(self.compression_level);
253        self.state.last_huff_table = None;
254        self.state.fse_tables.reset();
255        self.state.offset_history = OffsetHistory::new();
256        let (newest, second, third) = self.state.offset_history.as_offsets();
257        self.state.matcher.set_repeat_offsets(newest, second, third);
258        #[cfg(feature = "hash")]
259        {
260            self.hasher = XxHash64::with_seed(0);
261        }
262        let source = self.uncompressed_data.as_mut().unwrap();
263        let drain = self.compressed_data.as_mut().unwrap();
264        // As the frame is compressed, it's stored here
265        let output: &mut Vec<u8> = &mut Vec::with_capacity(1024 * 130);
266        // First write the frame header
267        let header = FrameHeader {
268            frame_content_size: None,
269            single_segment: false,
270            content_checksum: cfg!(feature = "hash"),
271            dictionary_id: None,
272            window_size: Some(self.state.matcher.window_size()),
273        };
274        header.serialize(output).expect("owned legacy codec allocation failed");
275        // Now compress block by block
276        let mut pending_byte = None;
277        loop {
278            // Read a single block's worth of uncompressed data from the input
279            let mut uncompressed_data = self.state.matcher.get_next_space();
280            let mut read_bytes = if let Some(byte) = pending_byte.take() {
281                uncompressed_data[0] = byte;
282                1
283            } else {
284                0
285            };
286            let mut last_block;
287            'read_loop: loop {
288                let new_bytes = source.read(&mut uncompressed_data[read_bytes..]).unwrap();
289                if new_bytes == 0 {
290                    last_block = true;
291                    break 'read_loop;
292                }
293                read_bytes += new_bytes;
294                if read_bytes == uncompressed_data.len() {
295                    last_block = false;
296                    break 'read_loop;
297                }
298            }
299            if !last_block {
300                let mut lookahead = [0u8; 1];
301                match source.read(&mut lookahead).unwrap() {
302                    0 => last_block = true,
303                    1 => pending_byte = Some(lookahead[0]),
304                    _ => unreachable!("single-byte read cannot return more than one byte"),
305                }
306            }
307            uncompressed_data.resize(read_bytes, 0);
308            // As we read, hash that data too
309            #[cfg(feature = "hash")]
310            self.hasher.write(&uncompressed_data);
311            // Special handling is needed for compression of a totally empty file (why you'd want to do that, I don't know)
312            if uncompressed_data.is_empty() {
313                let header = BlockHeader {
314                    last_block: true,
315                    block_type: crate::blocks::block::BlockType::Raw,
316                    block_size: 0,
317                };
318                // Write the header, then the block
319                header.serialize(output).expect("owned legacy codec allocation failed");
320                drain.write_all(output).unwrap();
321                output.clear();
322                break;
323            }
324
325            compress_with_level_policy(
326                &mut self.state,
327                self.compression_level,
328                last_block,
329                uncompressed_data,
330                output,
331            );
332            drain.write_all(output).unwrap();
333            output.clear();
334            if last_block {
335                break;
336            }
337        }
338
339        // If the `hash` feature is enabled, then `content_checksum` is set to true in the header
340        // and a 32 bit hash is written at the end of the data.
341        #[cfg(feature = "hash")]
342        {
343            // Because we only have the data as a reader, we need to read all of it to calculate the checksum
344            // Possible TODO: create a wrapper around self.uncompressed data that hashes the data as it's read?
345            let content_checksum = self.hasher.finish();
346            drain
347                .write_all(&(content_checksum as u32).to_le_bytes())
348                .unwrap();
349        }
350    }
351
352    /// Get a mutable reference to the source
353    pub fn source_mut(&mut self) -> Option<&mut R> {
354        self.uncompressed_data.as_mut()
355    }
356
357    /// Get a mutable reference to the drain
358    pub fn drain_mut(&mut self) -> Option<&mut W> {
359        self.compressed_data.as_mut()
360    }
361
362    /// Get a reference to the source
363    pub fn source(&self) -> Option<&R> {
364        self.uncompressed_data.as_ref()
365    }
366
367    /// Get a reference to the drain
368    pub fn drain(&self) -> Option<&W> {
369        self.compressed_data.as_ref()
370    }
371
372    /// Retrieve the source
373    pub fn take_source(&mut self) -> Option<R> {
374        self.uncompressed_data.take()
375    }
376
377    /// Retrieve the drain
378    pub fn take_drain(&mut self) -> Option<W> {
379        self.compressed_data.take()
380    }
381
382    /// Before calling [FrameCompressor::compress] you can replace the matcher
383    pub fn replace_matcher(&mut self, mut match_generator: M) -> M {
384        core::mem::swap(&mut match_generator, &mut self.state.matcher);
385        match_generator
386    }
387
388    /// Before calling [FrameCompressor::compress] you can replace the compression level
389    pub fn set_compression_level(
390        &mut self,
391        compression_level: CompressionLevel,
392    ) -> CompressionLevel {
393        let old = self.compression_level;
394        self.compression_level = compression_level;
395        old
396    }
397
398    /// Get the current compression level
399    pub fn compression_level(&self) -> CompressionLevel {
400        self.compression_level
401    }
402
403    /// Replace the coarse file-type hint used to choose internal starting points.
404    pub fn set_file_type_hint(
405        &mut self,
406        file_type_hint: CompressionFileType,
407    ) -> CompressionFileType {
408        let old = self.file_type_hint;
409        self.file_type_hint = file_type_hint;
410        old
411    }
412}
413
414fn compress_with_level_policy<M: Matcher>(
415    state: &mut CompressState<M>,
416    level: CompressionLevel,
417    last_block: bool,
418    uncompressed_data: Vec<u8>,
419    output: &mut Vec<u8>,
420) {
421    if level.is_uncompressed() {
422        let header = BlockHeader {
423            last_block,
424            block_type: crate::blocks::block::BlockType::Raw,
425            block_size: uncompressed_data.len().try_into().unwrap(),
426        };
427        header.serialize(output).expect("owned legacy codec allocation failed");
428        output.extend_from_slice(&uncompressed_data);
429    } else if level.uses_fastest_legacy_profile() {
430        compress_fastest(state, last_block, uncompressed_data, output);
431    } else if level.uses_best_legacy_profile() {
432        compress_best_adaptive(state, level, last_block, uncompressed_data, output);
433    } else {
434        compress_at_level(state, level, last_block, uncompressed_data, output);
435    }
436}
437
438fn compress_best_adaptive<M: Matcher>(
439    state: &mut CompressState<M>,
440    level: CompressionLevel,
441    last_block: bool,
442    uncompressed_data: Vec<u8>,
443    output: &mut Vec<u8>,
444) {
445    let segment_lengths = best_block_segment_lengths(uncompressed_data.as_slice());
446    if segment_lengths.len() == 1 {
447        compress_at_level(state, level, last_block, uncompressed_data, output);
448        return;
449    }
450
451    let mut remaining = uncompressed_data;
452    let segment_count = segment_lengths.len();
453    for (segment_idx, segment_len) in segment_lengths.into_iter().enumerate() {
454        let segment_last = last_block && segment_idx + 1 == segment_count;
455        let segment = if segment_idx + 1 == segment_count {
456            core::mem::take(&mut remaining)
457        } else {
458            let tail = remaining.split_off(segment_len);
459            core::mem::replace(&mut remaining, tail)
460        };
461        compress_at_level_without_incompressible_probe(state, level, segment_last, segment, output);
462    }
463}
464
465#[cfg(test)]
466mod tests;