qubit_codec_misc/misc_codec_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//! Shared codec error type.
9
10use core::num::NonZeroUsize;
11use std::string::FromUtf8Error;
12
13use ::base64::DecodeError;
14use qubit_codec::DecodeFailure;
15use thiserror::Error;
16
17use crate::base64_error_kind::Base64ErrorKind;
18
19/// Result alias returned by codec operations.
20pub type MiscCodecResult<T> = Result<T, MiscCodecError>;
21
22/// Error returned by codec operations.
23#[derive(Debug, Error)]
24pub enum MiscCodecError {
25 /// Base64 decoding failed in the configured dependency engine.
26 #[error("invalid Base64 input ({kind:?}): {source}")]
27 InvalidBase64 {
28 /// Structured category of the Base64 failure.
29 kind: Base64ErrorKind,
30 /// Input byte index, when the dependency reports one.
31 input_index: Option<usize>,
32 /// Invalid symbol, when the dependency reports one.
33 symbol: Option<u8>,
34 /// Invalid input length, when the dependency reports one.
35 input_length: Option<usize>,
36 /// Underlying Base64 decoding error.
37 #[source]
38 source: DecodeError,
39 },
40
41 /// A configured prefix was required but missing.
42 #[error("missing required prefix '{prefix}'")]
43 MissingPrefix {
44 /// Required prefix.
45 prefix: String,
46 },
47
48 /// Input contained a digit that is invalid for the requested radix.
49 #[error("invalid radix-{radix} digit '{character}' at index {index}")]
50 InvalidDigit {
51 /// Numeric radix expected by the codec.
52 radix: u32,
53 /// Character byte index in the original input.
54 index: usize,
55 /// Invalid character.
56 character: char,
57 },
58
59 /// Input length does not satisfy a codec requirement.
60 #[error("invalid length for {context}: expected {expected}, got {actual}")]
61 InvalidLength {
62 /// Human-readable input part whose length was invalid.
63 context: &'static str,
64 /// Human-readable length requirement.
65 expected: String,
66 /// Actual length observed by the codec.
67 actual: usize,
68 },
69
70 /// Input contained a malformed or unsupported escape sequence.
71 #[error("invalid escape {escape:?} at index {index}: {reason}")]
72 InvalidEscape {
73 /// Byte index of the escape marker in the original input.
74 index: usize,
75 /// Escape sequence fragment that caused the error.
76 escape: String,
77 /// Human-readable reason the escape was rejected.
78 reason: String,
79 },
80
81 /// Input contained a character that cannot appear in that context.
82 #[error("invalid character '{character}' at index {index}: {reason}")]
83 InvalidCharacter {
84 /// Character byte index in the original input.
85 index: usize,
86 /// Invalid character.
87 character: char,
88 /// Human-readable reason the character was rejected.
89 reason: String,
90 },
91
92 /// Input was rejected by a codec-specific validator.
93 #[error("invalid {codec} input: {reason}")]
94 InvalidInput {
95 /// Stable codec name, such as `base64`.
96 codec: &'static str,
97 /// Human-readable reason reported by the codec.
98 reason: String,
99 },
100
101 /// Decoded bytes were not valid UTF-8.
102 #[error("decoded bytes are not valid UTF-8: {source}")]
103 InvalidUtf8 {
104 /// Underlying UTF-8 conversion error.
105 #[from]
106 source: FromUtf8Error,
107 },
108}
109
110impl From<DecodeError> for MiscCodecError {
111 /// Preserves a Base64 dependency error and exposes its structured fields.
112 #[inline]
113 fn from(source: DecodeError) -> Self {
114 Self::InvalidBase64 {
115 kind: Base64ErrorKind::from_decode_error(&source),
116 input_index: Base64ErrorKind::input_index(&source),
117 symbol: Base64ErrorKind::symbol(&source),
118 input_length: Base64ErrorKind::input_length(&source),
119 source,
120 }
121 }
122}
123
124/// Maps a malformed value to a decode failure with a known input width.
125#[inline]
126pub(crate) fn map_misc_decode_failure_with_consumed(
127 error: MiscCodecError,
128 consumed: NonZeroUsize,
129) -> DecodeFailure<MiscCodecError> {
130 DecodeFailure::invalid(error, consumed)
131}