Skip to main content

zstd_complete/encoding/
workspace.rs

1//! Reusable slice-to-slice compression workspaces.
2
3use alloc::vec::Vec;
4use core::{fmt, marker::PhantomData, mem::MaybeUninit};
5
6use super::{levels::c_port, CompressionLevel};
7use crate::workspace::{Arena, ArenaError, ArenaSize, ReusableVec};
8
9use c_port::{
10    WorkspaceCctxParameters as CctxParameters, WorkspaceDFastMatchState as DFastMatchState,
11    WorkspaceFastMatchState as FastMatchState, WorkspaceFrameBlockState as FrameBlockState,
12    WorkspaceGreedyMatchState as GreedyMatchState, WorkspaceLazyBlockStrategy as LazyBlockStrategy,
13    WorkspaceLdmWorkspace as LdmWorkspace, WorkspaceOptBlockState as OptBlockState,
14    WorkspaceOptFrameStrategy as OptFrameStrategy, WorkspaceParamSwitch as ParamSwitch,
15    WorkspaceStrategy as Strategy,
16};
17
18/// Errors reported while creating or using an encoder workspace.
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum EncoderWorkspaceError {
21    InsufficientWorkspace { required: usize, provided: usize },
22    InputTooLarge { maximum: usize, provided: usize },
23    OutputTooSmall { required: usize, provided: usize },
24    SizeOverflow,
25}
26
27impl fmt::Display for EncoderWorkspaceError {
28    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self {
30            Self::InsufficientWorkspace { required, provided } => write!(
31                formatter,
32                "encoder workspace needs {required} bytes but only {provided} were provided"
33            ),
34            Self::InputTooLarge { maximum, provided } => write!(
35                formatter,
36                "encoder workspace accepts at most {maximum} input bytes but received {provided}"
37            ),
38            Self::OutputTooSmall { required, provided } => write!(
39                formatter,
40                "encoded output needs {required} bytes but only {provided} were provided"
41            ),
42            Self::SizeOverflow => formatter.write_str("encoder workspace size overflowed"),
43        }
44    }
45}
46
47// Boxing a strategy would add a heap allocation to caller-backed contexts;
48// the enum deliberately keeps the selected state inline in the arena owner.
49#[allow(clippy::large_enum_variant)]
50enum MatchWorkspace {
51    Uncompressed,
52    Fast(FastMatchState),
53    DFast(DFastMatchState),
54    Greedy {
55        state: GreedyMatchState,
56        depth: LazyBlockStrategy,
57    },
58    Opt {
59        state: OptBlockState,
60        strategy: OptFrameStrategy,
61    },
62}
63
64struct EncoderCore {
65    level: i32,
66    maximum_input: usize,
67    base_cctx: CctxParameters,
68    frame: FrameBlockState,
69    matcher: MatchWorkspace,
70    ldm: Option<LdmWorkspace>,
71}
72
73impl EncoderCore {
74    fn add_size(
75        size: &mut ArenaSize,
76        level: i32,
77        maximum_input: usize,
78    ) -> Result<CctxParameters, EncoderWorkspaceError> {
79        let cctx = CctxParameters::for_level(level, maximum_input as u64, 0);
80        FrameBlockState::add_workspace_size(size).map_err(map_arena)?;
81        let block_size = crate::common::MAX_BLOCK_SIZE as usize;
82        if level == 0 {
83            return Ok(cctx);
84        }
85        match cctx.compression.strategy {
86            Strategy::Fast => {
87                FastMatchState::add_workspace_size(size, cctx.compression, block_size)
88                    .map_err(map_arena)?;
89            }
90            Strategy::DFast => {
91                DFastMatchState::add_workspace_size(size, cctx.compression, block_size)
92                    .map_err(map_arena)?;
93            }
94            Strategy::Greedy | Strategy::Lazy | Strategy::Lazy2 | Strategy::BtLazy2 => {
95                GreedyMatchState::add_workspace_size(size, cctx.compression, block_size)
96                    .map_err(map_arena)?;
97            }
98            Strategy::BtOpt | Strategy::BtUltra | Strategy::BtUltra2 => {
99                OptBlockState::add_workspace_size(size, cctx.compression, block_size)
100                    .map_err(map_arena)?;
101            }
102        }
103        if cctx.ldm.enable_ldm == ParamSwitch::Enable {
104            LdmWorkspace::add_workspace_size(size, cctx.ldm, maximum_input).map_err(map_arena)?;
105        }
106        Ok(cctx)
107    }
108
109    fn new_in(
110        arena: &mut Arena<'_>,
111        level: i32,
112        maximum_input: usize,
113        cctx: CctxParameters,
114    ) -> Result<Self, EncoderWorkspaceError> {
115        let block_size = crate::common::MAX_BLOCK_SIZE as usize;
116        let frame = FrameBlockState::new_in(arena, cctx.compression, cctx.max_block_size)
117            .map_err(map_arena)?;
118        let matcher = if level == 0 {
119            MatchWorkspace::Uncompressed
120        } else {
121            match cctx.compression.strategy {
122                Strategy::Fast => MatchWorkspace::Fast(
123                    FastMatchState::new_in(arena, cctx.compression, block_size)
124                        .map_err(map_arena)?,
125                ),
126                Strategy::DFast => MatchWorkspace::DFast(
127                    DFastMatchState::new_in(arena, cctx.compression, block_size)
128                        .map_err(map_arena)?,
129                ),
130                strategy @ (Strategy::Greedy
131                | Strategy::Lazy
132                | Strategy::Lazy2
133                | Strategy::BtLazy2) => MatchWorkspace::Greedy {
134                    state: GreedyMatchState::new_in(arena, cctx.compression, block_size)
135                        .map_err(map_arena)?,
136                    depth: match strategy {
137                        Strategy::Greedy => LazyBlockStrategy::Greedy,
138                        Strategy::Lazy => LazyBlockStrategy::Lazy,
139                        Strategy::Lazy2 => LazyBlockStrategy::Lazy2,
140                        Strategy::BtLazy2 => LazyBlockStrategy::BtLazy2,
141                        _ => unreachable!(),
142                    },
143                },
144                strategy @ (Strategy::BtOpt | Strategy::BtUltra | Strategy::BtUltra2) => {
145                    MatchWorkspace::Opt {
146                        state: OptBlockState::new_in(arena, cctx.compression, block_size)
147                            .map_err(map_arena)?,
148                        strategy: match strategy {
149                            Strategy::BtOpt => OptFrameStrategy::BtOpt,
150                            Strategy::BtUltra => OptFrameStrategy::BtUltra,
151                            Strategy::BtUltra2 => OptFrameStrategy::BtUltra2,
152                            _ => unreachable!(),
153                        },
154                    }
155                }
156            }
157        };
158        let ldm = if cctx.ldm.enable_ldm == ParamSwitch::Enable {
159            Some(LdmWorkspace::new_in(arena, cctx.ldm, maximum_input).map_err(map_arena)?)
160        } else {
161            None
162        };
163        Ok(Self {
164            level,
165            maximum_input,
166            base_cctx: cctx,
167            frame,
168            matcher,
169            ldm,
170        })
171    }
172
173    fn encode_into<'output>(
174        &mut self,
175        input: &[u8],
176        output: &'output mut [u8],
177    ) -> Result<&'output [u8], EncoderWorkspaceError> {
178        if input.len() > self.maximum_input {
179            return Err(EncoderWorkspaceError::InputTooLarge {
180                maximum: self.maximum_input,
181                provided: input.len(),
182            });
183        }
184        let required = checked_output_size(input.len())?;
185        if output.len() < required {
186            return Err(EncoderWorkspaceError::OutputTooSmall {
187                required,
188                provided: output.len(),
189            });
190        }
191        let cctx = CctxParameters::from_compression_parameters(
192            self.level,
193            self.base_cctx.compression,
194            input.len() as u64,
195        );
196        // SAFETY: the temporary vector is recovered before this function
197        // returns and the capacity check above prevents growth.
198        let reusable = unsafe { ReusableVec::from_static_parts(output.as_mut_ptr(), output.len()) };
199        let (mut output_vec, lease) = reusable.lease_vec();
200        let ldm = &mut self.ldm;
201        match &mut self.matcher {
202            MatchWorkspace::Uncompressed => encode_uncompressed(input, &mut output_vec),
203            MatchWorkspace::Fast(state) => {
204                c_port::workspace_encode_fast(input, cctx, &mut self.frame, state, &mut output_vec)
205            }
206            MatchWorkspace::DFast(state) => {
207                c_port::workspace_encode_dfast(input, cctx, &mut self.frame, state, &mut output_vec)
208            }
209            MatchWorkspace::Greedy { state, depth } => c_port::workspace_encode_greedy(
210                input,
211                cctx,
212                *depth,
213                &mut self.frame,
214                state,
215                &mut output_vec,
216            ),
217            MatchWorkspace::Opt { state, strategy } => c_port::workspace_encode_opt(
218                input,
219                cctx,
220                *strategy,
221                &mut self.frame,
222                state,
223                ldm.as_mut(),
224                &mut output_vec,
225            ),
226        }
227        let length = output_vec.len();
228        let reusable = ReusableVec::recover_vec(output_vec, lease);
229        drop(reusable);
230        Ok(&output[..length])
231    }
232}
233
234fn encode_uncompressed(input: &[u8], output: &mut Vec<u8>) {
235    use crate::{
236        blocks::block::BlockType,
237        encoding::{block_header::BlockHeader, frame_header::FrameHeader},
238    };
239    output.clear();
240    FrameHeader {
241        frame_content_size: Some(input.len() as u64),
242        single_segment: true,
243        content_checksum: false,
244        dictionary_id: None,
245        window_size: None,
246    }
247    .serialize(output);
248    if input.is_empty() {
249        BlockHeader {
250            last_block: true,
251            block_type: BlockType::Raw,
252            block_size: 0,
253        }
254        .serialize(output);
255        return;
256    }
257    let block_size = crate::common::MAX_BLOCK_SIZE as usize;
258    for (index, block) in input.chunks(block_size).enumerate() {
259        BlockHeader {
260            last_block: (index + 1) * block_size >= input.len(),
261            block_type: BlockType::Raw,
262            block_size: block.len() as u32,
263        }
264        .serialize(output);
265        output.extend_from_slice(block);
266    }
267}
268
269/// An owned compression workspace reusable without further allocation.
270pub struct EncoderWorkspace {
271    core: EncoderCore,
272    storage: Vec<MaybeUninit<u8>>,
273}
274
275impl EncoderWorkspace {
276    pub fn new(
277        level: CompressionLevel,
278        maximum_input: usize,
279    ) -> Result<Self, EncoderWorkspaceError> {
280        let required = Self::required_size(level, maximum_input)?;
281        let mut storage = Vec::with_capacity(required);
282        storage.resize_with(required, MaybeUninit::uninit);
283        let mut arena = Arena::new(&mut storage);
284        let mut size = ArenaSize::new();
285        let cctx = EncoderCore::add_size(&mut size, level.get(), maximum_input)?;
286        let core = EncoderCore::new_in(&mut arena, level.get(), maximum_input, cctx)?;
287        Ok(Self { core, storage })
288    }
289
290    pub fn required_size(
291        level: CompressionLevel,
292        maximum_input: usize,
293    ) -> Result<usize, EncoderWorkspaceError> {
294        let mut size = ArenaSize::new();
295        EncoderCore::add_size(&mut size, level.get(), maximum_input)?;
296        Ok(size.finish())
297    }
298
299    pub fn required_output_size(maximum_input: usize) -> Result<usize, EncoderWorkspaceError> {
300        checked_output_size(maximum_input)
301    }
302
303    pub fn encode_into<'output>(
304        &mut self,
305        input: &[u8],
306        output: &'output mut [u8],
307    ) -> Result<&'output [u8], EncoderWorkspaceError> {
308        self.core.encode_into(input, output)
309    }
310
311    pub fn workspace_bytes(&self) -> usize {
312        self.storage.len()
313    }
314
315    pub const fn maximum_input(&self) -> usize {
316        self.core.maximum_input
317    }
318}
319
320#[cfg(feature = "std")]
321impl std::error::Error for EncoderWorkspaceError {}
322
323/// A compression context whose working state lives entirely in caller storage.
324pub struct StaticEncoderWorkspace<'storage> {
325    core: EncoderCore,
326    workspace_bytes: usize,
327    marker: PhantomData<&'storage mut [MaybeUninit<u8>]>,
328}
329
330impl<'storage> StaticEncoderWorkspace<'storage> {
331    pub fn new(
332        storage: &'storage mut [u8],
333        level: CompressionLevel,
334        maximum_input: usize,
335    ) -> Result<Self, EncoderWorkspaceError> {
336        Self::new_uninit(initialized_bytes_as_uninit(storage), level, maximum_input)
337    }
338
339    /// Constructs a workspace without initializing caller-provided bytes.
340    pub fn new_uninit(
341        storage: &'storage mut [MaybeUninit<u8>],
342        level: CompressionLevel,
343        maximum_input: usize,
344    ) -> Result<Self, EncoderWorkspaceError> {
345        let required = EncoderWorkspace::required_size(level, maximum_input)?;
346        if storage.len() < required {
347            return Err(EncoderWorkspaceError::InsufficientWorkspace {
348                required,
349                provided: storage.len(),
350            });
351        }
352        let mut size = ArenaSize::new();
353        let cctx = EncoderCore::add_size(&mut size, level.get(), maximum_input)?;
354        let mut arena = Arena::new(storage);
355        let core = EncoderCore::new_in(&mut arena, level.get(), maximum_input, cctx)?;
356        Ok(Self {
357            core,
358            workspace_bytes: storage.len(),
359            marker: PhantomData,
360        })
361    }
362
363    pub fn encode_into<'output>(
364        &mut self,
365        input: &[u8],
366        output: &'output mut [u8],
367    ) -> Result<&'output [u8], EncoderWorkspaceError> {
368        self.core.encode_into(input, output)
369    }
370
371    pub const fn maximum_input(&self) -> usize {
372        self.core.maximum_input
373    }
374
375    pub const fn workspace_bytes(&self) -> usize {
376        self.workspace_bytes
377    }
378}
379
380fn initialized_bytes_as_uninit(storage: &mut [u8]) -> &mut [MaybeUninit<u8>] {
381    // SAFETY: `MaybeUninit<u8>` has the same layout as `u8`, and treating
382    // initialized bytes as possibly uninitialized only weakens the invariant.
383    unsafe { core::slice::from_raw_parts_mut(storage.as_mut_ptr().cast(), storage.len()) }
384}
385
386fn map_arena(error: ArenaError) -> EncoderWorkspaceError {
387    match error {
388        ArenaError::InsufficientStorage { required, provided } => {
389            EncoderWorkspaceError::InsufficientWorkspace { required, provided }
390        }
391        ArenaError::CapacityExceeded { .. } | ArenaError::SizeOverflow => {
392            EncoderWorkspaceError::SizeOverflow
393        }
394    }
395}
396
397fn checked_output_size(input_size: usize) -> Result<usize, EncoderWorkspaceError> {
398    let bound = c_port::workspace_compress_bound(input_size);
399    if bound == 0 {
400        Err(EncoderWorkspaceError::SizeOverflow)
401    } else {
402        Ok(bound)
403    }
404}