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