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 {}