secure_gate/error.rs
1//! Centralized error types for the secure-gate crate.
2//!
3//! # Error types
4//!
5//! | Type | Produced by | Feature |
6//! |------|------------|---------|
7//! | [`FromSliceError`] | [`Fixed::try_from(&[u8])`](crate::Fixed) | Always |
8//! | [`HexError`] | [`Fixed::try_from_hex`](crate::Fixed::try_from_hex), [`FromHexStr`](crate::FromHexStr) | `encoding-hex` |
9//! | [`Base64Error`] | [`Fixed::try_from_base64url`](crate::Fixed::try_from_base64url), [`FromBase64UrlStr`](crate::FromBase64UrlStr) | `encoding-base64` |
10//! | [`Bech32Error`] | `try_from_bech32*`, [`FromBech32Str`](crate::FromBech32Str), [`FromBech32mStr`](crate::FromBech32mStr) | `encoding-bech32` / `encoding-bech32m` |
11//! | [`DecodingError`] | Unified wrapper for all above | Always |
12//!
13//! # Design: build-invariant, heap-free, forward-compatible
14//!
15//! - **Identical shapes in debug and release builds.** No variant is gated on
16//! `cfg(debug_assertions)`, so code that matches on these enums compiles and
17//! behaves the same under every profile.
18//! - **No heap data.** Errors carry at most `usize` length metadata; they never
19//! contain payload bytes, HRP strings, or other input-derived text. All error
20//! types are `Copy` and work without `alloc`.
21//! - **`#[non_exhaustive]`.** Variants (and fields of struct variants) may be
22//! added in future releases without a semver-major bump; downstream matches
23//! need a wildcard arm.
24//!
25//! # Security: what errors may reveal
26//!
27//! `InvalidLength` variants carry the expected and actual byte counts in all
28//! builds. Expected lengths are compile-time protocol parameters (key sizes,
29//! nonce sizes) and actual lengths derive from the caller's own input, so
30//! neither is treated as secret — this matches the crate's threat model
31//! (see SECURITY.md). Genuinely input-derived strings (received HRPs, encoding
32//! hints) are **never** captured, in any build. If even coarse error categories
33//! are sensitive in your deployment, redact errors at the logging boundary.
34//!
35//! # 0.8 LTS note: hand-written impls instead of `thiserror`
36//!
37//! Unlike the 0.9 line, this branch does not use `thiserror`: its `no_std`
38//! support requires `core::error::Error` (Rust 1.81+), above this branch's
39//! MSRV of 1.70. `Display` is implemented manually for every build, and
40//! `std::error::Error` (including `source()` chaining on [`DecodingError`])
41//! is provided behind the `std` feature.
42
43use core::fmt;
44
45/// Error returned when a byte slice cannot be converted to a fixed-size array.
46///
47/// Carries the expected and actual lengths in all build profiles. Lengths are
48/// public protocol parameters, not secret material.
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50#[non_exhaustive]
51pub enum FromSliceError {
52 /// The slice length does not match the target array length.
53 #[non_exhaustive]
54 InvalidLength {
55 /// Number of bytes the target array requires.
56 expected: usize,
57 /// Number of bytes actually provided.
58 got: usize,
59 },
60}
61
62impl fmt::Display for FromSliceError {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 match self {
65 Self::InvalidLength { expected, got } => {
66 write!(f, "slice length mismatch: expected {expected}, got {got}")
67 }
68 }
69 }
70}
71
72#[cfg(feature = "std")]
73impl std::error::Error for FromSliceError {}
74
75/// Errors produced when decoding Bech32 (BIP-173) or Bech32m (BIP-350) strings.
76///
77/// *Requires feature `encoding-bech32` or `encoding-bech32m`.*
78///
79/// Variant shapes are identical in debug and release builds. No input-derived
80/// strings (such as the received HRP) are ever captured — the caller already
81/// holds the input and the expected HRP.
82#[cfg(any(feature = "encoding-bech32", feature = "encoding-bech32m"))]
83#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84#[non_exhaustive]
85pub enum Bech32Error {
86 /// The Human-Readable Part (HRP) is invalid.
87 InvalidHrp,
88 /// Bit conversion during encoding/decoding failed.
89 ///
90 /// **Currently unreachable.** After `CheckedHrpstring::new()` succeeds, the
91 /// `.byte_iter()` iterator is infallible — all bit-conversion happens during
92 /// the `new()` call and any failure surfaces as `OperationFailed` instead.
93 /// This variant is preserved as public API for forward compatibility should a
94 /// fallible conversion path be introduced in a future release of the `bech32` crate.
95 ConversionFailed,
96 /// General bech32 operation failure (malformed string or checksum mismatch).
97 OperationFailed,
98 /// The decoded HRP does not match the HRP the caller required.
99 ///
100 /// The received HRP is deliberately not captured — it is input-derived text.
101 /// The caller passed the expected HRP and holds the input string.
102 UnexpectedHrp,
103 /// The decoded payload length does not match the target type's length.
104 #[non_exhaustive]
105 InvalidLength {
106 /// Number of bytes the target type requires.
107 expected: usize,
108 /// Number of bytes actually decoded.
109 got: usize,
110 },
111}
112
113#[cfg(any(feature = "encoding-bech32", feature = "encoding-bech32m"))]
114impl fmt::Display for Bech32Error {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 match self {
117 Self::InvalidHrp => f.write_str("invalid Human-Readable Part (HRP)"),
118 Self::ConversionFailed => f.write_str("bit conversion failed"),
119 Self::OperationFailed => f.write_str("bech32 operation failed"),
120 Self::UnexpectedHrp => f.write_str("unexpected HRP"),
121 Self::InvalidLength { expected, got } => {
122 write!(f, "decoded length mismatch: expected {expected}, got {got}")
123 }
124 }
125 }
126}
127
128#[cfg(all(
129 feature = "std",
130 any(feature = "encoding-bech32", feature = "encoding-bech32m")
131))]
132impl std::error::Error for Bech32Error {}
133
134/// Errors produced when decoding base64url strings.
135///
136/// *Requires feature `encoding-base64`.*
137///
138/// Variant shapes are identical in debug and release builds; only numeric
139/// length metadata is carried.
140#[cfg(feature = "encoding-base64")]
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
142#[non_exhaustive]
143pub enum Base64Error {
144 /// The string is not valid base64url.
145 InvalidBase64,
146 /// The decoded payload length does not match the target type's length.
147 #[non_exhaustive]
148 InvalidLength {
149 /// Number of bytes the target type requires.
150 expected: usize,
151 /// Number of bytes actually decoded.
152 got: usize,
153 },
154}
155
156#[cfg(feature = "encoding-base64")]
157impl fmt::Display for Base64Error {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 match self {
160 Self::InvalidBase64 => f.write_str("invalid base64 string"),
161 Self::InvalidLength { expected, got } => {
162 write!(f, "decoded length mismatch: expected {expected}, got {got}")
163 }
164 }
165 }
166}
167
168#[cfg(all(feature = "std", feature = "encoding-base64"))]
169impl std::error::Error for Base64Error {}
170
171/// Errors produced when decoding hexadecimal strings.
172///
173/// *Requires feature `encoding-hex`.*
174///
175/// Variant shapes are identical in debug and release builds; only numeric
176/// length metadata is carried.
177#[cfg(feature = "encoding-hex")]
178#[derive(Clone, Copy, Debug, PartialEq, Eq)]
179#[non_exhaustive]
180pub enum HexError {
181 /// The string is not valid hexadecimal.
182 InvalidHex,
183 /// The decoded payload length does not match the target type's length.
184 #[non_exhaustive]
185 InvalidLength {
186 /// Number of bytes the target type requires.
187 expected: usize,
188 /// Number of bytes actually decoded.
189 got: usize,
190 },
191}
192
193#[cfg(feature = "encoding-hex")]
194impl fmt::Display for HexError {
195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196 match self {
197 Self::InvalidHex => f.write_str("invalid hex string"),
198 Self::InvalidLength { expected, got } => {
199 write!(f, "decoded length mismatch: expected {expected}, got {got}")
200 }
201 }
202 }
203}
204
205#[cfg(all(feature = "std", feature = "encoding-hex"))]
206impl std::error::Error for HexError {}
207
208/// Unified error type for multi-format decoding operations.
209///
210/// Wraps format-specific errors from hex, base64url, bech32, and bech32m decoders.
211/// Always available; variants depend on enabled features. Like the format-specific
212/// errors it wraps, this type is heap-free, `Copy`, and build-invariant.
213#[derive(Clone, Copy, Debug, PartialEq, Eq)]
214#[non_exhaustive]
215pub enum DecodingError {
216 /// The input is not valid Bech32.
217 #[cfg(feature = "encoding-bech32")]
218 InvalidBech32(Bech32Error),
219 /// The input is not valid Base64url.
220 #[cfg(feature = "encoding-base64")]
221 InvalidBase64(Base64Error),
222 /// The input is not valid hexadecimal.
223 #[cfg(feature = "encoding-hex")]
224 InvalidHex(HexError),
225 /// The encoding could not be identified.
226 ///
227 /// Deliberately carries no hint text — free-form diagnostics derived from
228 /// the input would embed input data in the error value.
229 InvalidEncoding,
230}
231
232impl fmt::Display for DecodingError {
233 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234 match self {
235 #[cfg(feature = "encoding-bech32")]
236 Self::InvalidBech32(_) => f.write_str("invalid bech32 string"),
237 #[cfg(feature = "encoding-base64")]
238 Self::InvalidBase64(_) => f.write_str("invalid base64 string"),
239 #[cfg(feature = "encoding-hex")]
240 Self::InvalidHex(_) => f.write_str("invalid hex string"),
241 Self::InvalidEncoding => f.write_str("invalid encoding"),
242 }
243 }
244}
245
246#[cfg(feature = "std")]
247impl std::error::Error for DecodingError {
248 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
249 match self {
250 #[cfg(feature = "encoding-bech32")]
251 Self::InvalidBech32(inner) => Some(inner),
252 #[cfg(feature = "encoding-base64")]
253 Self::InvalidBase64(inner) => Some(inner),
254 #[cfg(feature = "encoding-hex")]
255 Self::InvalidHex(inner) => Some(inner),
256 Self::InvalidEncoding => None,
257 }
258 }
259}