Skip to main content

zrip_decode/
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_decoder;
9#[cfg(feature = "std")]
10pub mod context;
11pub(crate) mod exec;
12pub(crate) mod fast_vec;
13pub(crate) mod literals;
14pub(crate) mod ring_buffer;
15pub(crate) mod seq_table;
16pub(crate) mod sequences;
17#[cfg(feature = "std")]
18pub mod streaming;
19
20#[cfg(feature = "alloc")]
21use alloc::boxed::Box;
22#[cfg(feature = "alloc")]
23use alloc::vec::Vec;
24
25use crate::exec::decode_execute_sequences;
26use crate::literals::decode_literals_ws;
27use crate::sequences::{SequenceDecodeTables, parse_sequence_count, parse_sequence_tables_ws};
28use zrip_core::block::{BlockType, parse_block_header};
29use zrip_core::error::DecompressError;
30use zrip_core::frame::MAX_WINDOW_SIZE;
31use zrip_core::frame::header::parse_frame_header;
32use zrip_core::huffman::HuffmanDecodeEntry;
33use zrip_core::xxhash::Xxh64State;
34
35pub(crate) struct BlockDecodeWorkspace {
36    pub literal_buf: Vec<u8>,
37    pub huf_table: Vec<HuffmanDecodeEntry>,
38    pub huf_table_log: u8,
39    pub huf_valid: bool,
40    pub huf_all_weights: Vec<u8>,
41    pub huf_rank_count: Vec<u32>,
42    pub huf_rank_start: Vec<u32>,
43    pub huf_weights: Vec<u8>,
44    pub huf_last_weights: Vec<u8>,
45    pub huf_last_weights_valid: bool,
46    pub fse_dist: Vec<i16>,
47    pub fse_symbol_next: Vec<u16>,
48    pub fse_build_buf: Vec<zrip_core::fse::FseDecodeEntry>,
49    pub cached_dict_tables: Option<SequenceDecodeTables>,
50    pub cached_dict_rep: [u32; 3],
51    pub cached_dict_huf: Option<(Vec<HuffmanDecodeEntry>, u8)>,
52}
53
54impl BlockDecodeWorkspace {
55    pub(crate) fn new() -> Self {
56        Self {
57            literal_buf: Vec::new(),
58            huf_table: Vec::new(),
59            huf_table_log: 0,
60            huf_valid: false,
61            huf_all_weights: Vec::new(),
62            huf_rank_count: Vec::new(),
63            huf_rank_start: Vec::new(),
64            huf_weights: Vec::new(),
65            huf_last_weights: Vec::new(),
66            huf_last_weights_valid: false,
67            fse_dist: Vec::new(),
68            fse_symbol_next: Vec::new(),
69            fse_build_buf: Vec::new(),
70            cached_dict_tables: None,
71            cached_dict_rep: [1, 4, 8],
72            cached_dict_huf: None,
73        }
74    }
75
76    pub(crate) fn reset_huffman_state(&mut self) {
77        self.huf_valid = false;
78        self.huf_last_weights_valid = false;
79    }
80
81    #[cfg(feature = "std")]
82    pub(crate) fn cache_dict(&mut self, dict: &zrip_core::dict::Dictionary) {
83        let mut st = SequenceDecodeTables::new_default();
84        if let Some((t, l)) = dict.of_table() {
85            st.of_table = crate::seq_table::SeqTable::promote_of(t);
86            st.of_accuracy = l;
87            st.of_set = true;
88        }
89        if let Some((t, l)) = dict.ml_table() {
90            st.ml_table = crate::seq_table::SeqTable::promote_ml(t);
91            st.ml_accuracy = l;
92            st.ml_set = true;
93        }
94        if let Some((t, l)) = dict.ll_table() {
95            st.ll_table = crate::seq_table::SeqTable::promote_ll(t);
96            st.ll_accuracy = l;
97            st.ll_set = true;
98        }
99        self.cached_dict_tables = Some(st);
100        self.cached_dict_rep = *dict.rep_offsets();
101        if let Some((t, l)) = dict.huf_table() {
102            self.cached_dict_huf = Some((t.to_vec(), l));
103        }
104    }
105}
106
107pub(crate) fn skip_skippable_frame(data: &[u8]) -> Option<usize> {
108    if data.len() < 8 {
109        return None;
110    }
111    let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
112    if (magic & 0xFFFF_FFF0) != 0x184D_2A50 {
113        return None;
114    }
115    let frame_size = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize;
116    let total = 8 + frame_size;
117    if total > data.len() {
118        return None;
119    }
120    Some(total)
121}
122
123pub fn decompress(input: &[u8]) -> Result<Vec<u8>, DecompressError> {
124    decompress_with_dict(input, None)
125}
126
127/// Decompress with an explicit output size limit.
128///
129/// Returns [`DecompressError::OutputTooSmall`] if the decompressed output would
130/// exceed `max_output_size` bytes. Use [`SAFE_DECOMPRESS_LIMIT`](zrip_core::SAFE_DECOMPRESS_LIMIT)
131/// when processing untrusted input to prevent memory exhaustion attacks.
132pub fn decompress_with_limit(
133    input: &[u8],
134    max_output_size: usize,
135) -> Result<Vec<u8>, DecompressError> {
136    let mut output = Vec::new();
137    let mut ws = Box::new(BlockDecodeWorkspace::new());
138    let mut offset = 0;
139    while offset < input.len() {
140        let remaining = &input[offset..];
141        if let Some(skip_len) = skip_skippable_frame(remaining) {
142            offset += skip_len;
143            continue;
144        }
145        let consumed = decompress_frame(remaining, &mut output, max_output_size, None, &mut ws)?;
146        offset += consumed;
147    }
148    Ok(output)
149}
150
151pub fn decompress_into(input: &[u8], output: &mut Vec<u8>) -> Result<usize, DecompressError> {
152    let max_output = zrip_core::DEFAULT_DECOMPRESS_LIMIT;
153    let mut ws = Box::new(BlockDecodeWorkspace::new());
154    let start = output.len();
155    let mut offset = 0;
156    while offset < input.len() {
157        let remaining = &input[offset..];
158        if let Some(skip_len) = skip_skippable_frame(remaining) {
159            offset += skip_len;
160            continue;
161        }
162        let consumed = decompress_frame(remaining, output, max_output, None, &mut ws)?;
163        offset += consumed;
164    }
165    Ok(output.len() - start)
166}
167
168pub fn decompress_with_dict(
169    input: &[u8],
170    dict: Option<&zrip_core::dict::Dictionary>,
171) -> Result<Vec<u8>, DecompressError> {
172    let max_output = zrip_core::DEFAULT_DECOMPRESS_LIMIT;
173    let mut output = Vec::new();
174    let mut ws = Box::new(BlockDecodeWorkspace::new());
175    let mut offset = 0;
176
177    while offset < input.len() {
178        let remaining = &input[offset..];
179        if let Some(skip_len) = skip_skippable_frame(remaining) {
180            offset += skip_len;
181            continue;
182        }
183        let consumed = decompress_frame(remaining, &mut output, max_output, dict, &mut ws)?;
184        offset += consumed;
185    }
186
187    Ok(output)
188}
189
190pub(crate) fn decompress_frame(
191    input: &[u8],
192    output: &mut Vec<u8>,
193    max_output: usize,
194    dict: Option<&zrip_core::dict::Dictionary>,
195    ws: &mut BlockDecodeWorkspace,
196) -> Result<usize, DecompressError> {
197    let header = parse_frame_header(input)?;
198
199    if header.window_size > MAX_WINDOW_SIZE && !header.single_segment {
200        return Err(DecompressError::WindowTooLarge {
201            requested: header.window_size,
202            max: MAX_WINDOW_SIZE,
203        });
204    }
205
206    if let Some(frame_dict_id) = header.dict_id {
207        match dict {
208            Some(d) if d.id() == frame_dict_id => {}
209            Some(d) => {
210                return Err(DecompressError::DictMismatch {
211                    expected: frame_dict_id,
212                    got: d.id(),
213                });
214            }
215            None => return Err(DecompressError::DictRequired),
216        }
217    }
218
219    if let Some(fcs) = header.frame_content_size {
220        if max_output < usize::MAX && fcs as usize > max_output {
221            return Err(DecompressError::OutputTooSmall);
222        }
223        let hint = (fcs as usize).min(MAX_WINDOW_SIZE as usize);
224        output.reserve(hint + 32);
225    }
226
227    let mut offset = header.header_size;
228    let output_start = output.len();
229
230    let dict_history: &[u8] = if let Some(d) = dict { d.content() } else { &[] };
231
232    let (mut seq_tables, mut rep_offsets) = if let Some(ref cached) = ws.cached_dict_tables {
233        (cached.clone(), ws.cached_dict_rep)
234    } else if let Some(d) = dict {
235        let mut st = SequenceDecodeTables::new_default();
236        if let Some((t, l)) = d.of_table() {
237            st.of_table = crate::seq_table::SeqTable::promote_of(t);
238            st.of_accuracy = l;
239            st.of_set = true;
240        }
241        if let Some((t, l)) = d.ml_table() {
242            st.ml_table = crate::seq_table::SeqTable::promote_ml(t);
243            st.ml_accuracy = l;
244            st.ml_set = true;
245        }
246        if let Some((t, l)) = d.ll_table() {
247            st.ll_table = crate::seq_table::SeqTable::promote_ll(t);
248            st.ll_accuracy = l;
249            st.ll_set = true;
250        }
251        (st, *d.rep_offsets())
252    } else {
253        (SequenceDecodeTables::new_default(), [1u32, 4, 8])
254    };
255    ws.reset_huffman_state();
256    if let Some((ref t, l)) = ws.cached_dict_huf {
257        ws.huf_table.clear();
258        ws.huf_table.extend_from_slice(t);
259        ws.huf_table_log = l;
260        ws.huf_valid = true;
261    } else if let Some(d) = dict
262        && let Some((t, l)) = d.huf_table()
263    {
264        ws.huf_table.clear();
265        ws.huf_table.extend_from_slice(t);
266        ws.huf_table_log = l;
267        ws.huf_valid = true;
268    }
269
270    let mut hasher = if header.content_checksum {
271        Some(Xxh64State::new(0))
272    } else {
273        None
274    };
275
276    loop {
277        if offset + 3 > input.len() {
278            return Err(DecompressError::InputExhausted);
279        }
280        let block_header = parse_block_header(&input[offset..])?;
281        offset += 3;
282
283        let block_size = block_header.block_size as usize;
284
285        if block_size > zrip_core::frame::MAX_BLOCK_SIZE {
286            match block_header.block_type {
287                BlockType::Raw | BlockType::Rle => {
288                    return Err(DecompressError::BlockTooLarge);
289                }
290                BlockType::Compressed => {}
291            }
292        }
293
294        let block_output_start = output.len();
295        match block_header.block_type {
296            BlockType::Raw => {
297                if offset + block_size > input.len() {
298                    return Err(DecompressError::InputExhausted);
299                }
300                if output.len() - output_start + block_size > max_output {
301                    return Err(DecompressError::OutputTooSmall);
302                }
303                output.extend_from_slice(&input[offset..offset + block_size]);
304                offset += block_size;
305            }
306            BlockType::Rle => {
307                if offset >= input.len() {
308                    return Err(DecompressError::InputExhausted);
309                }
310                if output.len() - output_start + block_size > max_output {
311                    return Err(DecompressError::OutputTooSmall);
312                }
313                let byte = input[offset];
314                output.resize(output.len() + block_size, byte);
315                offset += 1;
316            }
317            BlockType::Compressed => {
318                if offset + block_size > input.len() {
319                    return Err(DecompressError::InputExhausted);
320                }
321                let block_data = &input[offset..offset + block_size];
322                decode_compressed_block(
323                    block_data,
324                    output,
325                    output_start,
326                    max_output,
327                    &mut seq_tables,
328                    &mut rep_offsets,
329                    ws,
330                    dict_history,
331                )?;
332                offset += block_size;
333            }
334        }
335        if let Some(ref mut hasher) = hasher {
336            hasher.update(&output[block_output_start..]);
337        }
338
339        if block_header.last_block {
340            break;
341        }
342    }
343
344    if let Some(ref mut hasher) = hasher {
345        let hash = hasher.finish();
346        let expected_checksum = (hash & 0xFFFF_FFFF) as u32;
347
348        if offset + 4 > input.len() {
349            return Err(DecompressError::InputExhausted);
350        }
351        let stored_checksum = u32::from_le_bytes([
352            input[offset],
353            input[offset + 1],
354            input[offset + 2],
355            input[offset + 3],
356        ]);
357        offset += 4;
358
359        if expected_checksum != stored_checksum {
360            return Err(DecompressError::ChecksumMismatch {
361                expected: stored_checksum,
362                got: expected_checksum,
363            });
364        }
365    }
366
367    if let Some(fcs) = header.frame_content_size
368        && (output.len() - output_start) as u64 != fcs
369    {
370        return Err(DecompressError::FrameSizeMismatch);
371    }
372
373    Ok(offset)
374}
375
376#[allow(clippy::too_many_arguments)]
377fn decode_compressed_block(
378    data: &[u8],
379    output: &mut Vec<u8>,
380    output_start: usize,
381    max_output: usize,
382    seq_tables: &mut SequenceDecodeTables,
383    rep_offsets: &mut [u32; 3],
384    ws: &mut BlockDecodeWorkspace,
385    dict_history: &[u8],
386) -> Result<(), DecompressError> {
387    let lit_consumed = decode_literals_ws(data, ws)?;
388
389    let remaining = &data[lit_consumed..];
390
391    if remaining.is_empty() {
392        if output.len() - output_start + ws.literal_buf.len() > max_output {
393            return Err(DecompressError::OutputTooSmall);
394        }
395        output.extend_from_slice(&ws.literal_buf);
396        return Ok(());
397    }
398
399    let (num_sequences, seq_count_size) = parse_sequence_count(remaining)?;
400
401    if num_sequences == 0 {
402        if output.len() - output_start + ws.literal_buf.len() > max_output {
403            return Err(DecompressError::OutputTooSmall);
404        }
405        output.extend_from_slice(&ws.literal_buf);
406        return Ok(());
407    }
408
409    let table_data = &remaining[seq_count_size..];
410    let tables_consumed = parse_sequence_tables_ws(table_data, seq_tables, ws)?;
411
412    let seq_data = &table_data[tables_consumed..];
413
414    let before = output.len();
415
416    let result = decode_sequences_dispatch(
417        seq_data,
418        num_sequences,
419        seq_tables,
420        rep_offsets,
421        &ws.literal_buf,
422        output,
423        dict_history,
424    );
425    result?;
426    if output.len() - before > zrip_core::frame::MAX_BLOCK_SIZE {
427        return Err(DecompressError::BlockTooLarge);
428    }
429
430    Ok(())
431}
432
433#[inline(always)]
434pub(crate) fn decode_sequences_dispatch(
435    seq_data: &[u8],
436    num_sequences: u32,
437    seq_tables: &mut SequenceDecodeTables,
438    rep_offsets: &mut [u32; 3],
439    literals: &[u8],
440    output: &mut Vec<u8>,
441    history: &[u8],
442) -> Result<(), DecompressError> {
443    #[cfg(all(feature = "std", feature = "simd"))]
444    {
445        use std::sync::OnceLock;
446        static LEVEL: OnceLock<fearless_simd::Level> = OnceLock::new();
447        let level = *LEVEL.get_or_init(fearless_simd::Level::new);
448        return fearless_simd::dispatch!(level, _simd => {
449            if history.is_empty() {
450                decode_execute_sequences::<false>(
451                    seq_data,
452                    num_sequences,
453                    seq_tables,
454                    rep_offsets,
455                    literals,
456                    output,
457                    history,
458                )
459            } else {
460                decode_execute_sequences::<true>(
461                    seq_data,
462                    num_sequences,
463                    seq_tables,
464                    rep_offsets,
465                    literals,
466                    output,
467                    history,
468                )
469            }
470        });
471    }
472
473    #[allow(unreachable_code)]
474    if history.is_empty() {
475        decode_execute_sequences::<false>(
476            seq_data,
477            num_sequences,
478            seq_tables,
479            rep_offsets,
480            literals,
481            output,
482            history,
483        )
484    } else {
485        decode_execute_sequences::<true>(
486            seq_data,
487            num_sequences,
488            seq_tables,
489            rep_offsets,
490            literals,
491            output,
492            history,
493        )
494    }
495}
496
497#[cfg(all(test, miri, not(feature = "paranoid")))]
498mod ub_tests {
499    use super::*;
500    use alloc::vec::Vec;
501    use zrip_core::bitstream::writer::BitWriter;
502    use zrip_core::frame::{MAX_BLOCK_SIZE, ZSTD_MAGIC};
503
504    fn push_block_header(out: &mut Vec<u8>, last: bool, block_type: u32, block_size: usize) {
505        let raw = ((block_size as u32) << 3) | (block_type << 1) | u32::from(last);
506        out.push(raw as u8);
507        out.push((raw >> 8) as u8);
508        out.push((raw >> 16) as u8);
509    }
510
511    fn frame_with_oversized_compressed_block_output() -> Vec<u8> {
512        let mut frame = Vec::new();
513        frame.extend_from_slice(&ZSTD_MAGIC.to_le_bytes());
514        frame.push(0x00);
515        frame.push(0x00);
516
517        push_block_header(&mut frame, false, 0, 1);
518        frame.push(b'A');
519
520        let mut block = Vec::new();
521        let trailing_literals = 65usize;
522        block.push(0x04 | (((trailing_literals & 0x0f) as u8) << 4));
523        block.push((trailing_literals >> 4) as u8);
524        block.extend(core::iter::repeat_n(b'B', trailing_literals));
525
526        block.push(1);
527        block.push(0x54);
528        block.extend_from_slice(&[0, 2, 52]);
529
530        let mut seq_bits = BitWriter::new();
531        let ml_extra = MAX_BLOCK_SIZE as u32 - 65_539;
532        seq_bits.write_bits(ml_extra, 16);
533        seq_bits.write_bits(0, 2);
534        seq_bits.close_reverse_stream();
535        block.extend_from_slice(&seq_bits.into_bytes());
536
537        push_block_header(&mut frame, true, 2, block.len());
538        frame.extend_from_slice(&block);
539        frame
540    }
541
542    #[test]
543    fn compressed_block_trailing_literals_overrun_wildcopy_headroom() {
544        // Issue: sequence execution reserves one block plus 64 bytes of wild-copy
545        // headroom and checks each sequence against that limit, but the final
546        // trailing literals are appended afterward without a matching capacity or
547        // block-size check. This safe frame seeds one prior byte, emits a 128 KiB
548        // match at offset 1, then appends 65 literals; miri reports the resulting
549        // out-of-bounds write in fast_extend_from_slice.
550        let frame = frame_with_oversized_compressed_block_output();
551        let _ = decompress(&frame);
552    }
553}