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///
24/// # Examples
25///
26/// ```
27/// use qubit_codec::TranscodeFailure;
28///
29/// let failure = TranscodeFailure::invalid_input_index(4, 3);
30/// assert_eq!(failure.to_string(), "invalid input index 4 for input length 3");
31/// assert_eq!(TranscodeFailure::ensure_input_index(3, 4), Err(failure));
32/// ```
33#[derive(Clone, Copy, Debug, Eq, Error, Hash, PartialEq)]
34#[non_exhaustive]
35#[must_use]
36pub enum TranscodeFailure {
37    /// The caller supplied an input index outside the input slice.
38    #[error("invalid input index {index} for input length {input_len}")]
39    InvalidInputIndex {
40        /// Invalid input index supplied by the caller.
41        index: usize,
42        /// Length of the input slice.
43        input_len: usize,
44    },
45
46    /// The caller supplied an output index outside the output slice.
47    #[error("invalid output index {index} for output length {output_len}")]
48    InvalidOutputIndex {
49        /// Invalid output index supplied by the caller.
50        index: usize,
51        /// Length of the output slice.
52        output_len: usize,
53    },
54
55    /// The caller supplied an output range extending beyond the output slice.
56    #[error("invalid output range at index {output_index} with length {range_len} for output length {output_len}")]
57    InvalidOutputRange {
58        /// Absolute output index where the range starts.
59        output_index: usize,
60        /// Length of the caller-supplied output range.
61        range_len: usize,
62        /// Length of the output slice.
63        output_len: usize,
64    },
65
66    /// The output slice cannot hold all required output.
67    #[error("insufficient output at index {output_index}: required {required} units, available {available}")]
68    InsufficientOutput {
69        /// Absolute output index where writing would start.
70        output_index: usize,
71        /// Output units required from `output_index`.
72        required: usize,
73        /// Output units available from `output_index`.
74        available: usize,
75    },
76
77    /// Output length arithmetic overflowed.
78    #[error("output length arithmetic overflow")]
79    OutputLengthOverflow,
80
81    /// The allocator could not reserve the requested output storage.
82    #[error("output allocation failed")]
83    AllocationFailed,
84
85    /// The complete input ended with an incomplete value.
86    #[error(
87        "incomplete input at index {input_index}: at least {required} units required to retry, {available} available"
88    )]
89    IncompleteInput {
90        /// Absolute input index where the incomplete value starts.
91        input_index: usize,
92        /// Current minimum total input units required from `input_index`
93        /// before retrying. A later retry may raise this lower bound.
94        required: usize,
95        /// Input units available from `input_index`.
96        available: usize,
97    },
98
99    /// A transcoder returned progress inconsistent with the supplied buffers.
100    #[error("invalid transcoder progress: {source}")]
101    InvalidProgress {
102        /// Contract error describing the invalid progress.
103        #[source]
104        source: TranscodeContractError,
105    },
106
107    /// The input contains exactly one decoded value plus trailing units.
108    #[error("trailing input after value: consumed {consumed} units, remaining {remaining}")]
109    TrailingInput {
110        /// Units consumed by the decoded value.
111        consumed: usize,
112        /// Extra units left after the decoded value.
113        remaining: usize,
114    },
115
116    /// A strict single-value decoder cannot expose codec lifecycle output.
117    #[error(
118        "strict single-value decoding does not support lifecycle output: reset bound {reset_bound}, finish bound {finish_bound}"
119    )]
120    UnsupportedDecodeLifecycleOutput {
121        /// Maximum values the codec may emit while resetting decode state.
122        reset_bound: usize,
123        /// Maximum values the codec may emit while finishing decode state.
124        finish_bound: usize,
125    },
126
127    /// `transcode` was called after the logical stream was finished.
128    #[error("transcode called after finish without an intervening reset")]
129    TranscodeAfterFinish,
130
131    /// `transcode` was called before the first successful reset.
132    #[error("transcode called before reset")]
133    TranscodeBeforeReset,
134
135    /// `finish` was called after the logical stream was already finished.
136    #[error("finish called twice without an intervening reset")]
137    FinishAfterFinish,
138
139    /// `finish` was called before the first successful reset.
140    #[error("finish called before reset")]
141    FinishBeforeReset,
142
143    /// Reset or finish failed after codec, hook, or buffered state may have
144    /// changed. Continuing could repeat side effects or observe inconsistent
145    /// state; a successful reset is required before further use.
146    #[error("transcoder lifecycle is poisoned; a successful reset is required")]
147    LifecyclePoisoned,
148}
149
150impl TranscodeFailure {
151    /// Creates an invalid-input-index error.
152    ///
153    /// # Parameters
154    ///
155    /// - `index`: Absolute input index rejected by the caller boundary.
156    /// - `len`: Total number of input units.
157    ///
158    /// # Returns
159    ///
160    /// Returns a failure retaining the supplied index and input length.
161    #[inline]
162    pub const fn invalid_input_index(index: usize, len: usize) -> Self {
163        Self::InvalidInputIndex { index, input_len: len }
164    }
165
166    /// Creates an invalid-output-index error.
167    ///
168    /// # Parameters
169    ///
170    /// - `index`: Absolute output index rejected by the caller boundary.
171    /// - `len`: Total number of writable output units.
172    ///
173    /// # Returns
174    ///
175    /// Returns a failure retaining the supplied index and output length.
176    #[inline]
177    pub const fn invalid_output_index(index: usize, len: usize) -> Self {
178        Self::InvalidOutputIndex { index, output_len: len }
179    }
180
181    /// Creates an invalid-output-range error.
182    ///
183    /// # Parameters
184    ///
185    /// - `output_index`: Absolute start of the requested output range.
186    /// - `range_len`: Requested range length.
187    /// - `output_len`: Total output slice length.
188    ///
189    /// # Returns
190    ///
191    /// Returns a failure describing the range that exceeds the output slice.
192    #[inline]
193    pub const fn invalid_output_range(output_index: usize, range_len: usize, output_len: usize) -> Self {
194        Self::InvalidOutputRange {
195            output_index,
196            range_len,
197            output_len,
198        }
199    }
200
201    /// Creates an insufficient-output error.
202    ///
203    /// # Parameters
204    ///
205    /// - `output_index`: Absolute start of the writable output.
206    /// - `required`: Units needed from that index.
207    /// - `available`: Units actually available from that index.
208    ///
209    /// # Returns
210    ///
211    /// Returns a failure retaining the required and available capacities.
212    #[inline]
213    pub const fn insufficient_output(output_index: usize, required: usize, available: usize) -> Self {
214        Self::InsufficientOutput {
215            output_index,
216            required,
217            available,
218        }
219    }
220
221    /// Creates an output-length-overflow error.
222    ///
223    /// # Returns
224    ///
225    /// Returns the framework marker for unrepresentable output capacity.
226    #[inline]
227    pub const fn output_length_overflow() -> Self {
228        Self::OutputLengthOverflow
229    }
230
231    /// Creates an output-allocation failure.
232    ///
233    /// # Returns
234    ///
235    /// Returns the framework marker for a failed output allocation.
236    #[inline]
237    pub const fn allocation_failed() -> Self {
238        Self::AllocationFailed
239    }
240
241    /// Creates an incomplete-input error.
242    ///
243    /// # Parameters
244    ///
245    /// - `input_index`: Absolute index where the incomplete value starts.
246    /// - `required`: Current minimum total units needed from that index.
247    /// - `available`: Units currently available from that index.
248    ///
249    /// # Returns
250    ///
251    /// Returns a failure preserving the incomplete tail and retry requirement.
252    #[inline]
253    pub const fn incomplete_input(input_index: usize, required: usize, available: usize) -> Self {
254        Self::IncompleteInput {
255            input_index,
256            required,
257            available,
258        }
259    }
260
261    /// Creates an invalid-progress error.
262    ///
263    /// # Parameters
264    ///
265    /// - `source`: Contract violation detected in a transcoder report.
266    ///
267    /// # Returns
268    ///
269    /// Returns a framework failure that retains the contract error as its
270    /// source.
271    #[inline]
272    pub const fn invalid_progress(source: TranscodeContractError) -> Self {
273        Self::InvalidProgress { source }
274    }
275
276    /// Creates a trailing-input error.
277    ///
278    /// # Parameters
279    ///
280    /// - `consumed`: Units used by the decoded value.
281    /// - `remaining`: Extra units after that value.
282    ///
283    /// # Returns
284    ///
285    /// Returns a failure describing the otherwise successful single-value
286    /// decode.
287    #[inline]
288    pub const fn trailing_input(consumed: usize, remaining: usize) -> Self {
289        Self::TrailingInput { consumed, remaining }
290    }
291
292    /// Creates an unsupported decode-lifecycle-output error.
293    ///
294    /// # Parameters
295    ///
296    /// - `reset_bound`: Maximum values produced by decode reset.
297    /// - `finish_bound`: Maximum values produced by decode finish.
298    ///
299    /// # Returns
300    ///
301    /// Returns a failure retaining the lifecycle output bounds rejected by
302    /// strict decoding.
303    #[inline]
304    pub const fn unsupported_decode_lifecycle_output(reset_bound: usize, finish_bound: usize) -> Self {
305        Self::UnsupportedDecodeLifecycleOutput {
306            reset_bound,
307            finish_bound,
308        }
309    }
310
311    /// Validates that `input_index` is within an input slice.
312    ///
313    /// # Parameters
314    ///
315    /// - `input_len`: Total input slice length.
316    /// - `input_index`: Absolute start index, including the end boundary.
317    ///
318    /// # Returns
319    ///
320    /// Returns `Ok(())` when the index is at most the input length.
321    ///
322    /// # Errors
323    ///
324    /// Returns [`Self::InvalidInputIndex`] when the index exceeds the length.
325    #[inline]
326    pub fn ensure_input_index(input_len: usize, input_index: usize) -> Result<(), Self> {
327        if input_index > input_len {
328            return Err(Self::invalid_input_index(input_index, input_len));
329        }
330        Ok(())
331    }
332
333    /// Validates that enough input units are available from `input_index`.
334    ///
335    /// # Parameters
336    ///
337    /// - `input_len`: Total input slice length.
338    /// - `input_index`: Absolute start of the input region.
339    /// - `min_required`: Minimum readable units required from that start.
340    ///
341    /// # Returns
342    ///
343    /// Returns `Ok(())` when the start index and minimum readable capacity are
344    /// valid.
345    ///
346    /// # Errors
347    ///
348    /// Returns [`Self::InvalidInputIndex`] for an invalid start, or
349    /// [`Self::IncompleteInput`] when fewer than `min_required` units remain.
350    #[inline]
351    pub fn ensure_min_input(input_len: usize, input_index: usize, min_required: usize) -> Result<(), Self> {
352        Self::ensure_input_index(input_len, input_index)?;
353        let available = input_len - input_index;
354        if available < min_required {
355            return Err(Self::incomplete_input(input_index, min_required, available));
356        }
357        Ok(())
358    }
359
360    /// Validates a consumed input count and rejects trailing input.
361    ///
362    /// # Parameters
363    ///
364    /// - `consumed`: Number of input units consumed by the decoded value.
365    /// - `input_len`: Total number of input units supplied.
366    ///
367    /// # Returns
368    ///
369    /// Returns `Ok(())` when `consumed == input_len`.
370    ///
371    /// # Errors
372    ///
373    /// Returns [`TranscodeFailure::InvalidInputIndex`] when `consumed` exceeds
374    /// `input_len`, or [`TranscodeFailure::TrailingInput`] when unconsumed
375    /// input remains.
376    #[inline]
377    pub fn ensure_no_trailing_input(consumed: usize, input_len: usize) -> Result<(), Self> {
378        Self::ensure_input_index(input_len, consumed)?;
379        let remaining = input_len - consumed;
380        if remaining != 0 {
381            return Err(Self::trailing_input(consumed, remaining));
382        }
383        Ok(())
384    }
385
386    /// Validates that `output_index` is within an output slice.
387    ///
388    /// # Parameters
389    ///
390    /// - `output_len`: Total output slice length.
391    /// - `output_index`: Absolute start index, including the end boundary.
392    ///
393    /// # Returns
394    ///
395    /// Returns `Ok(())` when the index is at most the output length.
396    ///
397    /// # Errors
398    ///
399    /// Returns [`Self::InvalidOutputIndex`] when the index exceeds the length.
400    #[inline]
401    pub fn ensure_output_index(output_len: usize, output_index: usize) -> Result<(), Self> {
402        if output_index > output_len {
403            return Err(Self::invalid_output_index(output_index, output_len));
404        }
405        Ok(())
406    }
407
408    /// Validates input and output start indices for a transcode call.
409    ///
410    /// # Parameters
411    ///
412    /// - `input_len`: Total input slice length.
413    /// - `input_index`: Absolute starting input index.
414    /// - `output_len`: Total output slice length.
415    /// - `output_index`: Absolute starting output index.
416    ///
417    /// # Returns
418    ///
419    /// Returns `Ok(())` when both indices lie within their slices or at their
420    /// ends.
421    ///
422    /// # Errors
423    ///
424    /// Returns [`Self::InvalidInputIndex`] first for invalid input, otherwise
425    /// [`Self::InvalidOutputIndex`] for invalid output.
426    #[inline]
427    pub fn ensure_transcode_indices(
428        input_len: usize,
429        input_index: usize,
430        output_len: usize,
431        output_index: usize,
432    ) -> Result<(), Self> {
433        Self::ensure_input_index(input_len, input_index)?;
434        Self::ensure_output_index(output_len, output_index)
435    }
436
437    /// Validates that an output slice can hold one-shot finalization output.
438    ///
439    /// # Parameters
440    ///
441    /// - `output_len`: Total output slice length.
442    /// - `output_index`: Absolute start of the writable region.
443    /// - `required`: Minimum output units needed from that start.
444    ///
445    /// # Returns
446    ///
447    /// Returns `Ok(())` when the output region has at least `required` units.
448    ///
449    /// # Errors
450    ///
451    /// Returns [`Self::InvalidOutputIndex`] for an invalid start, or
452    /// [`Self::InsufficientOutput`] when the remaining capacity is too small.
453    #[inline]
454    pub fn ensure_output_capacity(output_len: usize, output_index: usize, required: usize) -> Result<(), Self> {
455        Self::ensure_output_index(output_len, output_index)?;
456        let available = output_len - output_index;
457        if available < required {
458            return Err(Self::insufficient_output(output_index, required, available));
459        }
460        Ok(())
461    }
462
463    /// Validates an indexed output range and its minimum writable capacity.
464    ///
465    /// # Parameters
466    ///
467    /// - `output_len`: Total output slice length.
468    /// - `output_index`: Absolute start of the writable range.
469    /// - `range_len`: Length of the caller-selected writable range.
470    /// - `required`: Minimum capacity required within the selected range.
471    ///
472    /// # Returns
473    ///
474    /// Returns `Ok(())` when the selected range fits and has enough capacity.
475    ///
476    /// # Errors
477    ///
478    /// Returns [`Self::InvalidOutputIndex`] for an invalid start,
479    /// [`Self::InvalidOutputRange`] when the range exceeds the slice, or
480    /// [`Self::InsufficientOutput`] when the selected range is too short.
481    #[inline]
482    pub fn ensure_output_range(
483        output_len: usize,
484        output_index: usize,
485        range_len: usize,
486        required: usize,
487    ) -> Result<(), Self> {
488        Self::ensure_output_index(output_len, output_index)?;
489        let available = output_len - output_index;
490        if range_len > available {
491            return Err(Self::invalid_output_range(output_index, range_len, output_len));
492        }
493        if range_len < required {
494            return Err(Self::insufficient_output(output_index, required, range_len));
495        }
496        Ok(())
497    }
498
499    /// Rejects lifecycle output for a strict single-value decode.
500    ///
501    /// # Type Parameters
502    ///
503    /// - `C`: Codec whose decode lifecycle bounds are checked.
504    ///
505    /// # Returns
506    ///
507    /// Returns `Ok(())` when both decode lifecycle output bounds are zero.
508    ///
509    /// # Errors
510    ///
511    /// Returns [`Self::UnsupportedDecodeLifecycleOutput`] when reset or finish
512    /// may emit values.
513    #[inline]
514    pub(crate) fn ensure_no_decode_lifecycle_output<C>() -> Result<(), Self>
515    where
516        C: Codec,
517    {
518        assert_unit_bounds::<C>();
519        if C::MAX_DECODE_RESET_VALUES != 0 || C::MAX_DECODE_FINISH_VALUES != 0 {
520            return Err(Self::unsupported_decode_lifecycle_output(
521                C::MAX_DECODE_RESET_VALUES,
522                C::MAX_DECODE_FINISH_VALUES,
523            ));
524        }
525        Ok(())
526    }
527}
528
529impl From<CapacityError> for TranscodeFailure {
530    /// Converts capacity planning errors into transcode framework failures.
531    ///
532    /// # Parameters
533    ///
534    /// - `error`: Capacity-planning failure to translate.
535    ///
536    /// # Returns
537    ///
538    /// Returns the corresponding framework output-length-overflow failure.
539    #[inline]
540    fn from(error: CapacityError) -> Self {
541        match error {
542            CapacityError::OutputLengthOverflow => Self::OutputLengthOverflow,
543        }
544    }
545}
546
547impl From<TranscodeContractError> for TranscodeFailure {
548    /// Converts a broken transcoder progress report into a framework failure.
549    ///
550    /// # Parameters
551    ///
552    /// - `error`: Progress-contract violation to retain as the error source.
553    ///
554    /// # Returns
555    ///
556    /// Returns [`Self::InvalidProgress`] carrying the original violation.
557    #[inline]
558    fn from(error: TranscodeContractError) -> Self {
559        Self::invalid_progress(error)
560    }
561}