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