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