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}