Skip to main content

base64_ng/v2/
incremental_decoder.rs

1//! Heapless strict incremental ordinary decoding.
2
3use core::num::NonZeroUsize;
4
5use super::{
6    contracts::{
7        BackendFault, Failure, InputError, Lifecycle, OperationError, Progress, SourceSpan, Step,
8    },
9    decode_primitives::{
10        is_legacy_ascii_whitespace, one_byte_tail_is_canonical, pack_full_quantum,
11        two_byte_tail_is_canonical,
12    },
13    specifications::{Base64, Codec, CodecSettings, DecodePadding, TrailingBits},
14};
15
16const INPUT_QUANTUM: usize = 4;
17const OUTPUT_QUANTUM: usize = 3;
18
19/// Heapless strict Base64 decoder state.
20///
21/// The current input quantum and pending output quantum are mutually
22/// exclusive. This keeps retry state bounded while allowing one-byte output
23/// destinations without asking the caller to replay accepted input.
24#[derive(Clone, Debug, Eq, PartialEq)]
25pub struct DecoderState {
26    settings: CodecSettings,
27    input_mode: InputMode,
28    quantum: [u8; INPUT_QUANTUM],
29    quantum_indexes: [usize; INPUT_QUANTUM],
30    quantum_len: usize,
31    pending: [u8; OUTPUT_QUANTUM],
32    pending_start: usize,
33    pending_len: usize,
34    terminal_padding: bool,
35    lifecycle: Lifecycle,
36}
37
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39enum InputMode {
40    Strict,
41    IgnoreLegacyAsciiWhitespace,
42}
43
44impl DecoderState {
45    /// Constructs a decoder whose caller selected canonical padded decoding.
46    pub(crate) const fn new_padded(settings: CodecSettings) -> Self {
47        Self {
48            settings,
49            input_mode: InputMode::Strict,
50            quantum: [0; INPUT_QUANTUM],
51            quantum_indexes: [0; INPUT_QUANTUM],
52            quantum_len: 0,
53            pending: [0; OUTPUT_QUANTUM],
54            pending_start: 0,
55            pending_len: 0,
56            terminal_padding: false,
57            lifecycle: Lifecycle::new(),
58        }
59    }
60
61    /// Constructs a decoder whose caller selected strict unpadded decoding.
62    pub(crate) const fn new_unpadded(settings: CodecSettings) -> Self {
63        Self::new_padded(settings)
64    }
65
66    pub(crate) const fn new_legacy_ascii_whitespace(settings: CodecSettings) -> Self {
67        let mut state = Self::new_padded(settings);
68        state.input_mode = InputMode::IgnoreLegacyAsciiWhitespace;
69        state
70    }
71
72    /// Accepts a strict padded input prefix and writes decoded output that fits.
73    pub fn update(&mut self, input: &[u8], output: &mut [u8]) -> Result<Step, OperationError> {
74        let span = self.lifecycle.reserve_input(input.len())?;
75        let consumed = match self.plan_update(input, output.len(), span) {
76            Ok(consumed) => consumed,
77            Err(failure) => return Err(self.lifecycle.fail(failure)),
78        };
79        self.lifecycle.commit_input(span, consumed)?;
80
81        let mut produced = self.drain_pending(output);
82        let source_start = self.lifecycle.source_position() - consumed;
83        let mut input_offset = 0;
84        while input_offset < consumed {
85            if self.ignores(input[input_offset]) {
86                input_offset += 1;
87                continue;
88            }
89            self.quantum[self.quantum_len] = input[input_offset];
90            self.quantum_indexes[self.quantum_len] = source_start + input_offset;
91            self.quantum_len += 1;
92            input_offset += 1;
93
94            if self.quantum_len == INPUT_QUANTUM {
95                let Ok(decoded) = decode_quantum(self.settings, self.quantum, self.quantum_indexes)
96                else {
97                    return Err(self
98                        .lifecycle
99                        .fail(Failure::Backend(BackendFault::ImpossibleState)));
100                };
101                self.pending = decoded.bytes;
102                self.pending_start = 0;
103                self.pending_len = decoded.len;
104                self.terminal_padding = decoded.terminal_padding;
105                self.quantum_len = 0;
106                produced += self.drain_pending(&mut output[produced..]);
107            }
108        }
109
110        let progress = Progress::new(consumed, produced);
111        if self.pending_len != 0 || consumed != input.len() {
112            self.lifecycle.output_full(progress, NonZeroUsize::MIN)
113        } else {
114            self.lifecycle.need_input(progress)
115        }
116    }
117
118    /// Declares end of input and resolves the selected padding policy.
119    pub fn finish(&mut self, output: &mut [u8]) -> Result<Step, OperationError> {
120        if self.lifecycle.begin_finish()? {
121            return self.lifecycle.finish(Progress::ZERO);
122        }
123
124        let mut produced = self.drain_pending(output);
125        if self.pending_len != 0 {
126            return self
127                .lifecycle
128                .output_full(Progress::new(0, produced), NonZeroUsize::MIN);
129        }
130        if self.quantum_len != 0 {
131            let decoded = match self.settings.decode_padding() {
132                DecodePadding::RequireCanonical => {
133                    let failure = Failure::Input(InputError::TruncatedInput {
134                        index: self.lifecycle.source_position(),
135                    });
136                    return Err(self.lifecycle.fail(failure));
137                }
138                DecodePadding::Forbid | DecodePadding::Indifferent => {
139                    match decode_final_tail(
140                        self.settings,
141                        &self.quantum[..self.quantum_len],
142                        &self.quantum_indexes[..self.quantum_len],
143                    ) {
144                        Ok(decoded) => decoded,
145                        Err(error) => return Err(self.lifecycle.fail(Failure::Input(error))),
146                    }
147                }
148            };
149            self.pending = decoded.bytes;
150            self.pending_start = 0;
151            self.pending_len = decoded.len;
152            self.quantum_len = 0;
153            produced += self.drain_pending(&mut output[produced..]);
154            if self.pending_len != 0 {
155                return self
156                    .lifecycle
157                    .output_full(Progress::new(0, produced), NonZeroUsize::MIN);
158            }
159        }
160        self.lifecycle.finish(Progress::new(0, produced))
161    }
162
163    /// Resets the state for one unrelated ordinary message.
164    pub fn reset(&mut self) {
165        self.quantum = [0; INPUT_QUANTUM];
166        self.quantum_indexes = [0; INPUT_QUANTUM];
167        self.quantum_len = 0;
168        self.pending = [0; OUTPUT_QUANTUM];
169        self.pending_start = 0;
170        self.pending_len = 0;
171        self.terminal_padding = false;
172        self.lifecycle.reset();
173    }
174
175    /// Clears retained ordinary input and output through the reviewed wipe
176    /// boundary, then resets the state for reuse.
177    ///
178    /// This is explicit best-effort cleanup. It cannot retract plaintext
179    /// already returned to a caller or clear copies outside this state.
180    pub fn clear(&mut self) {
181        self.wipe();
182    }
183
184    /// Returns the absolute number of input bytes accepted since reset.
185    #[must_use]
186    pub const fn source_position(&self) -> usize {
187        self.lifecycle.source_position()
188    }
189
190    /// Returns encoded input bytes retained until the next complete quantum.
191    #[must_use]
192    pub const fn buffered_input_len(&self) -> usize {
193        self.quantum_len
194    }
195
196    /// Returns whether a terminal padded quantum has been accepted.
197    #[must_use]
198    pub const fn has_terminal_padding(&self) -> bool {
199        self.terminal_padding
200    }
201
202    /// Clears retained input and output through the reviewed wipe boundary.
203    pub(crate) fn wipe(&mut self) {
204        crate::wipe_bytes(&mut self.quantum);
205        crate::wipe_bytes(&mut self.pending);
206        self.quantum_indexes = [0; INPUT_QUANTUM];
207        self.quantum_len = 0;
208        self.pending_start = 0;
209        self.pending_len = 0;
210        self.terminal_padding = false;
211        self.lifecycle.reset();
212    }
213
214    fn plan_update(
215        &self,
216        input: &[u8],
217        output_len: usize,
218        span: SourceSpan,
219    ) -> Result<usize, Failure> {
220        let pending_written = self.pending_len.min(output_len);
221        let mut pending = self.pending_len - pending_written;
222        if pending != 0 {
223            return Ok(0);
224        }
225
226        let mut available_output = output_len - pending_written;
227        let mut quantum = self.quantum;
228        let mut indexes = self.quantum_indexes;
229        let mut quantum_len = self.quantum_len;
230        let mut terminal_padding = self.terminal_padding;
231        let mut consumed = 0;
232
233        while consumed < input.len() {
234            let index = span
235                .index(consumed)
236                .ok_or(Failure::Backend(BackendFault::ImpossibleState))?;
237            if self.ignores(input[consumed]) {
238                consumed += 1;
239                continue;
240            }
241            if terminal_padding {
242                return Err(Failure::Input(InputError::TrailingData { index }));
243            }
244
245            validate_partial_symbol(
246                self.settings,
247                quantum,
248                &indexes,
249                quantum_len,
250                input[consumed],
251                index,
252            )
253            .map_err(Failure::Input)?;
254            quantum[quantum_len] = input[consumed];
255            indexes[quantum_len] = index;
256            quantum_len += 1;
257            consumed += 1;
258
259            if quantum_len == INPUT_QUANTUM {
260                let decoded =
261                    decode_quantum(self.settings, quantum, indexes).map_err(Failure::Input)?;
262                quantum_len = 0;
263                terminal_padding = decoded.terminal_padding;
264                let written = decoded.len.min(available_output);
265                available_output -= written;
266                pending = decoded.len - written;
267                if pending != 0 {
268                    break;
269                }
270            }
271        }
272        Ok(consumed)
273    }
274
275    const fn ignores(&self, byte: u8) -> bool {
276        matches!(self.input_mode, InputMode::IgnoreLegacyAsciiWhitespace)
277            && is_legacy_ascii_whitespace(byte)
278    }
279
280    fn drain_pending(&mut self, output: &mut [u8]) -> usize {
281        let written = self.pending_len.min(output.len());
282        let pending_end = self.pending_start + written;
283        output[..written].copy_from_slice(&self.pending[self.pending_start..pending_end]);
284        self.pending_start = pending_end;
285        self.pending_len -= written;
286        if self.pending_len == 0 {
287            self.pending_start = 0;
288        }
289        written
290    }
291
292    #[cfg(kani)]
293    pub(crate) fn proof_invariants(&self) -> bool {
294        self.quantum_len < INPUT_QUANTUM
295            && self.pending_start <= OUTPUT_QUANTUM
296            && self.pending_len <= OUTPUT_QUANTUM
297            && self.pending_start + self.pending_len <= OUTPUT_QUANTUM
298            && !(self.quantum_len != 0 && self.pending_len != 0)
299            && matches!(
300                self.settings.decode_padding(),
301                DecodePadding::RequireCanonical
302                    | DecodePadding::Forbid
303                    | DecodePadding::Indifferent
304            )
305    }
306
307    #[cfg(test)]
308    pub(crate) fn set_source_position_for_test(&mut self, source_position: usize) {
309        self.lifecycle = Lifecycle::at_source_position(source_position);
310    }
311}
312
313impl<S: Codec> Base64<S> {
314    /// Constructs a fresh heapless ordinary decoder.
315    pub fn decoder(&self) -> DecoderState {
316        match self.settings().decode_padding() {
317            DecodePadding::RequireCanonical => DecoderState::new_padded(self.settings()),
318            DecodePadding::Forbid | DecodePadding::Indifferent => {
319                DecoderState::new_unpadded(self.settings())
320            }
321        }
322    }
323}
324
325#[derive(Clone, Copy)]
326struct DecodedQuantum {
327    bytes: [u8; OUTPUT_QUANTUM],
328    len: usize,
329    terminal_padding: bool,
330}
331
332fn validate_partial_symbol(
333    settings: CodecSettings,
334    quantum: [u8; INPUT_QUANTUM],
335    indexes: &[usize; INPUT_QUANTUM],
336    position: usize,
337    byte: u8,
338    index: usize,
339) -> Result<(), InputError> {
340    if matches!(settings.decode_padding(), DecodePadding::Forbid) {
341        return decode_symbol(settings, byte, index).map(|_| ());
342    }
343    match position {
344        0 | 1 => decode_symbol(settings, byte, index).map(|_| ()),
345        2 => {
346            if byte == b'=' {
347                Ok(())
348            } else {
349                decode_symbol(settings, byte, index).map(|_| ())
350            }
351        }
352        3 if quantum[2] == b'=' && byte != b'=' => {
353            Err(InputError::InvalidPadding { index: indexes[2] })
354        }
355        3 => {
356            if byte == b'=' {
357                Ok(())
358            } else {
359                decode_symbol(settings, byte, index).map(|_| ())
360            }
361        }
362        _ => Err(InputError::InvalidLength),
363    }
364}
365
366fn decode_quantum(
367    settings: CodecSettings,
368    input: [u8; INPUT_QUANTUM],
369    indexes: [usize; INPUT_QUANTUM],
370) -> Result<DecodedQuantum, InputError> {
371    let first = decode_symbol(settings, input[0], indexes[0])?;
372    let second = decode_symbol(settings, input[1], indexes[1])?;
373
374    if matches!(settings.decode_padding(), DecodePadding::Forbid) {
375        let third = decode_symbol(settings, input[2], indexes[2])?;
376        let fourth = decode_symbol(settings, input[3], indexes[3])?;
377        return Ok(DecodedQuantum {
378            bytes: pack_full_quantum(first, second, third, fourth),
379            len: 3,
380            terminal_padding: false,
381        });
382    }
383
384    match (input[2], input[3]) {
385        (b'=', b'=') => {
386            if !one_byte_tail_is_canonical(second)
387                && settings.trailing_bits() == TrailingBits::RequireCanonical
388            {
389                return Err(InputError::NonCanonicalTrailingBits { index: indexes[1] });
390            }
391            Ok(DecodedQuantum {
392                bytes: [(first << 2) | (second >> 4), 0, 0],
393                len: 1,
394                terminal_padding: true,
395            })
396        }
397        (b'=', _) => Err(InputError::InvalidPadding { index: indexes[2] }),
398        (third, b'=') => {
399            let third = decode_symbol(settings, third, indexes[2])?;
400            if !two_byte_tail_is_canonical(third)
401                && settings.trailing_bits() == TrailingBits::RequireCanonical
402            {
403                return Err(InputError::NonCanonicalTrailingBits { index: indexes[2] });
404            }
405            Ok(DecodedQuantum {
406                bytes: [
407                    (first << 2) | (second >> 4),
408                    (second << 4) | (third >> 2),
409                    0,
410                ],
411                len: 2,
412                terminal_padding: true,
413            })
414        }
415        (third, fourth) => {
416            let third = decode_symbol(settings, third, indexes[2])?;
417            let fourth = decode_symbol(settings, fourth, indexes[3])?;
418            Ok(DecodedQuantum {
419                bytes: pack_full_quantum(first, second, third, fourth),
420                len: 3,
421                terminal_padding: false,
422            })
423        }
424    }
425}
426
427fn decode_unpadded_tail(
428    settings: CodecSettings,
429    input: &[u8],
430    indexes: &[usize],
431) -> Result<DecodedQuantum, InputError> {
432    match input {
433        [first, second] => {
434            let first = decode_symbol(settings, *first, indexes[0])?;
435            let second = decode_symbol(settings, *second, indexes[1])?;
436            if !one_byte_tail_is_canonical(second)
437                && settings.trailing_bits() == TrailingBits::RequireCanonical
438            {
439                return Err(InputError::NonCanonicalTrailingBits { index: indexes[1] });
440            }
441            Ok(DecodedQuantum {
442                bytes: [(first << 2) | (second >> 4), 0, 0],
443                len: 1,
444                terminal_padding: false,
445            })
446        }
447        [first, second, third] => {
448            let first = decode_symbol(settings, *first, indexes[0])?;
449            let second = decode_symbol(settings, *second, indexes[1])?;
450            let third = decode_symbol(settings, *third, indexes[2])?;
451            if !two_byte_tail_is_canonical(third)
452                && settings.trailing_bits() == TrailingBits::RequireCanonical
453            {
454                return Err(InputError::NonCanonicalTrailingBits { index: indexes[2] });
455            }
456            Ok(DecodedQuantum {
457                bytes: [
458                    (first << 2) | (second >> 4),
459                    (second << 4) | (third >> 2),
460                    0,
461                ],
462                len: 2,
463                terminal_padding: false,
464            })
465        }
466        _ => Err(InputError::InvalidLength),
467    }
468}
469
470fn decode_final_tail(
471    settings: CodecSettings,
472    input: &[u8],
473    indexes: &[usize],
474) -> Result<DecodedQuantum, InputError> {
475    if settings.decode_padding() == DecodePadding::Indifferent
476        && let [first, second, b'='] = input
477    {
478        let first = decode_symbol(settings, *first, indexes[0])?;
479        let second = decode_symbol(settings, *second, indexes[1])?;
480        if !one_byte_tail_is_canonical(second)
481            && settings.trailing_bits() == TrailingBits::RequireCanonical
482        {
483            return Err(InputError::NonCanonicalTrailingBits { index: indexes[1] });
484        }
485        return Ok(DecodedQuantum {
486            bytes: [(first << 2) | (second >> 4), 0, 0],
487            len: 1,
488            terminal_padding: true,
489        });
490    }
491    decode_unpadded_tail(settings, input, indexes)
492}
493
494fn decode_symbol(settings: CodecSettings, byte: u8, index: usize) -> Result<u8, InputError> {
495    match settings.alphabet().decode_byte(byte) {
496        Some(value) => Ok(value),
497        None if byte == b'=' => Err(InputError::InvalidPadding { index }),
498        None => Err(InputError::InvalidByte { index, byte }),
499    }
500}