Skip to main content

qubit_codec/transcode/engine/
transcode_convert_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 converter engine for codec-backed transcoding.
9//!
10//! Bridges a source [`crate::TranscodeDecodeEngine`] and a target
11//! [`crate::TranscodeEncodeEngine`] into one unit-to-unit conversion pipeline.
12
13use super::super::internal::{
14    convert_error_of::ConvertErrorOf,
15    convert_state::ConvertState,
16    lifecycle::LifecycleGuard,
17    pending_value::PendingValue,
18    pending_value_slot::PendingValueSlot,
19};
20use super::{
21    transcode_decode_engine::TranscodeDecodeEngine,
22    transcode_encode_engine::TranscodeEncodeEngine,
23};
24use crate::codec::assert_unit_bounds;
25use crate::{
26    CapacityError,
27    Codec,
28    EncodeContext,
29    TranscodeConvertEngineError,
30    TranscodeDecodeEngineError,
31    TranscodeDecodeHooks,
32    TranscodeEncodeEngineError,
33    TranscodeEncodeHooks,
34    TranscodeError,
35    TranscodeProgress,
36    Transcoder,
37};
38
39/// Reusable buffered conversion engine for codec-backed converters.
40///
41/// The engine owns reusable buffered decode and encode engines. It keeps
42/// common converter control flow private: index validation, pending-value
43/// retention, pending flush, decode-error policy dispatch, encode attempts,
44/// output-capacity checks, and [`crate::TranscodeStatus`] reporting.
45///
46/// Use this type to build a streaming converter over two one-value [`Codec`]
47/// implementations that share the same logical value type. Each hot-path step
48/// decodes one source unit sequence into a value, then immediately tries to
49/// encode that value into the target output buffer. If the target buffer lacks
50/// capacity, the decoded value is retained in an internal pending slot and
51/// must be drained before more source input is consumed, preserving output
52/// order across buffer turns.
53///
54/// `TranscodeConvertEngine` is intentionally batch-oriented. Its public
55/// [`Self::transcode`] method drives a source/output buffer loop and reuses the
56/// same unchecked codec and hook primitives as [`crate::TranscodeDecodeEngine`]
57/// and [`crate::TranscodeEncodeEngine`]. It does not call one-value public
58/// transcoders in the hot path.
59///
60/// For strict codec-backed conversion with default decode and encode policies,
61/// use [`crate::CodecTranscodeConverter`]. Use `TranscodeConvertEngine`
62/// directly when either side needs custom malformed-input repair, encode
63/// planning, skipped values, or finish-time output.
64///
65/// The engine follows the same lifecycle as [`crate::Transcoder`]:
66/// `reset → transcode* → finish → reset`. Call [`Self::reset`] before starting
67/// a new logical stream and [`Self::finish`] after EOF once any incomplete
68/// source tail has been handled.
69///
70/// # Example
71///
72/// ```rust
73/// use core::{
74///     convert::Infallible,
75///     num::NonZeroUsize,
76/// };
77/// use qubit_codec::{
78///     Codec,
79///     DecodeContext,
80///     DecodeFailure,
81///     EncodeContext,
82///     EncodeOutcome,
83///     TranscodeConvertEngine,
84///     TranscodeDecodeHooks,
85///     TranscodeEncodeHooks,
86///     TranscodeStatus,
87/// };
88///
89/// #[derive(Clone, Copy)]
90/// struct SourceCodec;
91///
92/// #[derive(Clone, Copy)]
93/// struct TargetCodec;
94///
95/// impl Codec for SourceCodec {
96///     type Value = u8;
97///     type Unit = u8;
98///     type DecodeError = Infallible;
99///     type EncodeError = Infallible;
100///
101///     const MIN_UNITS_PER_VALUE: NonZeroUsize = NonZeroUsize::MIN;
102///     const MAX_UNITS_PER_VALUE: NonZeroUsize = NonZeroUsize::MIN;
103///
104///     unsafe fn decode(
105///         &mut self,
106///         input: &[u8],
107///         index: usize,
108///     ) -> Result<(u8, NonZeroUsize), DecodeFailure<Self::DecodeError>> {
109///         Ok((input[index].wrapping_add(1), NonZeroUsize::MIN))
110///     }
111///
112///     unsafe fn encode(
113///         &mut self,
114///         value: &u8,
115///         output: &mut [u8],
116///         index: usize,
117///     ) -> Result<NonZeroUsize, Self::EncodeError> {
118///         output[index] = *value;
119///         Ok(NonZeroUsize::MIN)
120///     }
121/// }
122///
123/// impl Codec for TargetCodec {
124///     type Value = u8;
125///     type Unit = u8;
126///     type DecodeError = Infallible;
127///     type EncodeError = Infallible;
128///
129///     const MIN_UNITS_PER_VALUE: NonZeroUsize = NonZeroUsize::MIN;
130///     const MAX_UNITS_PER_VALUE: NonZeroUsize = NonZeroUsize::MIN;
131///
132///     unsafe fn decode(
133///         &mut self,
134///         input: &[u8],
135///         index: usize,
136///     ) -> Result<(u8, NonZeroUsize), DecodeFailure<Self::DecodeError>> {
137///         Ok((input[index], NonZeroUsize::MIN))
138///     }
139///
140///     unsafe fn encode(
141///         &mut self,
142///         value: &u8,
143///         output: &mut [u8],
144///         index: usize,
145///     ) -> Result<NonZeroUsize, Self::EncodeError> {
146///         output[index] = *value;
147///         Ok(NonZeroUsize::MIN)
148///     }
149/// }
150///
151/// struct StrictDecodeHooks;
152///
153/// impl TranscodeDecodeHooks<SourceCodec> for StrictDecodeHooks {
154///     type Error = Infallible;
155///
156///     fn handle_invalid_decode(
157///         &mut self,
158///         _codec: &mut SourceCodec,
159///         error: Infallible,
160///         _consumed: Option<NonZeroUsize>,
161///         _context: DecodeContext,
162///     ) -> Result<qubit_codec::DecodeInvalidAction<u8>, Self::Error> {
163///         match error {}
164///     }
165/// }
166///
167/// struct StrictEncodeHooks;
168///
169/// impl TranscodeEncodeHooks<TargetCodec> for StrictEncodeHooks {
170///     type Error = Infallible;
171///
172///     fn encode_value(
173///         &mut self,
174///         codec: &mut TargetCodec,
175///         context: EncodeContext<'_, u8, u8>,
176///     ) -> Result<EncodeOutcome, Self::Error> {
177///         let required = TargetCodec::MAX_UNITS_PER_VALUE;
178///         if context.available_output() < required.get() {
179///             return Ok(EncodeOutcome::need_output(required));
180///         }
181///         let (value, _, output, output_index) = context.into_parts();
182///         let written = unsafe { codec.encode(value, output, output_index) }
183///             .map(NonZeroUsize::get)
184///             .unwrap();
185///         Ok(EncodeOutcome::consumed(written))
186///     }
187/// }
188///
189/// let mut engine = TranscodeConvertEngine::new(
190///     SourceCodec,
191///     TargetCodec,
192///     StrictDecodeHooks,
193///     StrictEncodeHooks,
194/// );
195/// let input = [1_u8, 2, 3];
196/// let mut output = [0_u8; 2];
197///
198/// let progress = engine.transcode(&input, 0, &mut output, 0)?;
199/// match progress.status() {
200///     TranscodeStatus::NeedOutput { output_index, .. } => {
201///         assert_eq!(2, output_index);
202///         assert_eq!([2, 3], output);
203///         // Drain `output[..output_index]`, then resume at
204///         // `progress.read()` with fresh output capacity.
205///     }
206///     TranscodeStatus::Complete => unreachable!("output is intentionally short"),
207///     TranscodeStatus::NeedInput { .. } => unreachable!("input is complete"),
208/// }
209/// # Ok::<(), qubit_codec::TranscodeError<qubit_codec::TranscodeConvertEngineError<
210/// #     qubit_codec::TranscodeDecodeEngineError<Infallible, Infallible>,
211/// #     qubit_codec::TranscodeEncodeEngineError<Infallible, Infallible>,
212/// # >>>(())
213/// ```
214///
215/// # Type Parameters
216///
217/// - `D`: Source-side decoder codec.
218/// - `E`: Target-side encoder codec.
219/// - `DH`: Source-side decode hooks.
220/// - `EH`: Target-side encode hooks.
221#[derive(Debug)]
222pub struct TranscodeConvertEngine<D, E, DH, EH>
223where
224    D: Codec,
225    E: Codec<Value = D::Value>,
226    DH: TranscodeDecodeHooks<D>,
227    EH: TranscodeEncodeHooks<E>,
228{
229    /// Source-side buffered decoder engine.
230    decode_engine: TranscodeDecodeEngine<D, DH>,
231    /// Target-side buffered encoder engine.
232    encode_engine: TranscodeEncodeEngine<E, EH>,
233    /// Decoded value waiting for target output capacity.
234    pending: PendingValueSlot<D::Value>,
235    /// Debug-only guard for the `reset → transcode* → finish` lifecycle.
236    /// Zero-sized in release builds. The converter owns its own guard rather
237    /// than delegating to the inner decode/encode engines, because lifecycle
238    /// events here describe the converter as a whole.
239    lifecycle: LifecycleGuard,
240}
241
242impl<D, E, DH, EH> TranscodeConvertEngine<D, E, DH, EH>
243where
244    D: Codec,
245    E: Codec<Value = D::Value>,
246    DH: TranscodeDecodeHooks<D>,
247    EH: TranscodeEncodeHooks<E>,
248{
249    /// Creates a buffered converter engine.
250    ///
251    /// The caller supplies decode hooks and encode hooks directly.
252    ///
253    /// # Parameters
254    ///
255    /// - `decoder`: Low-level codec used for source decoding.
256    /// - `encoder`: Low-level codec used for target encoding.
257    /// - `decode_hooks`: Decode-side policy hooks.
258    /// - `encode_hooks`: Encode-side policy hooks.
259    ///
260    /// # Returns
261    ///
262    /// Returns a buffered converter engine.
263    ///
264    /// # Panics
265    ///
266    /// In debug builds, panics when either codec violates the
267    /// [`Codec::MIN_UNITS_PER_VALUE`] / [`Codec::MAX_UNITS_PER_VALUE`] ordering
268    /// invariant. Release builds skip this check because the invariant is the
269    /// responsibility of each [`Codec`] implementation.
270    #[inline]
271    #[must_use]
272    pub fn new(
273        decoder: D,
274        encoder: E,
275        decode_hooks: DH,
276        encode_hooks: EH,
277    ) -> Self {
278        assert_unit_bounds::<D>();
279        assert_unit_bounds::<E>();
280        Self {
281            decode_engine: TranscodeDecodeEngine::new(decoder, decode_hooks),
282            encode_engine: TranscodeEncodeEngine::new(encoder, encode_hooks),
283            pending: PendingValueSlot::empty(),
284            lifecycle: LifecycleGuard::new(),
285        }
286    }
287
288    /// Returns an upper bound for target units produced from `input_len` units.
289    ///
290    /// The bound sums three parts: any retained pending value, the maximum
291    /// decoded values from the source side, and the maximum target units for
292    /// those values on the encode side.
293    ///
294    /// # Parameters
295    ///
296    /// - `input_len`: Number of source units the caller plans to convert.
297    ///
298    /// # Returns
299    ///
300    /// Returns a conservative upper bound, or a capacity error on arithmetic
301    /// overflow.
302    #[must_use = "capacity planning can fail on overflow"]
303    pub fn max_output_len(
304        &self,
305        input_len: usize,
306    ) -> Result<usize, CapacityError> {
307        let pending_units = self.pending_output_len()?;
308        let decoded_values = self.decode_engine.max_output_len(input_len)?;
309        let converted_units =
310            self.encode_engine.max_output_len(decoded_values)?;
311        converted_units
312            .checked_add(pending_units)
313            .ok_or(CapacityError::OutputLengthOverflow)
314    }
315
316    /// Returns the maximum target units emitted when resetting stream state.
317    ///
318    /// Covers decode-side reset values (encoded to target units) plus
319    /// encode-side reset units. Most codecs are stateless and return `0`
320    /// for [`Codec::MAX_DECODE_RESET_VALUES`]; in that case this equals the
321    /// encode reset bound only.
322    ///
323    /// # Returns
324    ///
325    /// Returns the combined decode-reset and encode-reset output bound, or a
326    /// capacity error on arithmetic overflow.
327    #[must_use = "capacity planning can fail on overflow"]
328    pub fn max_reset_output_len(&self) -> Result<usize, CapacityError> {
329        let decode_reset_units = self
330            .encode_engine
331            .max_output_len(D::MAX_DECODE_RESET_VALUES)?;
332        let encode_reset_units = E::MAX_ENCODE_RESET_UNITS;
333        decode_reset_units
334            .checked_add(encode_reset_units)
335            .ok_or(CapacityError::OutputLengthOverflow)
336    }
337
338    /// Returns the maximum target units emitted by finishing retained state.
339    ///
340    /// The bound covers a retained pending value, decode-side finish values
341    /// (encoded to target units), and encode-side finish units.
342    ///
343    /// # Returns
344    ///
345    /// Returns the combined pending, decode-finish, and encode-finish output
346    /// bound, or a capacity error on arithmetic overflow.
347    #[must_use = "capacity planning can fail on overflow"]
348    pub fn max_finish_output_len(&self) -> Result<usize, CapacityError> {
349        let pending_units = self.pending_output_len()?;
350        let decoder_finish_values =
351            self.decode_engine.max_finish_output_len()?;
352        let decoder_finish_units =
353            self.encode_engine.max_output_len(decoder_finish_values)?;
354        let encoder_finish_units =
355            self.encode_engine.max_finish_output_len()?;
356        let pending_and_decoder = pending_units
357            .checked_add(decoder_finish_units)
358            .ok_or(CapacityError::OutputLengthOverflow)?;
359        pending_and_decoder
360            .checked_add(encoder_finish_units)
361            .ok_or(CapacityError::OutputLengthOverflow)
362    }
363
364    /// Converts source units into target units.
365    ///
366    /// The engine drains any retained pending value before consuming new input.
367    /// Each loop iteration decodes one source value and immediately attempts to
368    /// encode it. Conversion stops when the input tail is incomplete, when the
369    /// output buffer cannot hold the next encoded value, or when the visible
370    /// input is exhausted.
371    ///
372    /// # Parameters
373    ///
374    /// - `input`: Complete input unit slice visible to the converter.
375    /// - `input_index`: Absolute input index where conversion starts.
376    /// - `output`: Complete output unit slice visible to the converter.
377    /// - `output_index`: Absolute output index where writing starts.
378    ///
379    /// # Returns
380    ///
381    /// Returns conversion progress describing input units consumed, target
382    /// units written, and why conversion stopped.
383    ///
384    /// # Errors
385    ///
386    /// Returns hook errors when indices are invalid or concrete conversion
387    /// fails. Invalid output indices are reported through the encode-side
388    /// error path.
389    pub fn transcode(
390        &mut self,
391        input: &[D::Unit],
392        input_index: usize,
393        output: &mut [E::Unit],
394        output_index: usize,
395    ) -> Result<TranscodeProgress, ConvertErrorOf<D, E, DH, EH>> {
396        self.lifecycle.on_transcode();
397        TranscodeError::ensure_transcode_indices(
398            input.len(),
399            input_index,
400            output.len(),
401            output_index,
402        )?;
403
404        let mut state =
405            ConvertState::new(input, input_index, output, output_index);
406
407        // A retained decoded value must be written before consuming more input,
408        // otherwise callers could observe output reordered across buffer turns.
409        if let Some(progress) = self.drain_pending(&mut state)? {
410            return Ok(progress);
411        }
412
413        let min_input_units = D::MIN_UNITS_PER_VALUE;
414        while state.has_input() {
415            let available = state.available_input();
416            if available < min_input_units.get() {
417                return Ok(
418                    state.need_input_progress(min_input_units, available)
419                );
420            }
421
422            let previous_read = state.read();
423            // Each hot-path step decodes one source value and immediately tries
424            // to encode it, preserving backpressure at the target output.
425            if let Some(progress) = self.convert_next(&mut state)? {
426                return Ok(progress);
427            }
428            debug_assert!(
429                state.read() > previous_read,
430                "TranscodeConvertEngine conversion step must consume input or stop",
431            );
432        }
433
434        Ok(state.complete_progress())
435    }
436
437    /// Finishes retained output after EOF.
438    ///
439    /// Finalization drains a pending decoded value first, then lets the
440    /// source-side decode hooks emit final values, encodes those values through
441    /// the target-side encode hooks, and finally finishes target-side encode
442    /// hook state. The decode-finish value buffer used for this cold path
443    /// requires `D::Value: Default`; the normal [`Self::transcode`] loop does
444    /// not.
445    ///
446    /// # Parameters
447    ///
448    /// - `output`: Complete output unit slice visible to the converter.
449    /// - `output_index`: Absolute output index where writing starts.
450    ///
451    /// # Returns
452    ///
453    /// Returns the number of target units written during finalization.
454    ///
455    /// # Errors
456    ///
457    /// Returns a converter error when output capacity checks fail or when
458    /// hook finalization fails.
459    ///
460    /// # Panics
461    ///
462    /// Panics in debug builds when a retained pending value or decode-finish
463    /// value cannot be encoded within the capacity reserved by
464    /// [`Self::max_finish_output_len`].
465    pub fn finish(
466        &mut self,
467        output: &mut [E::Unit],
468        output_index: usize,
469    ) -> Result<usize, ConvertErrorOf<D, E, DH, EH>>
470    where
471        D::Value: Default,
472    {
473        self.lifecycle.on_finish_attempt();
474        let required = self.max_finish_output_len()?;
475        TranscodeError::ensure_output_capacity(
476            output.len(),
477            output_index,
478            required,
479        )?;
480
481        let empty_input: &[D::Unit] = &[];
482        let mut state = ConvertState::new(empty_input, 0, output, output_index);
483        // Finish keeps the same priority as transcode: output any retained
484        // decoded value before asking source-side hooks for final values.
485        if self.drain_pending(&mut state)?.is_some() {
486            unreachable!(
487                "converter finish bound must reserve space for pending values"
488            );
489        }
490
491        // Source-side finish may emit one or more final values. Drain them into
492        // the target encoder before finishing target-side hook state.
493        self.drain_decoder_finish(&mut state)?;
494
495        let output_cursor = state.output_cursor();
496        let written = self
497            .encode_engine
498            .finish(state.output_mut(), output_cursor)
499            .map_err(|error| {
500                error.map_domain(TranscodeConvertEngineError::encode)
501            })?;
502        state.advance_output(written);
503        self.lifecycle.on_finish_success();
504        Ok(state.written())
505    }
506
507    /// Clears retained conversion state, runs before-reset hooks, and emits
508    /// stream-start encode output.
509    ///
510    /// Reset clears any retained pending value, drains decode-side reset
511    /// values through the target encoder, then emits encode-side reset units.
512    /// The caller must provide enough output capacity for
513    /// [`Self::max_reset_output_len`].
514    ///
515    /// # Parameters
516    ///
517    /// - `output`: Complete output unit slice visible to the converter.
518    /// - `output_index`: Absolute output index where writing starts.
519    ///
520    /// # Returns
521    ///
522    /// Returns the number of target units written while resetting stream state.
523    ///
524    /// # Errors
525    ///
526    /// Returns a converter error if reset validation or target reset output
527    /// emission fails.
528    ///
529    /// # Panics
530    ///
531    /// Panics in debug builds when decode-reset values cannot be encoded
532    /// within the capacity reserved by [`Self::max_reset_output_len`].
533    pub fn reset(
534        &mut self,
535        output: &mut [E::Unit],
536        output_index: usize,
537    ) -> Result<usize, ConvertErrorOf<D, E, DH, EH>>
538    where
539        D::Value: Default,
540    {
541        self.lifecycle.on_reset();
542        let required = self.max_reset_output_len()?;
543        TranscodeError::ensure_output_capacity(
544            output.len(),
545            output_index,
546            required,
547        )?;
548
549        self.pending.clear();
550
551        // Source-side reset may emit stream-start values (such as a BOM) that
552        // must be piped through the target encoder before any encoder-owned
553        // reset output. `max_reset_output_len` already reserves space for both
554        // halves of the pipeline, so encode_pending should never report
555        // `NeedOutput` here.
556        let empty_input: &[D::Unit] = &[];
557        let mut state = ConvertState::new(empty_input, 0, output, output_index);
558        self.drain_decoder_reset(&mut state)?;
559
560        let output_cursor = state.output_cursor();
561        let encoder_written = self
562            .encode_engine
563            .reset(state.output_mut(), output_cursor)
564            .map_err(|error| {
565                error.map_domain(TranscodeConvertEngineError::encode)
566            })?;
567        state.advance_output(encoder_written);
568        Ok(state.written())
569    }
570
571    /// Drains source-side decode reset output and encodes emitted reset
572    /// values.
573    ///
574    /// Stateless decoders still call [`TranscodeDecodeEngine::reset`] so hook
575    /// teardown side effects run even when no reset values are emitted.
576    ///
577    /// # Parameters
578    ///
579    /// - `state`: Current conversion cursors and output buffer.
580    ///
581    /// # Returns
582    ///
583    /// Returns `Ok(())` after all decode-reset values have been encoded.
584    ///
585    /// # Errors
586    ///
587    /// Returns a converter error when decode reset or encode reset handling
588    /// fails.
589    ///
590    /// # Panics
591    ///
592    /// Panics when a decode-reset value cannot be encoded within the capacity
593    /// reserved by [`Self::max_reset_output_len`].
594    fn drain_decoder_reset(
595        &mut self,
596        state: &mut ConvertState<'_, D::Unit, E::Unit>,
597    ) -> Result<(), ConvertErrorOf<D, E, DH, EH>>
598    where
599        D::Value: Default,
600    {
601        let value_count = D::MAX_DECODE_RESET_VALUES;
602        if value_count == 0 {
603            // Stateless decoder: still call decode_reset so codecs whose
604            // hooks own teardown side effects (e.g. clearing accumulators)
605            // run them. The empty slice is safe because the capacity check
606            // inside `reset` accepts `required == 0` against any slice.
607            self.decode_engine.reset(&mut [], 0).map_err(|error| {
608                error.map_domain(TranscodeConvertEngineError::decode)
609            })?;
610            return Ok(());
611        }
612        // `D::Value: Default` is only consulted when the decoder declares
613        // reset output. Stateless codecs never reach this branch.
614        let mut reset_values: Vec<D::Value> =
615            (0..value_count).map(|_| D::Value::default()).collect();
616        let written = self.decode_engine.reset(&mut reset_values, 0).map_err(
617            |error| error.map_domain(TranscodeConvertEngineError::decode),
618        )?;
619        for value in reset_values.into_iter().take(written) {
620            let pending = PendingValue::new(value, 0);
621            if self.encode_pending(pending, state)?.is_some() {
622                unreachable!(
623                    "converter reset bound must reserve space for decode reset values"
624                );
625            }
626        }
627        Ok(())
628    }
629
630    /// Converts one source value from the current state cursors.
631    ///
632    /// Decodes one value through the source engine, then immediately attempts
633    /// to encode it through the target engine.
634    ///
635    /// # Parameters
636    ///
637    /// - `state`: Current conversion cursors and output buffer.
638    ///
639    /// # Returns
640    ///
641    /// Returns conversion progress when the step stops early, or `None` when
642    /// the value was fully consumed and encoded.
643    ///
644    /// # Errors
645    ///
646    /// Returns a converter error when decode or encode handling fails.
647    #[inline(always)]
648    fn convert_next(
649        &mut self,
650        state: &mut ConvertState<'_, D::Unit, E::Unit>,
651    ) -> Result<Option<TranscodeProgress>, ConvertErrorOf<D, E, DH, EH>> {
652        let step = self
653            .decode_engine
654            .decode_step(state.input(), state.decode_context())
655            .map_err(|error| {
656                error.map_domain(TranscodeConvertEngineError::decode)
657            })?;
658        state.apply_decode_step(step, |pending, state| {
659            self.encode_pending(pending, state)
660        })
661    }
662
663    /// Returns the output bound for the retained pending value.
664    ///
665    /// # Returns
666    ///
667    /// Returns the maximum target units needed to encode the pending value,
668    /// or `0` when no value is retained. Returns a capacity error when hook
669    /// planning overflows.
670    #[inline(always)]
671    fn pending_output_len(&self) -> Result<usize, CapacityError> {
672        self.pending.max_output_len(&self.encode_engine)
673    }
674
675    /// Writes a retained decoded value before new input is consumed.
676    ///
677    /// # Parameters
678    ///
679    /// - `state`: Current conversion cursors and output buffer.
680    ///
681    /// # Returns
682    ///
683    /// Returns conversion progress when the pending value needs more output
684    /// capacity, or `None` when the pending value was fully encoded.
685    ///
686    /// # Errors
687    ///
688    /// Returns a converter error when encode handling fails.
689    #[inline(always)]
690    fn drain_pending(
691        &mut self,
692        state: &mut ConvertState<'_, D::Unit, E::Unit>,
693    ) -> Result<Option<TranscodeProgress>, ConvertErrorOf<D, E, DH, EH>> {
694        let Some(pending) = self.pending.take() else {
695            return Ok(None);
696        };
697        self.encode_pending(pending, state)
698    }
699
700    /// Drains source-side decode finish output and encodes emitted final
701    /// values.
702    ///
703    /// When the decoder declares no finish output, still calls
704    /// [`TranscodeDecodeEngine::finish`] so codec flush and hook teardown can
705    /// run and fail even when zero values are emitted.
706    ///
707    /// # Parameters
708    ///
709    /// - `state`: Current conversion cursors and output buffer.
710    ///
711    /// # Returns
712    ///
713    /// Returns `Ok(())` after all decode-finish values have been encoded.
714    ///
715    /// # Errors
716    ///
717    /// Returns a converter error when decode finish or encode handling fails.
718    ///
719    /// # Panics
720    ///
721    /// Panics when a decode-finish value cannot be encoded within the
722    /// capacity reserved by [`Self::max_finish_output_len`].
723    fn drain_decoder_finish(
724        &mut self,
725        state: &mut ConvertState<'_, D::Unit, E::Unit>,
726    ) -> Result<(), ConvertErrorOf<D, E, DH, EH>>
727    where
728        D::Value: Default,
729    {
730        let value_count = self.decode_engine.max_finish_output_len()?;
731        if value_count == 0 {
732            // Skip the Vec allocation when the decoder declares no finish
733            // output. We still call finish() so that
734            // codec.decode_flush and hooks.finish_hooks both run —
735            // hooks may do validation or teardown (e.g. checksum
736            // verification) that can fail even when emitting zero
737            // values. Passing an empty slice is safe here because the capacity
738            // check inside finish() accepts required == 0 against any slice.
739            self.decode_engine.finish(&mut [], 0).map_err(|error| {
740                error.map_domain(TranscodeConvertEngineError::decode)
741            })?;
742            return Ok(());
743        }
744        // D::Value: Default is required only when value_count > 0. The bound
745        // remains on the method signature for the general case; stateless
746        // codecs never reach this branch.
747        let mut decoded: Vec<D::Value> =
748            (0..value_count).map(|_| D::Value::default()).collect();
749        let written =
750            self.decode_engine
751                .finish(&mut decoded, 0)
752                .map_err(|error| {
753                    error.map_domain(TranscodeConvertEngineError::decode)
754                })?;
755        for value in decoded.into_iter().take(written) {
756            let pending = PendingValue::new(value, 0);
757            if self.encode_pending(pending, state)?.is_some() {
758                unreachable!(
759                    "converter finish bound must reserve space for decode finish values"
760                );
761            }
762        }
763        Ok(())
764    }
765
766    /// Encodes one pending value and applies output/pending state changes.
767    ///
768    /// When the target buffer lacks capacity, the value is put back into the
769    /// pending slot and progress reports
770    /// [`crate::TranscodeStatus::NeedOutput`].
771    ///
772    /// # Parameters
773    ///
774    /// - `pending`: Decoded value waiting for target output capacity.
775    /// - `state`: Current conversion cursors and output buffer.
776    ///
777    /// # Returns
778    ///
779    /// Returns conversion progress when the value needs more output capacity,
780    /// or `None` when the value was fully encoded.
781    ///
782    /// # Errors
783    ///
784    /// Returns a converter error when encode hook handling fails.
785    fn encode_pending(
786        &mut self,
787        pending: PendingValue<D::Value>,
788        state: &mut ConvertState<'_, D::Unit, E::Unit>,
789    ) -> Result<Option<TranscodeProgress>, ConvertErrorOf<D, E, DH, EH>> {
790        let input_index = pending.input_index();
791        let output_index = state.output_cursor();
792        let context = EncodeContext::new(
793            pending.value(),
794            input_index,
795            state.output_mut(),
796            output_index,
797        );
798        let outcome =
799            self.encode_engine.encode_one(context).map_err(|error| {
800                TranscodeError::domain(TranscodeConvertEngineError::encode(
801                    TranscodeEncodeEngineError::hook(error),
802                ))
803            })?;
804        let progress = state.apply_encode_outcome(outcome);
805        if progress.is_some() {
806            self.pending.put(pending);
807        }
808        Ok(progress)
809    }
810}
811
812impl<D, E, DH, EH> Default for TranscodeConvertEngine<D, E, DH, EH>
813where
814    D: Codec + Default,
815    E: Codec<Value = D::Value> + Default,
816    DH: TranscodeDecodeHooks<D> + Default,
817    EH: TranscodeEncodeHooks<E> + Default,
818{
819    /// Creates a default buffered converter engine.
820    ///
821    /// # Returns
822    ///
823    /// Returns a converter engine constructed from default codecs and hooks.
824    #[inline(always)]
825    fn default() -> Self {
826        Self::new(D::default(), E::default(), DH::default(), EH::default())
827    }
828}
829
830impl<D, E, DH, EH> Transcoder<D::Unit, E::Unit>
831    for TranscodeConvertEngine<D, E, DH, EH>
832where
833    D: Codec,
834    E: Codec<Value = D::Value>,
835    D::Value: Default,
836    DH: TranscodeDecodeHooks<D>,
837    EH: TranscodeEncodeHooks<E>,
838{
839    type Error = TranscodeConvertEngineError<
840        TranscodeDecodeEngineError<D::DecodeError, DH::Error>,
841        TranscodeEncodeEngineError<E::EncodeError, EH::Error>,
842    >;
843
844    /// Returns an upper bound for target units produced from `input_len`
845    /// units.
846    #[inline(always)]
847    fn max_output_len(&self, input_len: usize) -> Result<usize, CapacityError> {
848        TranscodeConvertEngine::max_output_len(self, input_len)
849    }
850
851    /// Returns an upper bound for target units emitted by finishing retained
852    /// state.
853    #[inline(always)]
854    fn max_finish_output_len(&self) -> Result<usize, CapacityError> {
855        TranscodeConvertEngine::max_finish_output_len(self)
856    }
857
858    /// Returns an upper bound for target units emitted when resetting stream
859    /// state.
860    #[inline(always)]
861    fn max_reset_output_len(&self) -> Result<usize, CapacityError> {
862        TranscodeConvertEngine::max_reset_output_len(self)
863    }
864
865    /// Clears retained conversion state and emits target reset output.
866    #[inline(always)]
867    fn reset(
868        &mut self,
869        output: &mut [E::Unit],
870        output_index: usize,
871    ) -> Result<usize, TranscodeError<Self::Error>> {
872        TranscodeConvertEngine::reset(self, output, output_index)
873    }
874
875    /// Converts source units into target units.
876    #[inline(always)]
877    fn transcode(
878        &mut self,
879        input: &[D::Unit],
880        input_index: usize,
881        output: &mut [E::Unit],
882        output_index: usize,
883    ) -> Result<TranscodeProgress, TranscodeError<Self::Error>> {
884        TranscodeConvertEngine::transcode(
885            self,
886            input,
887            input_index,
888            output,
889            output_index,
890        )
891    }
892
893    /// Finishes retained converter output after EOF.
894    #[inline(always)]
895    fn finish(
896        &mut self,
897        output: &mut [E::Unit],
898        output_index: usize,
899    ) -> Result<usize, TranscodeError<Self::Error>> {
900        TranscodeConvertEngine::finish(self, output, output_index)
901    }
902}