Skip to main content

qubit_codec/codec/
codec_decode_error.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//! Generic decode error used by codec adapters.
9
10use thiserror::Error;
11
12/// Error reported by codec-backed value and buffered decoder adapters.
13///
14/// The wrapped codec remains responsible for domain-specific decode failures.
15/// This type adds adapter-level domain failures that cannot be represented by
16/// the wrapped codec itself, such as closed-input incomplete values and
17/// trailing units in exact-value decodes. Buffer index and capacity failures
18/// are represented by [`crate::TranscodeError`].
19#[derive(Clone, Copy, Debug, Eq, Error, Hash, PartialEq)]
20pub enum CodecDecodeError<E> {
21    /// The wrapped codec reported a decode error.
22    #[error("codec decode error at input index {input_index}: {source}")]
23    Decode {
24        /// Error returned by the wrapped codec.
25        #[source]
26        source: E,
27        /// Absolute input index at which the adapter called the wrapped codec.
28        input_index: usize,
29    },
30
31    /// The wrapped codec reported an error while resetting decode state.
32    #[error("codec decode reset error: {source}")]
33    DecodeReset {
34        /// Error returned by [`crate::Codec::decode_reset`].
35        #[source]
36        source: E,
37    },
38
39    /// The wrapped codec reported an error while flushing decode state.
40    #[error("codec decode flush error: {source}")]
41    DecodeFlush {
42        /// Error returned by [`crate::Codec::decode_flush`].
43        #[source]
44        source: E,
45    },
46
47    /// The adapter could not safely call the wrapped codec because input ended.
48    #[error(
49        "incomplete input at index {input_index}: required {required_total} units, available {available}"
50    )]
51    Incomplete {
52        /// Absolute input index where the incomplete value starts.
53        input_index: usize,
54        /// Total units required from `input_index`.
55        required_total: usize,
56        /// Units available from `input_index`.
57        available: usize,
58    },
59
60    /// A whole-value decode succeeded but left trailing input units.
61    #[error(
62        "trailing input after decoded value: consumed {consumed} units, remaining {remaining}"
63    )]
64    TrailingInput {
65        /// Units consumed by the decoded value.
66        consumed: usize,
67        /// Extra units left after the decoded value.
68        remaining: usize,
69    },
70}
71
72impl<E> CodecDecodeError<E> {
73    /// Creates an error wrapping a codec-specific decode error.
74    ///
75    /// # Parameters
76    ///
77    /// - `source`: Error returned by the wrapped codec.
78    /// - `input_index`: Absolute input index used for the codec call.
79    ///
80    /// # Returns
81    ///
82    /// Returns a codec decode error wrapper.
83    #[inline(always)]
84    #[must_use]
85    pub const fn decode(source: E, input_index: usize) -> Self {
86        Self::Decode {
87            source,
88            input_index,
89        }
90    }
91
92    /// Creates an error wrapping a codec-specific decode-reset error.
93    ///
94    /// # Parameters
95    ///
96    /// - `source`: Error returned by [`crate::Codec::decode_reset`].
97    ///
98    /// # Returns
99    ///
100    /// Returns a codec decode-reset error wrapper.
101    #[inline(always)]
102    #[must_use]
103    pub const fn decode_reset(source: E) -> Self {
104        Self::DecodeReset { source }
105    }
106
107    /// Creates an error wrapping a codec-specific decode-flush error.
108    ///
109    /// # Parameters
110    ///
111    /// - `source`: Error returned by [`crate::Codec::decode_flush`].
112    ///
113    /// # Returns
114    ///
115    /// Returns a codec decode-flush error wrapper.
116    #[inline(always)]
117    #[must_use]
118    pub const fn decode_flush(source: E) -> Self {
119        Self::DecodeFlush { source }
120    }
121
122    /// Creates an adapter-level incomplete-input error.
123    ///
124    /// # Parameters
125    ///
126    /// - `input_index`: Absolute input index where the incomplete value starts.
127    /// - `required_total`: Total units required from `input_index`.
128    /// - `available`: Units available from `input_index`.
129    ///
130    /// # Returns
131    ///
132    /// Returns an incomplete-input error.
133    #[inline(always)]
134    #[must_use]
135    pub const fn incomplete(
136        input_index: usize,
137        required_total: usize,
138        available: usize,
139    ) -> Self {
140        Self::Incomplete {
141            input_index,
142            required_total,
143            available,
144        }
145    }
146
147    /// Creates a trailing-input error for whole-value decoding.
148    ///
149    /// # Parameters
150    ///
151    /// - `consumed`: Units consumed by the decoded value.
152    /// - `remaining`: Extra units left after the decoded value.
153    ///
154    /// # Returns
155    ///
156    /// Returns a trailing-input error.
157    #[inline(always)]
158    #[must_use]
159    pub const fn trailing_input(consumed: usize, remaining: usize) -> Self {
160        Self::TrailingInput {
161            consumed,
162            remaining,
163        }
164    }
165
166    /// Extracts the wrapped codec source error, when this variant has one.
167    ///
168    /// # Returns
169    ///
170    /// Returns `Some(source)` for codec decode, reset, and flush failures.
171    /// Returns `None` for adapter-only failures.
172    #[inline(always)]
173    #[must_use]
174    pub fn into_source(self) -> Option<E> {
175        match self {
176            Self::Decode { source, .. }
177            | Self::DecodeReset { source }
178            | Self::DecodeFlush { source } => Some(source),
179            Self::Incomplete { .. } | Self::TrailingInput { .. } => None,
180        }
181    }
182
183    /// Returns whether this error indicates an incomplete input prefix.
184    ///
185    /// # Returns
186    ///
187    /// Returns `true` only for the [`Incomplete`](Self::Incomplete) variant.
188    #[inline(always)]
189    #[must_use]
190    pub const fn is_incomplete(&self) -> bool {
191        matches!(self, Self::Incomplete { .. })
192    }
193
194    /// Returns the additional input units needed to make progress.
195    ///
196    /// This is a convenience accessor over the [`Incomplete`](Self::Incomplete)
197    /// variant's fields. Streaming callers can use it to determine how many
198    /// more units they must buffer before retrying a decode.
199    ///
200    /// # Returns
201    ///
202    /// Returns `Some(needed)` for [`Incomplete`](Self::Incomplete) errors,
203    /// where `needed` is the strictly positive difference between the minimum
204    /// units required and those already available. Returns `None` for all
205    /// other variants.
206    #[inline]
207    #[must_use]
208    pub fn needed_additional(&self) -> Option<core::num::NonZeroUsize> {
209        match *self {
210            Self::Incomplete {
211                required_total,
212                available,
213                ..
214            } => {
215                let needed = required_total.saturating_sub(available);
216                core::num::NonZeroUsize::new(needed)
217            }
218            _ => None,
219        }
220    }
221
222    /// Validates that enough input units are available from `input_index`.
223    ///
224    /// # Parameters
225    ///
226    /// - `input_len`: Length of the input slice.
227    /// - `input_index`: Absolute input index where reading starts.
228    /// - `min_required`: Minimum units required from `input_index`.
229    ///
230    /// # Returns
231    ///
232    /// Returns `Ok(())` when at least `min_required` units are available.
233    ///
234    /// # Errors
235    ///
236    /// Returns an incomplete-input error when fewer than `min_required` units
237    /// are available from `input_index`.
238    #[inline]
239    pub fn ensure_min_input(
240        input_len: usize,
241        input_index: usize,
242        min_required: usize,
243    ) -> Result<(), Self> {
244        let available = input_len.saturating_sub(input_index);
245        if available < min_required {
246            return Err(Self::incomplete(input_index, min_required, available));
247        }
248        Ok(())
249    }
250
251    /// Validates that decoding consumed the entire input slice.
252    ///
253    /// # Parameters
254    ///
255    /// - `consumed`: Units consumed by the decoded value.
256    /// - `total`: Total units in the input slice.
257    ///
258    /// # Returns
259    ///
260    /// Returns `Ok(())` when `consumed == total`.
261    ///
262    /// # Errors
263    ///
264    /// Returns a trailing-input error when extra units remain after the
265    /// decoded value.
266    #[inline]
267    pub fn ensure_no_trailing_input(
268        consumed: usize,
269        total: usize,
270    ) -> Result<(), Self> {
271        let remaining = total.saturating_sub(consumed);
272        if remaining != 0 {
273            return Err(Self::trailing_input(consumed, remaining));
274        }
275        Ok(())
276    }
277}