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