base64_ng/engine/decode.rs
1#[cfg(feature = "alloc")]
2use crate::SecretBuffer;
3#[cfg(feature = "alloc")]
4use crate::validate_decode;
5use crate::{
6 Alphabet, DecodeError, DecodedBuffer, Engine, LineWrap, decode_backend, is_legacy_whitespace,
7 validate_legacy_decode, validate_wrapped_decode, wipe_bytes, wipe_tail,
8};
9
10impl<A, const PAD: bool> Engine<A, PAD>
11where
12 A: Alphabet,
13{
14 /// Decodes `input` into `output`, returning the number of bytes written.
15 ///
16 /// This is strict decoding. Whitespace, mixed alphabets, malformed padding,
17 /// and trailing non-padding data are rejected.
18 ///
19 /// # Security
20 ///
21 /// This default scalar decoder prioritizes strict validation, exact error
22 /// reporting, and ordinary throughput. It may branch or return early based
23 /// on byte validity, malformed input, padding position, and output
24 /// capacity. It also reports exact failure positions and invalid byte
25 /// values through [`DecodeError`]. Do not use this method for token
26 /// comparison, key-material decoding, or secret-bearing validation where
27 /// malformed-input timing matters. Do not log strict decode errors
28 /// verbatim for secret-bearing input; log [`DecodeError::kind`] instead.
29 /// Use [`crate::ct`],
30 /// [`crate::ct::STANDARD`], [`crate::ct::URL_SAFE_NO_PAD`], or
31 /// [`Self::ct_decoder`] with `decode_slice_clear_tail` for
32 /// constant-time-oriented secret decoding.
33 #[must_use = "handle decode errors; use crate::ct for secret-bearing payloads"]
34 pub fn decode_slice(&self, input: &[u8], output: &mut [u8]) -> Result<usize, DecodeError> {
35 decode_backend::decode_slice::<A, PAD>(input, output)
36 }
37
38 /// Decodes `input` into `output` and clears all bytes after the decoded
39 /// prefix.
40 ///
41 /// If decoding fails, the entire output buffer is cleared before the error
42 /// is returned.
43 ///
44 /// # Examples
45 ///
46 /// ```
47 /// use base64_ng::STANDARD;
48 ///
49 /// let mut output = [0xff; 8];
50 /// let written = STANDARD
51 /// .decode_slice_clear_tail(b"aGk=", &mut output)
52 /// .unwrap();
53 ///
54 /// assert_eq!(&output[..written], b"hi");
55 /// assert!(output[written..].iter().all(|byte| *byte == 0));
56 /// ```
57 pub fn decode_slice_clear_tail(
58 &self,
59 input: &[u8],
60 output: &mut [u8],
61 ) -> Result<usize, DecodeError> {
62 let written = match self.decode_slice(input, output) {
63 Ok(written) => written,
64 Err(err) => {
65 wipe_bytes(output);
66 return Err(err);
67 }
68 };
69 wipe_tail(output, written);
70 Ok(written)
71 }
72
73 /// Decodes `input` into a stack-backed buffer.
74 ///
75 /// This helper is useful for short decoded values where callers want the
76 /// convenience of an owned result without enabling `alloc`.
77 ///
78 /// # Examples
79 ///
80 /// ```
81 /// use base64_ng::STANDARD;
82 ///
83 /// let decoded = STANDARD.decode_buffer::<5>(b"aGVsbG8=").unwrap();
84 ///
85 /// assert_eq!(decoded.as_bytes(), b"hello");
86 /// ```
87 pub fn decode_buffer<const CAP: usize>(
88 &self,
89 input: &[u8],
90 ) -> Result<DecodedBuffer<CAP>, DecodeError> {
91 let mut output = DecodedBuffer::new();
92 let written = match self.decode_slice_clear_tail(input, output.as_mut_capacity()) {
93 Ok(written) => written,
94 Err(err) => {
95 output.clear();
96 return Err(err);
97 }
98 };
99 output.set_filled(written)?;
100 Ok(output)
101 }
102
103 /// Decodes `input` using the explicit legacy whitespace profile.
104 ///
105 /// ASCII space, tab, carriage return, and line feed bytes are ignored.
106 /// Alphabet selection, padding placement, trailing data after padding, and
107 /// non-canonical trailing bits remain strict.
108 ///
109 /// # Security
110 ///
111 /// This method uses the normal strict decode path after legacy whitespace
112 /// handling. It may branch or return early based on malformed input and is
113 /// not a constant-time token validator or key-material decoder. Use
114 /// [`crate::ct`] for secret-bearing payloads.
115 #[must_use = "handle decode errors; use crate::ct for secret-bearing payloads"]
116 pub fn decode_slice_legacy(
117 &self,
118 input: &[u8],
119 output: &mut [u8],
120 ) -> Result<usize, DecodeError> {
121 let required = validate_legacy_decode::<A, PAD>(input)?;
122 if output.len() < required {
123 return Err(DecodeError::OutputTooSmall {
124 required,
125 available: output.len(),
126 });
127 }
128 Self::decode_legacy_via_strict_backend(input, output)
129 }
130
131 /// Decodes `input` using the explicit legacy whitespace profile and clears
132 /// all bytes after the decoded prefix.
133 ///
134 /// If validation or decoding fails, the entire output buffer is cleared
135 /// before the error is returned.
136 ///
137 /// # Examples
138 ///
139 /// ```
140 /// use base64_ng::STANDARD;
141 ///
142 /// let mut output = [0xff; 8];
143 /// let written = STANDARD
144 /// .decode_slice_legacy_clear_tail(b" aG\r\nk= ", &mut output)
145 /// .unwrap();
146 ///
147 /// assert_eq!(&output[..written], b"hi");
148 /// assert!(output[written..].iter().all(|byte| *byte == 0));
149 /// ```
150 pub fn decode_slice_legacy_clear_tail(
151 &self,
152 input: &[u8],
153 output: &mut [u8],
154 ) -> Result<usize, DecodeError> {
155 let written = match self.decode_slice_legacy(input, output) {
156 Ok(written) => written,
157 Err(err) => {
158 wipe_bytes(output);
159 return Err(err);
160 }
161 };
162 wipe_tail(output, written);
163 Ok(written)
164 }
165
166 /// Decodes `input` into a stack-backed buffer using the explicit legacy
167 /// whitespace profile.
168 ///
169 /// ASCII space, tab, carriage return, and line feed bytes are ignored.
170 /// Alphabet selection, padding placement, trailing data after padding, and
171 /// non-canonical trailing bits remain strict. If decoding fails, the
172 /// internal backing array is cleared before the error is returned.
173 pub fn decode_buffer_legacy<const CAP: usize>(
174 &self,
175 input: &[u8],
176 ) -> Result<DecodedBuffer<CAP>, DecodeError> {
177 let mut output = DecodedBuffer::new();
178 let written = match self.decode_slice_legacy_clear_tail(input, output.as_mut_capacity()) {
179 Ok(written) => written,
180 Err(err) => {
181 output.clear();
182 return Err(err);
183 }
184 };
185 output.set_filled(written)?;
186 Ok(output)
187 }
188
189 /// Decodes `input` using a strict line-wrapped profile.
190 ///
191 /// The wrapped profile accepts only the configured line ending. Non-final
192 /// lines must contain exactly `wrap.line_len` encoded bytes; the final line
193 /// may be shorter. A single trailing line ending after the final line is
194 /// accepted.
195 ///
196 /// # Security
197 ///
198 /// This method uses the normal strict decode path after line-profile
199 /// validation. It may branch or return early based on malformed input and
200 /// is not a constant-time token validator or key-material decoder. Use
201 /// [`crate::ct`] for secret-bearing payloads.
202 #[must_use = "handle decode errors; use crate::ct for secret-bearing payloads"]
203 pub fn decode_slice_wrapped(
204 &self,
205 input: &[u8],
206 output: &mut [u8],
207 wrap: LineWrap,
208 ) -> Result<usize, DecodeError> {
209 let required = validate_wrapped_decode::<A, PAD>(input, wrap)?;
210 if output.len() < required {
211 return Err(DecodeError::OutputTooSmall {
212 required,
213 available: output.len(),
214 });
215 }
216 Self::decode_wrapped_via_strict_backend(input, output, wrap)
217 }
218
219 /// Decodes `input` using a strict line-wrapped profile and clears all bytes
220 /// after the decoded prefix.
221 ///
222 /// If validation or decoding fails, the entire output buffer is cleared
223 /// before the error is returned.
224 pub fn decode_slice_wrapped_clear_tail(
225 &self,
226 input: &[u8],
227 output: &mut [u8],
228 wrap: LineWrap,
229 ) -> Result<usize, DecodeError> {
230 let written = match self.decode_slice_wrapped(input, output, wrap) {
231 Ok(written) => written,
232 Err(err) => {
233 wipe_bytes(output);
234 return Err(err);
235 }
236 };
237 wipe_tail(output, written);
238 Ok(written)
239 }
240
241 /// Decodes `input` using a strict line-wrapped profile into a stack-backed
242 /// buffer.
243 ///
244 /// The wrapped profile accepts only the configured line ending. Non-final
245 /// lines must contain exactly `wrap.line_len` encoded bytes; the final line
246 /// may be shorter. A single trailing line ending after the final line is
247 /// accepted. If decoding fails, the internal backing array is cleared
248 /// before the error is returned.
249 pub fn decode_wrapped_buffer<const CAP: usize>(
250 &self,
251 input: &[u8],
252 wrap: LineWrap,
253 ) -> Result<DecodedBuffer<CAP>, DecodeError> {
254 let mut output = DecodedBuffer::new();
255 let written =
256 match self.decode_slice_wrapped_clear_tail(input, output.as_mut_capacity(), wrap) {
257 Ok(written) => written,
258 Err(err) => {
259 output.clear();
260 return Err(err);
261 }
262 };
263 output.set_filled(written)?;
264 Ok(output)
265 }
266
267 /// Decodes `input` into a newly allocated byte vector.
268 ///
269 /// This is strict decoding with the same semantics as [`Self::decode_slice`].
270 #[cfg(feature = "alloc")]
271 #[must_use = "for secret-bearing payloads use decode_secret, which returns a redacted buffer with drop-time cleanup"]
272 pub fn decode_vec(&self, input: &[u8]) -> Result<alloc::vec::Vec<u8>, DecodeError> {
273 let required = validate_decode::<A, PAD>(input)?;
274 let mut output = alloc::vec![0; required];
275 let written = match self.decode_slice(input, &mut output) {
276 Ok(written) => written,
277 Err(err) => {
278 wipe_bytes(&mut output);
279 return Err(err);
280 }
281 };
282 output.truncate(written);
283 Ok(output)
284 }
285
286 /// Decodes `input` into a redacted owned secret buffer.
287 ///
288 /// On malformed input, the intermediate output buffer is cleared before the
289 /// error is returned by [`Self::decode_vec`].
290 #[cfg(feature = "alloc")]
291 pub fn decode_secret(&self, input: &[u8]) -> Result<SecretBuffer, DecodeError> {
292 self.decode_vec(input).map(SecretBuffer::from_vec)
293 }
294
295 /// Decodes `input` into a newly allocated byte vector using the explicit
296 /// legacy whitespace profile.
297 #[cfg(feature = "alloc")]
298 #[must_use = "for secret-bearing payloads use decode_secret_legacy, which returns a redacted buffer with drop-time cleanup"]
299 pub fn decode_vec_legacy(&self, input: &[u8]) -> Result<alloc::vec::Vec<u8>, DecodeError> {
300 let required = validate_legacy_decode::<A, PAD>(input)?;
301 let mut output = alloc::vec![0; required];
302 let written = match self.decode_slice_legacy(input, &mut output) {
303 Ok(written) => written,
304 Err(err) => {
305 wipe_bytes(&mut output);
306 return Err(err);
307 }
308 };
309 output.truncate(written);
310 Ok(output)
311 }
312
313 /// Decodes `input` into a redacted owned secret buffer using the explicit
314 /// legacy whitespace profile.
315 ///
316 /// ASCII space, tab, carriage return, and line feed bytes are ignored.
317 /// Alphabet selection, padding placement, trailing data after padding, and
318 /// non-canonical trailing bits remain strict.
319 #[cfg(feature = "alloc")]
320 pub fn decode_secret_legacy(&self, input: &[u8]) -> Result<SecretBuffer, DecodeError> {
321 self.decode_vec_legacy(input).map(SecretBuffer::from_vec)
322 }
323
324 /// Decodes line-wrapped input into a newly allocated byte vector.
325 #[cfg(feature = "alloc")]
326 #[must_use = "for secret-bearing payloads use decode_wrapped_secret, which returns a redacted buffer with drop-time cleanup"]
327 pub fn decode_wrapped_vec(
328 &self,
329 input: &[u8],
330 wrap: LineWrap,
331 ) -> Result<alloc::vec::Vec<u8>, DecodeError> {
332 let required = validate_wrapped_decode::<A, PAD>(input, wrap)?;
333 let mut output = alloc::vec![0; required];
334 let written = match self.decode_slice_wrapped(input, &mut output, wrap) {
335 Ok(written) => written,
336 Err(err) => {
337 wipe_bytes(&mut output);
338 return Err(err);
339 }
340 };
341 output.truncate(written);
342 Ok(output)
343 }
344
345 /// Decodes line-wrapped input into a redacted owned secret buffer.
346 ///
347 /// The wrapped profile accepts only the configured line ending. Non-final
348 /// lines must contain exactly `wrap.line_len` encoded bytes; the final line
349 /// may be shorter. A single trailing line ending after the final line is
350 /// accepted.
351 #[cfg(feature = "alloc")]
352 pub fn decode_wrapped_secret(
353 &self,
354 input: &[u8],
355 wrap: LineWrap,
356 ) -> Result<SecretBuffer, DecodeError> {
357 self.decode_wrapped_vec(input, wrap)
358 .map(SecretBuffer::from_vec)
359 }
360
361 fn decode_wrapped_via_strict_backend(
362 input: &[u8],
363 output: &mut [u8],
364 wrap: LineWrap,
365 ) -> Result<usize, DecodeError> {
366 let line_ending = wrap.line_ending.as_bytes();
367 let mut scratch = [0u8; 1024];
368 let mut scratch_indexes = [0usize; 1024];
369 let mut scratch_len = 0;
370 let mut read = 0;
371 let mut write = 0;
372
373 while read < input.len() {
374 let line_end = read
375 .checked_add(line_ending.len())
376 .filter(|end| *end <= input.len());
377 if line_end.and_then(|end| input.get(read..end)) == Some(line_ending) {
378 read += line_ending.len();
379 continue;
380 }
381
382 scratch[scratch_len] = input[read];
383 scratch_indexes[scratch_len] = read;
384 scratch_len += 1;
385 read += 1;
386
387 if scratch_len == scratch.len() {
388 Self::decode_strict_scratch_chunk(
389 &mut scratch,
390 &scratch_indexes,
391 scratch_len,
392 output,
393 &mut write,
394 )?;
395 scratch_len = 0;
396 }
397 }
398
399 if scratch_len == 0 {
400 return Ok(write);
401 }
402
403 Self::decode_strict_scratch_chunk(
404 &mut scratch,
405 &scratch_indexes,
406 scratch_len,
407 output,
408 &mut write,
409 )?;
410 Ok(write)
411 }
412
413 fn decode_legacy_via_strict_backend(
414 input: &[u8],
415 output: &mut [u8],
416 ) -> Result<usize, DecodeError> {
417 let mut scratch = [0u8; 1024];
418 let mut scratch_indexes = [0usize; 1024];
419 let mut scratch_len = 0;
420 let mut write = 0;
421
422 for (index, byte) in input.iter().enumerate() {
423 if is_legacy_whitespace(*byte) {
424 continue;
425 }
426
427 scratch[scratch_len] = *byte;
428 scratch_indexes[scratch_len] = index;
429 scratch_len += 1;
430
431 if scratch_len == scratch.len() {
432 Self::decode_strict_scratch_chunk(
433 &mut scratch,
434 &scratch_indexes,
435 scratch_len,
436 output,
437 &mut write,
438 )?;
439 scratch_len = 0;
440 }
441 }
442
443 if scratch_len != 0 {
444 Self::decode_strict_scratch_chunk(
445 &mut scratch,
446 &scratch_indexes,
447 scratch_len,
448 output,
449 &mut write,
450 )?;
451 }
452
453 Ok(write)
454 }
455
456 fn decode_strict_scratch_chunk(
457 scratch: &mut [u8; 1024],
458 scratch_indexes: &[usize; 1024],
459 scratch_len: usize,
460 output: &mut [u8],
461 write: &mut usize,
462 ) -> Result<(), DecodeError> {
463 let available = output.len();
464 let Some(output_tail) = output.get_mut(*write..) else {
465 wipe_bytes(&mut scratch[..scratch_len]);
466 return Err(DecodeError::OutputTooSmall {
467 required: *write,
468 available,
469 });
470 };
471 let written =
472 match decode_backend::decode_slice::<A, PAD>(&scratch[..scratch_len], output_tail) {
473 Ok(written) => written,
474 Err(err) => {
475 wipe_bytes(&mut scratch[..scratch_len]);
476 return Err(err.with_index_map(&scratch_indexes[..scratch_len]));
477 }
478 };
479 wipe_bytes(&mut scratch[..scratch_len]);
480 *write += written;
481 Ok(())
482 }
483}