Skip to main content

zrip_encode/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2#![cfg_attr(feature = "nightly", feature(optimize_attribute))]
3#![cfg_attr(feature = "paranoid", forbid(unsafe_code))]
4
5#[cfg(feature = "alloc")]
6extern crate alloc;
7
8pub(crate) mod block_encoder;
9#[cfg(feature = "std")]
10pub mod context;
11pub(crate) mod dfast;
12pub(crate) mod fast;
13#[cfg(feature = "ldm")]
14pub(crate) mod ldm;
15pub(crate) mod primitives;
16pub(crate) mod sequences;
17pub mod strategy;
18#[cfg(feature = "std")]
19pub mod streaming;
20
21#[cfg(feature = "alloc")]
22use alloc::vec;
23#[cfg(feature = "alloc")]
24use alloc::vec::Vec;
25
26use crate::strategy::Strategy;
27use zrip_core::error::CompressError;
28use zrip_core::frame::{MAX_BLOCK_SIZE, ZSTD_MAGIC};
29use zrip_core::xxhash::xxh64;
30
31pub(crate) fn write_frame_header(output: &mut Vec<u8>, content_size: usize, dict_id: Option<u32>) {
32    output.extend_from_slice(&ZSTD_MAGIC.to_le_bytes());
33
34    let fcs_size = if content_size <= 255 {
35        1
36    } else if content_size <= 0xFFFF + 256 {
37        2
38    } else if content_size <= 0xFFFF_FFFF {
39        4
40    } else {
41        8
42    };
43    let fcs_flag: u8 = match fcs_size {
44        1 => 0,
45        2 => 1,
46        4 => 2,
47        _ => 3,
48    };
49
50    let dict_id_flag: u8 = match dict_id {
51        None => 0,
52        Some(id) if id <= 0xFF => 1,
53        Some(id) if id <= 0xFFFF => 2,
54        Some(_) => 3,
55    };
56
57    let descriptor = 0x20 | 0x04 | (fcs_flag << 6) | dict_id_flag;
58    output.push(descriptor);
59
60    match dict_id {
61        Some(id) if id <= 0xFF => output.push(id as u8),
62        Some(id) if id <= 0xFFFF => output.extend_from_slice(&(id as u16).to_le_bytes()),
63        Some(id) => output.extend_from_slice(&id.to_le_bytes()),
64        None => {}
65    }
66
67    match fcs_size {
68        1 => output.push(content_size as u8),
69        2 => {
70            let v = (content_size - 256) as u16;
71            output.extend_from_slice(&v.to_le_bytes());
72        }
73        4 => output.extend_from_slice(&(content_size as u32).to_le_bytes()),
74        _ => output.extend_from_slice(&(content_size as u64).to_le_bytes()),
75    }
76}
77
78pub(crate) fn block_looks_incompressible(data: &[u8]) -> bool {
79    const SAMPLE: usize = 1024;
80    const DISTINCT_THRESHOLD: u32 = 200;
81    const MAX_FREQ_DENOM: u32 = 24;
82    if data.len() < SAMPLE {
83        return false;
84    }
85    let mut counts = [0u16; 256];
86    for &b in &data[..SAMPLE] {
87        counts[b as usize] += 1;
88    }
89    let mut distinct: u32 = 0;
90    let mut max_freq: u16 = 0;
91    for &c in &counts {
92        distinct += (c > 0) as u32;
93        max_freq = max_freq.max(c);
94    }
95    distinct >= DISTINCT_THRESHOLD && (max_freq as u32) <= SAMPLE as u32 / MAX_FREQ_DENOM
96}
97
98pub(crate) fn clamp_params_to_src_size(params: &mut strategy::LevelParams, src_len: usize) {
99    params.hash_log = params
100        .hash_log
101        .clamp(strategy::HASH_LOG_MIN, strategy::HASH_LOG_MAX);
102    params.chain_log = params
103        .chain_log
104        .clamp(strategy::HASH_LOG_MIN, strategy::HASH_LOG_MAX);
105    if src_len >= 2 {
106        let src_log = 32 - ((src_len as u32) - 1).leading_zeros();
107        params.hash_log = params.hash_log.min(src_log).max(strategy::HASH_LOG_MIN);
108        params.chain_log = params.chain_log.min(src_log).max(strategy::HASH_LOG_MIN);
109        params.window_log = params.window_log.min(src_log);
110    }
111}
112
113pub fn compress_with_params(
114    input: &[u8],
115    params: &strategy::LevelParams,
116) -> Result<Vec<u8>, CompressError> {
117    let mut params = *params;
118    clamp_params_to_src_size(&mut params, input.len());
119    compress_inner(input, &params)
120}
121
122pub fn compress(input: &[u8], level: i32) -> Result<Vec<u8>, CompressError> {
123    let params = strategy::level_params_for_size(level, input.len())
124        .ok_or(CompressError::InvalidLevel(level))?;
125    compress_inner(input, &params)
126}
127
128pub fn compress_opts(
129    input: &[u8],
130    level: i32,
131    opts: &strategy::Options,
132) -> Result<Vec<u8>, CompressError> {
133    let mut params = strategy::level_params_for_size(level, input.len())
134        .ok_or(CompressError::InvalidLevel(level))?;
135    strategy::apply_options(&mut params, opts);
136    compress_inner(input, &params)
137}
138
139#[allow(clippy::unnecessary_wraps)]
140fn compress_inner(input: &[u8], params: &strategy::LevelParams) -> Result<Vec<u8>, CompressError> {
141    let mut params = *params;
142    strategy::apply_raw_literals_size_override(&mut params, input.len());
143    let mut output = Vec::with_capacity(input.len() + 32);
144    compress_frame(input, &params, &mut output);
145    Ok(output)
146}
147
148fn compress_frame(input: &[u8], params: &strategy::LevelParams, output: &mut Vec<u8>) {
149    write_frame_header(output, input.len(), None);
150
151    if input.is_empty() {
152        block_encoder::encode_raw_block(&[], true, output);
153    } else {
154        let mut rep_offsets = [1u32, 4, 8];
155        let mut offset = 0;
156        let mut sequences = Vec::with_capacity(MAX_BLOCK_SIZE / 8);
157        let mut workspace = block_encoder::BlockEncodeWorkspace::new();
158
159        #[cfg(feature = "ldm")]
160        let mut ldm_state = params.ldm_params.as_ref().map(ldm::LdmState::new);
161
162        match params.strategy {
163            Strategy::Fast => {
164                let hash_size = 1usize << params.hash_log;
165                let mut hash_table = vec![0u32; hash_size];
166                while offset < input.len() {
167                    let chunk_size = (input.len() - offset).min(MAX_BLOCK_SIZE);
168                    let block_end = offset + chunk_size;
169                    let is_last = block_end >= input.len();
170
171                    if block_looks_incompressible(&input[offset..block_end]) {
172                        block_encoder::encode_raw_block(&input[offset..block_end], is_last, output);
173                    } else {
174                        #[cfg(feature = "ldm")]
175                        let used_ldm = if let Some(ref mut ldm) = ldm_state {
176                            let mut empty = Vec::new();
177                            ldm.compress_block(
178                                input,
179                                offset,
180                                block_end,
181                                params,
182                                &rep_offsets,
183                                &mut hash_table,
184                                &mut empty,
185                                &mut sequences,
186                            );
187                            true
188                        } else {
189                            false
190                        };
191                        #[cfg(not(feature = "ldm"))]
192                        let used_ldm = false;
193
194                        if !used_ldm {
195                            fast::compress_fast_block(
196                                input,
197                                offset,
198                                block_end,
199                                params,
200                                &rep_offsets,
201                                &mut hash_table,
202                                &mut sequences,
203                            );
204                        }
205                        if params.force_raw_literals {
206                            block_encoder::encode_compressed_block_raw(
207                                &input[offset..block_end],
208                                &sequences,
209                                &mut rep_offsets,
210                                is_last,
211                                output,
212                                &mut workspace,
213                            );
214                        } else {
215                            block_encoder::encode_compressed_block(
216                                &input[offset..block_end],
217                                &sequences,
218                                &mut rep_offsets,
219                                is_last,
220                                output,
221                                &mut workspace,
222                                strategy::use_custom_sequence_tables(params, input.len()),
223                            );
224                        }
225                    }
226                    offset = block_end;
227                }
228            }
229            Strategy::DFast => {
230                let short_size = 1usize << params.chain_log;
231                let long_size = 1usize << params.hash_log;
232                let mut hash_short = vec![0u32; short_size];
233                let mut hash_long = vec![0u32; long_size];
234                while offset < input.len() {
235                    let chunk_size = (input.len() - offset).min(MAX_BLOCK_SIZE);
236                    let block_end = offset + chunk_size;
237                    let is_last = block_end >= input.len();
238
239                    if block_looks_incompressible(&input[offset..block_end]) {
240                        block_encoder::encode_raw_block(&input[offset..block_end], is_last, output);
241                    } else {
242                        #[cfg(feature = "ldm")]
243                        let used_ldm = if let Some(ref mut ldm) = ldm_state {
244                            ldm.compress_block(
245                                input,
246                                offset,
247                                block_end,
248                                params,
249                                &rep_offsets,
250                                &mut hash_short,
251                                &mut hash_long,
252                                &mut sequences,
253                            );
254                            true
255                        } else {
256                            false
257                        };
258                        #[cfg(not(feature = "ldm"))]
259                        let used_ldm = false;
260
261                        if !used_ldm {
262                            dfast::compress_dfast_block(
263                                input,
264                                offset,
265                                block_end,
266                                params,
267                                &rep_offsets,
268                                &mut hash_short,
269                                &mut hash_long,
270                                &mut sequences,
271                            );
272                        }
273                        block_encoder::encode_compressed_block(
274                            &input[offset..block_end],
275                            &sequences,
276                            &mut rep_offsets,
277                            is_last,
278                            output,
279                            &mut workspace,
280                            strategy::use_custom_sequence_tables(params, input.len()),
281                        );
282                    }
283                    offset = block_end;
284                }
285            }
286        }
287    }
288
289    let hash = xxh64(input, 0);
290    let checksum = (hash & 0xFFFF_FFFF) as u32;
291    output.extend_from_slice(&checksum.to_le_bytes());
292}
293
294pub fn compress_with_dict(
295    input: &[u8],
296    level: i32,
297    dict: &zrip_core::dict::Dictionary,
298) -> Result<Vec<u8>, CompressError> {
299    let total_window = dict.content().len() + input.len();
300    let mut params = strategy::level_params_for_size(level, total_window)
301        .ok_or(CompressError::InvalidLevel(level))?;
302    strategy::apply_raw_literals_size_override(&mut params, input.len());
303
304    let mut output = Vec::with_capacity(input.len() + 32);
305    write_frame_header(&mut output, input.len(), Some(dict.id()));
306
307    if input.is_empty() {
308        block_encoder::encode_raw_block(&[], true, &mut output);
309    } else {
310        let prefix = dict.content();
311        let mut rep_offsets = *dict.rep_offsets();
312        let mut workspace = block_encoder::BlockEncodeWorkspace::new();
313
314        workspace.prev_ll = dict
315            .ll_table()
316            .map(|(dt, al)| block_encoder::FseEncodeTable::from_decode_table(dt, al, 35));
317        workspace.prev_of = dict
318            .of_table()
319            .map(|(dt, al)| block_encoder::FseEncodeTable::from_decode_table(dt, al, 31));
320        workspace.prev_ml = dict
321            .ml_table()
322            .map(|(dt, al)| block_encoder::FseEncodeTable::from_decode_table(dt, al, 52));
323        workspace.prev_huffman = dict.huf_table().and_then(|(dt, tl)| {
324            zrip_core::huffman::encode::HuffmanEncodeTable::from_decode_table(dt, tl)
325        });
326
327        if input.len() <= MAX_BLOCK_SIZE {
328            let sequences = match params.strategy {
329                Strategy::Fast => {
330                    fast::compress_fast_with_prefix(input, &params, &rep_offsets, prefix)
331                }
332                Strategy::DFast => {
333                    dfast::compress_dfast_with_prefix(input, &params, &rep_offsets, prefix)
334                }
335            };
336            if params.force_raw_literals {
337                block_encoder::encode_compressed_block_raw(
338                    input,
339                    &sequences,
340                    &mut rep_offsets,
341                    true,
342                    &mut output,
343                    &mut workspace,
344                );
345            } else {
346                block_encoder::encode_compressed_block(
347                    input,
348                    &sequences,
349                    &mut rep_offsets,
350                    true,
351                    &mut output,
352                    &mut workspace,
353                    strategy::use_custom_sequence_tables(&params, input.len()),
354                );
355            }
356        } else {
357            let mut combined = Vec::with_capacity(prefix.len() + input.len());
358            combined.extend_from_slice(prefix);
359            combined.extend_from_slice(input);
360            let plen = prefix.len();
361            let hash_size = 1usize << params.hash_log;
362            let mut sequences = Vec::new();
363
364            match params.strategy {
365                Strategy::Fast => {
366                    let mut hash_table = vec![0u32; hash_size];
367                    fast::prefill_hash_table(&combined, plen, params.hash_log, &mut hash_table);
368                    let mut offset = 0;
369                    while offset < input.len() {
370                        let chunk_size = (input.len() - offset).min(MAX_BLOCK_SIZE);
371                        let is_last = offset + chunk_size >= input.len();
372                        fast::compress_fast_block(
373                            &combined,
374                            plen + offset,
375                            plen + offset + chunk_size,
376                            &params,
377                            &rep_offsets,
378                            &mut hash_table,
379                            &mut sequences,
380                        );
381                        if params.force_raw_literals {
382                            block_encoder::encode_compressed_block_raw(
383                                &input[offset..offset + chunk_size],
384                                &sequences,
385                                &mut rep_offsets,
386                                is_last,
387                                &mut output,
388                                &mut workspace,
389                            );
390                        } else {
391                            block_encoder::encode_compressed_block(
392                                &input[offset..offset + chunk_size],
393                                &sequences,
394                                &mut rep_offsets,
395                                is_last,
396                                &mut output,
397                                &mut workspace,
398                                strategy::use_custom_sequence_tables(&params, input.len()),
399                            );
400                        }
401                        offset += chunk_size;
402                    }
403                }
404                Strategy::DFast => {
405                    let short_size = 1usize << params.chain_log;
406                    let long_size = 1usize << params.hash_log;
407                    let mut hash_short = vec![0u32; short_size];
408                    let mut hash_long = vec![0u32; long_size];
409                    dfast::prefill_hash_tables(
410                        &combined,
411                        plen,
412                        params.hash_log,
413                        params.chain_log,
414                        params.min_match,
415                        &mut hash_short,
416                        &mut hash_long,
417                    );
418                    let mut offset = 0;
419                    while offset < input.len() {
420                        let chunk_size = (input.len() - offset).min(MAX_BLOCK_SIZE);
421                        let is_last = offset + chunk_size >= input.len();
422                        dfast::compress_dfast_block(
423                            &combined,
424                            plen + offset,
425                            plen + offset + chunk_size,
426                            &params,
427                            &rep_offsets,
428                            &mut hash_short,
429                            &mut hash_long,
430                            &mut sequences,
431                        );
432                        block_encoder::encode_compressed_block(
433                            &input[offset..offset + chunk_size],
434                            &sequences,
435                            &mut rep_offsets,
436                            is_last,
437                            &mut output,
438                            &mut workspace,
439                            strategy::use_custom_sequence_tables(&params, input.len()),
440                        );
441                        offset += chunk_size;
442                    }
443                }
444            }
445        }
446    }
447
448    let hash = xxh64(input, 0);
449    let checksum = (hash & 0xFFFF_FFFF) as u32;
450    output.extend_from_slice(&checksum.to_le_bytes());
451
452    Ok(output)
453}
454
455pub fn compress_into(input: &[u8], output: &mut [u8], level: i32) -> Result<usize, CompressError> {
456    let mut params = strategy::level_params_for_size(level, input.len())
457        .ok_or(CompressError::InvalidLevel(level))?;
458    strategy::apply_raw_literals_size_override(&mut params, input.len());
459    let mut buf = Vec::with_capacity(output.len());
460    compress_frame(input, &params, &mut buf);
461    if buf.len() > output.len() {
462        return Err(CompressError::OutputTooSmall);
463    }
464    output[..buf.len()].copy_from_slice(&buf);
465    Ok(buf.len())
466}
467
468#[cfg(all(test, miri, not(feature = "paranoid")))]
469mod ub_tests {
470    use super::*;
471
472    #[test]
473    fn public_compress_with_params_accepts_zero_hash_log() {
474        // Issue: LevelParams is public and compress_with_params only clamps log
475        // values downward for the input size. A caller can pass hash_log = 0,
476        // which allocates a one-entry hash table, while release-mode hash shifts
477        // produce indexes derived from the input bytes. The first hash_load then
478        // reaches get_unchecked with an out-of-bounds index.
479        let mut params = strategy::level_params(1).unwrap();
480        params.hash_log = 0;
481        params.chain_log = 0;
482        let _ = compress_with_params(b"abcdefghijklmnop", &params);
483    }
484}