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}