Skip to main content

base64_ng/engine/
decode_in_place.rs

1use crate::{
2    Alphabet, DecodeError, Engine, LineWrap, compact_wrapped_input, decode_backend,
3    is_legacy_whitespace, validate_decode, validate_legacy_decode, validate_wrapped_decode,
4    wipe_bytes, wipe_tail,
5};
6
7const IN_PLACE_DECODE_INPUT_CHUNK: usize = 1024;
8
9impl<A, const PAD: bool> Engine<A, PAD>
10where
11    A: Alphabet,
12{
13    /// Decodes `buffer` in place using a strict line-wrapped profile.
14    ///
15    /// The wrapped profile accepts only the configured line ending. Non-final
16    /// lines must contain exactly `wrap.line_len` encoded bytes; the final line
17    /// may be shorter. A single trailing line ending after the final line is
18    /// accepted.
19    ///
20    /// # Security
21    ///
22    /// This method compacts line endings in place before decoding. If
23    /// validation or decoding fails, the buffer contents are unspecified and
24    /// may contain the whitespace-stripped encoded form of the input. This is
25    /// still encoded material, not decoded plaintext, but it remains a modified
26    /// representation of the original payload. On success, bytes after the
27    /// returned decoded prefix may retain the compacted encoded representation.
28    /// Use
29    /// [`Self::decode_in_place_wrapped_clear_tail`] when the buffer may be
30    /// reused or freed without a caller-managed wipe; treat that clear-tail
31    /// variant as the default for secret-bearing wrapped payloads. If the
32    /// original encoded input must be preserved for audit logging or retry,
33    /// copy it before calling any in-place decode method or use a slice-output
34    /// decode API instead.
35    ///
36    /// # Examples
37    ///
38    /// ```
39    /// use base64_ng::{LineEnding, LineWrap, STANDARD};
40    ///
41    /// let mut buffer = *b"aGVs\nbG8=";
42    /// let decoded = STANDARD
43    ///     .decode_in_place_wrapped(&mut buffer, LineWrap::new(4, LineEnding::Lf))
44    ///     .unwrap();
45    ///
46    /// assert_eq!(decoded, b"hello");
47    /// ```
48    pub fn decode_in_place_wrapped<'a>(
49        &self,
50        buffer: &'a mut [u8],
51        wrap: LineWrap,
52    ) -> Result<&'a mut [u8], DecodeError> {
53        let _required = validate_wrapped_decode::<A, PAD>(buffer, wrap)?;
54        let compacted = compact_wrapped_input(buffer, wrap)?;
55        let len = Self::decode_slice_to_start(&mut buffer[..compacted])?;
56        Ok(&mut buffer[..len])
57    }
58
59    /// Decodes `buffer` in place using a strict line-wrapped profile and clears
60    /// all bytes after the decoded prefix.
61    ///
62    /// If validation or decoding fails, the entire buffer is cleared before the
63    /// error is returned.
64    ///
65    /// # Examples
66    ///
67    /// ```
68    /// use base64_ng::{LineEnding, LineWrap, STANDARD};
69    ///
70    /// let mut buffer = *b"aGVs\nbG8=";
71    /// let len = STANDARD
72    ///     .decode_in_place_wrapped_clear_tail(&mut buffer, LineWrap::new(4, LineEnding::Lf))
73    ///     .unwrap()
74    ///     .len();
75    ///
76    /// assert_eq!(&buffer[..len], b"hello");
77    /// assert!(buffer[len..].iter().all(|byte| *byte == 0));
78    /// ```
79    pub fn decode_in_place_wrapped_clear_tail<'a>(
80        &self,
81        buffer: &'a mut [u8],
82        wrap: LineWrap,
83    ) -> Result<&'a mut [u8], DecodeError> {
84        if let Err(err) = validate_wrapped_decode::<A, PAD>(buffer, wrap) {
85            wipe_bytes(buffer);
86            return Err(err);
87        }
88
89        let compacted = match compact_wrapped_input(buffer, wrap) {
90            Ok(compacted) => compacted,
91            Err(err) => {
92                wipe_bytes(buffer);
93                return Err(err);
94            }
95        };
96
97        let len = match Self::decode_slice_to_start(&mut buffer[..compacted]) {
98            Ok(len) => len,
99            Err(err) => {
100                wipe_bytes(buffer);
101                return Err(err);
102            }
103        };
104        wipe_tail(buffer, len);
105        Ok(&mut buffer[..len])
106    }
107
108    /// Decodes the buffer in place and returns the decoded prefix.
109    ///
110    /// On success, bytes after the returned decoded prefix may retain encoded
111    /// input bytes. Use [`Self::decode_in_place_clear_tail`] when the buffer
112    /// may be reused or freed without a caller-managed wipe.
113    ///
114    /// # Security
115    ///
116    /// This default strict decoder prioritizes validation, exact error
117    /// reporting, and ordinary throughput. It may branch or return early based
118    /// on malformed input and reports exact failure positions and invalid byte
119    /// values through [`DecodeError`]. For admitted Standard and URL-safe
120    /// runtime profiles, successful decode may use stack staging before the
121    /// strict decode backend writes behind the unread input cursor. Do not use
122    /// this method for token comparison, key-material decoding, or
123    /// secret-bearing validation where malformed-input timing matters. Do not
124    /// log strict decode errors verbatim for secret-bearing input; log
125    /// [`DecodeError::kind`] instead.
126    ///
127    /// # Examples
128    ///
129    /// ```
130    /// use base64_ng::STANDARD_NO_PAD;
131    ///
132    /// let mut buffer = *b"Zm9vYmFy";
133    /// let decoded = STANDARD_NO_PAD.decode_in_place(&mut buffer).unwrap();
134    /// assert_eq!(decoded, b"foobar");
135    /// ```
136    pub fn decode_in_place<'a>(&self, buffer: &'a mut [u8]) -> Result<&'a mut [u8], DecodeError> {
137        let len = Self::decode_slice_to_start(buffer)?;
138        Ok(&mut buffer[..len])
139    }
140
141    /// Decodes the buffer in place and clears all bytes after the decoded prefix.
142    ///
143    /// If decoding fails, the entire buffer is cleared before the error is
144    /// returned. Use this variant when the encoded or partially decoded data is
145    /// sensitive and the caller wants best-effort cleanup without adding a
146    /// dependency.
147    ///
148    /// # Examples
149    ///
150    /// ```
151    /// use base64_ng::STANDARD;
152    ///
153    /// let mut buffer = *b"aGk=";
154    /// let decoded = STANDARD.decode_in_place_clear_tail(&mut buffer).unwrap();
155    /// assert_eq!(decoded, b"hi");
156    /// ```
157    pub fn decode_in_place_clear_tail<'a>(
158        &self,
159        buffer: &'a mut [u8],
160    ) -> Result<&'a mut [u8], DecodeError> {
161        let len = match Self::decode_slice_to_start(buffer) {
162            Ok(len) => len,
163            Err(err) => {
164                wipe_bytes(buffer);
165                return Err(err);
166            }
167        };
168        wipe_tail(buffer, len);
169        Ok(&mut buffer[..len])
170    }
171
172    /// Decodes `buffer` in place using the explicit legacy whitespace profile.
173    ///
174    /// Ignored whitespace is compacted out before decoding. If validation
175    /// fails, the buffer contents are unspecified. On success, bytes after the
176    /// returned decoded prefix may retain the compacted encoded
177    /// representation. Use [`Self::decode_in_place_legacy_clear_tail`] when the
178    /// buffer may be reused or freed without a caller-managed wipe.
179    pub fn decode_in_place_legacy<'a>(
180        &self,
181        buffer: &'a mut [u8],
182    ) -> Result<&'a mut [u8], DecodeError> {
183        let _required = validate_legacy_decode::<A, PAD>(buffer)?;
184        let mut write = 0;
185        let mut read = 0;
186        while read < buffer.len() {
187            let byte = buffer[read];
188            if !is_legacy_whitespace(byte) {
189                buffer[write] = byte;
190                write += 1;
191            }
192            read += 1;
193        }
194        let len = Self::decode_slice_to_start(&mut buffer[..write])?;
195        Ok(&mut buffer[..len])
196    }
197
198    /// Decodes `buffer` in place using the explicit legacy whitespace profile
199    /// and clears all bytes after the decoded prefix.
200    ///
201    /// If validation or decoding fails, the entire buffer is cleared before the
202    /// error is returned.
203    pub fn decode_in_place_legacy_clear_tail<'a>(
204        &self,
205        buffer: &'a mut [u8],
206    ) -> Result<&'a mut [u8], DecodeError> {
207        if let Err(err) = validate_legacy_decode::<A, PAD>(buffer) {
208            wipe_bytes(buffer);
209            return Err(err);
210        }
211
212        let mut write = 0;
213        let mut read = 0;
214        while read < buffer.len() {
215            let byte = buffer[read];
216            if !is_legacy_whitespace(byte) {
217                buffer[write] = byte;
218                write += 1;
219            }
220            read += 1;
221        }
222
223        let len = match Self::decode_slice_to_start(&mut buffer[..write]) {
224            Ok(len) => len,
225            Err(err) => {
226                wipe_bytes(buffer);
227                return Err(err);
228            }
229        };
230        wipe_tail(buffer, len);
231        Ok(&mut buffer[..len])
232    }
233
234    fn decode_slice_to_start(buffer: &mut [u8]) -> Result<usize, DecodeError> {
235        let _required = validate_decode::<A, PAD>(buffer)?;
236        let input_len = buffer.len();
237        let mut scratch = [0u8; IN_PLACE_DECODE_INPUT_CHUNK];
238        let mut read = 0;
239        let mut write = 0;
240
241        while read < input_len {
242            let chunk_len = in_place_decode_chunk_len(input_len - read);
243            scratch[..chunk_len].copy_from_slice(&buffer[read..read + chunk_len]);
244            let available = buffer.len();
245            let Some(output_tail) = buffer.get_mut(write..) else {
246                wipe_bytes(&mut scratch[..chunk_len]);
247                return Err(DecodeError::OutputTooSmall {
248                    required: write,
249                    available,
250                });
251            };
252
253            let written =
254                match decode_backend::decode_slice::<A, PAD>(&scratch[..chunk_len], output_tail) {
255                    Ok(written) => written,
256                    Err(err) => {
257                        wipe_bytes(&mut scratch[..chunk_len]);
258                        return Err(err.with_index_offset(read));
259                    }
260                };
261            wipe_bytes(&mut scratch[..chunk_len]);
262
263            read += chunk_len;
264            write += written;
265            if written < decoded_chunk_max(chunk_len) {
266                break;
267            }
268        }
269
270        Ok(write)
271    }
272}
273
274const fn in_place_decode_chunk_len(remaining: usize) -> usize {
275    if remaining <= IN_PLACE_DECODE_INPUT_CHUNK {
276        remaining
277    } else {
278        IN_PLACE_DECODE_INPUT_CHUNK
279    }
280}
281
282const fn decoded_chunk_max(chunk_len: usize) -> usize {
283    (chunk_len / 4) * 3
284}