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