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, Debug, 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
106/// Redacted decoding error class.
107///
108/// This type intentionally omits input-derived bytes and indexes so callers can
109/// log error classes without logging secret-adjacent input content.
110#[derive(Clone, Copy, Debug, Eq, PartialEq)]
111#[non_exhaustive]
112pub enum DecodeErrorKind {
113    /// The encoded input is malformed, but the decoder intentionally does not
114    /// disclose a more specific error class.
115    InvalidInput,
116    /// The encoded input length is impossible for the selected padding policy.
117    InvalidLength,
118    /// A byte is not valid for the selected alphabet.
119    InvalidByte,
120    /// Padding is missing, misplaced, or non-canonical.
121    InvalidPadding,
122    /// Line wrapping is missing, misplaced, or uses the wrong line ending.
123    InvalidLineWrap,
124    /// The output buffer is too small.
125    OutputTooSmall,
126    /// The caller-provided constant-time staging buffer is too small.
127    StagingTooSmall,
128}
129
130impl DecodeErrorKind {
131    /// Returns the stable lowercase identifier for this error class.
132    #[must_use]
133    pub const fn as_str(self) -> &'static str {
134        match self {
135            Self::InvalidInput => "invalid-input",
136            Self::InvalidLength => "invalid-length",
137            Self::InvalidByte => "invalid-byte",
138            Self::InvalidPadding => "invalid-padding",
139            Self::InvalidLineWrap => "invalid-line-wrap",
140            Self::OutputTooSmall => "output-too-small",
141            Self::StagingTooSmall => "staging-too-small",
142        }
143    }
144}
145
146impl core::fmt::Display for DecodeErrorKind {
147    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
148        f.write_str(self.as_str())
149    }
150}
151
152impl core::fmt::Display for DecodeError {
153    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
154        match self {
155            Self::InvalidInput => f.write_str("malformed base64 input"),
156            Self::InvalidLength => f.write_str("invalid base64 input length"),
157            Self::InvalidByte { index, byte } => {
158                write!(f, "invalid base64 byte 0x{byte:02x} at index {index}")
159            }
160            Self::InvalidPadding { index } => write!(f, "invalid base64 padding at index {index}"),
161            Self::InvalidLineWrap { index } => {
162                write!(f, "invalid base64 line wrapping at index {index}")
163            }
164            Self::OutputTooSmall {
165                required,
166                available,
167            } => write!(
168                f,
169                "base64 decode output buffer too small: required {required}, available {available}"
170            ),
171            Self::StagingTooSmall {
172                required,
173                available,
174            } => write!(
175                f,
176                "base64 decode staging buffer too small: required {required}, available {available}"
177            ),
178        }
179    }
180}
181
182impl DecodeError {
183    /// Returns a redacted error class without input-derived bytes or indexes.
184    ///
185    /// Strict decoders keep exact diagnostics in [`DecodeError`] and
186    /// [`core::fmt::Display`] for developer debugging. When input may contain
187    /// secrets or secret-adjacent material, log this kind instead of logging
188    /// the full error value.
189    #[must_use]
190    pub const fn kind(self) -> DecodeErrorKind {
191        match self {
192            Self::InvalidInput => DecodeErrorKind::InvalidInput,
193            Self::InvalidLength => DecodeErrorKind::InvalidLength,
194            Self::InvalidByte { .. } => DecodeErrorKind::InvalidByte,
195            Self::InvalidPadding { .. } => DecodeErrorKind::InvalidPadding,
196            Self::InvalidLineWrap { .. } => DecodeErrorKind::InvalidLineWrap,
197            Self::OutputTooSmall { .. } => DecodeErrorKind::OutputTooSmall,
198            Self::StagingTooSmall { .. } => DecodeErrorKind::StagingTooSmall,
199        }
200    }
201
202    pub(crate) fn with_index_offset(self, offset: usize) -> Self {
203        match self {
204            Self::InvalidByte { index, byte } => Self::InvalidByte {
205                index: index.saturating_add(offset),
206                byte,
207            },
208            Self::InvalidPadding { index } => Self::InvalidPadding {
209                index: index.saturating_add(offset),
210            },
211            Self::InvalidLineWrap { index } => Self::InvalidLineWrap {
212                index: index.saturating_add(offset),
213            },
214            Self::InvalidInput
215            | Self::InvalidLength
216            | Self::OutputTooSmall { .. }
217            | Self::StagingTooSmall { .. } => self,
218        }
219    }
220}
221
222#[cfg(feature = "std")]
223impl std::error::Error for DecodeError {}
224
225#[cfg(test)]
226mod tests {
227    use super::DecodeError;
228
229    #[test]
230    fn index_offsets_saturate_on_overflow() {
231        assert_eq!(
232            DecodeError::InvalidByte {
233                index: 7,
234                byte: b'$'
235            }
236            .with_index_offset(usize::MAX),
237            DecodeError::InvalidByte {
238                index: usize::MAX,
239                byte: b'$'
240            }
241        );
242        assert_eq!(
243            DecodeError::InvalidPadding { index: 7 }.with_index_offset(usize::MAX),
244            DecodeError::InvalidPadding { index: usize::MAX }
245        );
246        assert_eq!(
247            DecodeError::InvalidLineWrap { index: 7 }.with_index_offset(usize::MAX),
248            DecodeError::InvalidLineWrap { index: usize::MAX }
249        );
250    }
251}