Skip to main content

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}