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}