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