Skip to main content

qubit_codec/transcode/
transcode_failure.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//! Framework-level failures reported by safe transcode APIs.
9
10use thiserror::Error;
11
12use super::capacity_error::CapacityError;
13use super::transcode_contract_error::TranscodeContractError;
14use crate::Codec;
15use crate::codec::assert_unit_bounds;
16
17/// Framework-level failure reported by a transcode operation.
18///
19/// These failures are part of the safe public transcode API. They describe
20/// caller-supplied buffer ranges, capacity planning, complete-input shape, and
21/// stream lifecycle misuse that the transcode layer can detect without
22/// interpreting a concrete codec error.
23#[derive(Clone, Copy, Debug, Eq, Error, Hash, PartialEq)]
24#[non_exhaustive]
25pub enum TranscodeFailure {
26    /// The caller supplied an input index outside the input slice.
27    #[error("invalid input index {index} for input length {input_len}")]
28    InvalidInputIndex {
29        /// Invalid input index supplied by the caller.
30        index: usize,
31        /// Length of the input slice.
32        input_len: usize,
33    },
34
35    /// The caller supplied an output index outside the output slice.
36    #[error("invalid output index {index} for output length {output_len}")]
37    InvalidOutputIndex {
38        /// Invalid output index supplied by the caller.
39        index: usize,
40        /// Length of the output slice.
41        output_len: usize,
42    },
43
44    /// The caller supplied an output range extending beyond the output slice.
45    #[error("invalid output range at index {output_index} with length {range_len} for output length {output_len}")]
46    InvalidOutputRange {
47        /// Absolute output index where the range starts.
48        output_index: usize,
49        /// Length of the caller-supplied output range.
50        range_len: usize,
51        /// Length of the output slice.
52        output_len: usize,
53    },
54
55    /// The output slice cannot hold all required output.
56    #[error("insufficient output at index {output_index}: required {required} units, available {available}")]
57    InsufficientOutput {
58        /// Absolute output index where writing would start.
59        output_index: usize,
60        /// Output units required from `output_index`.
61        required: usize,
62        /// Output units available from `output_index`.
63        available: usize,
64    },
65
66    /// Output length arithmetic overflowed.
67    #[error("output length arithmetic overflow")]
68    OutputLengthOverflow,
69
70    /// The allocator could not reserve the requested output storage.
71    #[error("output allocation failed")]
72    AllocationFailed,
73
74    /// The complete input ended with an incomplete value.
75    #[error(
76        "incomplete input at index {input_index}: at least {required} units required to retry, {available} available"
77    )]
78    IncompleteInput {
79        /// Absolute input index where the incomplete value starts.
80        input_index: usize,
81        /// Current minimum total input units required from `input_index`
82        /// before retrying. A later retry may raise this lower bound.
83        required: usize,
84        /// Input units available from `input_index`.
85        available: usize,
86    },
87
88    /// A transcoder returned progress inconsistent with the supplied buffers.
89    #[error("invalid transcoder progress: {source}")]
90    InvalidProgress {
91        /// Contract error describing the invalid progress.
92        #[source]
93        source: TranscodeContractError,
94    },
95
96    /// The input contains exactly one decoded value plus trailing units.
97    #[error("trailing input after value: consumed {consumed} units, remaining {remaining}")]
98    TrailingInput {
99        /// Units consumed by the decoded value.
100        consumed: usize,
101        /// Extra units left after the decoded value.
102        remaining: usize,
103    },
104
105    /// A strict single-value decoder cannot expose codec lifecycle output.
106    #[error(
107        "strict single-value decoding does not support lifecycle output: reset bound {reset_bound}, finish bound {finish_bound}"
108    )]
109    UnsupportedDecodeLifecycleOutput {
110        /// Maximum values the codec may emit while resetting decode state.
111        reset_bound: usize,
112        /// Maximum values the codec may emit while finishing decode state.
113        finish_bound: usize,
114    },
115
116    /// `transcode` was called after the logical stream was finished.
117    #[error("transcode called after finish without an intervening reset")]
118    TranscodeAfterFinish,
119
120    /// `transcode` was called before the first successful reset.
121    #[error("transcode called before reset")]
122    TranscodeBeforeReset,
123
124    /// `finish` was called after the logical stream was already finished.
125    #[error("finish called twice without an intervening reset")]
126    FinishAfterFinish,
127
128    /// `finish` was called before the first successful reset.
129    #[error("finish called before reset")]
130    FinishBeforeReset,
131
132    /// Reset or finish failed after codec, hook, or buffered state may have
133    /// changed. Continuing could repeat side effects or observe inconsistent
134    /// state; a successful reset is required before further use.
135    #[error("transcoder lifecycle is poisoned; a successful reset is required")]
136    LifecyclePoisoned,
137}
138
139impl TranscodeFailure {
140    /// Creates an invalid-input-index error.
141    #[inline(always)]
142    #[must_use]
143    pub const fn invalid_input_index(index: usize, len: usize) -> Self {
144        Self::InvalidInputIndex { index, input_len: len }
145    }
146
147    /// Creates an invalid-output-index error.
148    #[inline(always)]
149    #[must_use]
150    pub const fn invalid_output_index(index: usize, len: usize) -> Self {
151        Self::InvalidOutputIndex { index, output_len: len }
152    }
153
154    /// Creates an invalid-output-range error.
155    #[inline(always)]
156    #[must_use]
157    pub const fn invalid_output_range(output_index: usize, range_len: usize, output_len: usize) -> Self {
158        Self::InvalidOutputRange {
159            output_index,
160            range_len,
161            output_len,
162        }
163    }
164
165    /// Creates an insufficient-output error.
166    #[inline(always)]
167    #[must_use]
168    pub const fn insufficient_output(output_index: usize, required: usize, available: usize) -> Self {
169        Self::InsufficientOutput {
170            output_index,
171            required,
172            available,
173        }
174    }
175
176    /// Creates an output-length-overflow error.
177    #[inline(always)]
178    #[must_use]
179    pub const fn output_length_overflow() -> Self {
180        Self::OutputLengthOverflow
181    }
182
183    /// Creates an output-allocation failure.
184    #[inline(always)]
185    #[must_use]
186    pub const fn allocation_failed() -> Self {
187        Self::AllocationFailed
188    }
189
190    /// Creates an incomplete-input error.
191    #[inline(always)]
192    #[must_use]
193    pub const fn incomplete_input(input_index: usize, required: usize, available: usize) -> Self {
194        Self::IncompleteInput {
195            input_index,
196            required,
197            available,
198        }
199    }
200
201    /// Creates an invalid-progress error.
202    #[inline(always)]
203    #[must_use]
204    pub const fn invalid_progress(source: TranscodeContractError) -> Self {
205        Self::InvalidProgress { source }
206    }
207
208    /// Creates a trailing-input error.
209    #[inline(always)]
210    #[must_use]
211    pub const fn trailing_input(consumed: usize, remaining: usize) -> Self {
212        Self::TrailingInput { consumed, remaining }
213    }
214
215    /// Creates an unsupported decode-lifecycle-output error.
216    #[inline(always)]
217    #[must_use]
218    pub const fn unsupported_decode_lifecycle_output(reset_bound: usize, finish_bound: usize) -> Self {
219        Self::UnsupportedDecodeLifecycleOutput {
220            reset_bound,
221            finish_bound,
222        }
223    }
224
225    /// Rejects lifecycle output for a strict single-value decode.
226    ///
227    /// # Type Parameters
228    ///
229    /// - `C`: Codec whose decode lifecycle bounds are checked.
230    ///
231    /// # Returns
232    ///
233    /// Returns `Ok(())` when both decode lifecycle output bounds are zero.
234    ///
235    /// # Errors
236    ///
237    /// Returns [`Self::UnsupportedDecodeLifecycleOutput`] when reset or finish
238    /// may emit values.
239    #[inline(always)]
240    pub(crate) fn ensure_no_decode_lifecycle_output<C>() -> Result<(), Self>
241    where
242        C: Codec,
243    {
244        assert_unit_bounds::<C>();
245        if C::MAX_DECODE_RESET_VALUES != 0 || C::MAX_DECODE_FINISH_VALUES != 0 {
246            return Err(Self::unsupported_decode_lifecycle_output(
247                C::MAX_DECODE_RESET_VALUES,
248                C::MAX_DECODE_FINISH_VALUES,
249            ));
250        }
251        Ok(())
252    }
253
254    /// Validates that `input_index` is within an input slice.
255    #[inline]
256    pub fn ensure_input_index(input_len: usize, input_index: usize) -> Result<(), Self> {
257        if input_index > input_len {
258            return Err(Self::invalid_input_index(input_index, input_len));
259        }
260        Ok(())
261    }
262
263    /// Validates that enough input units are available from `input_index`.
264    #[inline]
265    pub fn ensure_min_input(input_len: usize, input_index: usize, min_required: usize) -> Result<(), Self> {
266        Self::ensure_input_index(input_len, input_index)?;
267        let available = input_len - input_index;
268        if available < min_required {
269            return Err(Self::incomplete_input(input_index, min_required, available));
270        }
271        Ok(())
272    }
273
274    /// Validates a consumed input count and rejects trailing input.
275    ///
276    /// # Parameters
277    ///
278    /// - `consumed`: Number of input units consumed by the decoded value.
279    /// - `input_len`: Total number of input units supplied.
280    ///
281    /// # Returns
282    ///
283    /// Returns `Ok(())` when `consumed == input_len`.
284    ///
285    /// # Errors
286    ///
287    /// Returns [`TranscodeFailure::InvalidInputIndex`] when `consumed` exceeds
288    /// `input_len`, or [`TranscodeFailure::TrailingInput`] when unconsumed
289    /// input remains.
290    #[inline]
291    pub fn ensure_no_trailing_input(consumed: usize, input_len: usize) -> Result<(), Self> {
292        Self::ensure_input_index(input_len, consumed)?;
293        let remaining = input_len - consumed;
294        if remaining != 0 {
295            return Err(Self::trailing_input(consumed, remaining));
296        }
297        Ok(())
298    }
299
300    /// Validates that `output_index` is within an output slice.
301    #[inline]
302    pub fn ensure_output_index(output_len: usize, output_index: usize) -> Result<(), Self> {
303        if output_index > output_len {
304            return Err(Self::invalid_output_index(output_index, output_len));
305        }
306        Ok(())
307    }
308
309    /// Validates input and output start indices for a transcode call.
310    #[inline]
311    pub fn ensure_transcode_indices(
312        input_len: usize,
313        input_index: usize,
314        output_len: usize,
315        output_index: usize,
316    ) -> Result<(), Self> {
317        Self::ensure_input_index(input_len, input_index)?;
318        Self::ensure_output_index(output_len, output_index)
319    }
320
321    /// Validates that an output slice can hold one-shot finalization output.
322    #[inline]
323    pub fn ensure_output_capacity(output_len: usize, output_index: usize, required: usize) -> Result<(), Self> {
324        Self::ensure_output_index(output_len, output_index)?;
325        let available = output_len - output_index;
326        if available < required {
327            return Err(Self::insufficient_output(output_index, required, available));
328        }
329        Ok(())
330    }
331
332    /// Validates an indexed output range and its minimum writable capacity.
333    #[inline]
334    pub fn ensure_output_range(
335        output_len: usize,
336        output_index: usize,
337        range_len: usize,
338        required: usize,
339    ) -> Result<(), Self> {
340        Self::ensure_output_index(output_len, output_index)?;
341        let available = output_len - output_index;
342        if range_len > available {
343            return Err(Self::invalid_output_range(output_index, range_len, output_len));
344        }
345        if range_len < required {
346            return Err(Self::insufficient_output(output_index, required, range_len));
347        }
348        Ok(())
349    }
350}
351
352impl From<CapacityError> for TranscodeFailure {
353    /// Converts capacity planning errors into transcode framework failures.
354    #[inline(always)]
355    fn from(error: CapacityError) -> Self {
356        match error {
357            CapacityError::OutputLengthOverflow => Self::OutputLengthOverflow,
358        }
359    }
360}
361
362impl From<TranscodeContractError> for TranscodeFailure {
363    /// Converts a broken transcoder progress report into a framework failure.
364    #[inline(always)]
365    fn from(error: TranscodeContractError) -> Self {
366        Self::invalid_progress(error)
367    }
368}