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}