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` |
12//!
13//! Every type in this module is produced by a function in this crate. There is no
14//! unified wrapper enum: a caller decoding several formats owns the union it needs,
15//! and nothing here would help it build one.
16//!
17//! # Design: build-invariant, heap-free, forward-compatible
18//!
19//! - **Identical shapes in debug and release builds.** No variant is gated on
20//!   `cfg(debug_assertions)`, so code that matches on these enums compiles and
21//!   behaves the same under every profile.
22//! - **No heap data.** Errors carry at most `usize` length metadata; they never
23//!   contain payload bytes, HRP strings, or other input-derived text. All error
24//!   types are `Copy` and work without `alloc`.
25//! - **`#[non_exhaustive]`.** Variants (and fields of struct variants) may be
26//!   added in future releases without a semver-major bump; downstream matches
27//!   need a wildcard arm. This is also what lets the crate ship a variant only
28//!   once something produces it, rather than reserving one in advance.
29//! - **No derive macros.** [`Display`](core::fmt::Display) and `std::error::Error`
30//!   are written out by hand below. The messages are fixed strings and two `usize`
31//!   fields, which is not enough work to justify putting a proc-macro (and `syn`,
32//!   `quote`, `proc-macro2`) in the dependency graph of every downstream build.
33//!
34//! # 0.8 (MSRV 1.70) note: `std::error::Error`, not `core::error::Error`
35//!
36//! The 0.9 line implements [`Error`] unconditionally against `core::error::Error`,
37//! which is stable only from Rust 1.81 — above this branch's MSRV of 1.70. Here
38//! [`Display`](core::fmt::Display) is implemented for every build and the `Error`
39//! impls are provided behind the `std` feature instead. This is the sole
40//! intentional divergence from 0.9 in this module; the variants, messages, and
41//! `#[non_exhaustive]` markers are identical.
42//!
43//! [`Error`]: https://doc.rust-lang.org/std/error/trait.Error.html
44//!
45//! # Security: what errors may reveal
46//!
47//! `InvalidLength` variants carry the expected and actual byte counts in all
48//! builds. Expected lengths are compile-time protocol parameters (key sizes,
49//! nonce sizes) and actual lengths derive from the caller's own input, so
50//! neither is treated as secret — this matches the crate's threat model
51//! (see SECURITY.md). Genuinely input-derived strings (received HRPs, encoding
52//! hints) are **never** captured, in any build. If even coarse error categories
53//! are sensitive in your deployment, redact errors at the logging boundary.
54
55use core::fmt;
56
57/// Error returned when a byte slice cannot be converted to a fixed-size array.
58///
59/// Carries the expected and actual lengths in all build profiles. Lengths are
60/// public protocol parameters, not secret material.
61///
62/// The `std::error::Error` impl requires the `std` feature. Without it the
63/// type still exists and still implements [`Display`](core::fmt::Display), but
64/// it is not an `Error`, so `?` into `Box<dyn Error>` and `std::io::Error::other`
65/// will not compile. This is an MSRV consequence, not a policy: `core::error::Error`
66/// needs Rust 1.81 and this line targets 1.70. On 0.9.x the impl is
67/// unconditional and available in `no_std`.
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69#[non_exhaustive]
70pub enum FromSliceError {
71    /// The slice length does not match the target array length.
72    #[non_exhaustive]
73    InvalidLength {
74        /// Number of bytes the target array requires.
75        expected: usize,
76        /// Number of bytes actually provided.
77        got: usize,
78    },
79}
80
81impl fmt::Display for FromSliceError {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        match *self {
84            Self::InvalidLength { expected, got } => {
85                write!(f, "slice length mismatch: expected {expected}, got {got}")
86            }
87        }
88    }
89}
90
91#[cfg(feature = "std")]
92impl std::error::Error for FromSliceError {}
93
94/// Errors produced when decoding Bech32 (BIP-173) or Bech32m (BIP-350) strings.
95///
96/// *Requires feature `encoding-bech32`.*
97///
98/// Variant shapes are identical in debug and release builds. No input-derived
99/// strings (such as the received HRP) are ever captured — the caller already
100/// holds the input and the expected HRP.
101///
102/// The `std::error::Error` impl requires the `std` feature. Without it the
103/// type still exists and still implements [`Display`](core::fmt::Display), but
104/// it is not an `Error`, so `?` into `Box<dyn Error>` and `std::io::Error::other`
105/// will not compile. This is an MSRV consequence, not a policy: `core::error::Error`
106/// needs Rust 1.81 and this line targets 1.70. On 0.9.x the impl is
107/// unconditional and available in `no_std`.
108#[cfg(feature = "encoding-bech32")]
109#[derive(Clone, Copy, Debug, PartialEq, Eq)]
110#[non_exhaustive]
111pub enum Bech32Error {
112    /// The Human-Readable Part (HRP) is invalid.
113    InvalidHrp,
114    /// General bech32 operation failure (malformed string or checksum mismatch).
115    ///
116    /// This is where bit-conversion failures surface too: all bit conversion
117    /// happens inside `CheckedHrpstring::new()`, and the `.byte_iter()` that
118    /// follows a successful `new()` is infallible.
119    OperationFailed,
120    /// The decoded HRP does not match the HRP the caller required.
121    ///
122    /// The received HRP is deliberately not captured — it is input-derived text.
123    /// The caller passed the expected HRP and holds the input string.
124    UnexpectedHrp,
125    /// The decoded payload length does not match the target type's length.
126    #[non_exhaustive]
127    InvalidLength {
128        /// Number of bytes the target type requires.
129        expected: usize,
130        /// Number of bytes actually decoded.
131        got: usize,
132    },
133}
134
135#[cfg(feature = "encoding-bech32")]
136impl fmt::Display for Bech32Error {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        match *self {
139            Self::InvalidHrp => f.write_str("invalid Human-Readable Part (HRP)"),
140            Self::OperationFailed => f.write_str("bech32 operation failed"),
141            Self::UnexpectedHrp => f.write_str("unexpected HRP"),
142            Self::InvalidLength { expected, got } => {
143                write!(f, "decoded length mismatch: expected {expected}, got {got}")
144            }
145        }
146    }
147}
148
149#[cfg(all(feature = "std", feature = "encoding-bech32"))]
150impl std::error::Error for Bech32Error {}
151
152/// Errors produced when decoding Base32 (RFC 4648 §6, uppercase, unpadded) strings.
153///
154/// *Requires feature `encoding-base32`.*
155///
156/// Variant shapes are identical in debug and release builds; only numeric
157/// length metadata is carried.
158///
159/// The `std::error::Error` impl requires the `std` feature. Without it the
160/// type still exists and still implements [`Display`](core::fmt::Display), but
161/// it is not an `Error`, so `?` into `Box<dyn Error>` and `std::io::Error::other`
162/// will not compile. This is an MSRV consequence, not a policy: `core::error::Error`
163/// needs Rust 1.81 and this line targets 1.70. On 0.9.x the impl is
164/// unconditional and available in `no_std`.
165#[cfg(feature = "encoding-base32")]
166#[derive(Clone, Copy, Debug, PartialEq, Eq)]
167#[non_exhaustive]
168pub enum Base32Error {
169    /// The string is not valid Base32 (wrong alphabet, wrong case, padding, or
170    /// an impossible length).
171    InvalidBase32,
172    /// The decoded payload length does not match the target type's length.
173    #[non_exhaustive]
174    InvalidLength {
175        /// Number of bytes the target type requires.
176        expected: usize,
177        /// Number of bytes actually decoded.
178        got: usize,
179    },
180}
181
182#[cfg(feature = "encoding-base32")]
183impl fmt::Display for Base32Error {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        match *self {
186            Self::InvalidBase32 => f.write_str("invalid base32 string"),
187            Self::InvalidLength { expected, got } => {
188                write!(f, "decoded length mismatch: expected {expected}, got {got}")
189            }
190        }
191    }
192}
193
194#[cfg(all(feature = "std", feature = "encoding-base32"))]
195impl std::error::Error for Base32Error {}
196
197/// Errors produced when decoding base64url strings.
198///
199/// *Requires feature `encoding-base64`.*
200///
201/// Variant shapes are identical in debug and release builds; only numeric
202/// length metadata is carried.
203///
204/// The `std::error::Error` impl requires the `std` feature. Without it the
205/// type still exists and still implements [`Display`](core::fmt::Display), but
206/// it is not an `Error`, so `?` into `Box<dyn Error>` and `std::io::Error::other`
207/// will not compile. This is an MSRV consequence, not a policy: `core::error::Error`
208/// needs Rust 1.81 and this line targets 1.70. On 0.9.x the impl is
209/// unconditional and available in `no_std`.
210#[cfg(feature = "encoding-base64")]
211#[derive(Clone, Copy, Debug, PartialEq, Eq)]
212#[non_exhaustive]
213pub enum Base64Error {
214    /// The string is not valid base64url.
215    InvalidBase64,
216    /// The decoded payload length does not match the target type's length.
217    #[non_exhaustive]
218    InvalidLength {
219        /// Number of bytes the target type requires.
220        expected: usize,
221        /// Number of bytes actually decoded.
222        got: usize,
223    },
224}
225
226#[cfg(feature = "encoding-base64")]
227impl fmt::Display for Base64Error {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        match *self {
230            Self::InvalidBase64 => f.write_str("invalid base64 string"),
231            Self::InvalidLength { expected, got } => {
232                write!(f, "decoded length mismatch: expected {expected}, got {got}")
233            }
234        }
235    }
236}
237
238#[cfg(all(feature = "std", feature = "encoding-base64"))]
239impl std::error::Error for Base64Error {}
240
241/// Errors produced when decoding hexadecimal strings.
242///
243/// *Requires feature `encoding-hex`.*
244///
245/// Variant shapes are identical in debug and release builds; only numeric
246/// length metadata is carried.
247///
248/// The `std::error::Error` impl requires the `std` feature. Without it the
249/// type still exists and still implements [`Display`](core::fmt::Display), but
250/// it is not an `Error`, so `?` into `Box<dyn Error>` and `std::io::Error::other`
251/// will not compile. This is an MSRV consequence, not a policy: `core::error::Error`
252/// needs Rust 1.81 and this line targets 1.70. On 0.9.x the impl is
253/// unconditional and available in `no_std`.
254#[cfg(feature = "encoding-hex")]
255#[derive(Clone, Copy, Debug, PartialEq, Eq)]
256#[non_exhaustive]
257pub enum HexError {
258    /// The string is not valid hexadecimal.
259    InvalidHex,
260    /// The decoded payload length does not match the target type's length.
261    #[non_exhaustive]
262    InvalidLength {
263        /// Number of bytes the target type requires.
264        expected: usize,
265        /// Number of bytes actually decoded.
266        got: usize,
267    },
268}
269
270#[cfg(feature = "encoding-hex")]
271impl fmt::Display for HexError {
272    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273        match *self {
274            Self::InvalidHex => f.write_str("invalid hex string"),
275            Self::InvalidLength { expected, got } => {
276                write!(f, "decoded length mismatch: expected {expected}, got {got}")
277            }
278        }
279    }
280}
281
282#[cfg(all(feature = "std", feature = "encoding-hex"))]
283impl std::error::Error for HexError {}