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::engine::TranscodeDecodeEngine`] and a target
11//! [`crate::engine::TranscodeEncodeEngine`] into one unit-to-unit conversion
12//! pipeline.
13
14use core::num::NonZeroUsize;
15
16use super::super::internal::convert_state::ConvertState;
17use super::super::internal::encode_attempt::EncodeAttempt;
18use super::super::internal::lifecycle_guard::LifecycleGuard;
19use super::super::internal::pending_value::PendingValue;
20use super::super::internal::pending_value_slot::PendingValueSlot;
21use super::TranscodeDecodeHooks;
22use super::TranscodeEncodeHooks;
23use super::transcode_decode_engine::TranscodeDecodeEngine;
24use super::transcode_encode_engine::TranscodeEncodeEngine;
25use crate::CapacityError;
26use crate::Codec;
27use crate::TranscodeConvertError;
28use crate::TranscodeConvertErrorOf;
29use crate::TranscodeConverter;
30use crate::TranscodeFailure;
31use crate::TranscodeProgress;
32use crate::Transcoder;
33use crate::codec::assert_unit_bounds;
34
35/// Adds two independent target-output capacity bounds.
36///
37/// # Parameters
38///
39/// - `first`: First independent output bound.
40/// - `second`: Second independent output bound.
41///
42/// # Returns
43///
44/// The checked sum of the supplied target-unit bounds.
45///
46/// # Errors
47///
48/// Returns [`CapacityError::OutputLengthOverflow`] when the sum exceeds
49/// `usize`.
50#[inline]
51fn add_convert_output_bounds(first: usize, second: usize) -> Result<usize, CapacityError> {
52    first.checked_add(second).ok_or(CapacityError::OutputLengthOverflow)
53}
54
55/// Adds three independent target-output capacity bounds.
56///
57/// # Parameters
58///
59/// - `first`: First independent target-output bound.
60/// - `second`: Second independent target-output bound.
61/// - `third`: Third independent target-output bound.
62///
63/// # Returns
64///
65/// The checked sum of the supplied target-unit bounds.
66///
67/// # Errors
68///
69/// Returns [`CapacityError::OutputLengthOverflow`] when the sum exceeds
70/// `usize`.
71#[inline]
72fn sum_convert_output_bounds(first: usize, second: usize, third: usize) -> Result<usize, CapacityError> {
73    let partial = add_convert_output_bounds(first, second)?;
74    add_convert_output_bounds(partial, third)
75}
76
77/// Asserts that a pre-reserved conversion phase did not need more output.
78///
79/// # Parameters
80///
81/// - `progress`: `None` after draining, or `Some` when output is still needed.
82/// - `message`: Invariant explanation included in a panic.
83///
84/// # Panics
85///
86/// Panics with `message` if `progress` is `Some`.
87#[inline]
88fn assert_reserved_output_drained(progress: Option<TranscodeProgress>, message: &'static str) {
89    assert!(progress.is_none(), "{message}");
90}
91
92/// Reusable buffered conversion engine for codec-backed converters.
93///
94/// The engine owns reusable buffered decode and encode engines. It keeps
95/// common converter control flow private: index validation, pending-value
96/// retention, finish draining, decode-error policy dispatch, encode attempts,
97/// output-capacity checks, and [`crate::TranscodeStatus`] reporting.
98///
99/// Use this type to build a streaming converter over two one-value [`Codec`]
100/// implementations that share the same logical value type. Each hot-path step
101/// decodes one source unit sequence into a value, then immediately tries to
102/// encode that value into the target output buffer. If the target buffer lacks
103/// capacity, the decoded value is retained in an internal pending slot and
104/// must be drained before more source input is consumed, preserving output
105/// order across buffer turns.
106///
107/// `TranscodeConvertEngine` is intentionally batch-oriented. Its public
108/// [`Self::transcode`] method drives a source/output buffer loop and reuses the
109/// same unchecked codec and hook primitives as
110/// [`crate::engine::TranscodeDecodeEngine`] and
111/// [`crate::engine::TranscodeEncodeEngine`]. It does not call one-value public
112/// transcoders in the hot path.
113///
114/// For strict codec-backed conversion with default decode and encode policies,
115/// use [`crate::CodecTranscodeConverter`]. Use `TranscodeConvertEngine`
116/// directly when either side needs custom malformed-input repair, encode
117/// planning, skipped values, or finish-time output.
118///
119/// The engine follows the same lifecycle as [`crate::Transcoder`]:
120/// `reset → transcode* → finish → reset`. Call [`Self::reset`] before starting
121/// a new logical stream and [`Self::finish`] after EOF once any incomplete
122/// source tail has been handled.
123///
124/// # Examples
125///
126/// ```rust
127/// use core::{
128///     convert::Infallible,
129///     num::NonZeroUsize,
130/// };
131/// use qubit_codec::{
132///     Codec,
133///     TranscodeDecodeErrorOf,
134///     TranscodeEncodeErrorOf,
135///     DecodeFailure,
136///     TranscodeStatus,
137/// };
138/// use qubit_codec::engine::{
139///     DecodeContext,
140///     EncodeContext,
141///     EncodeUnencodableAction,
142///     TranscodeConvertEngine,
143///     TranscodeDecodeHooks,
144///     TranscodeEncodeHooks,
145/// };
146///
147/// #[derive(Clone, Copy)]
148/// struct SourceCodec;
149///
150/// #[derive(Clone, Copy)]
151/// struct TargetCodec;
152///
153/// impl Codec for SourceCodec {
154///     type Value = u8;
155///     type Unit = u8;
156///     type DecodeError = Infallible;
157///     type EncodeError = Infallible;
158///
159///     const MIN_UNITS_PER_VALUE: usize = 1;
160///     const MAX_ENCODE_UNITS_PER_VALUE: usize = 1;
161///     const MAX_DECODE_UNITS_PER_VALUE: usize = 1;
162///
163///     unsafe fn decode(
164///         &mut self,
165///         input: &[u8],
166///         index: usize,
167///     ) -> Result<(u8, NonZeroUsize), DecodeFailure<Self::DecodeError>> {
168///         Ok((input[index].wrapping_add(1), NonZeroUsize::MIN))
169///     }
170///
171///     unsafe fn encode(
172///         &mut self,
173///         value: &u8,
174///         output: &mut [u8],
175///         index: usize,
176///     ) -> Result<usize, Self::EncodeError> {
177///         output[index] = *value;
178///         Ok(1)
179///     }
180/// }
181///
182/// impl Codec for TargetCodec {
183///     type Value = u8;
184///     type Unit = u8;
185///     type DecodeError = Infallible;
186///     type EncodeError = Infallible;
187///
188///     const MIN_UNITS_PER_VALUE: usize = 1;
189///     const MAX_ENCODE_UNITS_PER_VALUE: usize = 1;
190///     const MAX_DECODE_UNITS_PER_VALUE: usize = 1;
191///
192///     unsafe fn decode(
193///         &mut self,
194///         input: &[u8],
195///         index: usize,
196///     ) -> Result<(u8, NonZeroUsize), DecodeFailure<Self::DecodeError>> {
197///         Ok((input[index], NonZeroUsize::MIN))
198///     }
199///
200///     unsafe fn encode(
201///         &mut self,
202///         value: &u8,
203///         output: &mut [u8],
204///         index: usize,
205///     ) -> Result<usize, Self::EncodeError> {
206///         output[index] = *value;
207///         Ok(1)
208///     }
209/// }
210///
211/// struct StrictDecodeHooks;
212///
213/// impl TranscodeDecodeHooks<SourceCodec> for StrictDecodeHooks {
214///     fn handle_invalid_decode(
215///         &mut self,
216///         _codec: &mut SourceCodec,
217///         error: &Infallible,
218///         _consumed: Option<NonZeroUsize>,
219///         _context: DecodeContext,
220///     ) -> Result<qubit_codec::engine::DecodeInvalidAction<u8>, TranscodeDecodeErrorOf<SourceCodec>> {
221///         match *error {}
222///     }
223/// }
224///
225/// struct StrictEncodeHooks;
226///
227/// impl TranscodeEncodeHooks<TargetCodec> for StrictEncodeHooks {
228///     fn handle_unencodable_encode(
229///         &mut self,
230///         _codec: &mut TargetCodec,
231///         _context: &EncodeContext<'_, u8>,
232///     ) -> Result<EncodeUnencodableAction<u8>, TranscodeEncodeErrorOf<TargetCodec>> {
233///         unreachable!("TargetCodec accepts every u8")
234///     }
235/// }
236///
237/// let mut engine = TranscodeConvertEngine::new(
238///     SourceCodec,
239///     TargetCodec,
240///     StrictDecodeHooks,
241///     StrictEncodeHooks,
242/// );
243/// let input = [1_u8, 2, 3];
244/// let mut output = [0_u8; 2];
245/// let mut reset_output = [];
246/// engine.reset(&mut reset_output, 0)?;
247///
248/// let progress = engine.transcode(&input, 0, &mut output, 0)?;
249/// match progress.status() {
250///     TranscodeStatus::NeedOutput { .. } => {
251///         assert_eq!(2, progress.written());
252///         assert_eq!([2, 3], output);
253///         // Drain `output[..progress.written()]`, then resume at
254///         // `progress.read()` with fresh output capacity.
255///     }
256///     TranscodeStatus::Complete => unreachable!("output is intentionally short"),
257///     TranscodeStatus::NeedInput { .. } => unreachable!("input is complete"),
258/// }
259/// # Ok::<(), qubit_codec::TranscodeConvertErrorOf<SourceCodec, TargetCodec>>(())
260/// ```
261///
262/// # Type Parameters
263///
264/// - `D`: Source-side decoder codec.
265/// - `E`: Target-side encoder codec.
266/// - `DH`: Source-side decode hooks.
267/// - `EH`: Target-side encode hooks.
268#[derive(Debug)]
269pub struct TranscodeConvertEngine<D, E, DH, EH>
270where
271    D: Codec,
272    E: Codec<Value = D::Value>,
273    DH: TranscodeDecodeHooks<D>,
274    EH: TranscodeEncodeHooks<E>,
275{
276    /// Source-side buffered decoder engine.
277    decode_engine: TranscodeDecodeEngine<D, DH>,
278    /// Target-side buffered encoder engine.
279    encode_engine: TranscodeEncodeEngine<E, EH>,
280    /// Decoded value waiting for target output capacity.
281    pending: PendingValueSlot<D::Value>,
282    /// Guard for the `reset → transcode* → finish` lifecycle in every build
283    /// profile. The converter owns its own guard rather than delegating to the
284    /// inner decode/encode engines, because lifecycle events here describe the
285    /// converter as a whole.
286    lifecycle: LifecycleGuard,
287}
288
289impl<D, E, DH, EH> TranscodeConvertEngine<D, E, DH, EH>
290where
291    D: Codec,
292    E: Codec<Value = D::Value>,
293    DH: TranscodeDecodeHooks<D>,
294    EH: TranscodeEncodeHooks<E>,
295{
296    /// Creates a buffered converter engine.
297    ///
298    /// The caller supplies decode hooks and encode hooks directly.
299    ///
300    /// # Parameters
301    ///
302    /// - `decoder`: Low-level codec used for source decoding.
303    /// - `encoder`: Low-level codec used for target encoding.
304    /// - `decode_hooks`: Decode-side policy hooks.
305    /// - `encode_hooks`: Encode-side policy hooks.
306    ///
307    /// # Returns
308    ///
309    /// Returns a buffered converter engine.
310    ///
311    /// # Compile-Time Checks
312    ///
313    /// Fails to compile when either codec declares a zero decode unit bound or
314    /// when [`Codec::MIN_UNITS_PER_VALUE`] exceeds
315    /// [`Codec::MAX_DECODE_UNITS_PER_VALUE`].
316    #[inline]
317    #[must_use]
318    pub fn new(decoder: D, encoder: E, decode_hooks: DH, encode_hooks: EH) -> Self {
319        assert_unit_bounds::<D>();
320        assert_unit_bounds::<E>();
321        Self {
322            decode_engine: TranscodeDecodeEngine::new(decoder, decode_hooks),
323            encode_engine: TranscodeEncodeEngine::new(encoder, encode_hooks),
324            pending: PendingValueSlot::empty(),
325            lifecycle: LifecycleGuard::new(),
326        }
327    }
328
329    /// Returns the source-side codec.
330    ///
331    /// # Returns
332    ///
333    /// Returns a shared reference to the decoder codec owned by this engine.
334    #[inline]
335    #[must_use]
336    pub fn source_codec(&self) -> &D {
337        self.decode_engine.codec()
338    }
339
340    /// Returns the source-side codec mutably.
341    ///
342    /// # Returns
343    ///
344    /// Returns a mutable reference to the decoder codec owned by this engine.
345    #[inline]
346    #[must_use]
347    pub fn source_codec_mut(&mut self) -> &mut D {
348        self.decode_engine.codec_mut()
349    }
350
351    /// Returns the target-side codec.
352    ///
353    /// # Returns
354    ///
355    /// Returns a shared reference to the encoder codec owned by this engine.
356    #[inline]
357    #[must_use]
358    pub fn target_codec(&self) -> &E {
359        self.encode_engine.codec()
360    }
361
362    /// Returns the target-side codec mutably.
363    ///
364    /// # Returns
365    ///
366    /// Returns a mutable reference to the encoder codec owned by this engine.
367    #[inline]
368    #[must_use]
369    pub fn target_codec_mut(&mut self) -> &mut E {
370        self.encode_engine.codec_mut()
371    }
372
373    /// Returns the decode hooks used by this engine.
374    ///
375    /// # Returns
376    ///
377    /// Returns a shared reference to the decode hooks owned by this engine.
378    #[inline]
379    #[must_use]
380    pub const fn decode_hooks(&self) -> &DH {
381        self.decode_engine.hooks()
382    }
383
384    /// Returns the decode hooks mutably.
385    ///
386    /// Mutating the returned hooks does not reset the converter or clear a
387    /// pending value. The replacement hooks must continue to satisfy their
388    /// global output-capacity contract for every reachable converter state.
389    ///
390    /// # Returns
391    ///
392    /// Returns a mutable reference to the decode hooks owned by this engine.
393    #[inline]
394    #[must_use]
395    pub fn decode_hooks_mut(&mut self) -> &mut DH {
396        self.decode_engine.hooks_mut()
397    }
398
399    /// Returns the encode hooks used by this engine.
400    ///
401    /// # Returns
402    ///
403    /// Returns a shared reference to the encode hooks owned by this engine.
404    #[inline]
405    #[must_use]
406    pub const fn encode_hooks(&self) -> &EH {
407        self.encode_engine.hooks()
408    }
409
410    /// Returns the encode hooks mutably.
411    ///
412    /// Mutating the returned hooks does not reset the converter or clear a
413    /// pending value. The replacement hooks must continue to satisfy their
414    /// global output-capacity contract for every reachable converter state.
415    ///
416    /// # Returns
417    ///
418    /// Returns a mutable reference to the encode hooks owned by this engine.
419    #[inline]
420    #[must_use]
421    pub fn encode_hooks_mut(&mut self) -> &mut EH {
422        self.encode_engine.hooks_mut()
423    }
424
425    /// Consumes the engine and returns its codecs and hooks.
426    ///
427    /// Any pending value and lifecycle state owned by the converter are
428    /// discarded. Callers should use this only when no further conversion state
429    /// needs to be preserved.
430    ///
431    /// # Returns
432    ///
433    /// Returns the source codec, target codec, decode hooks, and encode hooks.
434    #[inline]
435    #[must_use]
436    pub fn into_parts(self) -> (D, E, DH, EH) {
437        let Self {
438            decode_engine,
439            encode_engine,
440            ..
441        } = self;
442        let (source, decode_hooks) = decode_engine.into_parts();
443        let (target, encode_hooks) = encode_engine.into_parts();
444        (source, target, decode_hooks, encode_hooks)
445    }
446
447    /// Returns an upper bound for target units produced from `input_len` units.
448    ///
449    /// The bound sums three parts: one possible retained pending value, the
450    /// maximum decoded values from the source side, and the maximum target
451    /// units for those values on the encode side. It covers only the streaming
452    /// convert phase and remains valid even when no value is currently pending.
453    /// Downstream converters must use this engine-level API for capacity
454    /// planning instead of recomputing the bound from the source or target
455    /// [`Codec`] constants.
456    ///
457    /// # Parameters
458    ///
459    /// - `input_len`: Number of source units the caller plans to convert.
460    ///
461    /// # Returns
462    ///
463    /// Returns a conservative upper bound, or a capacity error on arithmetic
464    /// overflow.
465    ///
466    /// # Errors
467    ///
468    /// Returns [`CapacityError`] when a component bound or their sum overflows
469    /// `usize`.
470    #[must_use = "capacity planning can fail on overflow"]
471    pub fn max_transcode_output_len(&self, input_len: usize) -> Result<usize, CapacityError> {
472        let pending_units = self.encode_engine.max_transcode_output_len(1)?;
473        let decoded_values = self.decode_engine.max_transcode_output_len(input_len)?;
474        let converted_units = self.encode_engine.max_transcode_output_len(decoded_values)?;
475        add_convert_output_bounds(converted_units, pending_units)
476    }
477
478    /// Returns the global maximum target units emitted when resetting stream
479    /// state.
480    ///
481    /// Covers decode-side reset values (encoded to target units) plus
482    /// encode-side reset units. Most codecs are stateless and return `0`
483    /// for [`Codec::MAX_DECODE_RESET_VALUES`]; in that case this equals the
484    /// encode reset bound only.
485    ///
486    /// # Returns
487    ///
488    /// Returns the combined decode-reset and encode-reset output bound, or a
489    /// capacity error on arithmetic overflow.
490    ///
491    /// # Errors
492    ///
493    /// Returns [`CapacityError`] when a component bound or their sum overflows
494    /// `usize`.
495    #[must_use = "capacity planning can fail on overflow"]
496    pub fn max_reset_output_len(&self) -> Result<usize, CapacityError> {
497        let decode_reset_units = self
498            .encode_engine
499            .max_transcode_output_len(D::MAX_DECODE_RESET_VALUES)?;
500        let encode_reset_units = E::MAX_ENCODE_RESET_UNITS;
501        add_convert_output_bounds(decode_reset_units, encode_reset_units)
502    }
503
504    /// Returns the maximum target units emitted by finishing retained state.
505    ///
506    /// The bound covers one possible retained pending value, the global
507    /// decode-side finish-value bound (encoded to target units), and the
508    /// global encode-side finish-unit bound. It is independent of whether a
509    /// pending value currently exists.
510    ///
511    /// # Returns
512    ///
513    /// Returns the combined pending, decode-finish, and encode-finish output
514    /// bound, or a capacity error on arithmetic overflow.
515    ///
516    /// # Errors
517    ///
518    /// Returns [`CapacityError`] when a component bound or their sum overflows
519    /// `usize`.
520    #[must_use = "capacity planning can fail on overflow"]
521    pub fn max_finish_output_len(&self) -> Result<usize, CapacityError> {
522        let pending_units = self.encode_engine.max_transcode_output_len(1)?;
523        let decoder_finish_values = self.decode_engine.max_finish_output_len()?;
524        let decoder_finish_units = self.encode_engine.max_transcode_output_len(decoder_finish_values)?;
525        let encoder_finish_units = self.encode_engine.max_finish_output_len()?;
526        sum_convert_output_bounds(pending_units, decoder_finish_units, encoder_finish_units)
527    }
528
529    /// Returns the maximum target units needed by a complete one-shot
530    /// conversion.
531    ///
532    /// The returned bound covers conversion reset output, the streaming convert
533    /// phase for `input_len` source units, and finish output. Its components
534    /// are global across transient state. Higher-level complete conversion
535    /// helpers should use this engine-level bound instead of recomputing
536    /// capacity from the source or target codec constants, because decode
537    /// and encode hooks may change streaming or finish output.
538    ///
539    /// # Parameters
540    ///
541    /// - `input_len`: Number of source units in the complete stream.
542    ///
543    /// # Returns
544    ///
545    /// Returns the complete-stream target-output bound, or a capacity error on
546    /// arithmetic overflow.
547    ///
548    /// # Errors
549    ///
550    /// Returns [`CapacityError`] when a component bound or their sum overflows
551    /// `usize`.
552    #[must_use = "capacity planning can fail on overflow"]
553    pub fn max_total_output_len(&self, input_len: usize) -> Result<usize, CapacityError> {
554        let reset = self.max_reset_output_len()?;
555        let transcode = self.max_transcode_output_len(input_len)?;
556        let finish = self.max_finish_output_len()?;
557        sum_convert_output_bounds(reset, transcode, finish)
558    }
559
560    /// Clears retained conversion state, runs before-reset hooks, and emits
561    /// stream-start encode output.
562    ///
563    /// Reset clears any retained pending value, resets the target encoder, then
564    /// drains decode-side reset values through that reset encoder state. The
565    /// caller must provide enough output capacity for
566    /// [`Self::max_reset_output_len`].
567    ///
568    /// # Parameters
569    ///
570    /// - `output`: Complete output unit slice visible to the converter.
571    /// - `output_index`: Absolute output index where writing starts.
572    ///
573    /// # Returns
574    ///
575    /// Returns the number of target units written while resetting stream state.
576    ///
577    /// # Errors
578    ///
579    /// Returns a converter error if reset validation or target reset output
580    /// emission fails. Capacity and index failures occur before converter state
581    /// is changed. Once reset execution starts, an error poisons the converter
582    /// until a later reset succeeds.
583    ///
584    /// # Panics
585    ///
586    /// Panics when decode-reset values cannot be encoded within the capacity
587    /// reserved by [`Self::max_reset_output_len`].
588    pub fn reset(&mut self, output: &mut [E::Unit], output_index: usize) -> Result<usize, TranscodeConvertErrorOf<D, E>>
589    where
590        D::Value: Default,
591    {
592        let required = self.max_reset_output_len()?;
593        TranscodeFailure::ensure_output_capacity(output.len(), output_index, required)?;
594        self.lifecycle.on_reset_start();
595
596        self.pending.clear();
597
598        // Reset the target first because source-side reset values must be
599        // encoded under the target's new-stream state. The reset bound reserves
600        // space for both target-owned output and the encoded source-reset
601        // values, so encode_pending should never report `NeedOutput` here.
602        let empty_input: &[D::Unit] = &[];
603        let mut state = ConvertState::new(empty_input, 0, output, output_index);
604        let output_cursor = state.output_cursor();
605        let encoder_written = self.encode_engine.reset(state.output_mut(), output_cursor)?;
606        state.advance_output(encoder_written);
607        self.drain_decoder_reset(&mut state)?;
608        self.lifecycle.on_reset_success();
609        Ok(state.written())
610    }
611
612    /// Converts source units into target units.
613    ///
614    /// The engine drains any retained pending value before consuming new input.
615    /// Each loop iteration decodes one source value and immediately attempts to
616    /// encode it. Conversion stops when the input tail is incomplete, when the
617    /// output buffer cannot hold the next encoded value, or when the visible
618    /// input is exhausted.
619    ///
620    /// # Parameters
621    ///
622    /// - `input`: Complete input unit slice visible to the converter.
623    /// - `input_index`: Absolute input index where conversion starts.
624    /// - `output`: Complete output unit slice visible to the converter.
625    /// - `output_index`: Absolute output index where writing starts.
626    ///
627    /// # Returns
628    ///
629    /// Returns conversion progress describing input units consumed, target
630    /// units written, and why conversion stopped.
631    ///
632    /// # Errors
633    ///
634    /// Returns hook errors when indices are invalid or concrete conversion
635    /// fails. Invalid output indices are reported through the encode-side
636    /// error path. Returns [`TranscodeFailure::TranscodeBeforeReset`] when the
637    /// engine has not completed its first reset. Returns
638    /// [`TranscodeFailure::TranscodeAfterFinish`] when the logical stream was
639    /// already finished and has not been reset, or
640    /// [`TranscodeFailure::LifecyclePoisoned`] when an earlier reset or finish
641    /// failed after execution started.
642    #[inline]
643    pub fn transcode(
644        &mut self,
645        input: &[D::Unit],
646        input_index: usize,
647        output: &mut [E::Unit],
648        output_index: usize,
649    ) -> Result<TranscodeProgress, TranscodeConvertErrorOf<D, E>> {
650        self.transcode_with_eof(input, input_index, output, output_index, false)
651    }
652
653    /// Converts source units after the caller has established end of input.
654    ///
655    /// # Parameters
656    ///
657    /// - `input`: Source units visible to this call.
658    /// - `input_index`: Starting index in `input`.
659    /// - `output`: Destination for target units.
660    /// - `output_index`: Starting index in `output`.
661    ///
662    /// # Returns
663    ///
664    /// Consumed source units, written target units, and the stopping reason.
665    ///
666    /// # Errors
667    ///
668    /// Returns an error for invalid indices, a missing reset, a finished or
669    /// poisoned stream, or codec/hook conversion failure.
670    #[inline]
671    pub fn transcode_eof(
672        &mut self,
673        input: &[D::Unit],
674        input_index: usize,
675        output: &mut [E::Unit],
676        output_index: usize,
677    ) -> Result<TranscodeProgress, TranscodeConvertErrorOf<D, E>> {
678        self.transcode_with_eof(input, input_index, output, output_index, true)
679    }
680
681    /// Finishes retained output after EOF.
682    ///
683    /// Finalization drains a pending decoded value first, then lets the
684    /// source-side decode hooks emit final values, encodes those values through
685    /// the target-side encode hooks, and finally finishes target-side encode
686    /// hook state. The decode-finish value buffer used for this cold path
687    /// requires `D::Value: Default`; the normal [`Self::transcode`] loop does
688    /// not.
689    ///
690    /// # Parameters
691    ///
692    /// - `output`: Complete output unit slice visible to the converter.
693    /// - `output_index`: Absolute output index where writing starts.
694    ///
695    /// # Returns
696    ///
697    /// Returns the number of target units written during finalization.
698    ///
699    /// # Errors
700    ///
701    /// Returns a converter error when output capacity checks fail or when
702    /// hook finalization fails. Returns
703    /// [`TranscodeFailure::FinishBeforeReset`] when the engine has not
704    /// completed its first reset. Returns
705    /// [`TranscodeFailure::FinishAfterFinish`] when the logical stream was
706    /// already finished and has not been reset, or
707    /// [`TranscodeFailure::LifecyclePoisoned`] when an earlier reset or finish
708    /// failed after execution started. Capacity and index failures occur before
709    /// finish execution and remain retryable; later failures poison the
710    /// converter until reset succeeds.
711    ///
712    /// # Panics
713    ///
714    /// Panics when a retained pending value or decode-finish value cannot be
715    /// encoded within the planned finish capacity.
716    pub fn finish(
717        &mut self,
718        output: &mut [E::Unit],
719        output_index: usize,
720    ) -> Result<usize, TranscodeConvertErrorOf<D, E>>
721    where
722        D::Value: Default,
723    {
724        self.lifecycle.on_finish_attempt()?;
725        let required = self.current_finish_output_len()?;
726        TranscodeFailure::ensure_output_capacity(output.len(), output_index, required)?;
727        self.lifecycle.on_finish_start();
728
729        let empty_input: &[D::Unit] = &[];
730        let mut state = ConvertState::new(empty_input, 0, output, output_index);
731        // Finish keeps the same priority as transcode: output any retained
732        // decoded value before asking source-side hooks for final values.
733        let progress = self.drain_pending(&mut state)?;
734        assert_reserved_output_drained(progress, "converter finish bound must reserve space for pending values");
735
736        // Source-side finish may emit one or more final values. Drain them into
737        // the target encoder before finishing target-side hook state.
738        self.drain_decoder_finish(&mut state)?;
739
740        let output_cursor = state.output_cursor();
741        let written = self.encode_engine.finish(state.output_mut(), output_cursor)?;
742        state.advance_output(written);
743        self.lifecycle.on_finish_success();
744        Ok(state.written())
745    }
746
747    /// Runs a complete one-shot `reset -> transcode -> finish` conversion.
748    ///
749    /// The complete input is supplied as `input`, and output starts at index
750    /// `0` in `output`. Callers that need subranges should slice their
751    /// buffers before calling this method. Downstream one-shot converter
752    /// helpers should call this engine method instead of reproducing the
753    /// reset, transcode, and finish sequence themselves.
754    ///
755    /// # Parameters
756    ///
757    /// - `input`: Complete source unit slice.
758    /// - `output`: Target unit slice for the whole converted stream.
759    ///
760    /// # Returns
761    ///
762    /// Returns the number of target units written.
763    ///
764    /// # Errors
765    ///
766    /// Returns framework errors for insufficient output, capacity overflow, or
767    /// an incomplete EOF tail, and domain errors from reset, conversion, or
768    /// finish.
769    ///
770    /// # Panics
771    ///
772    /// Panics if codec/hook lifecycle output exceeds its declared capacity
773    /// bound.
774    #[inline]
775    pub fn transcode_complete_into(
776        &mut self,
777        input: &[D::Unit],
778        output: &mut [E::Unit],
779    ) -> Result<usize, TranscodeConvertErrorOf<D, E>>
780    where
781        D::Value: Default,
782    {
783        <Self as Transcoder>::transcode_complete_into(self, input, output)
784    }
785
786    /// Returns the finish-output bound for the converter's current pending
787    /// state.
788    ///
789    /// Public capacity methods expose global bounds across every transient
790    /// state. A concrete finish call can use this narrower checked bound while
791    /// still relying on global component bounds for decoder and encoder
792    /// finalization.
793    ///
794    /// # Returns
795    ///
796    /// Returns the current finish-output bound, or a capacity error on
797    /// arithmetic overflow.
798    ///
799    /// # Errors
800    ///
801    /// Returns [`CapacityError`] when a component bound or their sum overflows
802    /// `usize`.
803    fn current_finish_output_len(&self) -> Result<usize, CapacityError> {
804        let pending_units = self.pending.current_output_len(&self.encode_engine)?;
805        let decoder_finish_values = self.decode_engine.max_finish_output_len()?;
806        let decoder_finish_units = self.encode_engine.max_transcode_output_len(decoder_finish_values)?;
807        let encoder_finish_units = self.encode_engine.max_finish_output_len()?;
808        sum_convert_output_bounds(pending_units, decoder_finish_units, encoder_finish_units)
809    }
810
811    /// Runs one conversion step with an explicit end-of-input flag.
812    ///
813    /// The helper owns the shared lifecycle and cursor checks used by both
814    /// streaming and EOF-aware public entry points.
815    ///
816    /// # Parameters
817    ///
818    /// - `input`: Source units visible to this call.
819    /// - `input_index`: Starting index in `input`.
820    /// - `output`: Destination for target units.
821    /// - `output_index`: Starting index in `output`.
822    /// - `end_of_input`: Whether the source is closed and EOF decode rules
823    ///   apply.
824    ///
825    /// # Returns
826    ///
827    /// Consumed source units, written target units, and the stopping reason.
828    ///
829    /// # Errors
830    ///
831    /// Returns an error for invalid indices, a missing reset, a finished or
832    /// poisoned stream, or codec/hook conversion failure.
833    fn transcode_with_eof(
834        &mut self,
835        input: &[D::Unit],
836        input_index: usize,
837        output: &mut [E::Unit],
838        output_index: usize,
839        end_of_input: bool,
840    ) -> Result<TranscodeProgress, TranscodeConvertErrorOf<D, E>> {
841        self.lifecycle.on_transcode()?;
842        TranscodeFailure::ensure_transcode_indices(input.len(), input_index, output.len(), output_index)?;
843
844        let mut state = ConvertState::new(input, input_index, output, output_index);
845
846        // A retained decoded value must be written before consuming more input,
847        // otherwise callers could observe output reordered across buffer turns.
848        if let Some(progress) = self.drain_pending(&mut state)? {
849            return Ok(progress);
850        }
851
852        let min_input_units =
853            NonZeroUsize::new(D::MIN_UNITS_PER_VALUE).expect("Codec::MIN_UNITS_PER_VALUE is non-zero");
854        let min_input_len = min_input_units.get();
855        while state.has_input() {
856            let available = state.available_input();
857            if available < min_input_len && !end_of_input {
858                return Ok(state.need_input_progress(min_input_units));
859            }
860
861            let previous_read = state.read();
862            // Each hot-path step decodes one source value and immediately tries
863            // to encode it, preserving backpressure at the target output.
864            if let Some(progress) = self.convert_next(&mut state, end_of_input)? {
865                return Ok(progress);
866            }
867            debug_assert!(
868                state.read() > previous_read,
869                "TranscodeConvertEngine conversion step must consume input or stop",
870            );
871        }
872
873        Ok(state.complete_progress())
874    }
875
876    /// Drains source-side decode reset output and encodes emitted reset
877    /// values.
878    ///
879    /// Stateless decoders still call [`TranscodeDecodeEngine::reset`] so hook
880    /// teardown side effects run even when no reset values are emitted.
881    ///
882    /// # Parameters
883    ///
884    /// - `state`: Current conversion cursors and output buffer.
885    ///
886    /// # Returns
887    ///
888    /// Returns `Ok(())` after all decode-reset values have been encoded.
889    ///
890    /// # Errors
891    ///
892    /// Returns a converter error when decode reset or encode reset handling
893    /// fails.
894    ///
895    /// # Panics
896    ///
897    /// Panics when a decode-reset value cannot be encoded within the capacity
898    /// reserved by [`Self::max_reset_output_len`].
899    fn drain_decoder_reset(
900        &mut self,
901        state: &mut ConvertState<'_, D::Unit, E::Unit>,
902    ) -> Result<(), TranscodeConvertErrorOf<D, E>>
903    where
904        D::Value: Default,
905    {
906        let value_count = D::MAX_DECODE_RESET_VALUES;
907        if value_count == 0 {
908            // Stateless decoder: still call decode_reset so codecs whose
909            // hooks own teardown side effects (e.g. clearing accumulators)
910            // run them. The empty slice is safe because the capacity check
911            // inside `reset` accepts `required == 0` against any slice.
912            self.decode_engine.reset(&mut [], 0)?;
913            return Ok(());
914        }
915        // `D::Value: Default` is only consulted when the decoder declares
916        // reset output. Stateless codecs never reach this branch.
917        let mut reset_values: Vec<D::Value> = (0..value_count).map(|_| D::Value::default()).collect();
918        let written = self.decode_engine.reset(&mut reset_values, 0)?;
919        for value in reset_values.into_iter().take(written) {
920            let pending = PendingValue::new(value, 0);
921            let progress = self.encode_pending(pending, state)?;
922            assert_reserved_output_drained(
923                progress,
924                "converter reset bound must reserve space for decode reset values",
925            );
926        }
927        Ok(())
928    }
929
930    /// Converts one source value from the current state cursors.
931    ///
932    /// Decodes one value through the source engine, then immediately attempts
933    /// to encode it through the target engine.
934    ///
935    /// # Parameters
936    ///
937    /// - `state`: Current conversion cursors and output buffer.
938    /// - `end_of_input`: Whether to apply EOF-aware decode rules.
939    ///
940    /// # Returns
941    ///
942    /// Returns conversion progress when the step stops early, or `None` when
943    /// the value was consumed and either encoded or skipped by policy.
944    ///
945    /// # Errors
946    ///
947    /// Returns a converter error when decode or encode handling fails.
948    #[inline]
949    fn convert_next(
950        &mut self,
951        state: &mut ConvertState<'_, D::Unit, E::Unit>,
952        end_of_input: bool,
953    ) -> Result<Option<TranscodeProgress>, TranscodeConvertErrorOf<D, E>> {
954        let (outcome, pending) =
955            self.decode_engine
956                .decode_one(state.input(), state.decode_context(), end_of_input, PendingValue::new)?;
957        if let Some(pending) = pending {
958            self.pending.put(pending);
959        }
960        if let Some(progress) = state.apply_decode_outcome(outcome) {
961            return Ok(Some(progress));
962        }
963        let Some(pending) = self.pending.take() else {
964            return Ok(None);
965        };
966        self.encode_pending(pending, state)
967    }
968
969    /// Writes a retained decoded value before new input is consumed.
970    ///
971    /// # Parameters
972    ///
973    /// - `state`: Current conversion cursors and output buffer.
974    ///
975    /// # Returns
976    ///
977    /// Returns conversion progress when the pending value needs more output
978    /// capacity, or `None` when no value was pending or the value was encoded
979    /// or skipped by policy.
980    ///
981    /// # Errors
982    ///
983    /// Returns a converter error when encode handling fails.
984    #[inline]
985    fn drain_pending(
986        &mut self,
987        state: &mut ConvertState<'_, D::Unit, E::Unit>,
988    ) -> Result<Option<TranscodeProgress>, TranscodeConvertErrorOf<D, E>> {
989        let Some(pending) = self.pending.take() else {
990            return Ok(None);
991        };
992        self.encode_pending(pending, state)
993    }
994
995    /// Drains source-side decode finish output and encodes emitted final
996    /// values.
997    ///
998    /// When the decoder declares no finish output, still calls
999    /// [`TranscodeDecodeEngine::finish`] so codec finish and hook teardown can
1000    /// run and fail even when zero values are emitted.
1001    ///
1002    /// # Parameters
1003    ///
1004    /// - `state`: Current conversion cursors and output buffer.
1005    ///
1006    /// # Returns
1007    ///
1008    /// Returns `Ok(())` after all decode-finish values have been encoded.
1009    ///
1010    /// # Errors
1011    ///
1012    /// Returns a converter error when decode finish or encode handling fails.
1013    ///
1014    /// # Panics
1015    ///
1016    /// Panics when a decode-finish value cannot be encoded within the
1017    /// capacity reserved by [`Self::max_finish_output_len`].
1018    fn drain_decoder_finish(
1019        &mut self,
1020        state: &mut ConvertState<'_, D::Unit, E::Unit>,
1021    ) -> Result<(), TranscodeConvertErrorOf<D, E>>
1022    where
1023        D::Value: Default,
1024    {
1025        let value_count = self.decode_engine.max_finish_output_len()?;
1026        if value_count == 0 {
1027            // Skip the Vec allocation when the decoder declares no finish
1028            // output. We still call finish() so that
1029            // codec.decode_finish and hooks.finish_hooks both run —
1030            // hooks may do validation or teardown (e.g. checksum
1031            // verification) that can fail even when emitting zero
1032            // values. Passing an empty slice is safe here because the capacity
1033            // check inside finish() accepts required == 0 against any slice.
1034            self.decode_engine.finish(&mut [], 0)?;
1035            return Ok(());
1036        }
1037        // D::Value: Default is required only when value_count > 0. The bound
1038        // remains on the method signature for the general case; stateless
1039        // codecs never reach this branch.
1040        let mut decoded: Vec<D::Value> = (0..value_count).map(|_| D::Value::default()).collect();
1041        let written = self.decode_engine.finish(&mut decoded, 0)?;
1042        for value in decoded.into_iter().take(written) {
1043            let pending = PendingValue::new(value, 0);
1044            let progress = self.encode_pending(pending, state)?;
1045            assert_reserved_output_drained(
1046                progress,
1047                "converter finish bound must reserve space for decode finish values",
1048            );
1049        }
1050        Ok(())
1051    }
1052
1053    /// Encodes one pending value and applies output/pending state changes.
1054    ///
1055    /// When the target buffer lacks capacity, the value is put back into the
1056    /// pending slot and progress reports
1057    /// [`crate::TranscodeStatus::NeedOutput`].
1058    ///
1059    /// # Parameters
1060    ///
1061    /// - `pending`: Decoded value waiting for target output capacity.
1062    /// - `state`: Current conversion cursors and output buffer.
1063    ///
1064    /// # Returns
1065    ///
1066    /// Returns conversion progress when the value needs more output capacity,
1067    /// or `None` when the value was encoded or skipped by policy.
1068    ///
1069    /// # Errors
1070    ///
1071    /// Returns a converter error when encode hook handling fails.
1072    fn encode_pending(
1073        &mut self,
1074        pending: PendingValue<D::Value>,
1075        state: &mut ConvertState<'_, D::Unit, E::Unit>,
1076    ) -> Result<Option<TranscodeProgress>, TranscodeConvertErrorOf<D, E>> {
1077        let input_index = pending.input_index();
1078        let output_index = state.output_cursor();
1079        let attempt = EncodeAttempt::new(pending.value(), input_index, state.output_mut(), output_index);
1080        let outcome = match self.encode_engine.encode_one(attempt) {
1081            Ok(outcome) => outcome,
1082            Err(error) => {
1083                return Err(TranscodeConvertError::from_encode_error_with_value(
1084                    error,
1085                    pending.into_value(),
1086                ));
1087            }
1088        };
1089        let progress = state.apply_encode_outcome(outcome);
1090        if progress.is_some() {
1091            self.pending.put(pending);
1092        }
1093        Ok(progress)
1094    }
1095}
1096
1097impl<D, E, DH, EH> Default for TranscodeConvertEngine<D, E, DH, EH>
1098where
1099    D: Codec + Default,
1100    E: Codec<Value = D::Value> + Default,
1101    DH: TranscodeDecodeHooks<D> + Default,
1102    EH: TranscodeEncodeHooks<E> + Default,
1103{
1104    /// Creates a default buffered converter engine.
1105    ///
1106    /// # Returns
1107    ///
1108    /// Returns a converter engine constructed from default codecs and hooks.
1109    fn default() -> Self {
1110        Self::new(D::default(), E::default(), DH::default(), EH::default())
1111    }
1112}
1113
1114impl<D, E, DH, EH> Transcoder for TranscodeConvertEngine<D, E, DH, EH>
1115where
1116    D: Codec,
1117    E: Codec<Value = D::Value>,
1118    D::Value: Default,
1119    DH: TranscodeDecodeHooks<D>,
1120    EH: TranscodeEncodeHooks<E>,
1121{
1122    /// Source representation units accepted by the decoder.
1123    type Input = D::Unit;
1124    /// Target representation units emitted by the encoder.
1125    type Output = E::Unit;
1126    /// Source, target, contract, and capacity failures in the pipeline.
1127    type Error = TranscodeConvertErrorOf<D, E>;
1128
1129    /// Returns an upper bound for target units produced from `input_len`
1130    /// units.
1131    ///
1132    /// # Parameters
1133    ///
1134    /// - `input_len`: Number of source units to include in capacity planning.
1135    ///
1136    /// # Returns
1137    ///
1138    /// The target-unit capacity bound, including the applicable retained state.
1139    ///
1140    /// # Errors
1141    ///
1142    /// Returns [`CapacityError`] when a component bound or their sum overflows
1143    /// `usize`.
1144    #[inline]
1145    fn max_transcode_output_len(&self, input_len: usize) -> Result<usize, CapacityError> {
1146        TranscodeConvertEngine::max_transcode_output_len(self, input_len)
1147    }
1148
1149    /// Returns an upper bound for target units emitted by finishing retained
1150    /// state.
1151    ///
1152    /// # Returns
1153    ///
1154    /// The target-unit capacity bound, including the applicable retained state.
1155    ///
1156    /// # Errors
1157    ///
1158    /// Returns [`CapacityError`] when a component bound or their sum overflows
1159    /// `usize`.
1160    #[inline]
1161    fn max_finish_output_len(&self) -> Result<usize, CapacityError> {
1162        TranscodeConvertEngine::max_finish_output_len(self)
1163    }
1164
1165    /// Returns an upper bound for target units emitted when resetting stream
1166    /// state.
1167    ///
1168    /// # Returns
1169    ///
1170    /// The target-unit capacity bound, including the applicable retained state.
1171    ///
1172    /// # Errors
1173    ///
1174    /// Returns [`CapacityError`] when a component bound or their sum overflows
1175    /// `usize`.
1176    #[inline]
1177    fn max_reset_output_len(&self) -> Result<usize, CapacityError> {
1178        TranscodeConvertEngine::max_reset_output_len(self)
1179    }
1180
1181    /// Clears retained conversion state and emits target reset output.
1182    ///
1183    /// # Parameters
1184    ///
1185    /// - `output`: Destination for lifecycle output units.
1186    /// - `output_index`: Starting index in `output`.
1187    ///
1188    /// # Returns
1189    ///
1190    /// The number of lifecycle output units written.
1191    ///
1192    /// # Errors
1193    ///
1194    /// Returns the engine error for invalid indices, insufficient capacity,
1195    /// an invalid lifecycle transition, or codec/hook failure. See the inherent
1196    /// [`TranscodeConvertEngine::reset`] method for retry and poisoning
1197    /// semantics.
1198    ///
1199    /// # Panics
1200    ///
1201    /// Panics if a codec/hook output bound fails to reserve enough space for
1202    /// emitted values.
1203    #[inline]
1204    fn reset(&mut self, output: &mut [E::Unit], output_index: usize) -> Result<usize, TranscodeConvertErrorOf<D, E>> {
1205        TranscodeConvertEngine::reset(self, output, output_index)
1206    }
1207
1208    /// Converts source units into target units.
1209    ///
1210    /// # Parameters
1211    ///
1212    /// - `input`: Source units visible to this call.
1213    /// - `input_index`: Starting index in `input`.
1214    /// - `output`: Destination for target units.
1215    /// - `output_index`: Starting index in `output`.
1216    ///
1217    /// # Returns
1218    ///
1219    /// Consumed source units, written target units, and the stopping reason.
1220    ///
1221    /// # Errors
1222    ///
1223    /// Returns an error for invalid indices, a missing reset, a finished or
1224    /// poisoned stream, or codec/hook conversion failure.
1225    #[inline]
1226    fn transcode(
1227        &mut self,
1228        input: &[D::Unit],
1229        input_index: usize,
1230        output: &mut [E::Unit],
1231        output_index: usize,
1232    ) -> Result<TranscodeProgress, TranscodeConvertErrorOf<D, E>> {
1233        TranscodeConvertEngine::transcode(self, input, input_index, output, output_index)
1234    }
1235
1236    /// Transcodes an input segment while marking it as the end of the stream.
1237    ///
1238    /// This forwarding implementation delegates to the engine's EOF-aware
1239    /// operation so the decoder can resolve trailing input using EOF rules.
1240    /// Call `finish` separately to drain retained lifecycle output.
1241    ///
1242    /// # Parameters
1243    ///
1244    /// - `input`: Source units visible to this call.
1245    /// - `input_index`: Starting index in `input`.
1246    /// - `output`: Destination for target units.
1247    /// - `output_index`: Starting index in `output`.
1248    ///
1249    /// # Returns
1250    ///
1251    /// Consumed source units, written target units, and the stopping reason.
1252    ///
1253    /// # Errors
1254    ///
1255    /// Returns an error for invalid indices, a missing reset, a finished or
1256    /// poisoned stream, or codec/hook conversion failure.
1257    #[inline]
1258    fn transcode_eof(
1259        &mut self,
1260        input: &[D::Unit],
1261        input_index: usize,
1262        output: &mut [E::Unit],
1263        output_index: usize,
1264    ) -> Result<TranscodeProgress, TranscodeConvertErrorOf<D, E>> {
1265        TranscodeConvertEngine::transcode_eof(self, input, input_index, output, output_index)
1266    }
1267
1268    /// Finishes retained converter output after EOF.
1269    ///
1270    /// # Parameters
1271    ///
1272    /// - `output`: Destination for lifecycle output units.
1273    /// - `output_index`: Starting index in `output`.
1274    ///
1275    /// # Returns
1276    ///
1277    /// The number of lifecycle output units written.
1278    ///
1279    /// # Errors
1280    ///
1281    /// Returns the engine error for invalid indices, insufficient capacity,
1282    /// an invalid lifecycle transition, or codec/hook failure. See the inherent
1283    /// [`TranscodeConvertEngine::finish`] method for retry and poisoning
1284    /// semantics.
1285    ///
1286    /// # Panics
1287    ///
1288    /// Panics if a codec/hook output bound fails to reserve enough space for
1289    /// emitted values.
1290    #[inline]
1291    fn finish(&mut self, output: &mut [E::Unit], output_index: usize) -> Result<usize, TranscodeConvertErrorOf<D, E>> {
1292        TranscodeConvertEngine::finish(self, output, output_index)
1293    }
1294}
1295
1296impl<D, E, DH, EH> TranscodeConverter for TranscodeConvertEngine<D, E, DH, EH>
1297where
1298    D: Codec,
1299    E: Codec<Value = D::Value>,
1300    D::Value: Default,
1301    DH: TranscodeDecodeHooks<D>,
1302    EH: TranscodeEncodeHooks<E>,
1303{
1304    /// Source codec failures retained in conversion errors.
1305    type DecodeError = D::DecodeError;
1306    /// Target codec failures retained in conversion errors.
1307    type EncodeError = E::EncodeError;
1308    /// Shared logical value transferred between the two codecs.
1309    type Value = D::Value;
1310}