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.
9
10use super::super::internal::{
11    convert_error_of::ConvertErrorOf,
12    convert_progress_result::ConvertProgressResult,
13    convert_state::ConvertState,
14    convert_step_result::ConvertStepResult,
15    encode_step::EncodeStep,
16    pending_encode_step::PendingEncodeStep,
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    TranscodeConvertHooks,
30    TranscodeError,
31};
32
33/// Reusable buffered conversion engine.
34///
35/// The engine owns reusable buffered decode and encode engines plus a small
36/// conversion-level hook object. It keeps common converter control flow
37/// private: index validation, pending-value retention, pending flush,
38/// decode-error policy dispatch, encode planning, output-capacity checks, and
39/// progress reporting.
40///
41/// `TranscodeConvertEngine` is intentionally batch-oriented. Its public
42/// `transcode` method drives a source/output buffer loop and reuses the same
43/// unchecked codec and hook primitives as [`crate::TranscodeDecodeEngine`] and
44/// [`crate::TranscodeEncodeEngine`]. It does not call one-value public
45/// transcoders in the hot path.
46///
47/// # Type Parameters
48///
49/// - `D`: Source-side decoder codec.
50/// - `E`: Target-side encoder codec.
51/// - `H`: Conversion-level policy hooks.
52#[derive(Clone, Debug, Eq, Hash, PartialEq)]
53pub struct TranscodeConvertEngine<D, E, H>
54where
55    D: Codec,
56    E: Codec<Value = D::Value>,
57    H: TranscodeConvertHooks<D, E>,
58{
59    /// Source-side buffered decoder engine.
60    decode_engine: TranscodeDecodeEngine<D, H::DecodeHooks>,
61    /// Target-side buffered encoder engine.
62    encode_engine: TranscodeEncodeEngine<E, H::EncodeHooks>,
63    /// Conversion-level policy hooks.
64    hooks: H,
65    /// Decoded value waiting for target output capacity.
66    pending: PendingValueSlot<D::Value>,
67}
68
69impl<D, E, H> TranscodeConvertEngine<D, E, H>
70where
71    D: Codec,
72    E: Codec<Value = D::Value>,
73    H: TranscodeConvertHooks<D, E>,
74{
75    /// Creates a buffered converter engine.
76    ///
77    /// The supplied conversion hooks create the internal decode and encode hook
78    /// instances. This keeps codec-specific hook initialization with the
79    /// conversion policy instead of requiring those hook types to implement
80    /// [`Default`].
81    ///
82    /// # Parameters
83    ///
84    /// - `decoder`: Low-level codec used for source decoding.
85    /// - `encoder`: Low-level codec used for target encoding.
86    /// - `hooks`: Conversion-level policy hooks.
87    ///
88    /// # Returns
89    ///
90    /// Returns a buffered converter engine.
91    /// # Panics
92    ///
93    /// Panics when either codec violates the
94    /// [`Codec::min_units_per_value`] / [`Codec::max_units_per_value`] ordering
95    /// invariant.
96    #[must_use]
97    #[inline]
98    pub fn new(decoder: D, encoder: E, hooks: H) -> Self {
99        assert_unit_bounds::<D>(&decoder);
100        assert_unit_bounds::<E>(&encoder);
101        let decode_hooks = hooks.create_decode_hooks(&decoder, &encoder);
102        let encode_hooks = hooks.create_encode_hooks(&decoder, &encoder);
103        Self::from_parts(decoder, encoder, hooks, decode_hooks, encode_hooks)
104    }
105
106    /// Builds the engine from already-created component hooks.
107    ///
108    /// Callers that use [`new`](Self::new) do not need to call this directly;
109    /// this method is provided for advanced cases where decode and encode
110    /// hooks are constructed externally. It constructs the component decode and
111    /// encode engines through their public constructors, so both codecs are
112    /// still validated against the [`Codec`] unit-bound invariant.
113    ///
114    /// # Type Parameters
115    ///
116    /// - `D`: Source-side decoder codec.
117    /// - `E`: Target-side encoder codec.
118    /// - `H`: Conversion-level policy hooks.
119    ///
120    /// # Parameters
121    ///
122    /// - `decoder`: Low-level decode codec.
123    /// - `encoder`: Low-level encode codec.
124    /// - `hooks`: Conversion-level hook aggregator.
125    /// - `decode_hooks`: Decode hooks instance created from `hooks`.
126    /// - `encode_hooks`: Encode hooks instance created from `hooks`.
127    ///
128    /// # Returns
129    ///
130    /// Returns an engine assembled from the provided codecs and hooks.
131    ///
132    /// # Panics
133    ///
134    /// Panics when either codec violates the
135    /// [`Codec::min_units_per_value`] / [`Codec::max_units_per_value`] ordering
136    /// invariant.
137    #[inline]
138    pub fn from_parts(
139        decoder: D,
140        encoder: E,
141        hooks: H,
142        decode_hooks: H::DecodeHooks,
143        encode_hooks: H::EncodeHooks,
144    ) -> Self {
145        Self {
146            decode_engine: TranscodeDecodeEngine::new(decoder, decode_hooks),
147            encode_engine: TranscodeEncodeEngine::new(encoder, encode_hooks),
148            hooks,
149            pending: PendingValueSlot::empty(),
150        }
151    }
152
153    /// Returns an upper bound for target units produced from `input_len` units.
154    #[must_use = "capacity planning can fail on overflow"]
155    pub fn max_output_len(
156        &self,
157        input_len: usize,
158    ) -> Result<usize, CapacityError> {
159        let pending_units = self.pending_output_len()?;
160        let decoded_values = self.decode_engine.max_output_len(input_len)?;
161        let converted_units =
162            self.encode_engine.max_output_len(decoded_values)?;
163        converted_units
164            .checked_add(pending_units)
165            .ok_or(CapacityError::OutputLengthOverflow)
166    }
167
168    /// Returns the maximum target units emitted when resetting stream state.
169    #[must_use = "capacity planning can fail on overflow"]
170    pub fn max_reset_output_len(&self) -> Result<usize, CapacityError> {
171        Ok(self.encode_engine.max_reset_output_len())
172    }
173
174    /// Returns the maximum target units emitted by finishing retained state.
175    #[must_use = "capacity planning can fail on overflow"]
176    pub fn max_finish_output_len(&self) -> Result<usize, CapacityError> {
177        let pending_units = self.pending_output_len()?;
178        let decoder_finish_values =
179            self.decode_engine.max_finish_output_len()?;
180        let decoder_finish_units =
181            self.encode_engine.max_output_len(decoder_finish_values)?;
182        let encoder_finish_units = self.encode_engine.max_finish_output_len();
183        let pending_and_decoder = pending_units
184            .checked_add(decoder_finish_units)
185            .ok_or(CapacityError::OutputLengthOverflow)?;
186        pending_and_decoder
187            .checked_add(encoder_finish_units)
188            .ok_or(CapacityError::OutputLengthOverflow)
189    }
190
191    /// Converts source units into target units.
192    ///
193    /// # Parameters
194    ///
195    /// - `input`: Complete input unit slice visible to the converter.
196    /// - `input_index`: Absolute input index where conversion starts.
197    /// - `output`: Complete output unit slice visible to the converter.
198    /// - `output_index`: Absolute output index where writing starts.
199    ///
200    /// # Returns
201    ///
202    /// Returns conversion progress.
203    ///
204    /// # Errors
205    ///
206    /// Returns hook errors when indices are invalid or concrete conversion
207    /// fails. Invalid output indices are reported through the encode-side
208    /// error path.
209    pub fn transcode(
210        &mut self,
211        input: &[D::Unit],
212        input_index: usize,
213        output: &mut [E::Unit],
214        output_index: usize,
215    ) -> ConvertProgressResult<D, E, H> {
216        TranscodeError::ensure_transcode_indices(
217            input.len(),
218            input_index,
219            output.len(),
220            output_index,
221        )?;
222
223        let mut state =
224            ConvertState::new(input, input_index, output, output_index);
225
226        // A retained decoded value must be written before consuming more input,
227        // otherwise callers could observe output reordered across buffer turns.
228        if let Some(progress) = self.drain_pending(&mut state)? {
229            return Ok(progress);
230        }
231
232        while state.has_input() {
233            let previous_read = state.read();
234            // Each hot-path step decodes one source value and immediately tries
235            // to encode it, preserving backpressure at the target output.
236            if let Some(progress) = self.convert_next(&mut state)? {
237                return Ok(progress);
238            }
239            debug_assert!(
240                state.read() > previous_read,
241                "TranscodeConvertEngine conversion step must consume input or stop",
242            );
243        }
244
245        Ok(state.complete_progress())
246    }
247
248    /// Finishes retained output after EOF.
249    ///
250    /// Finalization drains a pending decoded value first, then lets the
251    /// source-side decode hooks emit final values, encodes those values through
252    /// the target-side encode hooks, and finally finishes target-side encode
253    /// hook state. The decode-finish value buffer used for this cold path
254    /// requires `D::Value: Default`; the normal `transcode` loop does not.
255    ///
256    /// # Parameters
257    ///
258    /// - `output`: Complete output unit slice visible to the converter.
259    /// - `output_index`: Absolute output index where writing starts.
260    ///
261    /// # Returns
262    ///
263    /// Returns the number of target units written during finalization.
264    ///
265    /// # Errors
266    ///
267    /// Returns a converter error when output capacity checks fail or when
268    /// hook finalization fails.
269    pub fn finish(
270        &mut self,
271        output: &mut [E::Unit],
272        output_index: usize,
273    ) -> Result<usize, ConvertErrorOf<D, E, H>>
274    where
275        D::Value: Default,
276    {
277        let required = self
278            .max_finish_output_len()
279            .map_err(|_| TranscodeError::OutputLengthOverflow)?;
280        TranscodeError::ensure_output_capacity(
281            output.len(),
282            output_index,
283            required,
284        )?;
285
286        let empty_input: &[D::Unit] = &[];
287        let mut state = ConvertState::new(empty_input, 0, output, output_index);
288        // Finish keeps the same priority as transcode: output any retained
289        // decoded value before asking source-side hooks for final values.
290        if self.drain_pending(&mut state)?.is_some() {
291            unreachable!(
292                "converter finish bound must reserve space for pending values"
293            );
294        }
295
296        // Source-side finish may emit one or more final values. Drain them into
297        // the target encoder before finishing target-side hook state.
298        self.drain_decoder_finish(&mut state)?;
299
300        let output_cursor = state.output_cursor();
301        let written = self
302            .encode_engine
303            .finish(state.output_mut(), output_cursor)
304            .map_err(|error| {
305                error.map_domain(|domain| self.hooks.map_encode_error(domain))
306            })?;
307        state.advance_output(written);
308        Ok(state.written())
309    }
310
311    /// Resets hook-owned and component-owned state and emits stream-start
312    /// encode output.
313    ///
314    /// # Parameters
315    ///
316    /// - `output`: Complete output unit slice visible to the converter.
317    /// - `output_index`: Absolute output index where writing starts.
318    ///
319    /// # Returns
320    ///
321    /// Returns the number of target units written while resetting stream state.
322    ///
323    /// # Errors
324    ///
325    /// Returns a converter error if reset validation or finalization fails.
326    pub fn reset(
327        &mut self,
328        output: &mut [E::Unit],
329        output_index: usize,
330    ) -> Result<usize, ConvertErrorOf<D, E, H>> {
331        let required = self
332            .max_reset_output_len()
333            .map_err(|_| TranscodeError::OutputLengthOverflow)?;
334        TranscodeError::ensure_output_capacity(
335            output.len(),
336            output_index,
337            required,
338        )?;
339
340        self.pending.clear();
341        self.hooks.reset();
342        self.decode_engine.reset(&mut [], 0).map_err(|error| {
343            error.map_domain(|domain| self.hooks.map_decode_error(domain))
344        })?;
345        self.encode_engine
346            .reset(output, output_index)
347            .map_err(|error| {
348                error.map_domain(|domain| self.hooks.map_encode_error(domain))
349            })
350    }
351
352    /// Converts one value from the current state cursors.
353    #[inline(always)]
354    fn convert_next(
355        &mut self,
356        state: &mut ConvertState<'_, D::Unit, E::Unit>,
357    ) -> ConvertStepResult<D, E, H> {
358        let step = self
359            .decode_engine
360            .decode_step(state.input(), state.decode_context())
361            .map_err(|error| {
362                error.map_domain(|domain| self.hooks.map_decode_error(domain))
363            })?;
364        step.apply_to_convert_state(state, |pending, state| {
365            self.encode_pending(pending, state)
366        })
367    }
368
369    /// Returns the output bound for the retained pending value.
370    #[inline(always)]
371    fn pending_output_len(&self) -> Result<usize, CapacityError> {
372        self.pending.max_output_len(&self.encode_engine)
373    }
374
375    /// Writes a retained decoded value before new input is consumed.
376    #[inline(always)]
377    fn drain_pending(
378        &mut self,
379        state: &mut ConvertState<'_, D::Unit, E::Unit>,
380    ) -> ConvertStepResult<D, E, H> {
381        let Some(pending) = self.pending.take() else {
382            return Ok(None);
383        };
384        self.encode_pending(pending, state)
385    }
386
387    /// Drains source-side decode finish output and encodes emitted final
388    /// values.
389    fn drain_decoder_finish(
390        &mut self,
391        state: &mut ConvertState<'_, D::Unit, E::Unit>,
392    ) -> Result<(), ConvertErrorOf<D, E, H>>
393    where
394        D::Value: Default,
395    {
396        let value_count = self
397            .decode_engine
398            .max_finish_output_len()
399            .map_err(|_| TranscodeError::OutputLengthOverflow)?;
400        let mut decoded: Vec<D::Value> =
401            (0..value_count).map(|_| D::Value::default()).collect();
402        let written =
403            self.decode_engine
404                .finish(&mut decoded, 0)
405                .map_err(|error| {
406                    error.map_domain(|domain| {
407                        self.hooks.map_decode_error(domain)
408                    })
409                })?;
410        for value in decoded.into_iter().take(written) {
411            let pending = PendingValue::new(value, 0);
412            if self.encode_pending(pending, state)?.is_some() {
413                unreachable!(
414                    "converter finish bound must reserve space for decode finish values"
415                );
416            }
417        }
418        Ok(())
419    }
420
421    /// Encodes one pending value and applies output/pending state changes.
422    fn encode_pending(
423        &mut self,
424        pending: PendingValue<D::Value>,
425        state: &mut ConvertState<'_, D::Unit, E::Unit>,
426    ) -> ConvertStepResult<D, E, H> {
427        let input_index = pending.input_index();
428        let output_index = state.output_cursor();
429        let context = EncodeContext {
430            input_value: pending.value(),
431            input_index,
432            output: state.output_mut(),
433            output_index,
434        };
435        let step =
436            self.encode_engine.encode_step(context).map_err(|error| {
437                error.map_domain(|domain| self.hooks.map_encode_error(domain))
438            })?;
439        let step = match step {
440            EncodeStep::Written { written } => {
441                PendingEncodeStep::written(written)
442            }
443            EncodeStep::NeedOutput {
444                additional,
445                available,
446            } => PendingEncodeStep::need_output(pending, additional, available),
447        };
448        Ok(self.pending.apply_pending_encode_step(step, state))
449    }
450}
451
452impl<D, E, H> Default for TranscodeConvertEngine<D, E, H>
453where
454    D: Codec + Default,
455    E: Codec<Value = D::Value> + Default,
456    H: TranscodeConvertHooks<D, E> + Default,
457{
458    /// Creates a default buffered converter engine.
459    ///
460    /// # Returns
461    ///
462    /// Returns a converter engine constructed from default codecs and hooks.
463    #[inline(always)]
464    fn default() -> Self {
465        Self::new(D::default(), E::default(), H::default())
466    }
467}