Skip to main content

zrip_decode/
context.rs

1#[cfg(feature = "alloc")]
2use alloc::borrow::Cow;
3#[cfg(feature = "alloc")]
4use alloc::boxed::Box;
5#[cfg(feature = "alloc")]
6use alloc::vec::Vec;
7
8use crate::BlockDecodeWorkspace;
9use zrip_core::dict::Dictionary;
10use zrip_core::error::DecompressError;
11
12/// Reusable decompression context that amortizes buffer allocations.
13///
14/// Holds internal buffers (output, Huffman/FSE workspace) across calls.
15/// Useful when decompressing many small frames in a loop.
16///
17/// ```no_run
18/// let data = b"repeated decompression".repeat(100);
19/// let compressed = zrip::compress(&data, 1).unwrap();
20///
21/// let mut ctx = zrip::DecompressContext::new();
22/// for _ in 0..10 {
23///     let output = ctx.decompress(&compressed).unwrap();
24///     assert_eq!(&*output, &data[..]);
25/// }
26/// ```
27pub struct DecompressContext {
28    dict: Option<Dictionary>,
29    output: Vec<u8>,
30    ws: Box<BlockDecodeWorkspace>,
31}
32
33impl Default for DecompressContext {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl DecompressContext {
40    /// Creates a new context without a dictionary.
41    pub fn new() -> Self {
42        Self {
43            dict: None,
44            output: Vec::new(),
45            ws: Box::new(BlockDecodeWorkspace::new()),
46        }
47    }
48
49    /// Creates a new context with a pre-loaded dictionary.
50    pub fn with_dict(dict: Dictionary) -> Self {
51        let mut ws = Box::new(BlockDecodeWorkspace::new());
52        ws.cache_dict(&dict);
53        Self {
54            dict: Some(dict),
55            output: Vec::new(),
56            ws,
57        }
58    }
59
60    /// Decompresses `input` using [`DEFAULT_DECOMPRESS_LIMIT`](zrip_core::DEFAULT_DECOMPRESS_LIMIT).
61    pub fn decompress(&mut self, input: &[u8]) -> Result<Cow<'_, [u8]>, DecompressError> {
62        self.decompress_with_limit(input, zrip_core::DEFAULT_DECOMPRESS_LIMIT)
63    }
64
65    /// Decompresses `input` with an explicit output size limit.
66    ///
67    /// Returns [`DecompressError::OutputTooSmall`] if the decompressed output
68    /// would exceed `max_output` bytes.
69    pub fn decompress_with_limit(
70        &mut self,
71        input: &[u8],
72        max_output: usize,
73    ) -> Result<Cow<'_, [u8]>, DecompressError> {
74        self.output.clear();
75        let dict_ref = self.dict.as_ref();
76        if input.len() >= 4 {
77            let magic = u32::from_le_bytes([input[0], input[1], input[2], input[3]]);
78            if magic == zrip_core::frame::ZSTD_MAGIC {
79                let consumed = 4 + super::decompress_frame_after_magic(
80                    &input[4..],
81                    &mut self.output,
82                    max_output,
83                    dict_ref,
84                    &mut self.ws,
85                )?;
86                if consumed == input.len() {
87                    return Ok(Cow::Borrowed(&self.output));
88                }
89                return self.decompress_tail(input, consumed, max_output);
90            }
91        }
92        self.decompress_tail(input, 0, max_output)
93    }
94
95    fn decompress_tail(
96        &mut self,
97        input: &[u8],
98        mut offset: usize,
99        max_output: usize,
100    ) -> Result<Cow<'_, [u8]>, DecompressError> {
101        let dict_ref = self.dict.as_ref();
102        while offset < input.len() {
103            let remaining = &input[offset..];
104            if let Some(skip_len) = super::skip_skippable_frame(remaining) {
105                offset += skip_len;
106                continue;
107            }
108            let frame_limit = super::remaining_output_limit(self.output.len(), 0, max_output)?;
109            let consumed = super::decompress_frame(
110                remaining,
111                &mut self.output,
112                frame_limit,
113                dict_ref,
114                &mut self.ws,
115            )?;
116            offset += consumed;
117        }
118        Ok(Cow::Borrowed(&self.output))
119    }
120
121    /// Decompresses one zstd frame whose 4-byte magic number is stored out of band.
122    ///
123    /// OpenZL stores zstd payloads this way inside transform streams.
124    pub fn decompress_after_magic_with_limit(
125        &mut self,
126        input: &[u8],
127        max_output: usize,
128    ) -> Result<Cow<'_, [u8]>, DecompressError> {
129        self.output.clear();
130        super::decompress_frame_after_magic(
131            input,
132            &mut self.output,
133            max_output,
134            self.dict.as_ref(),
135            &mut self.ws,
136        )?;
137        Ok(Cow::Borrowed(&self.output))
138    }
139
140    /// Decompresses one zstd frame without its magic number into `output`.
141    ///
142    /// Appends to `output` and returns the number of bytes written.
143    pub fn decompress_after_magic_into(
144        &mut self,
145        input: &[u8],
146        output: &mut Vec<u8>,
147        max_output: usize,
148    ) -> Result<usize, DecompressError> {
149        let start = output.len();
150        super::decompress_frame_after_magic(
151            input,
152            output,
153            max_output,
154            self.dict.as_ref(),
155            &mut self.ws,
156        )?;
157        Ok(output.len() - start)
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use alloc::vec::Vec;
165
166    fn push_block_header(out: &mut Vec<u8>, last: bool, block_type: u32, block_size: usize) {
167        let raw = ((block_size as u32) << 3) | (block_type << 1) | u32::from(last);
168        out.push(raw as u8);
169        out.push((raw >> 8) as u8);
170        out.push((raw >> 16) as u8);
171    }
172
173    #[test]
174    fn decompress_after_magic_into_appends_output() {
175        let mut frame = Vec::new();
176        frame.push(0x20);
177        frame.push(5);
178        push_block_header(&mut frame, true, 0, 5);
179        frame.extend_from_slice(b"hello");
180
181        let mut ctx = DecompressContext::new();
182        let mut output = b"prefix".to_vec();
183        let written = ctx
184            .decompress_after_magic_into(&frame, &mut output, usize::MAX)
185            .unwrap();
186        assert_eq!(written, 5);
187        assert_eq!(output, b"prefixhello");
188    }
189
190    #[test]
191    fn decompress_fast_path_continues_after_first_frame() {
192        fn raw_frame(bytes: &[u8]) -> Vec<u8> {
193            let mut frame = Vec::new();
194            frame.extend_from_slice(&zrip_core::frame::ZSTD_MAGIC.to_le_bytes());
195            frame.push(0x20);
196            frame.push(bytes.len() as u8);
197            push_block_header(&mut frame, true, 0, bytes.len());
198            frame.extend_from_slice(bytes);
199            frame
200        }
201
202        let mut stream = raw_frame(b"hello");
203        stream.extend_from_slice(&raw_frame(b"there"));
204
205        let mut ctx = DecompressContext::new();
206        let output = ctx.decompress(&stream).unwrap();
207        assert_eq!(&*output, b"hellothere");
208    }
209}