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}