Skip to main content

base64_ng/
errors.rs

1//! Error types for encoding and decoding operations.
2
3/// Encoding error.
4#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5pub enum EncodeError {
6    /// The encoded output length would overflow `usize`.
7    LengthOverflow,
8    /// The selected alphabet produced non-ASCII output.
9    ///
10    /// Built-in alphabets and alphabets generated by
11    /// [`define_alphabet!`](crate::define_alphabet) cannot trigger this error.
12    /// It is returned when a hand-written [`Alphabet`](crate::Alphabet)
13    /// implementation violates the visible-ASCII alphabet contract.
14    InvalidAlphabet,
15    /// An accelerated backend produced bytes that did not match the scalar
16    /// reference implementation.
17    ///
18    /// This is an internal fail-closed integrity error. It indicates that a
19    /// platform backend, runtime, or compiler path diverged from the scalar
20    /// foundation of trust.
21    BackendMismatch,
22    /// The requested line wrapping policy is invalid.
23    InvalidLineWrap {
24        /// Requested line length.
25        line_len: usize,
26    },
27    /// The caller-provided input length exceeds the provided buffer.
28    InputTooLarge {
29        /// Requested input bytes.
30        input_len: usize,
31        /// Available buffer bytes.
32        buffer_len: usize,
33    },
34    /// The output buffer is too small.
35    OutputTooSmall {
36        /// Required output bytes.
37        required: usize,
38        /// Available output bytes.
39        available: usize,
40    },
41}
42
43impl core::fmt::Display for EncodeError {
44    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
45        match self {
46            Self::LengthOverflow => f.write_str("base64 output length overflows usize"),
47            Self::InvalidAlphabet => f.write_str("base64 alphabet produced non-ASCII output"),
48            Self::BackendMismatch => {
49                f.write_str("base64 accelerated backend diverged from scalar output")
50            }
51            Self::InvalidLineWrap { line_len } => {
52                write!(f, "base64 line wrap length {line_len} is invalid")
53            }
54            Self::InputTooLarge {
55                input_len,
56                buffer_len,
57            } => write!(
58                f,
59                "base64 input length {input_len} exceeds buffer length {buffer_len}"
60            ),
61            Self::OutputTooSmall {
62                required,
63                available,
64            } => write!(
65                f,
66                "base64 output buffer too small: required {required}, available {available}"
67            ),
68        }
69    }
70}
71
72#[cfg(feature = "std")]
73impl std::error::Error for EncodeError {}
74
75/// Decoding error.
76///
77/// # Security
78///
79/// Strict decoding errors are diagnostic values. Some variants carry
80/// input-derived bytes or exact input indexes, and [`core::fmt::Display`]
81/// intentionally prints those diagnostics for developer-facing debugging. Do
82/// not log or return full [`DecodeError`] values for secret-bearing input; log
83/// [`Self::kind`] instead.
84///
85/// ```
86/// use base64_ng::STANDARD;
87///
88/// let err = STANDARD.decode_buffer::<8>(b"!!!!").unwrap_err();
89/// // Production logs should use the redacted class, not `{err}`.
90/// assert_eq!(err.kind().as_str(), "invalid-byte");
91/// ```
92#[derive(Clone, Copy, Eq, PartialEq)]
93pub enum DecodeError {
94    /// The encoded input is malformed, but the decoder intentionally does not
95    /// disclose a more specific error class.
96    InvalidInput,
97    /// The encoded input length is impossible for the selected padding policy.
98    InvalidLength,
99    /// A byte is not valid for the selected alphabet.
100    InvalidByte {
101        /// Byte index in the input.
102        index: usize,
103        /// Invalid byte value.
104        byte: u8,
105    },
106    /// Padding is missing, misplaced, or non-canonical.
107    InvalidPadding {
108        /// Byte index where padding became invalid.
109        index: usize,
110    },
111    /// Line wrapping is missing, misplaced, or uses the wrong line ending.
112    InvalidLineWrap {
113        /// Byte index where line wrapping became invalid.
114        index: usize,
115    },
116    /// The output buffer is too small.
117    OutputTooSmall {
118        /// Required output bytes.
119        required: usize,
120        /// Available output bytes.
121        available: usize,
122    },
123    /// The caller-provided constant-time staging buffer is too small.
124    StagingTooSmall {
125        /// Required staging bytes.
126        required: usize,
127        /// Available staging bytes.
128        available: usize,
129    },
130}
131
132impl core::fmt::Debug for DecodeError {
133    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
134        f.debug_struct("DecodeError")
135            .field("kind", &self.kind())
136            .finish_non_exhaustive()
137    }
138}
139
140/// Redacted decoding error class.
141///
142/// This type intentionally omits input-derived bytes and indexes so callers can
143/// log error classes without logging secret-adjacent input content.
144#[derive(Clone, Copy, Debug, Eq, PartialEq)]
145#[non_exhaustive]
146pub enum DecodeErrorKind {
147    /// The encoded input is malformed, but the decoder intentionally does not
148    /// disclose a more specific error class.
149    InvalidInput,
150    /// The encoded input length is impossible for the selected padding policy.
151    InvalidLength,
152    /// A byte is not valid for the selected alphabet.
153    InvalidByte,
154    /// Padding is missing, misplaced, or non-canonical.
155    InvalidPadding,
156    /// Line wrapping is missing, misplaced, or uses the wrong line ending.
157    InvalidLineWrap,
158    /// The output buffer is too small.
159    OutputTooSmall,
160    /// The caller-provided constant-time staging buffer is too small.
161    StagingTooSmall,
162}
163
164impl DecodeErrorKind {
165    /// Returns the stable lowercase identifier for this error class.
166    #[must_use]
167    pub const fn as_str(self) -> &'static str {
168        match self {
169            Self::InvalidInput => "invalid-input",
170            Self::InvalidLength => "invalid-length",
171            Self::InvalidByte => "invalid-byte",
172            Self::InvalidPadding => "invalid-padding",
173            Self::InvalidLineWrap => "invalid-line-wrap",
174            Self::OutputTooSmall => "output-too-small",
175            Self::StagingTooSmall => "staging-too-small",
176        }
177    }
178}
179
180impl core::fmt::Display for DecodeErrorKind {
181    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
182        f.write_str(self.as_str())
183    }
184}
185
186impl core::fmt::Display for DecodeError {
187    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
188        match self {
189            Self::InvalidInput => f.write_str("malformed base64 input"),
190            Self::InvalidLength => f.write_str("invalid base64 input length"),
191            Self::InvalidByte { index, byte } => {
192                write!(f, "invalid base64 byte 0x{byte:02x} at index {index}")
193            }
194            Self::InvalidPadding { index } => write!(f, "invalid base64 padding at index {index}"),
195            Self::InvalidLineWrap { index } => {
196                write!(f, "invalid base64 line wrapping at index {index}")
197            }
198            Self::OutputTooSmall {
199                required,
200                available,
201            } => write!(
202                f,
203                "base64 decode output buffer too small: required {required}, available {available}"
204            ),
205            Self::StagingTooSmall {
206                required,
207                available,
208            } => write!(
209                f,
210                "base64 decode staging buffer too small: required {required}, available {available}"
211            ),
212        }
213    }
214}
215
216impl DecodeError {
217    /// Returns a redacted error class without input-derived bytes or indexes.
218    ///
219    /// Strict decoders keep exact diagnostics in [`DecodeError`] and
220    /// [`core::fmt::Display`] for developer debugging. When input may contain
221    /// secrets or secret-adjacent material, log this kind instead of logging
222    /// the full error value.
223    #[must_use]
224    pub const fn kind(self) -> DecodeErrorKind {
225        match self {
226            Self::InvalidInput => DecodeErrorKind::InvalidInput,
227            Self::InvalidLength => DecodeErrorKind::InvalidLength,
228            Self::InvalidByte { .. } => DecodeErrorKind::InvalidByte,
229            Self::InvalidPadding { .. } => DecodeErrorKind::InvalidPadding,
230            Self::InvalidLineWrap { .. } => DecodeErrorKind::InvalidLineWrap,
231            Self::OutputTooSmall { .. } => DecodeErrorKind::OutputTooSmall,
232            Self::StagingTooSmall { .. } => DecodeErrorKind::StagingTooSmall,
233        }
234    }
235
236    pub(crate) fn with_index_offset(self, offset: usize) -> Self {
237        match self {
238            Self::InvalidByte { index, byte } => Self::InvalidByte {
239                index: index.saturating_add(offset),
240                byte,
241            },
242            Self::InvalidPadding { index } => Self::InvalidPadding {
243                index: index.saturating_add(offset),
244            },
245            Self::InvalidLineWrap { index } => Self::InvalidLineWrap {
246                index: index.saturating_add(offset),
247            },
248            Self::InvalidInput
249            | Self::InvalidLength
250            | Self::OutputTooSmall { .. }
251            | Self::StagingTooSmall { .. } => self,
252        }
253    }
254
255    pub(crate) fn with_index_map(self, indexes: &[usize]) -> Self {
256        match self {
257            Self::InvalidByte { index, byte } => Self::InvalidByte {
258                index: indexes.get(index).copied().unwrap_or(index),
259                byte,
260            },
261            Self::InvalidPadding { index } => Self::InvalidPadding {
262                index: indexes.get(index).copied().unwrap_or(index),
263            },
264            Self::InvalidLineWrap { index } => Self::InvalidLineWrap {
265                index: indexes.get(index).copied().unwrap_or(index),
266            },
267            Self::InvalidInput
268            | Self::InvalidLength
269            | Self::OutputTooSmall { .. }
270            | Self::StagingTooSmall { .. } => self,
271        }
272    }
273}
274
275#[cfg(feature = "std")]
276impl std::error::Error for DecodeError {}
277
278#[cfg(test)]
279mod tests {
280    use super::DecodeError;
281
282    #[test]
283    fn index_offsets_saturate_on_overflow() {
284        assert_eq!(
285            DecodeError::InvalidByte {
286                index: 7,
287                byte: b'$'
288            }
289            .with_index_offset(usize::MAX),
290            DecodeError::InvalidByte {
291                index: usize::MAX,
292                byte: b'$'
293            }
294        );
295        assert_eq!(
296            DecodeError::InvalidPadding { index: 7 }.with_index_offset(usize::MAX),
297            DecodeError::InvalidPadding { index: usize::MAX }
298        );
299        assert_eq!(
300            DecodeError::InvalidLineWrap { index: 7 }.with_index_offset(usize::MAX),
301            DecodeError::InvalidLineWrap { index: usize::MAX }
302        );
303    }
304
305    #[cfg(feature = "alloc")]
306    #[test]
307    fn debug_redacts_input_derived_details() {
308        let error = DecodeError::InvalidByte {
309            index: 42,
310            byte: b'$',
311        };
312        let rendered = alloc::format!("{error:?}");
313        assert!(rendered.contains("InvalidByte"));
314        assert!(!rendered.contains("42"));
315        assert!(!rendered.contains("24"));
316    }
317}