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}