Skip to main content

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}