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::internal::{
11 encode_state::EncodeState,
12 encode_step::EncodeStep,
13};
14use crate::codec::assert_unit_bounds;
15use crate::{
16 CapacityError,
17 Codec,
18 EncodeContext,
19 TranscodeEncodeHooks,
20 TranscodeError,
21 TranscodeProgress,
22};
23
24/// Reusable buffered encoding engine for codec-backed encoders.
25///
26/// The engine owns the low-level codec and hook object. It keeps the common
27/// buffered encoding loop private: input-index validation, output-capacity
28/// checks, input consumption, output progress, and [`crate::TranscodeStatus`]
29/// reporting.
30///
31/// Use this type to build a streaming encoder over a one-value [`Codec`]. The
32/// engine does not allocate output. It repeatedly asks hooks to plan one input
33/// value, verifies that the caller-provided output slice can hold that plan,
34/// and then lets the hooks write the value. If the next value would not fit,
35/// the engine returns [`crate::TranscodeStatus::NeedOutput`] without consuming
36/// that value; the caller can provide a larger or fresh output buffer and
37/// resume with the returned input index.
38///
39/// For the common strict policy that simply wraps codec errors, use
40/// [`crate::CodecTranscodeEncoder`]. Use `TranscodeEncodeEngine` directly when
41/// the encode policy needs custom planning, replacement, skipped values, or
42/// finish-time output.
43///
44/// # Example
45///
46/// ```rust
47/// use core::{
48/// convert::Infallible,
49/// num::NonZeroUsize,
50/// };
51/// use qubit_codec::{
52/// TranscodeEncodeEngine,
53/// TranscodeEncodeHooks,
54/// Codec,
55/// CodecEncodeError,
56/// EncodeContext,
57/// EncodePlan,
58/// TranscodeStatus,
59/// };
60///
61/// #[derive(Clone, Copy)]
62/// struct ByteCodec;
63///
64/// unsafe impl Codec for ByteCodec {
65/// type Value = u8;
66/// type Unit = u8;
67/// type DecodeError = Infallible;
68/// type EncodeError = Infallible;
69///
70/// fn min_units_per_value(&self) -> NonZeroUsize {
71/// NonZeroUsize::MIN
72/// }
73///
74/// fn max_units_per_value(&self) -> NonZeroUsize {
75/// NonZeroUsize::MIN
76/// }
77///
78/// unsafe fn decode(
79/// &mut self,
80/// input: &[u8],
81/// index: usize,
82/// ) -> Result<(u8, NonZeroUsize), Self::DecodeError> {
83/// Ok((input[index], NonZeroUsize::MIN))
84/// }
85///
86/// unsafe fn encode(
87/// &mut self,
88/// value: &u8,
89/// output: &mut [u8],
90/// index: usize,
91/// ) -> Result<NonZeroUsize, Self::EncodeError> {
92/// output[index] = *value;
93/// Ok(NonZeroUsize::MIN)
94/// }
95/// }
96///
97/// struct StrictHooks;
98///
99/// impl TranscodeEncodeHooks<ByteCodec> for StrictHooks {
100/// type Error = CodecEncodeError<Infallible>;
101/// type PlanAction = ();
102///
103/// fn prepare_encode(
104/// &mut self,
105/// codec: &mut ByteCodec,
106/// _value: &u8,
107/// _input_index: usize,
108/// ) -> Result<EncodePlan<()>, Self::Error> {
109/// Ok(EncodePlan::new(codec.max_units_per_value().get(), ()))
110/// }
111///
112/// unsafe fn write_encode(
113/// &mut self,
114/// codec: &mut ByteCodec,
115/// context: EncodeContext<'_, u8, u8>,
116/// _plan: EncodePlan<()>,
117/// ) -> Result<usize, Self::Error> {
118/// unsafe {
119/// codec.encode(context.input_value, context.output, context.output_index)
120/// }
121/// .map(NonZeroUsize::get)
122/// .map_err(|error| CodecEncodeError::encode(error, context.input_index))
123/// }
124/// }
125///
126/// let mut engine = TranscodeEncodeEngine::new(ByteCodec, StrictHooks);
127/// let input = [1_u8, 2, 3];
128/// let mut output = [0_u8; 2];
129///
130/// let progress = engine.transcode(&input, 0, &mut output, 0)?;
131/// match progress.status() {
132/// TranscodeStatus::Complete => unreachable!("output is intentionally short"),
133/// TranscodeStatus::NeedOutput { output_index, .. } => {
134/// assert_eq!(2, output_index);
135/// assert_eq!([1, 2], output);
136/// // Write out `output[..output_index]`, then resume at
137/// // `progress.read()` with fresh output capacity.
138/// }
139/// TranscodeStatus::NeedInput { .. } => unreachable!("encoders do not read encoded input"),
140/// }
141/// # Ok::<(), qubit_codec::TranscodeError<CodecEncodeError<Infallible>>>(())
142/// ```
143///
144/// # Type Parameters
145///
146/// - `C`: Low-level codec used by the engine.
147/// - `H`: Policy hook object used by the engine.
148#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
149pub struct TranscodeEncodeEngine<C, H> {
150 /// Low-level codec used for one-value encoding.
151 pub(super) codec: C,
152 /// Policy hooks used for planning and writing values.
153 pub(super) hooks: H,
154}
155
156impl<C, H> TranscodeEncodeEngine<C, H>
157where
158 C: Codec,
159 H: TranscodeEncodeHooks<C>,
160{
161 /// Creates a buffered encoder engine.
162 ///
163 /// # Parameters
164 ///
165 /// - `codec`: Low-level codec used for one-value encoding.
166 /// - `hooks`: Policy hooks used for planning and writing values.
167 ///
168 /// # Returns
169 ///
170 /// Returns a buffered encoder engine.
171 ///
172 /// # Panics
173 ///
174 /// Panics when the supplied codec violates the
175 /// [`Codec::min_units_per_value`] / [`Codec::max_units_per_value`] ordering
176 /// invariant.
177 #[must_use]
178 #[inline]
179 pub fn new(codec: C, hooks: H) -> Self {
180 assert_unit_bounds::<C>(&codec);
181 Self { codec, hooks }
182 }
183
184 /// Gets a conservative upper bound for output units needed for
185 /// `input_len` values.
186 ///
187 /// # Parameters
188 ///
189 /// - `input_len`: Number of input values the caller plans to encode.
190 ///
191 /// # Returns
192 ///
193 /// a conservative upper bound for output units, or a capacity error on
194 /// arithmetic overflow.
195 #[must_use = "capacity planning can fail on overflow"]
196 #[inline(always)]
197 pub fn max_output_len(
198 &self,
199 input_len: usize,
200 ) -> Result<usize, CapacityError> {
201 self.hooks.max_output_len(&self.codec, input_len)
202 }
203
204 /// Gets the maximum output units emitted by stream reset.
205 ///
206 /// # Returns
207 ///
208 /// the codec's reset-output upper bound.
209 #[must_use]
210 #[inline(always)]
211 pub fn max_reset_output_len(&self) -> usize {
212 self.codec.max_encode_reset_units()
213 }
214
215 /// Gets the maximum output units emitted by finishing hook-owned state.
216 ///
217 /// # Returns
218 ///
219 /// the hook-provided final output bound.
220 #[must_use]
221 #[inline(always)]
222 pub fn max_finish_output_len(&self) -> usize {
223 self.hooks.max_finish_output_len(&self.codec)
224 }
225
226 /// Resets codec encode state, hook-owned state, and stream-start output.
227 ///
228 /// # Parameters
229 ///
230 /// - `output`: Complete output unit slice visible to the encoder.
231 /// - `output_index`: Absolute output unit index where writing starts.
232 ///
233 /// # Returns
234 ///
235 /// Returns the number of reset units written.
236 ///
237 /// # Errors
238 ///
239 /// Returns hook errors when the caller provides invalid or insufficient
240 /// output capacity, or when reset output cannot be emitted.
241 pub fn reset(
242 &mut self,
243 output: &mut [C::Unit],
244 output_index: usize,
245 ) -> Result<usize, TranscodeError<H::Error>> {
246 let required = self.max_reset_output_len();
247 TranscodeError::ensure_output_capacity(
248 output.len(),
249 output_index,
250 required,
251 )?;
252 self.hooks.reset(&mut self.codec);
253 let written = unsafe {
254 // SAFETY: The capacity check above reserves the codec's declared
255 // reset-output bound at `output_index`.
256 self.hooks
257 .write_encode_reset(&mut self.codec, output, output_index)
258 }
259 .map_err(TranscodeError::domain)?;
260 assert!(
261 written <= required,
262 "Codec::encode_reset wrote beyond its reset bound",
263 );
264 Ok(written)
265 }
266
267 /// Encodes values into a caller-provided output buffer.
268 ///
269 /// The engine stops before consuming the next input value when the current
270 /// output buffer does not satisfy that value's planned capacity bound.
271 ///
272 /// # Parameters
273 ///
274 /// - `input`: Complete input value slice visible to the encoder.
275 /// - `input_index`: Absolute input value index where encoding starts.
276 /// - `output`: Complete output unit slice visible to the encoder.
277 /// - `output_index`: Absolute output unit index where writing starts.
278 ///
279 /// # Returns
280 ///
281 /// Returns progress describing input values consumed, output units written,
282 /// and why encoding stopped.
283 ///
284 /// # Errors
285 ///
286 /// Returns hook errors when `input_index` is outside `input`, when
287 /// `output_index` is outside `output`, or when hook planning or writing
288 /// rejects a value.
289 pub fn transcode(
290 &mut self,
291 input: &[C::Value],
292 input_index: usize,
293 output: &mut [C::Unit],
294 output_index: usize,
295 ) -> Result<TranscodeProgress, TranscodeError<H::Error>> {
296 TranscodeError::ensure_transcode_indices(
297 input.len(),
298 input_index,
299 output.len(),
300 output_index,
301 )?;
302 let mut state =
303 EncodeState::new(input, input_index, output, output_index);
304
305 while state.has_input() {
306 // SAFETY: The loop condition proves that the current input cursor
307 // points at an available value.
308 let context = unsafe { state.context_unchecked() };
309 let step = self.encode_step(context)?;
310 if let Some(progress) = step.apply_to_state(&mut state) {
311 return Ok(progress);
312 }
313 }
314
315 Ok(state.complete_progress())
316 }
317
318 /// Finishes hook-owned output after EOF.
319 ///
320 /// The engine owns no final output state itself. Hook implementations may
321 /// finish their own retained state and emit final output after the caller
322 /// has supplied all input values. The caller must provide enough output
323 /// capacity for [`TranscodeEncodeEngine::max_finish_output_len`].
324 ///
325 /// # Parameters
326 ///
327 /// - `output`: Complete output unit slice visible to the encoder.
328 /// - `output_index`: Absolute output unit index where writing starts.
329 ///
330 /// # Returns
331 ///
332 /// Returns the number of units written by finalization.
333 ///
334 /// # Errors
335 ///
336 /// Returns hook errors when the caller provides invalid or insufficient
337 /// output capacity, or when hook finalization fails.
338 ///
339 /// # Panics
340 ///
341 /// Panics when the hook writes or reports more final output units than
342 /// [`TranscodeEncodeEngine::max_finish_output_len`] declared.
343 pub fn finish(
344 &mut self,
345 output: &mut [C::Unit],
346 output_index: usize,
347 ) -> Result<usize, TranscodeError<H::Error>> {
348 let required = self.max_finish_output_len();
349 TranscodeError::ensure_output_capacity(
350 output.len(),
351 output_index,
352 required,
353 )?;
354 let written = self
355 .hooks
356 .finish(&mut self.codec, output, output_index)
357 .map_err(TranscodeError::domain)?;
358 assert!(
359 written <= required,
360 "TranscodeEncodeEngine hook wrote beyond its finish bound",
361 );
362 Ok(written)
363 }
364
365 /// Encodes one value attempt into a normalized encode step.
366 ///
367 /// # Parameters
368 ///
369 /// - `context`: Current value, absolute input index, and target output
370 /// cursor.
371 ///
372 /// # Returns
373 ///
374 /// Returns an encode step describing either written output or missing
375 /// output capacity.
376 ///
377 /// # Errors
378 ///
379 /// Returns hook errors when planning or writing rejects the value.
380 pub(super) fn encode_step(
381 &mut self,
382 context: EncodeContext<'_, C::Value, C::Unit>,
383 ) -> Result<EncodeStep, TranscodeError<H::Error>> {
384 let plan = self
385 .hooks
386 .prepare_encode(
387 &mut self.codec,
388 context.input_value,
389 context.input_index,
390 )
391 .map_err(TranscodeError::domain)?;
392 let max_output_units = plan.max_output_units;
393 let available = context.available_output();
394 if available < max_output_units {
395 return Ok(EncodeStep::need_output(max_output_units, available));
396 }
397
398 // SAFETY: The capacity check above guarantees the bound requested by
399 // the prepared plan.
400 let written = match unsafe {
401 self.hooks.write_encode(&mut self.codec, context, plan)
402 } {
403 Ok(written) => written,
404 Err(error) => return Err(TranscodeError::domain(error)),
405 };
406 assert!(
407 written <= max_output_units,
408 "TranscodeEncodeEngine hook wrote beyond its prepared capacity bound",
409 );
410 Ok(EncodeStep::written(written))
411 }
412}
413
414impl<C, H> Default for TranscodeEncodeEngine<C, H>
415where
416 C: Codec + Default,
417 H: TranscodeEncodeHooks<C> + Default,
418{
419 /// Creates a default buffered encoder engine.
420 ///
421 /// # Returns
422 ///
423 /// Returns an engine with default codec and hooks.
424 #[inline(always)]
425 fn default() -> Self {
426 Self::new(C::default(), H::default())
427 }
428}