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/// # Examples
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::convert::Infallible;
46/// use core::num::NonZeroUsize;
47///
48/// use qubit_codec::Codec;
49/// use qubit_codec::DecodeFailure;
50/// use qubit_codec::TranscodeDecodeError;
51/// use qubit_codec::engine::DecodeContext;
52/// use qubit_codec::engine::DecodeInvalidAction;
53/// use qubit_codec::engine::TranscodeDecodeHooks;
54///
55/// #[derive(Clone, Copy)]
56/// struct MyCodec;
57///
58/// #[derive(Clone, Copy, Debug, Eq, PartialEq)]
59/// enum MyDecodeError {
60///     Malformed { consumed: NonZeroUsize },
61/// }
62///
63/// impl Codec for MyCodec {
64///     type Value = u8;
65///     type Unit = u8;
66///     type DecodeError = MyDecodeError;
67///     type EncodeError = Infallible;
68///
69///     const MIN_UNITS_PER_VALUE: usize = 1;
70///     const MAX_ENCODE_UNITS_PER_VALUE: usize = 1;
71///     const MAX_DECODE_UNITS_PER_VALUE: usize = 1;
72///
73///     unsafe fn decode(
74///         &mut self,
75///         input: &[u8],
76///         index: usize,
77///     ) -> Result<(u8, NonZeroUsize), DecodeFailure<Self::DecodeError>> {
78///         match input[index] {
79///             0xff => Err(DecodeFailure::invalid(
80///                 MyDecodeError::Malformed {
81///                     consumed: NonZeroUsize::MIN,
82///                 },
83///                 NonZeroUsize::MIN,
84///             )),
85///             value => Ok((value, NonZeroUsize::MIN)),
86///         }
87///     }
88///
89///     unsafe fn encode(
90///         &mut self,
91///         value: &u8,
92///         output: &mut [u8],
93///         index: usize,
94///     ) -> Result<usize, Self::EncodeError> {
95///         output[index] = *value;
96///         Ok(1)
97///     }
98/// }
99///
100/// #[derive(Default)]
101/// struct ReplacementHooks;
102///
103/// impl TranscodeDecodeHooks<MyCodec> for ReplacementHooks {
104///     fn handle_invalid_decode(
105///         &mut self,
106///         _codec: &mut MyCodec,
107///         error: &MyDecodeError,
108///         consumed: Option<NonZeroUsize>,
109///         _context: DecodeContext,
110///     ) -> Result<DecodeInvalidAction<u8>, TranscodeDecodeError<MyDecodeError>> {
111///         match error {
112///             MyDecodeError::Malformed { .. } => {
113///                 Ok(DecodeInvalidAction::Emit {
114///                     value: b'?',
115///                     consumed: consumed.expect("codec reported malformed width"),
116///                 })
117///             }
118///         }
119///     }
120/// }
121///
122/// let mut hooks = ReplacementHooks::default();
123/// let mut codec = MyCodec;
124/// let context = DecodeContext::new(0, 0, 0, 0, 1);
125/// let error = MyDecodeError::Malformed { consumed: NonZeroUsize::MIN };
126/// let action = hooks.handle_invalid_decode(
127///     &mut codec, &error, Some(NonZeroUsize::MIN), context,
128/// ).expect("replacement policy accepts malformed input");
129/// assert!(matches!(action, DecodeInvalidAction::Emit { value: b'?', consumed }
130///     if consumed == NonZeroUsize::MIN));
131/// ```
132///
133/// # Type Parameters
134///
135/// - `C`: Low-level codec owned by the engine.
136pub trait TranscodeDecodeHooks<C>
137where
138    C: Codec,
139{
140    /// Returns an upper bound for decoded values produced from `input_len`
141    /// units.
142    ///
143    /// This bound covers only the streaming decode phase. It must not include
144    /// codec reset values, codec finish values, or hook finish output.
145    /// It may depend on immutable hook and codec configuration, but it must
146    /// cover every reachable transient hook and codec state, including the
147    /// maximum hook-owned pending output that may be drained during transcode.
148    ///
149    /// The default implementation returns `input_len`, which remains
150    /// conservative when a replacement policy consumes fewer units than a
151    /// valid codec value. Override it when hook policy can emit more than one
152    /// value per source unit or when hook-owned pending output can be drained
153    /// during `transcode`.
154    ///
155    /// # Parameters
156    ///
157    /// - `codec`: Low-level codec owned by the engine.
158    /// - `input_len`: Number of source units the caller plans to decode.
159    ///
160    /// # Returns
161    ///
162    /// Returns a conservative upper bound for streaming output.
163    ///
164    /// # Errors
165    ///
166    /// Returns [`CapacityError::OutputLengthOverflow`] when the bound cannot
167    /// be represented as `usize`.
168    #[inline]
169    #[must_use = "capacity planning can fail on overflow"]
170    fn max_transcode_output_len(&self, _codec: &C, input_len: usize) -> Result<usize, CapacityError> {
171        Ok(input_len)
172    }
173
174    /// Returns an upper bound for values emitted by finishing hook-owned state.
175    ///
176    /// `finish` never receives more input. Implementations must report the
177    /// maximum output that could be derived from hook-owned state in any
178    /// reachable transient stream state, not only what happens to remain when
179    /// this method is called.
180    ///
181    /// The default implementation returns `0`. Override it when
182    /// [`finish_hooks`](Self::finish_hooks) can write trailers, delayed
183    /// replacements, or any other hook-owned final output. Do not include
184    /// [`Codec::MAX_DECODE_FINISH_VALUES`]; the engine adds the codec finish
185    /// bound separately.
186    ///
187    /// # Parameters
188    ///
189    /// - `codec`: Low-level codec owned by the engine.
190    ///
191    /// # Returns
192    ///
193    /// Returns the finite, transient-state-independent final-output upper
194    /// bound.
195    #[inline]
196    #[must_use]
197    fn max_finish_output_len(&self, _codec: &C) -> usize {
198        0
199    }
200
201    /// Handles a codec-domain invalid decode error during `transcode`.
202    ///
203    /// # Parameters
204    ///
205    /// - `codec`: Low-level codec owned by the engine.
206    /// - `error`: Invalid domain error returned by the codec.
207    /// - `consumed`: Known invalid input units that may be consumed by
208    ///   non-strict policies, or `None` when the codec cannot state an invalid
209    ///   span length.
210    /// - `context`: Decode attempt context.
211    ///
212    /// # Returns
213    ///
214    /// Returns the action selected by this hook policy.
215    ///
216    /// Returned consuming actions must not consume more than
217    /// `context.available()` input units.
218    ///
219    /// The engine treats violations as hook bugs and panics.
220    ///
221    /// # Errors
222    ///
223    /// Returns [`crate::TranscodeDecodeError`] when the policy rejects the
224    /// input with a codec-domain error.
225    fn handle_invalid_decode(
226        &mut self,
227        codec: &mut C,
228        error: &C::DecodeError,
229        consumed: Option<NonZeroUsize>,
230        context: DecodeContext,
231    ) -> Result<DecodeInvalidAction<C::Value>, TranscodeDecodeErrorOf<C>>;
232
233    /// Handles a codec value left incomplete after end of input.
234    ///
235    /// The engine calls this method only from `transcode_eof`. `source` is the
236    /// codec-domain context attached to [`crate::DecodeFailure::Incomplete`],
237    /// or `None` when the engine stopped before calling the codec because the
238    /// available tail is shorter than [`Codec::MIN_UNITS_PER_VALUE`].
239    ///
240    /// # Parameters
241    ///
242    /// - `codec`: Low-level codec owned by the engine.
243    /// - `source`: Optional domain error explaining the incomplete tail.
244    /// - `required_total`: Minimum source units required to retry decoding.
245    /// - `context`: Decode attempt context.
246    ///
247    /// # Returns
248    ///
249    /// Returns the EOF policy action. `Skip` and `Emit` consume every source
250    /// unit available in `context`; `Emit` additionally requires one writable
251    /// output slot.
252    ///
253    /// # Errors
254    ///
255    /// Returns [`crate::TranscodeDecodeError`] when policy evaluation fails.
256    #[inline]
257    fn handle_incomplete_decode(
258        &mut self,
259        _codec: &mut C,
260        _source: Option<&C::DecodeError>,
261        _required_total: NonZeroUsize,
262        _context: DecodeContext,
263    ) -> Result<DecodeIncompleteAction<C::Value>, TranscodeDecodeErrorOf<C>> {
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]
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}