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