Skip to main content

base64_ng/v2/web/
decoder.rs

1//! Heapless incremental WHATWG forgiving decoder.
2
3use core::num::NonZeroUsize;
4
5use super::ForgivingBase64;
6use crate::v2::{BackendFault, OutputFull, Progress, Status, Step};
7
8const INPUT_QUANTUM: usize = 4;
9const OUTPUT_QUANTUM: usize = 3;
10
11/// Opaque WHATWG decode, lifecycle, or resource failure.
12#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
13#[non_exhaustive]
14pub enum ForgivingError {
15    /// The string does not satisfy the WHATWG forgiving Base64 algorithm.
16    InvalidInput,
17    /// The absolute accepted source length cannot be represented by `usize`.
18    PositionOverflow,
19    /// An internal state-machine progress invariant failed.
20    Backend(BackendFault),
21    /// Input was supplied after finalization started.
22    InputAfterFinish,
23    /// Input was supplied after successful completion.
24    InputAfterComplete,
25    /// The caller's one-shot destination is too small.
26    OutputTooSmall {
27        /// Exact required output bytes.
28        required: usize,
29        /// Available destination bytes.
30        available: usize,
31    },
32    /// The exact allocating result exceeds a caller-selected limit.
33    AllocationLimitExceeded {
34        /// Exact required output bytes.
35        required: usize,
36        /// Maximum permitted output bytes.
37        limit: usize,
38    },
39    /// Exact allocation reservation failed.
40    AllocationFailed {
41        /// Exact requested output bytes.
42        requested: usize,
43    },
44}
45
46impl core::fmt::Display for ForgivingError {
47    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
48        match self {
49            Self::InvalidInput => formatter.write_str("invalid forgiving Base64 input"),
50            Self::PositionOverflow => {
51                formatter.write_str("forgiving Base64 source position overflows usize")
52            }
53            Self::Backend(fault) => write!(
54                formatter,
55                "forgiving Base64 internal backend failure: {}",
56                fault.as_str()
57            ),
58            Self::InputAfterFinish => formatter.write_str("input supplied after finish started"),
59            Self::InputAfterComplete => formatter.write_str("input supplied after completion"),
60            Self::OutputTooSmall {
61                required,
62                available,
63            } => write!(
64                formatter,
65                "forgiving Base64 output too small: required {required}, available {available}"
66            ),
67            Self::AllocationLimitExceeded { required, limit } => write!(
68                formatter,
69                "forgiving Base64 output length {required} exceeds allocation limit {limit}"
70            ),
71            Self::AllocationFailed { requested } => write!(
72                formatter,
73                "failed to reserve {requested} forgiving Base64 output bytes"
74            ),
75        }
76    }
77}
78
79#[cfg(feature = "std")]
80impl std::error::Error for ForgivingError {}
81
82#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
83enum Phase {
84    Active,
85    Finishing,
86    Complete,
87    Failed(ForgivingError),
88}
89
90/// Heapless incremental WHATWG forgiving Base64 decoder.
91///
92/// State is bounded to one input quantum and one pending output quantum.
93/// This ordinary compatibility state does not wipe on drop and must not be
94/// used for secret-bearing input.
95#[derive(Clone, Debug, Eq, PartialEq)]
96pub struct ForgivingDecoder {
97    quantum: [u8; INPUT_QUANTUM],
98    quantum_len: usize,
99    pending: [u8; OUTPUT_QUANTUM],
100    pending_start: usize,
101    pending_len: usize,
102    terminal_padding: bool,
103    source_position: usize,
104    phase: Phase,
105}
106
107impl ForgivingBase64 {
108    /// Constructs a fresh heapless incremental decoder.
109    #[must_use]
110    pub const fn decoder(self) -> ForgivingDecoder {
111        ForgivingDecoder::new()
112    }
113}
114
115impl ForgivingDecoder {
116    const fn new() -> Self {
117        Self {
118            quantum: [0; INPUT_QUANTUM],
119            quantum_len: 0,
120            pending: [0; OUTPUT_QUANTUM],
121            pending_start: 0,
122            pending_len: 0,
123            terminal_padding: false,
124            source_position: 0,
125            phase: Phase::Active,
126        }
127    }
128
129    /// Accepts one string fragment and writes every decoded byte that fits.
130    ///
131    /// `input_consumed` counts UTF-8 bytes. Every accepted WHATWG symbol is
132    /// ASCII, so a successful partial count is always a valid string boundary.
133    pub fn update(&mut self, input: &str, output: &mut [u8]) -> Result<Step, ForgivingError> {
134        self.require_active()?;
135        self.source_position
136            .checked_add(input.len())
137            .ok_or_else(|| self.fail(ForgivingError::PositionOverflow))?;
138        let consumed = match self.plan_update(input, output.len()) {
139            Ok(consumed) => consumed,
140            Err(error) => return Err(self.fail(error)),
141        };
142
143        let mut produced = self.drain_pending(output);
144        if self.pending_len != 0 {
145            return Ok(output_full(0, produced));
146        }
147
148        for &byte in &input.as_bytes()[..consumed] {
149            if is_ascii_whitespace(byte) {
150                continue;
151            }
152
153            self.quantum[self.quantum_len] = byte;
154            self.quantum_len += 1;
155            if self.quantum_len == INPUT_QUANTUM {
156                let Some((bytes, len, terminal)) = decode_quantum(self.quantum) else {
157                    return Err(self.fail(ForgivingError::InvalidInput));
158                };
159                self.quantum_len = 0;
160                self.pending = bytes;
161                self.pending_start = 0;
162                self.pending_len = len;
163                self.terminal_padding = terminal;
164                produced += self.drain_pending(&mut output[produced..]);
165                if self.pending_len != 0 {
166                    break;
167                }
168            }
169        }
170        self.source_position += consumed;
171
172        if self.pending_len != 0 || consumed != input.len() {
173            Ok(output_full(consumed, produced))
174        } else {
175            Ok(Step::new(
176                Progress::new(consumed, produced),
177                Status::NeedInput,
178            ))
179        }
180    }
181
182    fn plan_update(&self, input: &str, output_len: usize) -> Result<usize, ForgivingError> {
183        let pending_written = self.pending_len.min(output_len);
184        if self.pending_len != pending_written {
185            return Ok(0);
186        }
187
188        let mut available_output = output_len - pending_written;
189        let mut quantum = self.quantum;
190        let mut quantum_len = self.quantum_len;
191        let mut terminal_padding = self.terminal_padding;
192        let mut consumed = 0;
193
194        for &byte in input.as_bytes() {
195            if is_ascii_whitespace(byte) {
196                consumed += 1;
197                continue;
198            }
199            if terminal_padding
200                || (!is_standard_symbol(byte) && byte != b'=')
201                || (byte == b'=' && quantum_len < 2)
202                || (quantum_len == 3 && quantum[2] == b'=' && byte != b'=')
203            {
204                return Err(ForgivingError::InvalidInput);
205            }
206
207            quantum[quantum_len] = byte;
208            quantum_len += 1;
209            consumed += 1;
210            if quantum_len == INPUT_QUANTUM {
211                let Some((_, decoded_len, terminal)) = decode_quantum(quantum) else {
212                    return Err(ForgivingError::InvalidInput);
213                };
214                quantum_len = 0;
215                terminal_padding = terminal;
216                let written = decoded_len.min(available_output);
217                available_output -= written;
218                if written != decoded_len {
219                    break;
220                }
221            }
222        }
223        Ok(consumed)
224    }
225
226    /// Finalizes omitted padding and drains any bounded pending output.
227    pub fn finish(&mut self, output: &mut [u8]) -> Result<Step, ForgivingError> {
228        match self.phase {
229            Phase::Active => self.phase = Phase::Finishing,
230            Phase::Finishing => {}
231            Phase::Complete => return Ok(Step::new(Progress::ZERO, Status::Complete)),
232            Phase::Failed(error) => return Err(error),
233        }
234
235        let mut produced = self.drain_pending(output);
236        if self.pending_len != 0 {
237            return Ok(output_full(0, produced));
238        }
239        if self.quantum_len != 0 {
240            let Some((bytes, len)) = decode_tail(&self.quantum[..self.quantum_len]) else {
241                return Err(self.fail(ForgivingError::InvalidInput));
242            };
243            self.quantum_len = 0;
244            self.pending = bytes;
245            self.pending_start = 0;
246            self.pending_len = len;
247            produced += self.drain_pending(&mut output[produced..]);
248            if self.pending_len != 0 {
249                return Ok(output_full(0, produced));
250            }
251        }
252        self.phase = Phase::Complete;
253        Ok(Step::new(Progress::new(0, produced), Status::Complete))
254    }
255
256    /// Resets all ordinary state for an unrelated web string.
257    pub fn reset(&mut self) {
258        *self = Self::new();
259    }
260
261    /// Returns the number of UTF-8 source bytes accepted since reset.
262    #[must_use]
263    pub const fn source_position(&self) -> usize {
264        self.source_position
265    }
266
267    fn require_active(&self) -> Result<(), ForgivingError> {
268        match self.phase {
269            Phase::Active => Ok(()),
270            Phase::Finishing => Err(ForgivingError::InputAfterFinish),
271            Phase::Complete => Err(ForgivingError::InputAfterComplete),
272            Phase::Failed(error) => Err(error),
273        }
274    }
275
276    fn fail(&mut self, error: ForgivingError) -> ForgivingError {
277        if let Phase::Failed(existing) = self.phase {
278            existing
279        } else {
280            self.phase = Phase::Failed(error);
281            error
282        }
283    }
284
285    fn drain_pending(&mut self, output: &mut [u8]) -> usize {
286        let written = self.pending_len.min(output.len());
287        let end = self.pending_start + written;
288        output[..written].copy_from_slice(&self.pending[self.pending_start..end]);
289        self.pending_start = end;
290        self.pending_len -= written;
291        if self.pending_len == 0 {
292            self.pending_start = 0;
293        }
294        written
295    }
296}
297
298fn output_full(consumed: usize, produced: usize) -> Step {
299    Step::new(
300        Progress::new(consumed, produced),
301        Status::OutputFull(OutputFull::new(NonZeroUsize::MIN)),
302    )
303}
304
305pub(super) const fn is_ascii_whitespace(byte: u8) -> bool {
306    matches!(byte, b'\t' | b'\n' | 0x0c | b'\r' | b' ')
307}
308
309const fn is_standard_symbol(byte: u8) -> bool {
310    byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/')
311}
312
313const fn decode_value(byte: u8) -> Option<u8> {
314    match byte {
315        b'A'..=b'Z' => Some(byte - b'A'),
316        b'a'..=b'z' => Some(byte - b'a' + 26),
317        b'0'..=b'9' => Some(byte - b'0' + 52),
318        b'+' => Some(62),
319        b'/' => Some(63),
320        _ => None,
321    }
322}
323
324fn decode_quantum(input: [u8; 4]) -> Option<([u8; 3], usize, bool)> {
325    let first = decode_value(input[0])?;
326    let second = decode_value(input[1])?;
327    let one = (first << 2) | (second >> 4);
328    match (input[2], input[3]) {
329        (b'=', b'=') => Some(([one, 0, 0], 1, true)),
330        (b'=', _) => None,
331        (third, b'=') => {
332            let third = decode_value(third)?;
333            Some(([one, (second << 4) | (third >> 2), 0], 2, true))
334        }
335        (third, fourth) => {
336            let third = decode_value(third)?;
337            let fourth = decode_value(fourth)?;
338            Some((
339                [one, (second << 4) | (third >> 2), (third << 6) | fourth],
340                3,
341                false,
342            ))
343        }
344    }
345}
346
347fn decode_tail(input: &[u8]) -> Option<([u8; 3], usize)> {
348    match input {
349        [] => Some(([0; 3], 0)),
350        [first, second] => {
351            let first = decode_value(*first)?;
352            let second = decode_value(*second)?;
353            Some(([(first << 2) | (second >> 4), 0, 0], 1))
354        }
355        [first, second, third] => {
356            let first = decode_value(*first)?;
357            let second = decode_value(*second)?;
358            let third = decode_value(*third)?;
359            Some((
360                [
361                    (first << 2) | (second >> 4),
362                    (second << 4) | (third >> 2),
363                    0,
364                ],
365                2,
366            ))
367        }
368        _ => None,
369    }
370}