Skip to main content

qubit_codec/value/
codec_value_ext.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//! Value-level convenience methods for low-level codecs.
9
10use core::num::NonZeroUsize;
11
12use crate::{
13    CapacityError,
14    Codec,
15    CodecDecodeError,
16    CodecEncodeError,
17    DecodeFailure,
18    TranscodeError,
19};
20
21/// Result type returned by reset-prefixed one-value encode helpers.
22///
23/// Framework buffer failures are reported by [`TranscodeError`]. Codec-domain
24/// encode failures are wrapped in [`CodecEncodeError`] and stored in the
25/// [`TranscodeError::Domain`] branch.
26pub type CodecEncodeValueResult<E> =
27    Result<usize, TranscodeError<CodecEncodeError<E>>>;
28
29/// Result type returned by decode-and-flush one-value helpers.
30///
31/// The successful value is `(value, consumed, flushed)`. Framework buffer
32/// failures are reported by [`TranscodeError`]. Codec-domain decode failures
33/// are wrapped in [`CodecDecodeError`] and stored in the
34/// [`TranscodeError::Domain`] branch.
35pub type CodecDecodeValueWithFlushResult<V, E> =
36    Result<(V, NonZeroUsize, usize), TranscodeError<CodecDecodeError<E>>>;
37
38/// Result type returned by exact decode-and-flush one-value helpers.
39///
40/// The successful value is `(value, flushed)`. Framework buffer failures are
41/// reported by [`TranscodeError`]. Codec-domain decode failures are wrapped in
42/// [`CodecDecodeError`] and stored in the [`TranscodeError::Domain`] branch.
43pub type CodecDecodeExactValueWithFlushResult<V, E> =
44    Result<(V, usize), TranscodeError<CodecDecodeError<E>>>;
45
46/// Extension trait for checked one-value codec operations.
47///
48/// `CodecValueExt` keeps convenience operations out of the low-level
49/// [`Codec`] contract while still making them available to all codec
50/// implementations. The methods compose primitive reset, encode, decode, and
51/// flush hooks with capacity checks and adapter-level error wrapping.
52pub trait CodecValueExt: Codec {
53    /// Returns the maximum unit count emitted by one reset-prefixed value
54    /// encode.
55    ///
56    /// This is the checked sum of
57    /// [`Codec::MAX_ENCODE_RESET_UNITS`] and
58    /// [`Codec::MAX_UNITS_PER_VALUE`]. It is useful for callers that want to
59    /// reuse scratch storage for repeated one-value encodes.
60    ///
61    /// # Returns
62    ///
63    /// Returns the maximum reset-plus-value output length.
64    ///
65    /// # Errors
66    ///
67    /// Returns [`CapacityError::OutputLengthOverflow`] when the sum cannot be
68    /// represented as `usize`.
69    #[inline(always)]
70    #[must_use = "capacity planning can fail on overflow"]
71    fn max_encode_value_units(&self) -> Result<usize, CapacityError> {
72        Self::MAX_ENCODE_RESET_UNITS
73            .checked_add(Self::MAX_UNITS_PER_VALUE.get())
74            .ok_or(CapacityError::OutputLengthOverflow)
75    }
76
77    /// Encodes one value after emitting reset output into a caller buffer.
78    ///
79    /// The method validates the output index and the combined reset-plus-value
80    /// capacity before calling the unchecked codec hooks. It is a convenience
81    /// wrapper for code paths that need one complete value and want to reuse
82    /// caller-owned storage.
83    ///
84    /// # Parameters
85    ///
86    /// - `value`: Value to encode.
87    /// - `output`: Destination unit buffer.
88    /// - `output_index`: Start index in `output`.
89    ///
90    /// # Returns
91    ///
92    /// Returns the total number of reset and value units written.
93    ///
94    /// # Errors
95    ///
96    /// Returns [`TranscodeError`] when output bounds are invalid, when output
97    /// capacity is insufficient, when output length arithmetic overflows, or
98    /// when the codec cannot encode `value`.
99    ///
100    /// # Panics
101    ///
102    /// Panics when the codec writes or reports more units than its declared
103    /// reset or value bound.
104    fn encode_value_with_reset(
105        &mut self,
106        value: &Self::Value,
107        output: &mut [Self::Unit],
108        output_index: usize,
109    ) -> CodecEncodeValueResult<Self::EncodeError> {
110        if !self.can_encode_value(value) {
111            return Err(TranscodeError::domain(
112                CodecEncodeError::unencodable_value(0),
113            ));
114        }
115        let reset_units = Self::MAX_ENCODE_RESET_UNITS;
116        let value_units = self.encode_len(value).get();
117        let required = reset_units
118            .checked_add(value_units)
119            .ok_or_else(TranscodeError::output_length_overflow)?;
120        TranscodeError::ensure_output_capacity(
121            output.len(),
122            output_index,
123            required,
124        )?;
125
126        let reset_written = unsafe {
127            // SAFETY: The capacity check above reserves the combined
128            // reset-plus-value output bound at `output_index`.
129            self.encode_reset(output, output_index)
130        }
131        .map_err(|error| {
132            TranscodeError::domain(CodecEncodeError::encode_reset(error))
133        })?;
134        assert!(
135            reset_written <= reset_units,
136            "Codec::encode_reset wrote beyond its reset bound",
137        );
138
139        let value_written = unsafe {
140            // SAFETY: `reset_written <= reset_units` and the earlier combined
141            // capacity check leave the exact value width writable.
142            self.encode(value, output, output_index + reset_written)
143        }
144        .map_err(|error| {
145            TranscodeError::domain(CodecEncodeError::encode(error, 0))
146        })?
147        .get();
148        assert!(
149            value_written == value_units,
150            "Codec::encode wrote a different length than Codec::encode_len",
151        );
152        Ok(reset_written + value_written)
153    }
154
155    /// Decodes one value and flushes decode-side state into caller storage.
156    ///
157    /// The method validates input and flush-output bounds before entering the
158    /// unchecked codec hooks. It returns the decoded value, the consumed input
159    /// count, and the number of flushed values written to `flush_output`.
160    ///
161    /// # Parameters
162    ///
163    /// - `input`: Source unit buffer.
164    /// - `input_index`: Start index in `input`.
165    /// - `flush_output`: Destination value buffer for decode-flush output.
166    /// - `flush_output_index`: Start index in `flush_output`.
167    ///
168    /// # Returns
169    ///
170    /// Returns `(value, consumed, flushed)`.
171    ///
172    /// # Errors
173    ///
174    /// Returns [`TranscodeError`] when input or output bounds are invalid, when
175    /// flush output capacity is insufficient, or when decoding or flushing
176    /// fails.
177    ///
178    /// # Panics
179    ///
180    /// Panics when the codec consumes beyond available input or flushes more
181    /// values than its declared bound.
182    fn decode_value_with_flush(
183        &mut self,
184        input: &[Self::Unit],
185        input_index: usize,
186        flush_output: &mut [Self::Value],
187        flush_output_index: usize,
188    ) -> CodecDecodeValueWithFlushResult<Self::Value, Self::DecodeError> {
189        TranscodeError::ensure_input_index(input.len(), input_index)?;
190        let min_units = Self::MIN_UNITS_PER_VALUE.get();
191        ensure_min_input(input.len(), input_index, min_units)?;
192
193        let flush_cap = Self::MAX_DECODE_FLUSH_VALUES;
194        TranscodeError::ensure_output_capacity(
195            flush_output.len(),
196            flush_output_index,
197            flush_cap,
198        )?;
199
200        let (value, consumed) = unsafe {
201            // SAFETY: The input checks above guarantee the minimum readable
202            // units required by `Codec::decode`.
203            self.decode(input, input_index)
204        }
205        .map_err(|failure| {
206            TranscodeError::domain(map_decode_failure(
207                failure,
208                input_index,
209                input.len() - input_index,
210            ))
211        })?;
212        let available = input.len() - input_index;
213        assert!(
214            consumed.get() <= available,
215            "Codec::decode consumed beyond available input",
216        );
217
218        let flushed = unsafe {
219            // SAFETY: The flush-output checks above reserve the declared flush
220            // output bound at `flush_output_index`.
221            self.decode_flush(flush_output, flush_output_index)
222        }
223        .map_err(|error| {
224            TranscodeError::domain(CodecDecodeError::decode_flush(error))
225        })?;
226        assert!(
227            flushed <= flush_cap,
228            "Codec::decode_flush wrote beyond its flush bound",
229        );
230        Ok((value, consumed, flushed))
231    }
232
233    /// Decodes exactly one value and then flushes decode-side state.
234    ///
235    /// Unlike [`decode_value_with_flush`](Self::decode_value_with_flush), this
236    /// helper requires the supplied input slice to contain exactly one encoded
237    /// value. It validates trailing input before calling
238    /// [`Codec::decode_flush`], preserving whole-value decoder semantics while
239    /// still centralizing flush scratch-buffer handling.
240    ///
241    /// # Parameters
242    ///
243    /// - `input`: Source units for exactly one encoded value.
244    /// - `flush_output`: Destination value buffer for decode-flush output.
245    /// - `flush_output_index`: Start index in `flush_output`.
246    ///
247    /// # Returns
248    ///
249    /// Returns `(value, flushed)`.
250    ///
251    /// # Errors
252    ///
253    /// Returns [`TranscodeError`] when flush output bounds are invalid or
254    /// insufficient, when decoding fails, or when exact-value decode semantics
255    /// are not satisfied.
256    ///
257    /// # Panics
258    ///
259    /// Panics when the codec consumes beyond available input or flushes more
260    /// values than its declared bound.
261    fn decode_exact_value_with_flush(
262        &mut self,
263        input: &[Self::Unit],
264        flush_output: &mut [Self::Value],
265        flush_output_index: usize,
266    ) -> CodecDecodeExactValueWithFlushResult<Self::Value, Self::DecodeError>
267    {
268        let min_units = Self::MIN_UNITS_PER_VALUE.get();
269        ensure_min_input(input.len(), 0, min_units)?;
270
271        let flush_cap = Self::MAX_DECODE_FLUSH_VALUES;
272        TranscodeError::ensure_output_capacity(
273            flush_output.len(),
274            flush_output_index,
275            flush_cap,
276        )?;
277
278        let (value, consumed) = unsafe {
279            // SAFETY: The input check above guarantees the minimum readable
280            // units required by `Codec::decode` at index 0.
281            self.decode(input, 0)
282        }
283        .map_err(|failure| {
284            TranscodeError::domain(map_decode_failure(failure, 0, input.len()))
285        })?;
286        assert!(
287            consumed.get() <= input.len(),
288            "Codec::decode consumed beyond available input",
289        );
290        ensure_no_trailing_input(consumed.get(), input.len())?;
291
292        let flushed = unsafe {
293            // SAFETY: The flush-output checks above reserve the declared flush
294            // output bound at `flush_output_index`.
295            self.decode_flush(flush_output, flush_output_index)
296        }
297        .map_err(|error| {
298            TranscodeError::domain(CodecDecodeError::decode_flush(error))
299        })?;
300        assert!(
301            flushed <= flush_cap,
302            "Codec::decode_flush wrote beyond its flush bound",
303        );
304        Ok((value, flushed))
305    }
306}
307
308impl<C> CodecValueExt for C where C: Codec + ?Sized {}
309
310#[inline]
311fn map_decode_failure<E>(
312    failure: DecodeFailure<E>,
313    input_index: usize,
314    available: usize,
315) -> CodecDecodeError<E> {
316    match failure {
317        DecodeFailure::Incomplete { required_total } => {
318            CodecDecodeError::incomplete(
319                input_index,
320                required_total.get(),
321                available,
322            )
323        }
324        DecodeFailure::Invalid { source, .. } => {
325            CodecDecodeError::decode(source, input_index)
326        }
327    }
328}
329
330#[inline]
331fn ensure_min_input<E>(
332    input_len: usize,
333    input_index: usize,
334    min_required: usize,
335) -> Result<(), TranscodeError<CodecDecodeError<E>>> {
336    let available = input_len.saturating_sub(input_index);
337    if available < min_required {
338        return Err(TranscodeError::domain(CodecDecodeError::incomplete(
339            input_index,
340            min_required,
341            available,
342        )));
343    }
344    Ok(())
345}
346
347#[inline]
348fn ensure_no_trailing_input<E>(
349    consumed: usize,
350    total: usize,
351) -> Result<(), TranscodeError<CodecDecodeError<E>>> {
352    let remaining = total.saturating_sub(consumed);
353    if remaining != 0 {
354        return Err(TranscodeError::domain(CodecDecodeError::trailing_input(
355            consumed, remaining,
356        )));
357    }
358    Ok(())
359}