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