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