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    Capacity(crate::workspace::CapacityError),
22    /// A prior encode did not complete. Recreate the context before reuse.
23    ContextPoisoned,
24    /// Requested strategy would enter an owned-only entropy path.
25    UnsupportedWorkspaceConfiguration,
26    InsufficientWorkspace { required: usize, provided: usize },
27    InputTooLarge { maximum: usize, provided: usize },
28    OutputTooSmall { required: usize, provided: usize },
29    SizeOverflow,
30}
31
32impl fmt::Display for EncoderWorkspaceError {
33    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            Self::Capacity(error) => write!(formatter, "encoder storage admission failed: {error:?}"),
36            Self::UnsupportedWorkspaceConfiguration => formatter.write_str("encoder configuration is not supported by caller-backed storage"),
37            Self::ContextPoisoned => formatter.write_str("encoder context must be recreated after an incomplete encode"),
38            Self::InsufficientWorkspace { required, provided } => write!(
39                formatter,
40                "encoder workspace needs {required} bytes but only {provided} were provided"
41            ),
42            Self::InputTooLarge { maximum, provided } => write!(
43                formatter,
44                "encoder workspace accepts at most {maximum} input bytes but received {provided}"
45            ),
46            Self::OutputTooSmall { required, provided } => write!(
47                formatter,
48                "encoded output needs {required} bytes but only {provided} were provided"
49            ),
50            Self::SizeOverflow => formatter.write_str("encoder workspace size overflowed"),
51        }
52    }
53}
54
55// Boxing a strategy would add a heap allocation to caller-backed contexts;
56// the enum deliberately keeps the selected state inline in the arena owner.
57#[allow(clippy::large_enum_variant)]
58enum MatchWorkspace {
59    Uncompressed,
60    Fast(FastMatchState),
61    DFast(DFastMatchState),
62    Greedy {
63        state: GreedyMatchState,
64        depth: LazyBlockStrategy,
65    },
66    Opt {
67        state: OptBlockState,
68        strategy: OptFrameStrategy,
69    },
70}
71
72impl From<crate::workspace::CapacityError> for EncoderWorkspaceError {
73    fn from(error: crate::workspace::CapacityError) -> Self { Self::Capacity(error) }
74}
75
76struct EncoderCore {
77    // Set before any codec mutation and cleared only after complete success.
78    // This also survives caught unwinds: taken scratch cannot silently become
79    // an owned default buffer on a later sensitive operation.
80    poisoned: bool,
81    level: i32,
82    maximum_input: usize,
83    base_cctx: CctxParameters,
84    frame: FrameBlockState,
85    matcher: MatchWorkspace,
86    ldm: Option<LdmWorkspace>,
87}
88
89impl EncoderCore {
90    fn add_size(
91        size: &mut ArenaSize,
92        level: i32,
93        maximum_input: usize,
94    ) -> Result<CctxParameters, EncoderWorkspaceError> {
95        let cctx = CctxParameters::for_level(level, maximum_input as u64, 0);
96        FrameBlockState::add_workspace_size(size).map_err(map_arena)?;
97        let block_size = crate::common::MAX_BLOCK_SIZE as usize;
98        if level == 0 {
99            return Ok(cctx);
100        }
101        match cctx.compression.strategy {
102            Strategy::Fast => {
103                FastMatchState::add_workspace_size(size, cctx.compression, block_size)
104                    .map_err(map_arena)?;
105            }
106            Strategy::DFast => {
107                DFastMatchState::add_workspace_size(size, cctx.compression, block_size)
108                    .map_err(map_arena)?;
109            }
110            Strategy::Greedy | Strategy::Lazy | Strategy::Lazy2 | Strategy::BtLazy2 => {
111                GreedyMatchState::add_workspace_size(size, cctx.compression, block_size)
112                    .map_err(map_arena)?;
113            }
114            Strategy::BtOpt | Strategy::BtUltra | Strategy::BtUltra2 => {
115                OptBlockState::add_workspace_size(size, cctx.compression, block_size)
116                    .map_err(map_arena)?;
117            }
118        }
119        if cctx.ldm.enable_ldm == ParamSwitch::Enable {
120            LdmWorkspace::add_workspace_size(size, cctx.ldm, maximum_input).map_err(map_arena)?;
121        }
122        Ok(cctx)
123    }
124
125    fn new_in(
126        arena: &mut Arena<'_>,
127        level: i32,
128        maximum_input: usize,
129        cctx: CctxParameters,
130    ) -> Result<Self, EncoderWorkspaceError> {
131        let block_size = crate::common::MAX_BLOCK_SIZE as usize;
132        let frame = FrameBlockState::new_in(arena, cctx.compression, cctx.max_block_size)
133            .map_err(map_arena)?;
134        let matcher = if level == 0 {
135            MatchWorkspace::Uncompressed
136        } else {
137            match cctx.compression.strategy {
138                Strategy::Fast => MatchWorkspace::Fast(
139                    FastMatchState::new_in(arena, cctx.compression, block_size)
140                        .map_err(map_arena)?,
141                ),
142                Strategy::DFast => MatchWorkspace::DFast(
143                    DFastMatchState::new_in(arena, cctx.compression, block_size)
144                        .map_err(map_arena)?,
145                ),
146                strategy @ (Strategy::Greedy
147                | Strategy::Lazy
148                | Strategy::Lazy2
149                | Strategy::BtLazy2) => MatchWorkspace::Greedy {
150                    state: GreedyMatchState::new_in(arena, cctx.compression, block_size)
151                        .map_err(map_arena)?,
152                    depth: match strategy {
153                        Strategy::Greedy => LazyBlockStrategy::Greedy,
154                        Strategy::Lazy => LazyBlockStrategy::Lazy,
155                        Strategy::Lazy2 => LazyBlockStrategy::Lazy2,
156                        Strategy::BtLazy2 => LazyBlockStrategy::BtLazy2,
157                        _ => unreachable!(),
158                    },
159                },
160                strategy @ (Strategy::BtOpt | Strategy::BtUltra | Strategy::BtUltra2) => {
161                    MatchWorkspace::Opt {
162                        state: OptBlockState::new_in(arena, cctx.compression, block_size)
163                            .map_err(map_arena)?,
164                        strategy: match strategy {
165                            Strategy::BtOpt => OptFrameStrategy::BtOpt,
166                            Strategy::BtUltra => OptFrameStrategy::BtUltra,
167                            Strategy::BtUltra2 => OptFrameStrategy::BtUltra2,
168                            _ => unreachable!(),
169                        },
170                    }
171                }
172            }
173        };
174        let ldm = if cctx.ldm.enable_ldm == ParamSwitch::Enable {
175            Some(LdmWorkspace::new_in(arena, cctx.ldm, maximum_input).map_err(map_arena)?)
176        } else {
177            None
178        };
179        Ok(Self {
180            poisoned: false,
181            level,
182            maximum_input,
183            base_cctx: cctx,
184            frame,
185            matcher,
186            ldm,
187        })
188    }
189
190    fn encode_into<'output>(
191        &mut self,
192        input: &[u8],
193        output: &'output mut [u8],
194    ) -> Result<&'output [u8], EncoderWorkspaceError> {
195        if self.poisoned { return Err(EncoderWorkspaceError::ContextPoisoned); }
196        if input.len() > self.maximum_input {
197            return Err(EncoderWorkspaceError::InputTooLarge {
198                maximum: self.maximum_input,
199                provided: input.len(),
200            });
201        }
202        let required = checked_output_size(input.len())?;
203        if output.len() < required {
204            return Err(EncoderWorkspaceError::OutputTooSmall {
205                required,
206                provided: output.len(),
207            });
208        }
209        self.poisoned = true;
210        let cctx = CctxParameters::from_compression_parameters(
211            self.level,
212            self.base_cctx.compression,
213            input.len() as u64,
214        );
215        // Target mode uses owned clone-based alternatives. Static encoding
216        // currently never requests it; refuse a future accidental routing change.
217        if cctx.target_c_block_size != 0
218            || !FrameBlockState::supports_workspace_entropy(cctx.compression)
219        {
220            return Err(EncoderWorkspaceError::UnsupportedWorkspaceConfiguration);
221        }
222        // SAFETY: the guarded facade never owns caller storage, including on
223        // early exit. Every growth is admitted before writes by the fixed-capacity
224        // facade; caller storage cannot be reallocated even if sizing is wrong.
225        let reusable = unsafe { ReusableVec::from_static_parts(output.as_mut_ptr(), output.len()) };
226        let (mut output_vec, lease) = reusable.lease_vec();
227        let ldm = &mut self.ldm;
228        match &mut self.matcher {
229            MatchWorkspace::Uncompressed => encode_uncompressed(input, &mut output_vec),
230            MatchWorkspace::Fast(state) => {
231                c_port::workspace_encode_fast(input, cctx, &mut self.frame, state, &mut output_vec)
232            }
233            MatchWorkspace::DFast(state) => {
234                c_port::workspace_encode_dfast(input, cctx, &mut self.frame, state, &mut output_vec)
235            }
236            MatchWorkspace::Greedy { state, depth } => c_port::workspace_encode_greedy(
237                input,
238                cctx,
239                *depth,
240                &mut self.frame,
241                state,
242                &mut output_vec,
243            ),
244            MatchWorkspace::Opt { state, strategy } => c_port::workspace_encode_opt(
245                input,
246                cctx,
247                *strategy,
248                &mut self.frame,
249                state,
250                ldm.as_mut(),
251                &mut output_vec,
252            ),
253        }?;
254        let length = output_vec.len();
255        let reusable = ReusableVec::recover_vec(output_vec, lease);
256        drop(reusable);
257        self.poisoned = false;
258        Ok(&output[..length])
259    }
260}
261
262fn encode_uncompressed(input: &[u8], output: &mut impl crate::workspace::Growable<u8>) -> Result<(), crate::workspace::CapacityError> {
263    use crate::{
264        blocks::block::BlockType,
265        encoding::{block_header::BlockHeader, frame_header::FrameHeader},
266    };
267    output.clear();
268    FrameHeader {
269        frame_content_size: Some(input.len() as u64),
270        single_segment: true,
271        content_checksum: false,
272        dictionary_id: None,
273        window_size: None,
274    }
275    .serialize(output)?;
276    if input.is_empty() {
277        BlockHeader {
278            last_block: true,
279            block_type: BlockType::Raw,
280            block_size: 0,
281        }
282        .serialize(output)?;
283        return Ok(());
284    }
285    let block_size = crate::common::MAX_BLOCK_SIZE as usize;
286    for (index, block) in input.chunks(block_size).enumerate() {
287        BlockHeader {
288            last_block: (index + 1) * block_size >= input.len(),
289            block_type: BlockType::Raw,
290            block_size: block.len() as u32,
291        }
292        .serialize(output)?;
293        output.extend_from_slice(block)?;
294    }
295    Ok(())
296}
297
298/// An owned compression workspace reusable without further allocation.
299pub struct EncoderWorkspace {
300    core: EncoderCore,
301    storage: Vec<MaybeUninit<u8>>,
302}
303
304impl EncoderWorkspace {
305    pub fn new(
306        level: CompressionLevel,
307        maximum_input: usize,
308    ) -> Result<Self, EncoderWorkspaceError> {
309        let required = Self::required_size(level, maximum_input)?;
310        let mut storage = Vec::with_capacity(required);
311        storage.resize_with(required, MaybeUninit::uninit);
312        let mut arena = Arena::new(&mut storage);
313        let mut size = ArenaSize::new();
314        let cctx = EncoderCore::add_size(&mut size, level.get(), maximum_input)?;
315        let core = EncoderCore::new_in(&mut arena, level.get(), maximum_input, cctx)?;
316        Ok(Self { core, storage })
317    }
318
319    pub fn required_size(
320        level: CompressionLevel,
321        maximum_input: usize,
322    ) -> Result<usize, EncoderWorkspaceError> {
323        let mut size = ArenaSize::new();
324        EncoderCore::add_size(&mut size, level.get(), maximum_input)?;
325        Ok(size.finish())
326    }
327
328    pub fn required_output_size(maximum_input: usize) -> Result<usize, EncoderWorkspaceError> {
329        checked_output_size(maximum_input)
330    }
331
332    pub fn encode_into<'output>(
333        &mut self,
334        input: &[u8],
335        output: &'output mut [u8],
336    ) -> Result<&'output [u8], EncoderWorkspaceError> {
337        self.core.encode_into(input, output)
338    }
339
340    pub fn workspace_bytes(&self) -> usize {
341        self.storage.len()
342    }
343
344    pub const fn maximum_input(&self) -> usize {
345        self.core.maximum_input
346    }
347}
348
349#[cfg(feature = "std")]
350impl std::error::Error for EncoderWorkspaceError {}
351
352/// A compression context whose working state lives entirely in caller storage.
353pub struct StaticEncoderWorkspace<'storage> {
354    core: EncoderCore,
355    workspace_bytes: usize,
356    marker: PhantomData<&'storage mut [MaybeUninit<u8>]>,
357}
358
359impl<'storage> StaticEncoderWorkspace<'storage> {
360    pub fn new(
361        storage: &'storage mut [u8],
362        level: CompressionLevel,
363        maximum_input: usize,
364    ) -> Result<Self, EncoderWorkspaceError> {
365        Self::new_uninit(initialized_bytes_as_uninit(storage), level, maximum_input)
366    }
367
368    /// Constructs a workspace without initializing caller-provided bytes.
369    pub fn new_uninit(
370        storage: &'storage mut [MaybeUninit<u8>],
371        level: CompressionLevel,
372        maximum_input: usize,
373    ) -> Result<Self, EncoderWorkspaceError> {
374        let required = EncoderWorkspace::required_size(level, maximum_input)?;
375        if storage.len() < required {
376            return Err(EncoderWorkspaceError::InsufficientWorkspace {
377                required,
378                provided: storage.len(),
379            });
380        }
381        let mut size = ArenaSize::new();
382        let cctx = EncoderCore::add_size(&mut size, level.get(), maximum_input)?;
383        let mut arena = Arena::new(storage);
384        let core = EncoderCore::new_in(&mut arena, level.get(), maximum_input, cctx)?;
385        Ok(Self {
386            core,
387            workspace_bytes: storage.len(),
388            marker: PhantomData,
389        })
390    }
391
392    pub fn encode_into<'output>(
393        &mut self,
394        input: &[u8],
395        output: &'output mut [u8],
396    ) -> Result<&'output [u8], EncoderWorkspaceError> {
397        self.core.encode_into(input, output)
398    }
399
400    pub const fn maximum_input(&self) -> usize {
401        self.core.maximum_input
402    }
403
404    pub const fn workspace_bytes(&self) -> usize {
405        self.workspace_bytes
406    }
407}
408
409fn initialized_bytes_as_uninit(storage: &mut [u8]) -> &mut [MaybeUninit<u8>] {
410    // SAFETY: `MaybeUninit<u8>` has the same layout as `u8`, and treating
411    // initialized bytes as possibly uninitialized only weakens the invariant.
412    unsafe { core::slice::from_raw_parts_mut(storage.as_mut_ptr().cast(), storage.len()) }
413}
414
415fn map_arena(error: ArenaError) -> EncoderWorkspaceError {
416    match error {
417        ArenaError::Capacity(error) => EncoderWorkspaceError::Capacity(error),
418        ArenaError::InsufficientStorage { required, provided } => {
419            EncoderWorkspaceError::InsufficientWorkspace { required, provided }
420        }
421        ArenaError::CapacityExceeded { .. } | ArenaError::SizeOverflow => {
422            EncoderWorkspaceError::SizeOverflow
423        }
424    }
425}
426
427fn checked_output_size(input_size: usize) -> Result<usize, EncoderWorkspaceError> {
428    let bound = c_port::workspace_compress_bound(input_size);
429    if bound == 0 {
430        Err(EncoderWorkspaceError::SizeOverflow)
431    } else {
432        Ok(bound)
433    }
434}