Skip to main content

qubit_codec/transcode/engine/
transcode_encode_engine.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Reusable buffered encoder engine.
9
10use super::super::internal::{
11    encode_state::EncodeState,
12    encode_step::EncodeStep,
13};
14use crate::codec::assert_unit_bounds;
15use crate::{
16    CapacityError,
17    Codec,
18    EncodeContext,
19    TranscodeEncodeHooks,
20    TranscodeError,
21    TranscodeProgress,
22};
23
24/// Reusable buffered encoding engine for codec-backed encoders.
25///
26/// The engine owns the low-level codec and hook object. It keeps the common
27/// buffered encoding loop private: input-index validation, output-capacity
28/// checks, input consumption, output progress, and [`crate::TranscodeStatus`]
29/// reporting.
30///
31/// Use this type to build a streaming encoder over a one-value [`Codec`]. The
32/// engine does not allocate output. It repeatedly asks hooks to plan one input
33/// value, verifies that the caller-provided output slice can hold that plan,
34/// and then lets the hooks write the value. If the next value would not fit,
35/// the engine returns [`crate::TranscodeStatus::NeedOutput`] without consuming
36/// that value; the caller can provide a larger or fresh output buffer and
37/// resume with the returned input index.
38///
39/// For the common strict policy that simply wraps codec errors, use
40/// [`crate::CodecTranscodeEncoder`]. Use `TranscodeEncodeEngine` directly when
41/// the encode policy needs custom planning, replacement, skipped values, or
42/// finish-time output.
43///
44/// # Example
45///
46/// ```rust
47/// use core::{
48///     convert::Infallible,
49///     num::NonZeroUsize,
50/// };
51/// use qubit_codec::{
52///     TranscodeEncodeEngine,
53///     TranscodeEncodeHooks,
54///     Codec,
55///     CodecEncodeError,
56///     EncodeContext,
57///     EncodePlan,
58///     TranscodeStatus,
59/// };
60///
61/// #[derive(Clone, Copy)]
62/// struct ByteCodec;
63///
64/// unsafe impl Codec for ByteCodec {
65///     type Value = u8;
66///     type Unit = u8;
67///     type DecodeError = Infallible;
68///     type EncodeError = Infallible;
69///
70///     fn min_units_per_value(&self) -> NonZeroUsize {
71///         NonZeroUsize::MIN
72///     }
73///
74///     fn max_units_per_value(&self) -> NonZeroUsize {
75///         NonZeroUsize::MIN
76///     }
77///
78///     unsafe fn decode(
79///         &mut self,
80///         input: &[u8],
81///         index: usize,
82///     ) -> Result<(u8, NonZeroUsize), Self::DecodeError> {
83///         Ok((input[index], NonZeroUsize::MIN))
84///     }
85///
86///     unsafe fn encode(
87///         &mut self,
88///         value: &u8,
89///         output: &mut [u8],
90///         index: usize,
91///     ) -> Result<NonZeroUsize, Self::EncodeError> {
92///         output[index] = *value;
93///         Ok(NonZeroUsize::MIN)
94///     }
95/// }
96///
97/// struct StrictHooks;
98///
99/// impl TranscodeEncodeHooks<ByteCodec> for StrictHooks {
100///     type Error = CodecEncodeError<Infallible>;
101///     type PlanAction = ();
102///
103///     fn prepare_encode(
104///         &mut self,
105///         codec: &mut ByteCodec,
106///         _value: &u8,
107///         _input_index: usize,
108///     ) -> Result<EncodePlan<()>, Self::Error> {
109///         Ok(EncodePlan::new(codec.max_units_per_value().get(), ()))
110///     }
111///
112///     unsafe fn write_encode(
113///         &mut self,
114///         codec: &mut ByteCodec,
115///         context: EncodeContext<'_, u8, u8>,
116///         _plan: EncodePlan<()>,
117///     ) -> Result<usize, Self::Error> {
118///         unsafe {
119///             codec.encode(context.input_value, context.output, context.output_index)
120///         }
121///         .map(NonZeroUsize::get)
122///         .map_err(|error| CodecEncodeError::encode(error, context.input_index))
123///     }
124/// }
125///
126/// let mut engine = TranscodeEncodeEngine::new(ByteCodec, StrictHooks);
127/// let input = [1_u8, 2, 3];
128/// let mut output = [0_u8; 2];
129///
130/// let progress = engine.transcode(&input, 0, &mut output, 0)?;
131/// match progress.status() {
132///     TranscodeStatus::Complete => unreachable!("output is intentionally short"),
133///     TranscodeStatus::NeedOutput { output_index, .. } => {
134///         assert_eq!(2, output_index);
135///         assert_eq!([1, 2], output);
136///         // Write out `output[..output_index]`, then resume at
137///         // `progress.read()` with fresh output capacity.
138///     }
139///     TranscodeStatus::NeedInput { .. } => unreachable!("encoders do not read encoded input"),
140/// }
141/// # Ok::<(), qubit_codec::TranscodeError<CodecEncodeError<Infallible>>>(())
142/// ```
143///
144/// # Type Parameters
145///
146/// - `C`: Low-level codec used by the engine.
147/// - `H`: Policy hook object used by the engine.
148#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
149pub struct TranscodeEncodeEngine<C, H> {
150    /// Low-level codec used for one-value encoding.
151    pub(super) codec: C,
152    /// Policy hooks used for planning and writing values.
153    pub(super) hooks: H,
154}
155
156impl<C, H> TranscodeEncodeEngine<C, H>
157where
158    C: Codec,
159    H: TranscodeEncodeHooks<C>,
160{
161    /// Creates a buffered encoder engine.
162    ///
163    /// # Parameters
164    ///
165    /// - `codec`: Low-level codec used for one-value encoding.
166    /// - `hooks`: Policy hooks used for planning and writing values.
167    ///
168    /// # Returns
169    ///
170    /// Returns a buffered encoder engine.
171    ///
172    /// # Panics
173    ///
174    /// Panics when the supplied codec violates the
175    /// [`Codec::min_units_per_value`] / [`Codec::max_units_per_value`] ordering
176    /// invariant.
177    #[must_use]
178    #[inline]
179    pub fn new(codec: C, hooks: H) -> Self {
180        assert_unit_bounds::<C>(&codec);
181        Self { codec, hooks }
182    }
183
184    /// Gets a conservative upper bound for output units needed for
185    /// `input_len` values.
186    ///
187    /// # Parameters
188    ///
189    /// - `input_len`: Number of input values the caller plans to encode.
190    ///
191    /// # Returns
192    ///
193    /// a conservative upper bound for output units, or a capacity error on
194    /// arithmetic overflow.
195    #[must_use = "capacity planning can fail on overflow"]
196    #[inline(always)]
197    pub fn max_output_len(
198        &self,
199        input_len: usize,
200    ) -> Result<usize, CapacityError> {
201        self.hooks.max_output_len(&self.codec, input_len)
202    }
203
204    /// Gets the maximum output units emitted by stream reset.
205    ///
206    /// # Returns
207    ///
208    /// the codec's reset-output upper bound.
209    #[must_use]
210    #[inline(always)]
211    pub fn max_reset_output_len(&self) -> usize {
212        self.codec.max_encode_reset_units()
213    }
214
215    /// Gets the maximum output units emitted by finishing hook-owned state.
216    ///
217    /// # Returns
218    ///
219    /// the hook-provided final output bound.
220    #[must_use]
221    #[inline(always)]
222    pub fn max_finish_output_len(&self) -> usize {
223        self.hooks.max_finish_output_len(&self.codec)
224    }
225
226    /// Resets codec encode state, hook-owned state, and stream-start output.
227    ///
228    /// # Parameters
229    ///
230    /// - `output`: Complete output unit slice visible to the encoder.
231    /// - `output_index`: Absolute output unit index where writing starts.
232    ///
233    /// # Returns
234    ///
235    /// Returns the number of reset units written.
236    ///
237    /// # Errors
238    ///
239    /// Returns hook errors when the caller provides invalid or insufficient
240    /// output capacity, or when reset output cannot be emitted.
241    pub fn reset(
242        &mut self,
243        output: &mut [C::Unit],
244        output_index: usize,
245    ) -> Result<usize, TranscodeError<H::Error>> {
246        let required = self.max_reset_output_len();
247        TranscodeError::ensure_output_capacity(
248            output.len(),
249            output_index,
250            required,
251        )?;
252        self.hooks.reset(&mut self.codec);
253        let written = unsafe {
254            // SAFETY: The capacity check above reserves the codec's declared
255            // reset-output bound at `output_index`.
256            self.hooks
257                .write_encode_reset(&mut self.codec, output, output_index)
258        }
259        .map_err(TranscodeError::domain)?;
260        assert!(
261            written <= required,
262            "Codec::encode_reset wrote beyond its reset bound",
263        );
264        Ok(written)
265    }
266
267    /// Encodes values into a caller-provided output buffer.
268    ///
269    /// The engine stops before consuming the next input value when the current
270    /// output buffer does not satisfy that value's planned capacity bound.
271    ///
272    /// # Parameters
273    ///
274    /// - `input`: Complete input value slice visible to the encoder.
275    /// - `input_index`: Absolute input value index where encoding starts.
276    /// - `output`: Complete output unit slice visible to the encoder.
277    /// - `output_index`: Absolute output unit index where writing starts.
278    ///
279    /// # Returns
280    ///
281    /// Returns progress describing input values consumed, output units written,
282    /// and why encoding stopped.
283    ///
284    /// # Errors
285    ///
286    /// Returns hook errors when `input_index` is outside `input`, when
287    /// `output_index` is outside `output`, or when hook planning or writing
288    /// rejects a value.
289    pub fn transcode(
290        &mut self,
291        input: &[C::Value],
292        input_index: usize,
293        output: &mut [C::Unit],
294        output_index: usize,
295    ) -> Result<TranscodeProgress, TranscodeError<H::Error>> {
296        TranscodeError::ensure_transcode_indices(
297            input.len(),
298            input_index,
299            output.len(),
300            output_index,
301        )?;
302        let mut state =
303            EncodeState::new(input, input_index, output, output_index);
304
305        while state.has_input() {
306            // SAFETY: The loop condition proves that the current input cursor
307            // points at an available value.
308            let context = unsafe { state.context_unchecked() };
309            let step = self.encode_step(context)?;
310            if let Some(progress) = step.apply_to_state(&mut state) {
311                return Ok(progress);
312            }
313        }
314
315        Ok(state.complete_progress())
316    }
317
318    /// Finishes hook-owned output after EOF.
319    ///
320    /// The engine owns no final output state itself. Hook implementations may
321    /// finish their own retained state and emit final output after the caller
322    /// has supplied all input values. The caller must provide enough output
323    /// capacity for [`TranscodeEncodeEngine::max_finish_output_len`].
324    ///
325    /// # Parameters
326    ///
327    /// - `output`: Complete output unit slice visible to the encoder.
328    /// - `output_index`: Absolute output unit index where writing starts.
329    ///
330    /// # Returns
331    ///
332    /// Returns the number of units written by finalization.
333    ///
334    /// # Errors
335    ///
336    /// Returns hook errors when the caller provides invalid or insufficient
337    /// output capacity, or when hook finalization fails.
338    ///
339    /// # Panics
340    ///
341    /// Panics when the hook writes or reports more final output units than
342    /// [`TranscodeEncodeEngine::max_finish_output_len`] declared.
343    pub fn finish(
344        &mut self,
345        output: &mut [C::Unit],
346        output_index: usize,
347    ) -> Result<usize, TranscodeError<H::Error>> {
348        let required = self.max_finish_output_len();
349        TranscodeError::ensure_output_capacity(
350            output.len(),
351            output_index,
352            required,
353        )?;
354        let written = self
355            .hooks
356            .finish(&mut self.codec, output, output_index)
357            .map_err(TranscodeError::domain)?;
358        assert!(
359            written <= required,
360            "TranscodeEncodeEngine hook wrote beyond its finish bound",
361        );
362        Ok(written)
363    }
364
365    /// Encodes one value attempt into a normalized encode step.
366    ///
367    /// # Parameters
368    ///
369    /// - `context`: Current value, absolute input index, and target output
370    ///   cursor.
371    ///
372    /// # Returns
373    ///
374    /// Returns an encode step describing either written output or missing
375    /// output capacity.
376    ///
377    /// # Errors
378    ///
379    /// Returns hook errors when planning or writing rejects the value.
380    pub(super) fn encode_step(
381        &mut self,
382        context: EncodeContext<'_, C::Value, C::Unit>,
383    ) -> Result<EncodeStep, TranscodeError<H::Error>> {
384        let plan = self
385            .hooks
386            .prepare_encode(
387                &mut self.codec,
388                context.input_value,
389                context.input_index,
390            )
391            .map_err(TranscodeError::domain)?;
392        let max_output_units = plan.max_output_units;
393        let available = context.available_output();
394        if available < max_output_units {
395            return Ok(EncodeStep::need_output(max_output_units, available));
396        }
397
398        // SAFETY: The capacity check above guarantees the bound requested by
399        // the prepared plan.
400        let written = match unsafe {
401            self.hooks.write_encode(&mut self.codec, context, plan)
402        } {
403            Ok(written) => written,
404            Err(error) => return Err(TranscodeError::domain(error)),
405        };
406        assert!(
407            written <= max_output_units,
408            "TranscodeEncodeEngine hook wrote beyond its prepared capacity bound",
409        );
410        Ok(EncodeStep::written(written))
411    }
412}
413
414impl<C, H> Default for TranscodeEncodeEngine<C, H>
415where
416    C: Codec + Default,
417    H: TranscodeEncodeHooks<C> + Default,
418{
419    /// Creates a default buffered encoder engine.
420    ///
421    /// # Returns
422    ///
423    /// Returns an engine with default codec and hooks.
424    #[inline(always)]
425    fn default() -> Self {
426        Self::new(C::default(), H::default())
427    }
428}