Skip to main content

qubit_codec/transcode/engine/
transcode_decode_hooks.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//! Policy hooks used by buffered decoder engines.
9
10use core::num::NonZeroUsize;
11
12use super::{
13    decode_context::DecodeContext,
14    decode_invalid_action::DecodeInvalidAction,
15};
16use crate::{
17    CapacityError,
18    Codec,
19    TranscodeDecodeErrorOf,
20};
21
22/// Policy hooks for [`crate::engine::TranscodeDecodeEngine`].
23///
24/// Hooks own policy state, such as malformed-input replacement behavior. The
25/// engine passes the codec into hook methods when policy code needs codec
26/// metadata.
27///
28/// Implement this trait when a buffered decoder needs policy decisions after
29/// the low-level codec reports an error. The engine handles input/output cursor
30/// bookkeeping, output-capacity checks, and successful one-value decodes; hooks
31/// decide whether invalid input should be skipped, replaced, or returned as an
32/// error.
33///
34/// The hook receives a [`DecodeContext`] with absolute input/output cursors, so
35/// errors can include useful positions without duplicating engine arithmetic.
36/// Stateful hooks may also use [`finish_hooks`](Self::finish_hooks) to emit
37/// final values after the caller has supplied all input and handled any
38/// incomplete tail.
39///
40/// # Example
41///
42/// This hook replaces malformed units with `b'?'` and otherwise lets the engine
43/// keep decoding. Incomplete input is reported by
44/// [`crate::DecodeFailure`] before policy hooks are called.
45///
46/// ```rust
47/// use core::num::NonZeroUsize;
48/// use qubit_codec::{
49///     Codec,
50///     DecodeFailure,
51///     TranscodeDecodeError,
52/// };
53/// use qubit_codec::engine::{
54///     DecodeContext,
55///     DecodeInvalidAction,
56///     TranscodeDecodeHooks,
57/// };
58///
59/// #[derive(Clone, Copy)]
60/// struct MyCodec;
61///
62/// #[derive(Clone, Copy, Debug, Eq, PartialEq)]
63/// enum MyDecodeError {
64///     Malformed { consumed: NonZeroUsize },
65/// }
66///
67/// impl Codec for MyCodec {
68///     type Value = u8;
69///     type Unit = u8;
70///     type DecodeError = MyDecodeError;
71///     type EncodeError = core::convert::Infallible;
72///
73///     const MIN_UNITS_PER_VALUE: usize = 1;
74///     const MAX_ENCODE_UNITS_PER_VALUE: usize = 1;
75///     const MAX_DECODE_UNITS_PER_VALUE: usize = 1;
76///
77///     unsafe fn decode(
78///         &mut self,
79///         input: &[u8],
80///         index: usize,
81///     ) -> Result<(u8, NonZeroUsize), DecodeFailure<Self::DecodeError>> {
82///         match input[index] {
83///             0xff => Err(DecodeFailure::invalid(
84///                 MyDecodeError::Malformed {
85///                     consumed: NonZeroUsize::MIN,
86///                 },
87///                 NonZeroUsize::MIN,
88///             )),
89///             value => Ok((value, NonZeroUsize::MIN)),
90///         }
91///     }
92///
93///     unsafe fn encode(
94///         &mut self,
95///         value: &u8,
96///         output: &mut [u8],
97///         index: usize,
98///     ) -> Result<usize, Self::EncodeError> {
99///         output[index] = *value;
100///         Ok(1)
101///     }
102/// }
103///
104/// struct ReplacementHooks;
105///
106/// impl TranscodeDecodeHooks<MyCodec> for ReplacementHooks {
107///     fn handle_invalid_decode(
108///         &mut self,
109///         _codec: &mut MyCodec,
110///         error: &MyDecodeError,
111///         consumed: Option<NonZeroUsize>,
112///         _context: DecodeContext,
113///     ) -> Result<DecodeInvalidAction<u8>, TranscodeDecodeError<MyDecodeError>> {
114///         match error {
115///             MyDecodeError::Malformed { .. } => {
116///                 Ok(DecodeInvalidAction::Emit {
117///                     value: b'?',
118///                     consumed: consumed.expect("codec reported malformed width"),
119///                 })
120///             }
121///         }
122///     }
123/// }
124/// ```
125///
126/// # Type Parameters
127///
128/// - `C`: Low-level codec owned by the engine.
129pub trait TranscodeDecodeHooks<C>
130where
131    C: Codec,
132{
133    /// Returns an upper bound for decoded values produced from `input_len`
134    /// units.
135    ///
136    /// This bound covers only the streaming decode phase. It must not include
137    /// codec reset values, codec finish values, or hook finish output.
138    /// It may depend on immutable hook and codec configuration, but it must
139    /// cover every reachable transient hook and codec state, including the
140    /// maximum hook-owned pending output that may be drained during transcode.
141    ///
142    /// The default implementation returns `input_len`, which remains
143    /// conservative when a replacement policy consumes fewer units than a
144    /// valid codec value. Override it when hook policy can emit more than one
145    /// value per source unit or when hook-owned pending output can be drained
146    /// during `transcode`.
147    ///
148    /// # Parameters
149    ///
150    /// - `codec`: Low-level codec owned by the engine.
151    /// - `input_len`: Number of source units the caller plans to decode.
152    ///
153    /// # Returns
154    ///
155    /// Returns a conservative upper bound for streaming output.
156    ///
157    /// # Errors
158    ///
159    /// Returns [`CapacityError::OutputLengthOverflow`] when the bound cannot
160    /// be represented as `usize`.
161    #[inline(always)]
162    #[must_use = "capacity planning can fail on overflow"]
163    fn max_transcode_output_len(
164        &self,
165        _codec: &C,
166        input_len: usize,
167    ) -> Result<usize, CapacityError> {
168        Ok(input_len)
169    }
170
171    /// Returns an upper bound for values emitted by finishing hook-owned state.
172    ///
173    /// `finish` never receives more input. Implementations must report the
174    /// maximum output that could be derived from hook-owned state in any
175    /// reachable transient stream state, not only what happens to remain when
176    /// this method is called.
177    ///
178    /// The default implementation returns `0`. Override it when
179    /// [`finish_hooks`](Self::finish_hooks) can write trailers, delayed
180    /// replacements, or any other hook-owned final output. Do not include
181    /// [`Codec::MAX_DECODE_FINISH_VALUES`]; the engine adds the codec finish
182    /// bound separately.
183    ///
184    /// # Parameters
185    ///
186    /// - `codec`: Low-level codec owned by the engine.
187    ///
188    /// # Returns
189    ///
190    /// Returns the finite, transient-state-independent final-output upper
191    /// bound.
192    #[inline(always)]
193    #[must_use]
194    fn max_finish_output_len(&self, _codec: &C) -> usize {
195        0
196    }
197
198    /// Handles a codec-domain invalid decode error during `transcode`.
199    ///
200    /// # Parameters
201    ///
202    /// - `codec`: Low-level codec owned by the engine.
203    /// - `error`: Invalid domain error returned by the codec.
204    /// - `consumed`: Known invalid input units that may be consumed by
205    ///   non-strict policies, or `None` when the codec cannot state an invalid
206    ///   span length.
207    /// - `context`: Decode attempt context.
208    ///
209    /// # Returns
210    ///
211    /// Returns the action selected by this hook policy.
212    ///
213    /// Returned consuming actions must not consume more than
214    /// `context.available()` input units.
215    ///
216    /// The engine treats violations as hook bugs and panics.
217    ///
218    /// # Errors
219    ///
220    /// Returns [`crate::TranscodeDecodeError`] when the policy rejects the
221    /// input with a codec-domain error.
222    fn handle_invalid_decode(
223        &mut self,
224        codec: &mut C,
225        error: &C::DecodeError,
226        consumed: Option<NonZeroUsize>,
227        context: DecodeContext,
228    ) -> Result<DecodeInvalidAction<C::Value>, TranscodeDecodeErrorOf<C>>;
229
230    /// Runs hook-owned cleanup as part of stream reset.
231    ///
232    /// Called before [`Codec::decode_reset`](crate::Codec::decode_reset)
233    /// writes its own reset output. Stateless hooks may use the default no-op.
234    ///
235    /// # Parameters
236    ///
237    /// - `codec`: Low-level codec owned by the engine.
238    #[inline(always)]
239    fn reset_hooks(&mut self, _codec: &mut C) {}
240
241    /// Finishes hook-owned state and writes any retained output.
242    ///
243    /// The default implementation is a no-op for stateless decode hooks.
244    /// Stateful hooks may emit final values such as checksums, reset markers,
245    /// or other trailer data. The caller must provide at least
246    /// [`TranscodeDecodeHooks::max_finish_output_len`] writable slots from
247    /// `output_index`. Implementations must not write beyond that declared
248    /// final-output bound.
249    ///
250    /// Called after [`Codec::decode_finish`](crate::Codec::decode_finish) has
251    /// written its own finish output.
252    ///
253    /// # Parameters
254    ///
255    /// - `codec`: Low-level codec owned by the engine.
256    /// - `output`: Output value slice visible to the hook.
257    /// - `output_index`: Absolute output value index where writing starts.
258    ///
259    /// # Returns
260    ///
261    /// Returns the number of values written by finalization. This count must
262    /// not exceed [`TranscodeDecodeHooks::max_finish_output_len`].
263    ///
264    /// # Errors
265    ///
266    /// Returns a transcode decode error when hook-owned state cannot be
267    /// finalized.
268    #[inline]
269    fn finish_hooks(
270        &mut self,
271        _codec: &mut C,
272        _output: &mut [C::Value],
273        _output_index: usize,
274    ) -> Result<usize, TranscodeDecodeErrorOf<C>> {
275        Ok(0)
276    }
277}