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