Skip to main content

qubit_codec/transcode/engine/
transcode_encode_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 encoder engine.
9
10use super::super::encode_context::EncodeContext;
11use super::super::internal::{
12    encode_state::EncodeState,
13    lifecycle::LifecycleGuard,
14};
15use crate::codec::assert_unit_bounds;
16use crate::{
17    CapacityError,
18    Codec,
19    CodecEncodeError,
20    EncodeOutcome,
21    TranscodeEncodeEngineError,
22    TranscodeEncodeHooks,
23    TranscodeError,
24    TranscodeProgress,
25    Transcoder,
26};
27
28type EncodeEngineErrorOf<C, H> = TranscodeEncodeEngineError<
29    <C as Codec>::EncodeError,
30    <H as TranscodeEncodeHooks<C>>::Error,
31>;
32
33/// Reusable buffered encoding engine for codec-backed encoders.
34///
35/// The engine owns the low-level codec and hook object. It keeps the common
36/// buffered encoding loop private: input-index validation, output-capacity
37/// checks, input consumption, output progress, and [`crate::TranscodeStatus`]
38/// reporting.
39///
40/// Use this type to build a streaming encoder over a one-value [`Codec`]. The
41/// engine does not allocate output. It repeatedly asks hooks to process one
42/// input value at the current output cursor. If the hook reports insufficient
43/// output, the engine returns [`crate::TranscodeStatus::NeedOutput`] without
44/// consuming that value; the caller can provide a larger or fresh output buffer
45/// and resume with the returned input index.
46///
47/// For the common strict policy that simply wraps codec errors, use
48/// [`crate::CodecTranscodeEncoder`]. Use `TranscodeEncodeEngine` directly when
49/// the encode policy needs custom planning, replacement, skipped values, or
50/// finish-time output.
51///
52/// # Example
53///
54/// ```rust
55/// use core::{
56///     convert::Infallible,
57///     num::NonZeroUsize,
58/// };
59/// use qubit_codec::{
60///     TranscodeEncodeEngine,
61///     TranscodeEncodeHooks,
62///     Codec,
63///     DecodeFailure,
64///     CodecEncodeError,
65///     EncodeContext,
66///     EncodeOutcome,
67///     TranscodeStatus,
68/// };
69///
70/// #[derive(Clone, Copy)]
71/// struct ByteCodec;
72///
73/// impl Codec for ByteCodec {
74///     type Value = u8;
75///     type Unit = u8;
76///     type DecodeError = Infallible;
77///     type EncodeError = Infallible;
78///
79///     const MIN_UNITS_PER_VALUE: NonZeroUsize = NonZeroUsize::MIN;
80///     const MAX_UNITS_PER_VALUE: NonZeroUsize = NonZeroUsize::MIN;
81///
82///     unsafe fn decode(
83///         &mut self,
84///         input: &[u8],
85///         index: usize,
86///     ) -> Result<(u8, NonZeroUsize), DecodeFailure<Self::DecodeError>> {
87///         Ok((input[index], NonZeroUsize::MIN))
88///     }
89///
90///     unsafe fn encode(
91///         &mut self,
92///         value: &u8,
93///         output: &mut [u8],
94///         index: usize,
95///     ) -> Result<NonZeroUsize, Self::EncodeError> {
96///         output[index] = *value;
97///         Ok(NonZeroUsize::MIN)
98///     }
99/// }
100///
101/// struct StrictHooks;
102///
103/// impl TranscodeEncodeHooks<ByteCodec> for StrictHooks {
104///     type Error = CodecEncodeError<Infallible>;
105///
106///     fn encode_value(
107///         &mut self,
108///         codec: &mut ByteCodec,
109///         context: EncodeContext<'_, u8, u8>,
110///     ) -> Result<EncodeOutcome, Self::Error> {
111///         let required = ByteCodec::MAX_UNITS_PER_VALUE;
112///         if context.available_output() < required.get() {
113///             return Ok(EncodeOutcome::need_output(required));
114///         }
115///         let (value, input_index, output, output_index) = context.into_parts();
116///         let written = unsafe { codec.encode(value, output, output_index) }
117///             .map(NonZeroUsize::get)
118///             .map_err(|error| CodecEncodeError::encode(error, input_index))?;
119///         Ok(EncodeOutcome::consumed(written))
120///     }
121/// }
122///
123/// let mut engine = TranscodeEncodeEngine::new(ByteCodec, StrictHooks);
124/// let input = [1_u8, 2, 3];
125/// let mut output = [0_u8; 2];
126///
127/// let progress = engine.transcode(&input, 0, &mut output, 0)?;
128/// match progress.status() {
129///     TranscodeStatus::Complete => unreachable!("output is intentionally short"),
130///     TranscodeStatus::NeedOutput { output_index, .. } => {
131///         assert_eq!(2, output_index);
132///         assert_eq!([1, 2], output);
133///         // Write out `output[..output_index]`, then resume at
134///         // `progress.read()` with fresh output capacity.
135///     }
136///     TranscodeStatus::NeedInput { .. } => unreachable!("encoders do not read encoded input"),
137/// }
138/// # Ok::<(), qubit_codec::TranscodeError<qubit_codec::TranscodeEncodeEngineError<Infallible, CodecEncodeError<Infallible>>>>(())
139/// ```
140///
141/// # Type Parameters
142///
143/// - `C`: Low-level codec used by the engine.
144/// - `H`: Policy hook object used by the engine.
145#[derive(Debug)]
146pub struct TranscodeEncodeEngine<C, H> {
147    codec: C,
148    hooks: H,
149    /// Debug-only guard for the `reset → transcode* → finish` lifecycle.
150    /// Zero-sized in release builds.
151    lifecycle: LifecycleGuard,
152}
153
154impl<C, H> TranscodeEncodeEngine<C, H>
155where
156    C: Codec,
157    H: TranscodeEncodeHooks<C>,
158{
159    /// Creates a buffered encoder engine.
160    ///
161    /// # Parameters
162    ///
163    /// - `codec`: Low-level codec used for one-value encoding.
164    /// - `hooks`: Policy hooks used for planning and writing values.
165    ///
166    /// # Returns
167    ///
168    /// Returns a buffered encoder engine.
169    ///
170    /// # Panics
171    ///
172    /// In debug builds, panics when the supplied codec violates the
173    /// [`Codec::MIN_UNITS_PER_VALUE`] / [`Codec::MAX_UNITS_PER_VALUE`] ordering
174    /// invariant. Release builds skip this check because the invariant is the
175    /// responsibility of the [`Codec`] implementation.
176    #[inline]
177    #[must_use]
178    pub fn new(codec: C, hooks: H) -> Self {
179        assert_unit_bounds::<C>();
180        Self {
181            codec,
182            hooks,
183            lifecycle: LifecycleGuard::new(),
184        }
185    }
186
187    /// Encodes one value through the hook and codec.
188    ///
189    /// This is the single entry point for `TranscodeConvertEngine` to drive
190    /// the encode side without accessing `codec` and `hooks` directly.
191    ///
192    /// # Parameters
193    ///
194    /// - `context`: Encode context for the current value.
195    ///
196    /// # Returns
197    ///
198    /// Returns whether the value was consumed or needs more output capacity.
199    ///
200    /// # Errors
201    ///
202    /// Returns `H::Error` when the hook rejects or cannot encode the value.
203    #[inline(always)]
204    pub(crate) fn encode_one(
205        &mut self,
206        context: EncodeContext<'_, C::Value, C::Unit>,
207    ) -> Result<EncodeOutcome, H::Error> {
208        self.hooks.encode_value(&mut self.codec, context)
209    }
210
211    /// Gets a conservative upper bound for output units needed for
212    /// `input_len` values.
213    ///
214    /// # Parameters
215    ///
216    /// - `input_len`: Number of input values the caller plans to encode.
217    ///
218    /// # Returns
219    ///
220    /// a conservative upper bound for output units, or a capacity error on
221    /// arithmetic overflow.
222    #[inline(always)]
223    #[must_use = "capacity planning can fail on overflow"]
224    pub fn max_output_len(
225        &self,
226        input_len: usize,
227    ) -> Result<usize, CapacityError> {
228        self.hooks.max_output_len(&self.codec, input_len)
229    }
230
231    /// Gets the maximum output units emitted by stream reset.
232    ///
233    /// # Returns
234    ///
235    /// the codec's reset-output upper bound.
236    #[inline(always)]
237    #[must_use = "capacity planning can fail on overflow"]
238    pub fn max_reset_output_len(&self) -> Result<usize, CapacityError> {
239        Ok(C::MAX_ENCODE_RESET_UNITS)
240    }
241
242    /// Gets the maximum output units emitted by finishing codec and hook state.
243    ///
244    /// Returns the sum of [`Codec::MAX_ENCODE_FLUSH_UNITS`] and the
245    /// hook-provided final-output bound. The codec flush portion covers units
246    /// written by [`Codec::encode_flush`]; hook implementations must not
247    /// include that portion in
248    /// [`TranscodeEncodeHooks::max_finish_output_len`].
249    ///
250    /// # Returns
251    ///
252    /// the combined codec-flush and hook-finish output bound.
253    #[inline(always)]
254    #[must_use = "capacity planning can fail on overflow"]
255    pub fn max_finish_output_len(&self) -> Result<usize, CapacityError> {
256        C::MAX_ENCODE_FLUSH_UNITS
257            .checked_add(self.hooks.max_finish_output_len(&self.codec))
258            .ok_or(CapacityError::OutputLengthOverflow)
259    }
260
261    /// Resets codec encode state, hook-owned state, and stream-start output.
262    ///
263    /// # Parameters
264    ///
265    /// - `output`: Complete output unit slice visible to the encoder.
266    /// - `output_index`: Absolute output unit index where writing starts.
267    ///
268    /// # Returns
269    ///
270    /// Returns the number of reset units written.
271    ///
272    /// # Errors
273    ///
274    /// Returns framework errors when the caller provides invalid or
275    /// insufficient output capacity. Returns domain errors when codec reset or
276    /// hook reset handling fails.
277    pub fn reset(
278        &mut self,
279        output: &mut [C::Unit],
280        output_index: usize,
281    ) -> Result<usize, TranscodeError<EncodeEngineErrorOf<C, H>>> {
282        self.lifecycle.on_reset();
283        let required = self.max_reset_output_len()?;
284        TranscodeError::ensure_output_capacity(
285            output.len(),
286            output_index,
287            required,
288        )?;
289        self.hooks.reset_hooks(&mut self.codec);
290        let written = unsafe {
291            // SAFETY: The capacity check above reserves the codec's declared
292            // reset-output bound at `output_index`.
293            self.codec.encode_reset(output, output_index)
294        }
295        .map_err(|error| {
296            TranscodeError::domain(TranscodeEncodeEngineError::codec(
297                CodecEncodeError::encode_reset(error),
298            ))
299        })?;
300        assert!(
301            written <= required,
302            "Codec::encode_reset wrote beyond its reset bound",
303        );
304        Ok(written)
305    }
306
307    /// Encodes values into a caller-provided output buffer.
308    ///
309    /// The engine stops before consuming the next input value when the current
310    /// output buffer does not satisfy that value's planned capacity bound.
311    ///
312    /// # Parameters
313    ///
314    /// - `input`: Complete input value slice visible to the encoder.
315    /// - `input_index`: Absolute input value index where encoding starts.
316    /// - `output`: Complete output unit slice visible to the encoder.
317    /// - `output_index`: Absolute output unit index where writing starts.
318    ///
319    /// # Returns
320    ///
321    /// Returns progress describing input values consumed, output units written,
322    /// and why encoding stopped.
323    ///
324    /// # Errors
325    ///
326    /// Returns hook errors when `input_index` is outside `input`, when
327    /// `output_index` is outside `output`, or when hook planning or writing
328    /// rejects a value.
329    pub fn transcode(
330        &mut self,
331        input: &[C::Value],
332        input_index: usize,
333        output: &mut [C::Unit],
334        output_index: usize,
335    ) -> Result<TranscodeProgress, TranscodeError<EncodeEngineErrorOf<C, H>>>
336    {
337        self.lifecycle.on_transcode();
338        TranscodeError::ensure_transcode_indices(
339            input.len(),
340            input_index,
341            output.len(),
342            output_index,
343        )?;
344        let mut state =
345            EncodeState::new(input, input_index, output, output_index);
346
347        while state.has_input() {
348            // SAFETY: The loop condition proves that the current input cursor
349            // points at an available value.
350            let context = unsafe { state.context_unchecked() };
351            let outcome = self
352                .hooks
353                .encode_value(&mut self.codec, context)
354                .map_err(|error| {
355                    TranscodeError::domain(TranscodeEncodeEngineError::hook(
356                        error,
357                    ))
358                })?;
359            if let Some(progress) = state.apply_encode_outcome(outcome) {
360                return Ok(progress);
361            }
362        }
363
364        Ok(state.complete_progress())
365    }
366
367    /// Finishes codec and hook-owned output after EOF.
368    ///
369    /// Finalization first flushes encode-side codec state through
370    /// [`Codec::encode_flush`], then lets hook implementations finish their
371    /// own retained state. The caller must provide enough output capacity for
372    /// [`TranscodeEncodeEngine::max_finish_output_len`], which includes both
373    /// the codec flush bound and the hook-owned finish bound.
374    ///
375    /// # Parameters
376    ///
377    /// - `output`: Complete output unit slice visible to the encoder.
378    /// - `output_index`: Absolute output unit index where writing starts.
379    ///
380    /// # Returns
381    ///
382    /// Returns the number of units written by finalization.
383    ///
384    /// # Errors
385    ///
386    /// Returns framework errors when the caller provides invalid or
387    /// insufficient output capacity. Returns domain errors when codec flush or
388    /// hook finalization fails.
389    ///
390    /// # Panics
391    ///
392    /// Panics when the codec flush writes beyond
393    /// [`Codec::MAX_ENCODE_FLUSH_UNITS`] or when the combined codec and hook
394    /// finalization writes beyond
395    /// [`TranscodeEncodeEngine::max_finish_output_len`].
396    pub fn finish(
397        &mut self,
398        output: &mut [C::Unit],
399        output_index: usize,
400    ) -> Result<usize, TranscodeError<EncodeEngineErrorOf<C, H>>> {
401        self.lifecycle.on_finish_attempt();
402        let required = self.max_finish_output_len()?;
403        TranscodeError::ensure_output_capacity(
404            output.len(),
405            output_index,
406            required,
407        )?;
408        let flushed = unsafe {
409            // SAFETY: The capacity check above reserves the codec's declared
410            // flush-output bound at `output_index`.
411            self.codec.encode_flush(output, output_index)
412        }
413        .map_err(|error| {
414            TranscodeError::domain(TranscodeEncodeEngineError::codec(
415                CodecEncodeError::encode_flush(error),
416            ))
417        })?;
418        assert!(
419            flushed <= C::MAX_ENCODE_FLUSH_UNITS,
420            "Codec::encode_flush wrote beyond its flush bound",
421        );
422        let written = self
423            .hooks
424            .finish_hooks(&mut self.codec, output, output_index + flushed)
425            .map_err(|error| {
426                TranscodeError::domain(TranscodeEncodeEngineError::hook(error))
427            })?;
428        assert!(
429            flushed + written <= required,
430            "TranscodeEncodeEngine hook wrote beyond its finish bound",
431        );
432        self.lifecycle.on_finish_success();
433        Ok(flushed + written)
434    }
435}
436
437impl<C, H> Default for TranscodeEncodeEngine<C, H>
438where
439    C: Codec + Default,
440    H: TranscodeEncodeHooks<C> + Default,
441{
442    /// Creates a default buffered encoder engine.
443    ///
444    /// # Returns
445    ///
446    /// Returns an engine with default codec and hooks.
447    #[inline(always)]
448    fn default() -> Self {
449        Self::new(C::default(), H::default())
450    }
451}
452
453impl<C, H> Transcoder<C::Value, C::Unit> for TranscodeEncodeEngine<C, H>
454where
455    C: Codec,
456    H: TranscodeEncodeHooks<C>,
457{
458    type Error = EncodeEngineErrorOf<C, H>;
459
460    /// Returns an upper bound for units produced from `input_len` values.
461    #[inline(always)]
462    fn max_output_len(&self, input_len: usize) -> Result<usize, CapacityError> {
463        TranscodeEncodeEngine::max_output_len(self, input_len)
464    }
465
466    /// Returns the maximum units emitted when resetting stream state.
467    #[inline(always)]
468    fn max_reset_output_len(&self) -> Result<usize, CapacityError> {
469        TranscodeEncodeEngine::max_reset_output_len(self)
470    }
471
472    /// Returns the maximum units emitted by finishing internal state.
473    #[inline(always)]
474    fn max_finish_output_len(&self) -> Result<usize, CapacityError> {
475        TranscodeEncodeEngine::max_finish_output_len(self)
476    }
477
478    /// Resets codec encode state, hook-owned state, and stream-start output.
479    #[inline(always)]
480    fn reset(
481        &mut self,
482        output: &mut [C::Unit],
483        output_index: usize,
484    ) -> Result<usize, TranscodeError<Self::Error>> {
485        TranscodeEncodeEngine::reset(self, output, output_index)
486    }
487
488    /// Encodes input values into caller-provided output units.
489    #[inline(always)]
490    fn transcode(
491        &mut self,
492        input: &[C::Value],
493        input_index: usize,
494        output: &mut [C::Unit],
495        output_index: usize,
496    ) -> Result<TranscodeProgress, TranscodeError<Self::Error>> {
497        TranscodeEncodeEngine::transcode(
498            self,
499            input,
500            input_index,
501            output,
502            output_index,
503        )
504    }
505
506    /// Finishes hook-owned encoder state.
507    #[inline(always)]
508    fn finish(
509        &mut self,
510        output: &mut [C::Unit],
511        output_index: usize,
512    ) -> Result<usize, TranscodeError<Self::Error>> {
513        TranscodeEncodeEngine::finish(self, output, output_index)
514    }
515}