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