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