Skip to main content

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}