qubit_codec/transcode/engine/transcode_decode_engine.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//! Reusable buffered decoder engine.
9
10use core::num::NonZeroUsize;
11
12use super::super::internal::{
13 decode_state::DecodeState,
14 decode_step::DecodeStep,
15 lifecycle::LifecycleGuard,
16};
17use crate::codec::assert_unit_bounds;
18use crate::{
19 CapacityError,
20 Codec,
21 CodecDecodeError,
22 DecodeContext,
23 DecodeFailure,
24 TranscodeDecodeEngineError,
25 TranscodeDecodeHooks,
26 TranscodeError,
27 TranscodeProgress,
28 Transcoder,
29};
30
31type DecodeEngineErrorOf<C, H> = TranscodeDecodeEngineError<
32 <C as Codec>::DecodeError,
33 <H as TranscodeDecodeHooks<C>>::Error,
34>;
35
36/// Reusable buffered decoding engine for codec-backed decoders.
37///
38/// The engine owns the low-level codec and hook object. It keeps the common
39/// buffered decoding loop private: input-index validation, output-capacity
40/// checks, calls to [`Codec::decode`], hook dispatch, and
41/// [`crate::TranscodeStatus`] reporting. Incomplete input tails are left in the
42/// caller-provided input slice; callers own input-buffer refill.
43///
44/// Use this type to build a streaming decoder over a one-value [`Codec`]. The
45/// engine decodes into a caller-provided output slice and returns
46/// [`TranscodeProgress`] instead of allocating. On success it writes decoded
47/// values directly to output. On codec errors it delegates to
48/// [`crate::TranscodeDecodeHooks`], allowing a policy to skip invalid units,
49/// emit a replacement value, or fail.
50///
51/// The engine stops before reading an incomplete value when fewer than
52/// [`Codec::MIN_UNITS_PER_VALUE`] units are available. For variable-width
53/// codecs, the codec may still return an incomplete decode error after that
54/// minimum is satisfied; the engine converts that failure directly into
55/// [`crate::TranscodeStatus::NeedInput`].
56///
57/// For strict decoding that wraps codec errors, use
58/// [`crate::CodecTranscodeDecoder`]. Use `TranscodeDecodeEngine` directly when
59/// invalid input should be repaired, skipped, counted, or otherwise handled by
60/// policy.
61///
62/// # Example
63///
64/// ```rust
65/// use core::num::NonZeroUsize;
66/// use qubit_codec::{
67/// Codec,
68/// DecodeFailure,
69/// CodecDecodeError,
70/// DecodeInvalidAction,
71/// DecodeContext,
72/// TranscodeStatus,
73/// TranscodeDecodeEngine,
74/// TranscodeDecodeHooks,
75/// };
76///
77/// #[derive(Clone, Copy)]
78/// struct ByteCodec;
79///
80/// #[derive(Clone, Copy, Debug, Eq, PartialEq)]
81/// enum ByteDecodeError {
82/// Malformed { consumed: NonZeroUsize },
83/// }
84///
85/// impl Codec for ByteCodec {
86/// type Value = u8;
87/// type Unit = u8;
88/// type DecodeError = ByteDecodeError;
89/// type EncodeError = core::convert::Infallible;
90///
91/// const MIN_UNITS_PER_VALUE: NonZeroUsize = NonZeroUsize::MIN;
92/// const MAX_UNITS_PER_VALUE: NonZeroUsize = NonZeroUsize::MIN;
93///
94/// unsafe fn decode(
95/// &mut self,
96/// input: &[u8],
97/// index: usize,
98/// ) -> Result<(u8, NonZeroUsize), DecodeFailure<Self::DecodeError>> {
99/// match input[index] {
100/// 0xff => Err(DecodeFailure::invalid(
101/// ByteDecodeError::Malformed {
102/// consumed: NonZeroUsize::MIN,
103/// },
104/// NonZeroUsize::MIN,
105/// )),
106/// value => Ok((value, NonZeroUsize::MIN)),
107/// }
108/// }
109///
110/// unsafe fn encode(
111/// &mut self,
112/// value: &u8,
113/// output: &mut [u8],
114/// index: usize,
115/// ) -> Result<NonZeroUsize, Self::EncodeError> {
116/// output[index] = *value;
117/// Ok(NonZeroUsize::MIN)
118/// }
119/// }
120///
121/// struct ReplacementHooks;
122///
123/// impl TranscodeDecodeHooks<ByteCodec> for ReplacementHooks {
124/// type Error = CodecDecodeError<ByteDecodeError>;
125///
126/// fn handle_invalid_decode(
127/// &mut self,
128/// _codec: &mut ByteCodec,
129/// error: ByteDecodeError,
130/// consumed: Option<NonZeroUsize>,
131/// _context: DecodeContext,
132/// ) -> Result<DecodeInvalidAction<u8>, Self::Error> {
133/// match error {
134/// ByteDecodeError::Malformed { .. } => {
135/// Ok(DecodeInvalidAction::Emit {
136/// value: b'?',
137/// consumed: consumed.expect("codec reported malformed width"),
138/// })
139/// }
140/// }
141/// }
142/// }
143///
144/// let mut engine = TranscodeDecodeEngine::<_, _>::new(ByteCodec, ReplacementHooks);
145/// let input = [b'a', 0xff, b'b'];
146/// let mut output = [0_u8; 3];
147///
148/// let progress = engine.transcode(&input, 0, &mut output, 0)?;
149/// match progress.status() {
150/// TranscodeStatus::Complete => assert_eq!(&output[..progress.written()], b"a?b"),
151/// TranscodeStatus::NeedInput { input_index, .. } => {
152/// // Keep `input[input_index..]`, append more source units, and resume.
153/// }
154/// TranscodeStatus::NeedOutput { output_index, .. } => {
155/// // Drain `output[..output_index]`, then resume with more output room.
156/// }
157/// }
158/// # Ok::<(), qubit_codec::TranscodeError<qubit_codec::TranscodeDecodeEngineError<ByteDecodeError, CodecDecodeError<ByteDecodeError>>>>(())
159/// ```
160///
161/// # Type Parameters
162///
163/// - `C`: Low-level codec used by the engine.
164/// - `H`: Policy hook object used by the engine.
165#[derive(Debug, Default)]
166pub struct TranscodeDecodeEngine<C, H> {
167 /// Low-level codec used for one-value decoding.
168 pub(super) codec: C,
169 /// Policy hooks used for decode failures.
170 pub(super) hooks: H,
171 /// Debug-only guard for the `reset → transcode* → finish` lifecycle.
172 /// Zero-sized in release builds.
173 lifecycle: LifecycleGuard,
174}
175
176impl<C, H> TranscodeDecodeEngine<C, H>
177where
178 C: Codec,
179 H: TranscodeDecodeHooks<C>,
180{
181 /// Creates a buffered decoder engine.
182 ///
183 /// # Parameters
184 ///
185 /// - `codec`: Low-level codec used for one-value decoding.
186 /// - `hooks`: Policy hooks used for decode failures.
187 ///
188 /// # Returns
189 ///
190 /// Returns a buffered decoder engine.
191 ///
192 /// # Panics
193 ///
194 /// In debug builds, panics when the supplied codec violates the
195 /// [`Codec::MIN_UNITS_PER_VALUE`] / [`Codec::MAX_UNITS_PER_VALUE`] ordering
196 /// invariant. Release builds skip this check because the invariant is the
197 /// responsibility of the [`Codec`] implementation.
198 #[inline]
199 #[must_use]
200 pub fn new(codec: C, hooks: H) -> Self {
201 assert_unit_bounds::<C>();
202 Self {
203 codec,
204 hooks,
205 lifecycle: LifecycleGuard::new(),
206 }
207 }
208
209 /// Returns an upper bound for decoded values produced from `input_len`
210 /// units.
211 ///
212 /// # Parameters
213 ///
214 /// - `input_len`: Number of source units the caller plans to decode.
215 ///
216 /// # Returns
217 ///
218 /// Returns a conservative upper bound, or a capacity error on arithmetic
219 /// overflow.
220 #[must_use = "capacity planning can fail on overflow"]
221 #[inline(always)]
222 pub fn max_output_len(
223 &self,
224 input_len: usize,
225 ) -> Result<usize, CapacityError> {
226 self.hooks.max_output_len(&self.codec, input_len)
227 }
228
229 /// Returns the maximum values emitted by flushing codec state and finishing
230 /// hook-owned state.
231 ///
232 /// # Returns
233 ///
234 /// Returns the sum of [`Codec::MAX_DECODE_FLUSH_VALUES`] and the
235 /// hook-provided final-output bound. The codec flush portion covers values
236 /// written by [`Codec::decode_flush`]; hook implementations must not
237 /// include that portion in
238 /// [`TranscodeDecodeHooks::max_finish_output_len`].
239 #[must_use = "capacity planning can fail on overflow"]
240 #[inline(always)]
241 pub fn max_finish_output_len(&self) -> Result<usize, CapacityError> {
242 C::MAX_DECODE_FLUSH_VALUES
243 .checked_add(self.hooks.max_finish_output_len(&self.codec))
244 .ok_or(CapacityError::OutputLengthOverflow)
245 }
246
247 /// Returns the maximum values emitted when resetting stream state.
248 ///
249 /// Returns [`Codec::MAX_DECODE_RESET_VALUES`] for the wrapped codec.
250 /// Stateless decoders always return `0`.
251 #[inline(always)]
252 #[must_use = "capacity planning can fail on overflow"]
253 pub fn max_reset_output_len(&self) -> Result<usize, CapacityError> {
254 Ok(C::MAX_DECODE_RESET_VALUES)
255 }
256
257 /// Resets codec decode state, runs reset hooks, and emits stream-start
258 /// values.
259 ///
260 /// The sequence is: validate capacity → run `reset_hooks` → call
261 /// [`Codec::decode_reset`]. Stateless decoders (`MAX_DECODE_RESET_VALUES
262 /// == 0`) write nothing and return `Ok(0)`.
263 ///
264 /// # Parameters
265 ///
266 /// - `output`: Complete output value slice visible to the decoder.
267 /// - `output_index`: Absolute output value index where writing starts.
268 ///
269 /// # Returns
270 ///
271 /// Returns the number of reset values written.
272 ///
273 /// # Errors
274 ///
275 /// Returns framework errors when the caller provides invalid or
276 /// insufficient output capacity. Returns domain errors when codec reset
277 /// or hook reset handling fails.
278 pub fn reset(
279 &mut self,
280 output: &mut [C::Value],
281 output_index: usize,
282 ) -> Result<usize, TranscodeError<DecodeEngineErrorOf<C, H>>> {
283 self.lifecycle.on_reset();
284 let required = C::MAX_DECODE_RESET_VALUES;
285 TranscodeError::ensure_output_capacity(
286 output.len(),
287 output_index,
288 required,
289 )?;
290 self.hooks.reset_hooks(&mut self.codec);
291 let written = unsafe {
292 // SAFETY: The capacity check above reserves the codec's declared
293 // reset-output bound at `output_index`.
294 self.codec.decode_reset(output, output_index)
295 }
296 .map_err(|error| {
297 TranscodeError::domain(TranscodeDecodeEngineError::codec(
298 CodecDecodeError::decode_reset(error),
299 ))
300 })?;
301 assert!(
302 written <= required,
303 "Codec::decode_reset wrote beyond its reset bound",
304 );
305 Ok(written)
306 }
307
308 /// Decodes source units into caller-provided output values.
309 ///
310 /// # Parameters
311 ///
312 /// - `input`: Complete input unit slice visible to the decoder.
313 /// - `input_index`: Absolute input unit index where decoding starts.
314 /// - `output`: Complete output value slice visible to the decoder.
315 /// - `output_index`: Absolute output value index where writing starts.
316 ///
317 /// # Returns
318 ///
319 /// Returns progress describing input units consumed, output values written,
320 /// and why decoding stopped.
321 ///
322 /// # Errors
323 ///
324 /// Returns hook errors when `input_index` is outside `input`, when
325 /// `output_index` is outside `output`, or when a concrete policy hook
326 /// rejects a value.
327 pub fn transcode(
328 &mut self,
329 input: &[C::Unit],
330 input_index: usize,
331 output: &mut [C::Value],
332 output_index: usize,
333 ) -> Result<TranscodeProgress, TranscodeError<DecodeEngineErrorOf<C, H>>>
334 {
335 self.lifecycle.on_transcode();
336 TranscodeError::ensure_transcode_indices(
337 input.len(),
338 input_index,
339 output.len(),
340 output_index,
341 )?;
342
343 let min_units = C::MIN_UNITS_PER_VALUE;
344 let mut state =
345 DecodeState::new(input, input_index, output, output_index);
346 while state.has_input() {
347 let context = state.context();
348 let available = context.available();
349 if available < min_units.get() {
350 return Ok(state.need_input_progress_with(min_units, available));
351 }
352 if state.needs_output() {
353 return Ok(state.need_output_progress());
354 }
355 let step = self.decode_step(state.input(), context)?;
356 if let Some(progress) = state.apply_decode_step(step) {
357 return Ok(progress);
358 }
359 }
360
361 Ok(state.complete_progress())
362 }
363
364 /// Finishes codec and hook-owned output after EOF.
365 ///
366 /// Finalization first flushes decode-side codec state through
367 /// [`Codec::decode_flush`], then lets hook implementations finish their
368 /// own retained state. The caller must provide enough output capacity for
369 /// [`TranscodeDecodeEngine::max_finish_output_len`], which includes both
370 /// the codec flush bound and the hook-owned finish bound.
371 ///
372 /// # Parameters
373 ///
374 /// - `output`: Complete output value slice visible to the decoder.
375 /// - `output_index`: Absolute output value index where writing starts.
376 ///
377 /// # Returns
378 ///
379 /// Returns the number of values written by finalization.
380 ///
381 /// # Errors
382 ///
383 /// Returns framework errors when the caller provides invalid or
384 /// insufficient output capacity. Returns domain errors when codec flush or
385 /// hook finalization fails.
386 ///
387 /// # Panics
388 ///
389 /// Panics when the codec flush writes beyond
390 /// [`Codec::MAX_DECODE_FLUSH_VALUES`] or when the combined codec and hook
391 /// finalization writes beyond
392 /// [`TranscodeDecodeEngine::max_finish_output_len`].
393 pub fn finish(
394 &mut self,
395 output: &mut [C::Value],
396 output_index: usize,
397 ) -> Result<usize, TranscodeError<DecodeEngineErrorOf<C, H>>> {
398 self.lifecycle.on_finish_attempt();
399 let required = self.max_finish_output_len()?;
400 TranscodeError::ensure_output_capacity(
401 output.len(),
402 output_index,
403 required,
404 )?;
405 let flushed = unsafe { self.codec.decode_flush(output, output_index) }
406 .map_err(|error| {
407 TranscodeError::domain(TranscodeDecodeEngineError::codec(
408 CodecDecodeError::decode_flush(error),
409 ))
410 })?;
411 assert!(
412 flushed <= C::MAX_DECODE_FLUSH_VALUES,
413 "Codec::decode_flush wrote beyond its flush bound",
414 );
415 let written = self
416 .hooks
417 .finish_hooks(&mut self.codec, output, output_index + flushed)
418 .map_err(|error| {
419 TranscodeError::domain(TranscodeDecodeEngineError::hook(error))
420 })?;
421 assert!(
422 flushed + written <= required,
423 "TranscodeDecodeEngine hook wrote beyond its finish bound",
424 );
425 self.lifecycle.on_finish_success();
426 Ok(flushed + written)
427 }
428
429 /// Decodes one source value attempt into a normalized decode step.
430 ///
431 /// # Parameters
432 ///
433 /// - `input`: Complete input unit slice visible to the caller.
434 /// - `context`: Decode context describing the current source and output
435 /// cursors.
436 ///
437 /// # Returns
438 ///
439 /// Returns one internal decode step, including successful decode, policy
440 /// skip/emit, or variable-width need-input state.
441 ///
442 /// # Errors
443 ///
444 /// Returns hook errors when the decode policy rejects the input.
445 #[inline(always)]
446 pub(super) fn decode_step(
447 &mut self,
448 input: &[C::Unit],
449 context: DecodeContext,
450 ) -> Result<DecodeStep<C::Value>, TranscodeError<DecodeEngineErrorOf<C, H>>>
451 {
452 debug_assert!(
453 context.available() >= C::MIN_UNITS_PER_VALUE.get(),
454 "decode_step requires at least Codec::MIN_UNITS_PER_VALUE input units",
455 );
456
457 // SAFETY: The context reports at least `MIN_UNITS_PER_VALUE` source
458 // units available from `context.input_index()`.
459 let result = unsafe { self.codec.decode(input, context.input_index()) };
460 self.handle_decode_result(context, result)
461 }
462
463 /// Handles one low-level decode result and returns a normalized decode
464 /// step.
465 ///
466 /// # Parameters
467 ///
468 /// - `context`: Decode context used by policy hooks.
469 /// - `result`: Low-level codec decode result.
470 ///
471 /// # Returns
472 ///
473 /// Returns the normalized decode step selected by codec success or policy
474 /// hooks.
475 ///
476 /// # Errors
477 ///
478 /// Returns hook errors when the policy rejects the input.
479 fn handle_decode_result(
480 &mut self,
481 context: DecodeContext,
482 result: Result<(C::Value, NonZeroUsize), DecodeFailure<C::DecodeError>>,
483 ) -> Result<DecodeStep<C::Value>, TranscodeError<DecodeEngineErrorOf<C, H>>>
484 {
485 match result {
486 Ok((value, consumed)) => {
487 assert!(
488 consumed.get() <= context.available(),
489 "Codec::decode consumed beyond available input",
490 );
491 Ok(DecodeStep::decoded(value, consumed, context.input_index()))
492 }
493 Err(DecodeFailure::Incomplete { required_total }) => {
494 assert!(
495 required_total.get() > context.available(),
496 "Codec::decode incomplete required_total must exceed available input",
497 );
498 Ok(DecodeStep::need_input(required_total, context.available()))
499 }
500 Err(DecodeFailure::Invalid { source, consumed }) => {
501 let action = self
502 .hooks
503 .handle_invalid_decode(
504 &mut self.codec,
505 source,
506 consumed,
507 context,
508 )
509 .map_err(|error| {
510 TranscodeError::domain(
511 TranscodeDecodeEngineError::hook(error),
512 )
513 })?;
514 Ok(action.into_step(context.input_index(), context.available()))
515 }
516 }
517 }
518}
519
520impl<C, H> Transcoder<C::Unit, C::Value> for TranscodeDecodeEngine<C, H>
521where
522 C: Codec,
523 H: TranscodeDecodeHooks<C>,
524{
525 type Error = DecodeEngineErrorOf<C, H>;
526
527 /// Returns an upper bound for decoded values produced from `input_len`
528 /// units.
529 #[inline(always)]
530 fn max_output_len(&self, input_len: usize) -> Result<usize, CapacityError> {
531 TranscodeDecodeEngine::max_output_len(self, input_len)
532 }
533
534 /// Returns an upper bound for values produced by finishing codec and hook
535 /// state.
536 #[inline(always)]
537 fn max_finish_output_len(&self) -> Result<usize, CapacityError> {
538 TranscodeDecodeEngine::max_finish_output_len(self)
539 }
540
541 /// Returns an upper bound for values emitted when resetting stream state.
542 #[inline(always)]
543 fn max_reset_output_len(&self) -> Result<usize, CapacityError> {
544 TranscodeDecodeEngine::max_reset_output_len(self)
545 }
546
547 /// Runs hook-owned cleanup before a logical decoder reset.
548 #[inline(always)]
549 fn reset(
550 &mut self,
551 output: &mut [C::Value],
552 output_index: usize,
553 ) -> Result<usize, TranscodeError<Self::Error>> {
554 TranscodeDecodeEngine::reset(self, output, output_index)
555 }
556
557 /// Decodes source units into logical values.
558 #[inline(always)]
559 fn transcode(
560 &mut self,
561 input: &[C::Unit],
562 input_index: usize,
563 output: &mut [C::Value],
564 output_index: usize,
565 ) -> core::result::Result<TranscodeProgress, TranscodeError<Self::Error>>
566 {
567 TranscodeDecodeEngine::transcode(
568 self,
569 input,
570 input_index,
571 output,
572 output_index,
573 )
574 }
575
576 /// Finishes internally retained output after EOF.
577 #[inline(always)]
578 fn finish(
579 &mut self,
580 output: &mut [C::Value],
581 output_index: usize,
582 ) -> Result<usize, TranscodeError<Self::Error>> {
583 TranscodeDecodeEngine::finish(self, output, output_index)
584 }
585}