Skip to main content

qubit_codec/transcode/engine/
transcode_decode_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 decoder engine.
9
10use core::num::NonZeroUsize;
11
12use super::super::internal::{
13    decode_state::DecodeState,
14    decode_step::DecodeStep,
15};
16use crate::codec::assert_unit_bounds;
17use crate::{
18    CapacityError,
19    Codec,
20    DecodeAction,
21    DecodeContext,
22    TranscodeDecodeHooks,
23    TranscodeError,
24    TranscodeProgress,
25    Transcoder,
26};
27
28/// Reusable buffered decoding engine for codec-backed decoders.
29///
30/// The engine owns the low-level codec and hook object. It keeps the common
31/// buffered decoding loop private: input-index validation, output-capacity
32/// checks, calls to [`Codec::decode`], hook dispatch, and
33/// [`crate::TranscodeStatus`] reporting. Incomplete input tails are left in the
34/// caller-provided input slice; callers own input-buffer refill.
35///
36/// Use this type to build a streaming decoder over a one-value [`Codec`]. The
37/// engine decodes into a caller-provided output slice and returns
38/// [`TranscodeProgress`] instead of allocating. On success it writes decoded
39/// values directly to output. On codec errors it delegates to
40/// [`crate::TranscodeDecodeHooks`], allowing a policy to request more input,
41/// skip invalid units, emit a replacement value, or fail.
42///
43/// The engine stops before reading an incomplete value when fewer than
44/// [`Codec::min_units_per_value`] units are available. For variable-width
45/// codecs, the codec may still return an incomplete decode error after that
46/// minimum is satisfied; hooks should convert that error into
47/// [`crate::DecodeAction::NeedInput`] when the stream may continue.
48///
49/// For strict decoding that wraps codec errors, use
50/// [`crate::CodecTranscodeDecoder`]. Use `TranscodeDecodeEngine` directly when
51/// invalid input should be repaired, skipped, counted, or otherwise handled by
52/// policy.
53///
54/// # Example
55///
56/// ```rust
57/// use core::num::NonZeroUsize;
58/// use qubit_codec::{
59///     Codec,
60///     CodecDecodeError,
61///     DecodeAction,
62///     DecodeContext,
63///     TranscodeStatus,
64///     TranscodeDecodeEngine,
65///     TranscodeDecodeHooks,
66/// };
67///
68/// #[derive(Clone, Copy)]
69/// struct ByteCodec;
70///
71/// #[derive(Clone, Copy, Debug, Eq, PartialEq)]
72/// enum ByteDecodeError {
73///     Malformed { consumed: NonZeroUsize },
74/// }
75///
76/// unsafe impl Codec for ByteCodec {
77///     type Value = u8;
78///     type Unit = u8;
79///     type DecodeError = ByteDecodeError;
80///     type EncodeError = core::convert::Infallible;
81///
82///     fn min_units_per_value(&self) -> NonZeroUsize {
83///         NonZeroUsize::MIN
84///     }
85///
86///     fn max_units_per_value(&self) -> NonZeroUsize {
87///         NonZeroUsize::MIN
88///     }
89///
90///     unsafe fn decode(
91///         &mut self,
92///         input: &[u8],
93///         index: usize,
94///     ) -> Result<(u8, NonZeroUsize), Self::DecodeError> {
95///         match input[index] {
96///             0xff => Err(ByteDecodeError::Malformed {
97///                 consumed: NonZeroUsize::MIN,
98///             }),
99///             value => Ok((value, NonZeroUsize::MIN)),
100///         }
101///     }
102///
103///     unsafe fn encode(
104///         &mut self,
105///         value: &u8,
106///         output: &mut [u8],
107///         index: usize,
108///     ) -> Result<NonZeroUsize, Self::EncodeError> {
109///         output[index] = *value;
110///         Ok(NonZeroUsize::MIN)
111///     }
112/// }
113///
114/// struct ReplacementHooks;
115///
116/// impl TranscodeDecodeHooks<ByteCodec> for ReplacementHooks {
117///     type Error = CodecDecodeError<ByteDecodeError>;
118///
119///     fn handle_decode_error(
120///         &mut self,
121///         _codec: &mut ByteCodec,
122///         error: ByteDecodeError,
123///         _context: DecodeContext,
124///     ) -> Result<DecodeAction<u8>, Self::Error> {
125///         match error {
126///             ByteDecodeError::Malformed { consumed } => {
127///                 Ok(DecodeAction::Emit { value: b'?', consumed })
128///             }
129///         }
130///     }
131/// }
132///
133/// let mut engine = TranscodeDecodeEngine::<_, _>::new(ByteCodec, ReplacementHooks);
134/// let input = [b'a', 0xff, b'b'];
135/// let mut output = [0_u8; 3];
136///
137/// let progress = engine.transcode(&input, 0, &mut output, 0)?;
138/// match progress.status() {
139///     TranscodeStatus::Complete => assert_eq!(&output[..progress.written()], b"a?b"),
140///     TranscodeStatus::NeedInput { input_index, .. } => {
141///         // Keep `input[input_index..]`, append more source units, and resume.
142///     }
143///     TranscodeStatus::NeedOutput { output_index, .. } => {
144///         // Drain `output[..output_index]`, then resume with more output room.
145///     }
146/// }
147/// # Ok::<(), qubit_codec::TranscodeError<CodecDecodeError<ByteDecodeError>>>(())
148/// ```
149///
150/// # Type Parameters
151///
152/// - `C`: Low-level codec used by the engine.
153/// - `H`: Policy hook object used by the engine.
154#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
155pub struct TranscodeDecodeEngine<C, H> {
156    /// Low-level codec used for one-value decoding.
157    pub(super) codec: C,
158    /// Policy hooks used for decode failures.
159    pub(super) hooks: H,
160}
161
162impl<C, H> TranscodeDecodeEngine<C, H>
163where
164    C: Codec,
165    H: TranscodeDecodeHooks<C>,
166{
167    /// Creates a buffered decoder engine.
168    ///
169    /// # Parameters
170    ///
171    /// - `codec`: Low-level codec used for one-value decoding.
172    /// - `hooks`: Policy hooks used for decode failures.
173    ///
174    /// # Returns
175    ///
176    /// Returns a buffered decoder engine.
177    ///
178    /// # Panics
179    ///
180    /// Panics when the supplied codec violates the
181    /// [`Codec::min_units_per_value`] / [`Codec::max_units_per_value`] ordering
182    /// invariant.
183    #[must_use]
184    #[inline]
185    pub fn new(codec: C, hooks: H) -> Self {
186        assert_unit_bounds::<C>(&codec);
187        Self { codec, hooks }
188    }
189
190    /// Returns an upper bound for decoded values produced from `input_len`
191    /// units.
192    ///
193    /// # Parameters
194    ///
195    /// - `input_len`: Number of source units the caller plans to decode.
196    ///
197    /// # Returns
198    ///
199    /// Returns a conservative upper bound, or a capacity error on arithmetic
200    /// overflow.
201    #[must_use = "capacity planning can fail on overflow"]
202    #[inline(always)]
203    pub fn max_output_len(
204        &self,
205        input_len: usize,
206    ) -> Result<usize, CapacityError> {
207        self.hooks.max_output_len(&self.codec, input_len)
208    }
209
210    /// Returns the maximum values emitted by flushing codec state and finishing
211    /// hook-owned state.
212    ///
213    /// # Returns
214    ///
215    /// Returns the sum of [`Codec::max_decode_flush_values`] and the
216    /// hook-provided final-output bound. The codec flush portion covers values
217    /// written by [`Codec::decode_flush`]; hook implementations must not
218    /// include that portion in
219    /// [`TranscodeDecodeHooks::max_finish_output_len`].
220    #[must_use = "capacity planning can fail on overflow"]
221    #[inline(always)]
222    pub fn max_finish_output_len(&self) -> Result<usize, CapacityError> {
223        self.codec
224            .max_decode_flush_values()
225            .checked_add(self.hooks.max_finish_output_len(&self.codec))
226            .ok_or(CapacityError::OutputLengthOverflow)
227    }
228
229    /// Returns the maximum values emitted when resetting stream state.
230    ///
231    /// Decoders do not emit reset output; this bound is always `0`.
232    #[must_use]
233    #[inline(always)]
234    pub fn max_reset_output_len(&self) -> usize {
235        0
236    }
237
238    /// Resets codec decode state and hook-owned state.
239    ///
240    /// # Parameters
241    ///
242    /// - `output`: Complete output value slice visible to the decoder.
243    /// - `output_index`: Absolute output value index where writing would start.
244    ///
245    /// # Returns
246    ///
247    /// Returns `0` because decoders do not emit stream-start output on reset.
248    ///
249    /// # Errors
250    ///
251    /// Returns hook errors when `output_index` is invalid or reset fails.
252    #[inline(always)]
253    pub fn reset(
254        &mut self,
255        output: &mut [C::Value],
256        output_index: usize,
257    ) -> Result<usize, TranscodeError<H::Error>> {
258        TranscodeError::ensure_output_index(output.len(), output_index)?;
259        self.hooks
260            .reset(&mut self.codec)
261            .map_err(TranscodeError::domain)?;
262        Ok(0)
263    }
264
265    /// Decodes source units into caller-provided output values.
266    ///
267    /// # Parameters
268    ///
269    /// - `input`: Complete input unit slice visible to the decoder.
270    /// - `input_index`: Absolute input unit index where decoding starts.
271    /// - `output`: Complete output value slice visible to the decoder.
272    /// - `output_index`: Absolute output value index where writing starts.
273    ///
274    /// # Returns
275    ///
276    /// Returns progress describing input units consumed, output values written,
277    /// and why decoding stopped.
278    ///
279    /// # Errors
280    ///
281    /// Returns hook errors when `input_index` is outside `input`, when
282    /// `output_index` is outside `output`, or when a concrete policy hook
283    /// rejects a value.
284    pub fn transcode(
285        &mut self,
286        input: &[C::Unit],
287        input_index: usize,
288        output: &mut [C::Value],
289        output_index: usize,
290    ) -> Result<TranscodeProgress, TranscodeError<H::Error>> {
291        TranscodeError::ensure_transcode_indices(
292            input.len(),
293            input_index,
294            output.len(),
295            output_index,
296        )?;
297
298        let min_units = self.codec.min_units_per_value().get();
299        let mut state =
300            DecodeState::new(input, input_index, output, output_index);
301        while state.has_input() {
302            let context = state.context();
303            if context.available() < min_units {
304                let additional = qubit_io::nz!(min_units - context.available());
305                return Ok(state.need_input_progress_with(
306                    additional,
307                    context.available(),
308                ));
309            }
310            if state.needs_output() {
311                return Ok(state.need_output_progress());
312            }
313            let step = self.decode_step(state.input(), context)?;
314            if let Some(progress) = step.apply_to_decode_state(&mut state) {
315                return Ok(progress);
316            }
317        }
318
319        Ok(state.complete_progress())
320    }
321
322    /// Finishes codec and hook-owned output after EOF.
323    ///
324    /// Finalization first flushes decode-side codec state through
325    /// [`Codec::decode_flush`], then lets hook implementations finish their
326    /// own retained state. The caller must provide enough output capacity for
327    /// [`TranscodeDecodeEngine::max_finish_output_len`], which includes both
328    /// the codec flush bound and the hook-owned finish bound.
329    ///
330    /// # Parameters
331    ///
332    /// - `output`: Complete output value slice visible to the decoder.
333    /// - `output_index`: Absolute output value index where writing starts.
334    ///
335    /// # Returns
336    ///
337    /// Returns the number of values written by finalization.
338    ///
339    /// # Errors
340    ///
341    /// Returns hook errors when the caller provides invalid or insufficient
342    /// output capacity, when codec flush errors are mapped by the hooks, or
343    /// when hook finalization fails.
344    ///
345    /// # Panics
346    ///
347    /// Panics when the codec flush writes beyond
348    /// [`Codec::max_decode_flush_values`] or when the combined codec and hook
349    /// finalization writes beyond
350    /// [`TranscodeDecodeEngine::max_finish_output_len`].
351    pub fn finish(
352        &mut self,
353        output: &mut [C::Value],
354        output_index: usize,
355    ) -> Result<usize, TranscodeError<H::Error>> {
356        let required = self
357            .max_finish_output_len()
358            .map_err(|_| TranscodeError::OutputLengthOverflow)?;
359        TranscodeError::ensure_output_capacity(
360            output.len(),
361            output_index,
362            required,
363        )?;
364        let flushed = unsafe { self.codec.decode_flush(output, output_index) }
365            .map_err(|error| {
366                TranscodeError::domain(
367                    self.hooks.map_decode_flush_error(&mut self.codec, error),
368                )
369            })?;
370        assert!(
371            flushed <= self.codec.max_decode_flush_values(),
372            "Codec::decode_flush wrote beyond its flush bound",
373        );
374        let written = self
375            .hooks
376            .finish(&mut self.codec, output, output_index + flushed)
377            .map_err(TranscodeError::domain)?;
378        assert!(
379            flushed + written <= required,
380            "TranscodeDecodeEngine hook wrote beyond its finish bound",
381        );
382        Ok(flushed + written)
383    }
384
385    /// Decodes one value at a caller-proven readable input cursor.
386    ///
387    /// # Safety
388    ///
389    /// The caller must guarantee that at least `codec.min_units_per_value()`
390    /// units are readable from `input_index`.
391    #[inline(always)]
392    pub(crate) unsafe fn decode_at(
393        &mut self,
394        input: &[C::Unit],
395        input_index: usize,
396    ) -> Result<(C::Value, NonZeroUsize), C::DecodeError> {
397        // SAFETY: Forwarded from this method's safety contract.
398        unsafe { self.codec.decode(input, input_index) }
399    }
400
401    /// Lets the configured decode hooks classify a low-level decode error.
402    ///
403    /// # Parameters
404    ///
405    /// - `error`: Decode error returned by [`Codec::decode`].
406    /// - `context`: Decode context used by policy hooks.
407    ///
408    /// # Returns
409    ///
410    /// Returns the decoded action chosen by policy hooks.
411    ///
412    /// # Errors
413    ///
414    /// Returns a hook-level error when the decode policy rejects the value.
415    #[inline(always)]
416    pub(crate) fn handle_decode_error(
417        &mut self,
418        error: C::DecodeError,
419        context: DecodeContext,
420    ) -> Result<DecodeAction<C::Value>, H::Error> {
421        self.hooks
422            .handle_decode_error(&mut self.codec, error, context)
423    }
424
425    /// Decodes one source value attempt into a normalized decode step.
426    ///
427    /// # Parameters
428    ///
429    /// - `input`: Complete input unit slice visible to the caller.
430    /// - `context`: Decode context describing the current source and output
431    ///   cursors.
432    ///
433    /// # Returns
434    ///
435    /// Returns one internal decode step, including successful decode, policy
436    /// skip/emit, or need-input state.
437    ///
438    /// # Errors
439    ///
440    /// Returns hook errors when the decode policy rejects the input.
441    #[inline]
442    pub(super) fn decode_step(
443        &mut self,
444        input: &[C::Unit],
445        context: DecodeContext,
446    ) -> Result<DecodeStep<C::Value>, TranscodeError<H::Error>> {
447        let min_units = self.codec.min_units_per_value().get();
448        if context.available() < min_units {
449            let additional = qubit_io::nz!(min_units - context.available());
450            return Ok(DecodeStep::need_input(additional, context.available()));
451        }
452
453        // SAFETY: The context reports at least `min_units_per_value()` source
454        // units available from `context.input_index()`.
455        let result = unsafe { self.decode_at(input, context.input_index()) };
456        self.handle_decode_result(context, result)
457    }
458
459    /// Handles one low-level decode result and returns a normalized decode
460    /// step.
461    ///
462    /// # Parameters
463    ///
464    /// - `context`: Decode context used by policy hooks.
465    /// - `result`: Low-level codec decode result.
466    ///
467    /// # Returns
468    ///
469    /// Returns the normalized decode step selected by codec success or policy
470    /// hooks.
471    ///
472    /// # Errors
473    ///
474    /// Returns hook errors when the policy rejects the input.
475    #[inline]
476    fn handle_decode_result(
477        &mut self,
478        context: DecodeContext,
479        result: Result<(C::Value, NonZeroUsize), C::DecodeError>,
480    ) -> Result<DecodeStep<C::Value>, TranscodeError<H::Error>> {
481        match result {
482            Ok((value, consumed)) => {
483                assert!(
484                    consumed.get() <= context.available(),
485                    "Codec::decode consumed beyond available input",
486                );
487                Ok(DecodeStep::decoded(value, consumed, context.input_index()))
488            }
489            Err(error) => {
490                let action = self
491                    .handle_decode_error(error, context)
492                    .map_err(TranscodeError::domain)?;
493                Ok(action.into_step(context.input_index(), context.available()))
494            }
495        }
496    }
497}
498
499impl<C, H> Transcoder<C::Unit, C::Value> for TranscodeDecodeEngine<C, H>
500where
501    C: Codec,
502    H: TranscodeDecodeHooks<C>,
503{
504    type Error = H::Error;
505
506    /// Returns an upper bound for decoded values produced from `input_len`
507    /// units.
508    #[inline(always)]
509    fn max_output_len(&self, input_len: usize) -> Result<usize, CapacityError> {
510        TranscodeDecodeEngine::max_output_len(self, input_len)
511    }
512
513    /// Returns an upper bound for values produced by finishing codec and hook
514    /// state.
515    #[inline(always)]
516    fn max_finish_output_len(&self) -> Result<usize, CapacityError> {
517        TranscodeDecodeEngine::max_finish_output_len(self)
518    }
519
520    /// Returns an upper bound for values emitted when resetting stream state.
521    #[inline(always)]
522    fn max_reset_output_len(&self) -> Result<usize, CapacityError> {
523        Ok(TranscodeDecodeEngine::max_reset_output_len(self))
524    }
525
526    /// Resets codec decode state and hook-owned state.
527    #[inline(always)]
528    fn reset(
529        &mut self,
530        output: &mut [C::Value],
531        output_index: usize,
532    ) -> Result<usize, TranscodeError<Self::Error>> {
533        TranscodeDecodeEngine::reset(self, output, output_index)
534    }
535
536    /// Decodes source units into logical values.
537    #[inline(always)]
538    fn transcode(
539        &mut self,
540        input: &[C::Unit],
541        input_index: usize,
542        output: &mut [C::Value],
543        output_index: usize,
544    ) -> core::result::Result<TranscodeProgress, TranscodeError<Self::Error>>
545    {
546        TranscodeDecodeEngine::transcode(
547            self,
548            input,
549            input_index,
550            output,
551            output_index,
552        )
553    }
554
555    /// Finishes internally retained output after EOF.
556    #[inline(always)]
557    fn finish(
558        &mut self,
559        output: &mut [C::Value],
560        output_index: usize,
561    ) -> Result<usize, TranscodeError<Self::Error>> {
562        TranscodeDecodeEngine::finish(self, output, output_index)
563    }
564}