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::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}