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