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 the streaming decode phase, including conservative
144 /// capacity preflight in streaming and EOF operations, as required by
145 /// [`crate::Transcoder::max_transcode_output_len`]. It describes required
146 /// writable capacity, not only values actually emitted. It must not include
147 /// codec reset values, codec finish values, or hook finish output.
148 /// Streaming actions can emit at most one value for the current input
149 /// event or skip it; an emitted replacement consumes a non-zero number of
150 /// source units. The hook API has no separate operation for draining
151 /// hook-owned output without processing input. Use a custom
152 /// [`crate::Transcoder`] for streaming behavior beyond this action model,
153 /// such as one-to-many output or draining output without new input. The
154 /// bound may depend on immutable hook and codec configuration, but must
155 /// cover every reachable transient hook and codec state.
156 ///
157 /// The bound must cover the preflight required by
158 /// [`handle_invalid_decode`](Self::handle_invalid_decode) and
159 /// [`handle_incomplete_decode`](Self::handle_incomplete_decode), even when
160 /// the chosen action skips input and emits no value. Zero actual output
161 /// does not imply zero required writable capacity. See the
162 /// [trait example](#examples) for the hook action model.
163 ///
164 /// The default implementation returns `input_len`, which remains
165 /// conservative when a replacement policy consumes fewer units than a
166 /// valid codec value. Override it when codec or hook policy can produce a
167 /// larger streaming output bound than this default.
168 ///
169 /// # Parameters
170 ///
171 /// - `codec`: Low-level codec owned by the engine.
172 /// - `input_len`: Number of source units the caller plans to decode.
173 ///
174 /// # Returns
175 ///
176 /// Returns a conservative upper bound for streaming output.
177 ///
178 /// # Errors
179 ///
180 /// Returns [`CapacityError::OutputLengthOverflow`] when the bound cannot
181 /// be represented as `usize`.
182 #[inline]
183 fn max_transcode_output_len(&self, _codec: &C, input_len: usize) -> Result<usize, CapacityError> {
184 Ok(input_len)
185 }
186
187 /// Returns an upper bound for values emitted by finishing hook-owned state.
188 ///
189 /// `finish` never receives more input. Implementations must report the
190 /// maximum output that could be derived from hook-owned state in any
191 /// reachable transient stream state, not only what happens to remain when
192 /// this method is called.
193 ///
194 /// The default implementation returns `0`. Override it when
195 /// [`finish_hooks`](Self::finish_hooks) can write trailers, delayed
196 /// replacements, or any other hook-owned final output. Do not include
197 /// [`Codec::MAX_DECODE_FINISH_VALUES`]; the engine adds the codec finish
198 /// bound separately.
199 ///
200 /// # Parameters
201 ///
202 /// - `codec`: Low-level codec owned by the engine.
203 ///
204 /// # Returns
205 ///
206 /// Returns the finite, transient-state-independent final-output upper
207 /// bound.
208 #[must_use]
209 #[inline]
210 fn max_finish_output_len(&self, _codec: &C) -> usize {
211 0
212 }
213
214 /// Handles a codec-domain invalid decode error during `transcode`.
215 ///
216 /// # Parameters
217 ///
218 /// - `codec`: Low-level codec owned by the engine.
219 /// - `error`: Invalid domain error returned by the codec.
220 /// - `consumed`: Known invalid input units that may be consumed by
221 /// non-strict policies, or `None` when the codec cannot state an invalid
222 /// span length.
223 /// - `context`: Decode attempt context.
224 ///
225 /// # Returns
226 ///
227 /// Returns the action selected by this hook policy.
228 ///
229 /// Returned consuming actions must not consume more than
230 /// `context.available()` input units.
231 ///
232 /// # Errors
233 ///
234 /// Returns [`crate::TranscodeDecodeError`] when the policy rejects the
235 /// input with a codec-domain error.
236 ///
237 /// # Panics
238 ///
239 /// The engine panics if a consuming action exceeds
240 /// `context.available()` input units.
241 fn handle_invalid_decode(
242 &mut self,
243 codec: &mut C,
244 error: &C::DecodeError,
245 consumed: Option<NonZeroUsize>,
246 context: DecodeContext,
247 ) -> Result<DecodeInvalidAction<C::Value>, TranscodeDecodeErrorOf<C>>;
248
249 /// Handles a codec value left incomplete after end of input.
250 ///
251 /// The engine calls this method only from `transcode_eof`. `source` is
252 /// `Some` only when the codec returned an incomplete failure carrying a
253 /// source, such as through
254 /// [`crate::DecodeFailure::incomplete_with_source`]. It is `None` when
255 /// the available tail is shorter than [`Codec::MIN_UNITS_PER_VALUE`]
256 /// and the codec was not called, or when the codec was called but
257 /// returned [`crate::DecodeFailure::incomplete`] without a source.
258 ///
259 /// # Parameters
260 ///
261 /// - `codec`: Low-level codec owned by the engine.
262 /// - `source`: Optional domain error explaining the incomplete tail.
263 /// - `required_total`: Minimum source units required to retry decoding.
264 /// - `context`: Decode attempt context.
265 ///
266 /// # Returns
267 ///
268 /// Returns the EOF policy action. `Skip` and `Emit` consume every source
269 /// unit available in `context`. At confirmed EOF,
270 /// [`crate::engine::TranscodeDecodeEngine`] requires one writable value
271 /// slot before invoking this method, even when the eventual action is
272 /// `Skip` or `Reject`; a capacity shortage does not invoke the hook or
273 /// change its state. [`crate::engine::TranscodeConvertEngine`] may invoke
274 /// the hook without target capacity: `Skip` can consume the tail, while
275 /// `Emit` can retain a pending value for encoding. A retry drains that
276 /// pending value without invoking the hook again for the same tail.
277 ///
278 /// # Errors
279 ///
280 /// Returns [`crate::TranscodeDecodeError`] when policy evaluation fails.
281 #[inline]
282 fn handle_incomplete_decode(
283 &mut self,
284 _codec: &mut C,
285 _source: Option<&C::DecodeError>,
286 _required_total: NonZeroUsize,
287 _context: DecodeContext,
288 ) -> Result<DecodeIncompleteAction<C::Value>, TranscodeDecodeErrorOf<C>> {
289 Ok(DecodeIncompleteAction::Reject)
290 }
291
292 /// Runs hook-owned cleanup as part of stream reset.
293 ///
294 /// Called before [`Codec::decode_reset`]
295 /// writes its own reset output. Stateless hooks may use the default no-op.
296 ///
297 /// # Parameters
298 ///
299 /// - `codec`: Low-level codec owned by the engine.
300 #[inline]
301 fn reset_hooks(&mut self, _codec: &mut C) {}
302
303 /// Finishes hook-owned state and writes any retained output.
304 ///
305 /// The default implementation is a no-op for stateless decode hooks.
306 /// Stateful hooks may emit final values such as checksums, reset markers,
307 /// or other trailer data. The caller must provide at least
308 /// [`TranscodeDecodeHooks::max_finish_output_len`] writable slots from
309 /// `output_index`. Implementations must not write beyond that declared
310 /// final-output bound.
311 ///
312 /// Called after [`Codec::decode_finish`] has
313 /// written its own finish output.
314 ///
315 /// # Parameters
316 ///
317 /// - `codec`: Low-level codec owned by the engine.
318 /// - `output`: Output value slice visible to the hook.
319 /// - `output_index`: Absolute output value index where writing starts.
320 ///
321 /// # Returns
322 ///
323 /// Returns the number of values written by finalization. This count must
324 /// not exceed [`TranscodeDecodeHooks::max_finish_output_len`].
325 ///
326 /// # Errors
327 ///
328 /// Returns a transcode decode error when hook-owned state cannot be
329 /// finalized.
330 #[inline]
331 fn finish_hooks(
332 &mut self,
333 _codec: &mut C,
334 _output: &mut [C::Value],
335 _output_index: usize,
336 ) -> Result<usize, TranscodeDecodeErrorOf<C>> {
337 Ok(0)
338 }
339}