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 ///
514 /// # Panics
515 ///
516 /// Panics at compile time when `C` violates the required codec unit bounds.
517 #[inline]
518 pub(crate) fn ensure_no_decode_lifecycle_output<C>() -> Result<(), Self>
519 where
520 C: Codec,
521 {
522 assert_unit_bounds::<C>();
523 if C::MAX_DECODE_RESET_VALUES != 0 || C::MAX_DECODE_FINISH_VALUES != 0 {
524 return Err(Self::unsupported_decode_lifecycle_output(
525 C::MAX_DECODE_RESET_VALUES,
526 C::MAX_DECODE_FINISH_VALUES,
527 ));
528 }
529 Ok(())
530 }
531}
532
533impl From<CapacityError> for TranscodeFailure {
534 /// Converts capacity planning errors into transcode framework failures.
535 ///
536 /// # Parameters
537 ///
538 /// - `error`: Capacity-planning failure to translate.
539 ///
540 /// # Returns
541 ///
542 /// Returns the corresponding framework output-length-overflow failure.
543 #[inline]
544 fn from(error: CapacityError) -> Self {
545 match error {
546 CapacityError::OutputLengthOverflow => Self::OutputLengthOverflow,
547 }
548 }
549}
550
551impl From<TranscodeContractError> for TranscodeFailure {
552 /// Converts a broken transcoder progress report into a framework failure.
553 ///
554 /// # Parameters
555 ///
556 /// - `error`: Progress-contract violation to retain as the error source.
557 ///
558 /// # Returns
559 ///
560 /// Returns [`Self::InvalidProgress`] carrying the original violation.
561 #[inline]
562 fn from(error: TranscodeContractError) -> Self {
563 Self::invalid_progress(error)
564 }
565}