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 crate::{
13    Codec,
14    codec::assert_unit_bounds,
15};
16
17use super::capacity_error::CapacityError;
18
19/// Framework-level failure reported by a transcode operation.
20///
21/// These failures are part of the safe public transcode API. They describe
22/// caller-supplied buffer ranges, capacity planning, complete-input shape, and
23/// stream lifecycle misuse that the transcode layer can detect without
24/// interpreting a concrete codec error.
25#[derive(Clone, Copy, Debug, Eq, Error, Hash, PartialEq)]
26#[non_exhaustive]
27pub enum TranscodeFailure {
28    /// The caller supplied an input index outside the input slice.
29    #[error("invalid input index {index} for input length {input_len}")]
30    InvalidInputIndex {
31        /// Invalid input index supplied by the caller.
32        index: usize,
33        /// Length of the input slice.
34        input_len: usize,
35    },
36
37    /// The caller supplied an output index outside the output slice.
38    #[error("invalid output index {index} for output length {output_len}")]
39    InvalidOutputIndex {
40        /// Invalid output index supplied by the caller.
41        index: usize,
42        /// Length of the output slice.
43        output_len: usize,
44    },
45
46    /// The caller supplied an output range extending beyond the output slice.
47    #[error(
48        "invalid output range at index {output_index} with length {range_len} for output length {output_len}"
49    )]
50    InvalidOutputRange {
51        /// Absolute output index where the range starts.
52        output_index: usize,
53        /// Length of the caller-supplied output range.
54        range_len: usize,
55        /// Length of the output slice.
56        output_len: usize,
57    },
58
59    /// The output slice cannot hold all required output.
60    #[error(
61        "insufficient output at index {output_index}: required {required} units, available {available}"
62    )]
63    InsufficientOutput {
64        /// Absolute output index where writing would start.
65        output_index: usize,
66        /// Output units required from `output_index`.
67        required: usize,
68        /// Output units available from `output_index`.
69        available: usize,
70    },
71
72    /// Output length arithmetic overflowed.
73    #[error("output length arithmetic overflow")]
74    OutputLengthOverflow,
75
76    /// The allocator could not reserve the requested output storage.
77    #[error("output allocation failed")]
78    AllocationFailed,
79
80    /// The complete input ended with an incomplete value.
81    #[error(
82        "incomplete input at index {input_index}: at least {required} units required to retry, {available} available"
83    )]
84    IncompleteInput {
85        /// Absolute input index where the incomplete value starts.
86        input_index: usize,
87        /// Current minimum total input units required from `input_index`
88        /// before retrying. A later retry may raise this lower bound.
89        required: usize,
90        /// Input units available from `input_index`.
91        available: usize,
92    },
93
94    /// The input contains exactly one decoded value plus trailing units.
95    #[error(
96        "trailing input after value: consumed {consumed} units, remaining {remaining}"
97    )]
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 {
145            index,
146            input_len: len,
147        }
148    }
149
150    /// Creates an invalid-output-index error.
151    #[inline(always)]
152    #[must_use]
153    pub const fn invalid_output_index(index: usize, len: usize) -> Self {
154        Self::InvalidOutputIndex {
155            index,
156            output_len: len,
157        }
158    }
159
160    /// Creates an invalid-output-range error.
161    #[inline(always)]
162    #[must_use]
163    pub const fn invalid_output_range(
164        output_index: usize,
165        range_len: usize,
166        output_len: usize,
167    ) -> Self {
168        Self::InvalidOutputRange {
169            output_index,
170            range_len,
171            output_len,
172        }
173    }
174
175    /// Creates an insufficient-output error.
176    #[inline(always)]
177    #[must_use]
178    pub const fn insufficient_output(
179        output_index: usize,
180        required: usize,
181        available: usize,
182    ) -> Self {
183        Self::InsufficientOutput {
184            output_index,
185            required,
186            available,
187        }
188    }
189
190    /// Creates an output-length-overflow error.
191    #[inline(always)]
192    #[must_use]
193    pub const fn output_length_overflow() -> Self {
194        Self::OutputLengthOverflow
195    }
196
197    /// Creates an output-allocation failure.
198    #[inline(always)]
199    #[must_use]
200    pub const fn allocation_failed() -> Self {
201        Self::AllocationFailed
202    }
203
204    /// Creates an incomplete-input error.
205    #[inline(always)]
206    #[must_use]
207    pub const fn incomplete_input(
208        input_index: usize,
209        required: usize,
210        available: usize,
211    ) -> Self {
212        Self::IncompleteInput {
213            input_index,
214            required,
215            available,
216        }
217    }
218
219    /// Creates a trailing-input error.
220    #[inline(always)]
221    #[must_use]
222    pub const fn trailing_input(consumed: usize, remaining: usize) -> Self {
223        Self::TrailingInput {
224            consumed,
225            remaining,
226        }
227    }
228
229    /// Creates an unsupported decode-lifecycle-output error.
230    #[inline(always)]
231    #[must_use]
232    pub const fn unsupported_decode_lifecycle_output(
233        reset_bound: usize,
234        finish_bound: usize,
235    ) -> Self {
236        Self::UnsupportedDecodeLifecycleOutput {
237            reset_bound,
238            finish_bound,
239        }
240    }
241
242    /// Rejects lifecycle output for a strict single-value decode.
243    ///
244    /// # Type Parameters
245    ///
246    /// - `C`: Codec whose decode lifecycle bounds are checked.
247    ///
248    /// # Returns
249    ///
250    /// Returns `Ok(())` when both decode lifecycle output bounds are zero.
251    ///
252    /// # Errors
253    ///
254    /// Returns [`Self::UnsupportedDecodeLifecycleOutput`] when reset or finish
255    /// may emit values.
256    #[inline(always)]
257    pub(crate) fn ensure_no_decode_lifecycle_output<C>() -> Result<(), Self>
258    where
259        C: Codec,
260    {
261        assert_unit_bounds::<C>();
262        if C::MAX_DECODE_RESET_VALUES != 0 || C::MAX_DECODE_FINISH_VALUES != 0 {
263            return Err(Self::unsupported_decode_lifecycle_output(
264                C::MAX_DECODE_RESET_VALUES,
265                C::MAX_DECODE_FINISH_VALUES,
266            ));
267        }
268        Ok(())
269    }
270
271    /// Validates that `input_index` is within an input slice.
272    #[inline]
273    pub fn ensure_input_index(
274        input_len: usize,
275        input_index: usize,
276    ) -> Result<(), Self> {
277        if input_index > input_len {
278            return Err(Self::invalid_input_index(input_index, input_len));
279        }
280        Ok(())
281    }
282
283    /// Validates that enough input units are available from `input_index`.
284    #[inline]
285    pub fn ensure_min_input(
286        input_len: usize,
287        input_index: usize,
288        min_required: usize,
289    ) -> Result<(), Self> {
290        Self::ensure_input_index(input_len, input_index)?;
291        let available = input_len - input_index;
292        if available < min_required {
293            return Err(Self::incomplete_input(
294                input_index,
295                min_required,
296                available,
297            ));
298        }
299        Ok(())
300    }
301
302    /// Validates a consumed input count and rejects trailing input.
303    ///
304    /// # Parameters
305    ///
306    /// - `consumed`: Number of input units consumed by the decoded value.
307    /// - `input_len`: Total number of input units supplied.
308    ///
309    /// # Returns
310    ///
311    /// Returns `Ok(())` when `consumed == input_len`.
312    ///
313    /// # Errors
314    ///
315    /// Returns [`TranscodeFailure::InvalidInputIndex`] when `consumed` exceeds
316    /// `input_len`, or [`TranscodeFailure::TrailingInput`] when unconsumed
317    /// input remains.
318    #[inline]
319    pub fn ensure_no_trailing_input(
320        consumed: usize,
321        input_len: usize,
322    ) -> Result<(), Self> {
323        Self::ensure_input_index(input_len, consumed)?;
324        let remaining = input_len - consumed;
325        if remaining != 0 {
326            return Err(Self::trailing_input(consumed, remaining));
327        }
328        Ok(())
329    }
330
331    /// Validates that `output_index` is within an output slice.
332    #[inline]
333    pub fn ensure_output_index(
334        output_len: usize,
335        output_index: usize,
336    ) -> Result<(), Self> {
337        if output_index > output_len {
338            return Err(Self::invalid_output_index(output_index, output_len));
339        }
340        Ok(())
341    }
342
343    /// Validates input and output start indices for a transcode call.
344    #[inline]
345    pub fn ensure_transcode_indices(
346        input_len: usize,
347        input_index: usize,
348        output_len: usize,
349        output_index: usize,
350    ) -> Result<(), Self> {
351        Self::ensure_input_index(input_len, input_index)?;
352        Self::ensure_output_index(output_len, output_index)
353    }
354
355    /// Validates that an output slice can hold one-shot finalization output.
356    #[inline]
357    pub fn ensure_output_capacity(
358        output_len: usize,
359        output_index: usize,
360        required: usize,
361    ) -> Result<(), Self> {
362        Self::ensure_output_index(output_len, output_index)?;
363        let available = output_len - output_index;
364        if available < required {
365            return Err(Self::insufficient_output(
366                output_index,
367                required,
368                available,
369            ));
370        }
371        Ok(())
372    }
373
374    /// Validates an indexed output range and its minimum writable capacity.
375    #[inline]
376    pub fn ensure_output_range(
377        output_len: usize,
378        output_index: usize,
379        range_len: usize,
380        required: usize,
381    ) -> Result<(), Self> {
382        Self::ensure_output_index(output_len, output_index)?;
383        let available = output_len - output_index;
384        if range_len > available {
385            return Err(Self::invalid_output_range(
386                output_index,
387                range_len,
388                output_len,
389            ));
390        }
391        if range_len < required {
392            return Err(Self::insufficient_output(
393                output_index,
394                required,
395                range_len,
396            ));
397        }
398        Ok(())
399    }
400}
401
402impl From<CapacityError> for TranscodeFailure {
403    /// Converts capacity planning errors into transcode framework failures.
404    #[inline(always)]
405    fn from(error: CapacityError) -> Self {
406        match error {
407            CapacityError::OutputLengthOverflow => Self::OutputLengthOverflow,
408        }
409    }
410}