Skip to main content

qubit_codec_misc/
base64_decoder.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Streaming Base64 decoder configuration and state.
9
10use core::num::NonZeroUsize;
11
12use base64::DecodeError;
13use base64::DecodeSliceError;
14use base64::Engine;
15use qubit_codec::CapacityError;
16use qubit_codec::TranscodeDecodeError;
17use qubit_codec::TranscodeDecoder;
18use qubit_codec::TranscodeFailure;
19use qubit_codec::TranscodeProgress;
20use qubit_codec::Transcoder;
21
22use crate::MiscCodecError;
23use crate::internal::Base64Config;
24use crate::internal::Base64StreamPhase;
25use crate::internal::base64_decode_bound;
26
27/// Decodes Base64 incrementally using a strict configuration.
28///
29/// The decoder uses fixed-size state and does not allocate. Call `reset`
30/// before streaming. Incomplete non-EOF input remains caller-owned and must be
31/// supplied again with additional input or to `transcode_eof`. `Complete`
32/// describes the currently visible input and does not establish source EOF;
33/// call `transcode_eof` only after the source confirms EOF. A domain error may
34/// occur after earlier bytes in the same operation were written, and poisons
35/// the decoder; call `reset` before reusing it. If output capacity is
36/// insufficient, use the returned progress to retry with unconsumed input and
37/// a larger output buffer. `NeedOutput.required` is the minimum total capacity
38/// from the advanced output position (`output_index + progress.written()`) for
39/// the next step. When retaining the output buffer, its length must cover that
40/// position plus `required`; when switching to an empty buffer at index zero,
41/// provide at least `required` slots.
42///
43/// The outer transcoder error reports a position in the input slice passed to
44/// the current call. Its `MiscCodecError` and `base64::DecodeError` source keep
45/// the engine sub-slice position, which can vary with batching, EOF handling,
46/// and output capacity. Errors without an exact engine position use the
47/// failing segment's start in the outer error. These positions are not
48/// cumulative stream offsets.
49///
50/// # Examples
51///
52/// ```
53/// use qubit_codec::TranscodeStatus;
54/// use qubit_codec::Transcoder;
55/// use qubit_codec_misc::Base64Decoder;
56///
57/// let mut decoder = Base64Decoder::standard();
58/// let mut output = [0; 3];
59/// decoder.reset(&mut output, 0).unwrap();
60/// let progress = decoder.transcode(b"TQ", 0, &mut output, 0).unwrap();
61/// assert_eq!(progress.read(), 0);
62/// assert!(matches!(progress.status(), TranscodeStatus::NeedInput { .. }));
63/// // Keep the unconsumed TQ, append ==, and confirm EOF with the source.
64/// let progress = decoder.transcode_eof(b"TQ==", 0, &mut output, 0).unwrap();
65/// assert_eq!(progress.read(), 4);
66/// assert_eq!(&output[..progress.written()], b"M");
67/// assert_eq!(decoder.finish(&mut [], 0).unwrap(), 0);
68/// ```
69#[must_use]
70#[derive(Debug)]
71pub struct Base64Decoder {
72    /// Strict alphabet and padding policy used for decoding.
73    pub(crate) config: Base64Config,
74    /// Lifecycle state; domain errors poison the stream until reset.
75    pub(crate) phase: Base64StreamPhase,
76    /// Whether a successfully committed quantum contained terminal padding.
77    pub(crate) terminal_padding: bool,
78}
79
80impl Base64Decoder {
81    /// Creates a decoder for `config` in the uninitialized phase.
82    ///
83    /// Callers must reset the returned decoder before streaming.
84    ///
85    /// # Parameters
86    ///
87    /// - `config`: Alphabet and padding policy applied by the decoder.
88    ///
89    /// # Returns
90    ///
91    /// Returns an uninitialized decoder with the selected configuration.
92    #[inline]
93    pub(crate) fn from_config(config: Base64Config) -> Self {
94        Self {
95            config,
96            phase: Base64StreamPhase::Uninitialized,
97            terminal_padding: false,
98        }
99    }
100
101    /// Creates a strict standard Base64 decoder with padding.
102    ///
103    /// The decoder requires `reset` before streaming and does not allocate.
104    ///
105    /// # Returns
106    ///
107    /// Returns an uninitialized decoder using the standard alphabet and
108    /// required padding.
109    #[inline]
110    pub fn standard() -> Self {
111        Self::from_config(Base64Config::Standard)
112    }
113
114    /// Creates a strict standard Base64 decoder without padding.
115    ///
116    /// The decoder requires `reset` before streaming and does not allocate.
117    ///
118    /// # Returns
119    ///
120    /// Returns an uninitialized decoder using the standard alphabet without
121    /// required padding.
122    #[inline]
123    pub fn standard_no_pad() -> Self {
124        Self::from_config(Base64Config::StandardNoPad)
125    }
126
127    /// Creates a strict URL-safe Base64 decoder with padding.
128    ///
129    /// The decoder requires `reset` before streaming and does not allocate.
130    ///
131    /// # Returns
132    ///
133    /// Returns an uninitialized decoder using the URL-safe alphabet and
134    /// required padding.
135    #[inline]
136    pub fn url_safe() -> Self {
137        Self::from_config(Base64Config::UrlSafe)
138    }
139
140    /// Creates a strict URL-safe Base64 decoder without padding.
141    ///
142    /// The decoder requires `reset` before streaming and does not allocate.
143    ///
144    /// # Returns
145    ///
146    /// Returns an uninitialized decoder using the URL-safe alphabet without
147    /// required padding.
148    #[inline]
149    pub fn url_safe_no_pad() -> Self {
150        Self::from_config(Base64Config::UrlSafeNoPad)
151    }
152
153    /// Converts an engine error to the stream error, mapping its relative
154    /// position to the current call's input slice in the outer error while
155    /// preserving the engine sub-slice position in the domain error, then
156    /// poisons this stream.
157    ///
158    /// Errors without a precise engine position use the start of the current
159    /// segment, matching the existing decode error mapping.
160    ///
161    /// # Parameters
162    ///
163    /// - `source`: Engine error whose position is relative to the decoded
164    ///   segment.
165    /// - `segment_start`: Segment start in the current call's input slice.
166    ///
167    /// # Returns
168    ///
169    /// Returns the mapped error with the decoder moved to the poisoned phase.
170    fn domain_error(&mut self, source: DecodeError, segment_start: usize) -> TranscodeDecodeError<MiscCodecError> {
171        let relative = match &source {
172            DecodeError::InvalidByte(index, _) => *index,
173            DecodeError::InvalidLastSymbol { offset, .. } => *offset,
174            DecodeError::InvalidLength(_) | DecodeError::InvalidPadding => 0,
175        };
176        self.phase = Base64StreamPhase::Poisoned;
177        TranscodeDecodeError::domain_main(MiscCodecError::from(source), segment_start + relative)
178    }
179
180    /// Decodes and commits one final or partial Base64 quantum.
181    ///
182    /// The decoded bytes are copied only after the destination capacity is
183    /// confirmed, so a capacity failure leaves the input position and padding
184    /// state unchanged.
185    ///
186    /// # Parameters
187    ///
188    /// - `input`: Encoded input containing the quantum to decode.
189    /// - `input_index`: Start of the quantum in `input`.
190    /// - `output`: Destination buffer for decoded bytes.
191    /// - `output_index`: Start of the writable region in `output`.
192    /// - `take`: Number of input bytes in this quantum.
193    ///
194    /// # Returns
195    ///
196    /// Returns the next input and output indices when decoded bytes fit, or the
197    /// required output capacity when they do not.
198    ///
199    /// # Errors
200    ///
201    /// Returns the mapped Base64 decoding error when the quantum is invalid.
202    fn decode_quantum(
203        &mut self,
204        input: &[u8],
205        input_index: usize,
206        output: &mut [u8],
207        output_index: usize,
208        take: usize,
209    ) -> Result<Result<(usize, usize), usize>, TranscodeDecodeError<MiscCodecError>> {
210        let segment = &input[input_index..input_index + take];
211        let mut scratch = [0; 3];
212        let written = match self.config.engine().decode_slice(segment, &mut scratch) {
213            Ok(written) => written,
214            Err(DecodeSliceError::DecodeError(source)) => {
215                return Err(self.domain_error(source, input_index));
216            }
217            // A Base64 quantum can produce at most three bytes.
218            Err(DecodeSliceError::OutputSliceTooSmall) => {
219                unreachable!("three-byte scratch holds one Base64 quantum")
220            }
221        };
222
223        if output.len() - output_index < written {
224            return Ok(Err(written));
225        }
226        output[output_index..output_index + written].copy_from_slice(&scratch[..written]);
227        self.terminal_padding = segment.contains(&b'=');
228        Ok(Ok((input_index + take, output_index + written)))
229    }
230
231    /// Decodes one visible input segment, optionally treating its end as EOF.
232    ///
233    /// At EOF, a sufficiently large output buffer permits a whole-suffix
234    /// engine call. Otherwise complete unpadded groups are decoded before a
235    /// final quantum is handled separately. Errors can follow writes from
236    /// earlier groups; a capacity failure on the final quantum leaves that
237    /// quantum unconsumed and its decoder state uncommitted.
238    ///
239    /// # Parameters
240    ///
241    /// - `input`: Visible encoded input.
242    /// - `input_index`: Absolute position where this call begins reading.
243    /// - `output`: Destination for decoded bytes.
244    /// - `output_index`: Absolute position where this call begins writing.
245    /// - `eof`: Whether the caller has confirmed that the input source ended.
246    ///
247    /// # Returns
248    ///
249    /// Returns progress relative to the supplied indices when decoding can
250    /// continue or the visible input is complete.
251    ///
252    /// # Errors
253    ///
254    /// Returns invalid-index or lifecycle errors before decoding, and mapped
255    /// domain errors for invalid Base64 input. A domain error may follow bytes
256    /// written by earlier groups and poisons the decoder.
257    ///
258    /// # Panics
259    ///
260    /// Internal assertions rely on non-empty valid Base64 quanta producing
261    /// output and on the checked decode bound covering the engine estimate.
262    fn decode_segment(
263        &mut self,
264        input: &[u8],
265        input_index: usize,
266        output: &mut [u8],
267        output_index: usize,
268        eof: bool,
269    ) -> Result<TranscodeProgress, TranscodeDecodeError<MiscCodecError>> {
270        TranscodeFailure::ensure_transcode_indices(input.len(), input_index, output.len(), output_index)?;
271        self.phase.ensure_transcode()?;
272        let mut i = input_index;
273        let mut o = output_index;
274
275        if self.terminal_padding && i < input.len() {
276            return Err(self.domain_error(DecodeError::InvalidByte(0, input[i]), i));
277        }
278
279        if eof
280            && !self.terminal_padding
281            && let Ok(bound) = base64_decode_bound(input.len() - i)
282            && output.len() - o >= bound
283        {
284            let written = match self.config.engine().decode_slice(&input[i..], &mut output[o..]) {
285                Ok(written) => written,
286                Err(DecodeSliceError::DecodeError(source)) => {
287                    return Err(self.domain_error(source, i));
288                }
289                // The checked bound covers the engine's conservative
290                // decode estimate for the full input suffix.
291                Err(DecodeSliceError::OutputSliceTooSmall) => {
292                    unreachable!("decoded upper bound covers engine estimate")
293                }
294            };
295            self.terminal_padding = input[i..].contains(&b'=');
296            return Ok(TranscodeProgress::complete(input.len() - i, written));
297        }
298
299        while i < input.len() {
300            if self.terminal_padding {
301                return Err(self.domain_error(DecodeError::InvalidByte(0, input[i]), i));
302            }
303
304            let remaining = input.len() - i;
305            if remaining < 4 && !eof {
306                return Ok(TranscodeProgress::need_input(
307                    NonZeroUsize::new(4).expect("four is nonzero"),
308                    i - input_index,
309                    o - output_index,
310                ));
311            }
312
313            let groups_by_capacity = (remaining / 4).min((output.len() - o) / 3);
314            let candidate_len = groups_by_capacity * 4;
315            let groups_without_padding = input[i..i + candidate_len]
316                .iter()
317                .position(|byte| *byte == b'=')
318                .map_or(groups_by_capacity, |at| at / 4);
319
320            if groups_without_padding > 0 {
321                let take = groups_without_padding * 4;
322                let capacity = groups_without_padding * 3;
323                let written = match self
324                    .config
325                    .engine()
326                    .decode_slice(&input[i..i + take], &mut output[o..o + capacity])
327                {
328                    Ok(written) => written,
329                    Err(DecodeSliceError::DecodeError(source)) => {
330                        return Err(self.domain_error(source, i));
331                    }
332                    // Each complete unpadded quantum decodes to exactly three
333                    // bytes, which the selected output range holds.
334                    Err(DecodeSliceError::OutputSliceTooSmall) => {
335                        unreachable!("three output bytes fit each full quantum")
336                    }
337                };
338                debug_assert_eq!(written, capacity);
339                i += take;
340                o += written;
341                continue;
342            }
343
344            let take = remaining.min(4);
345            let (next_i, next_o) = match self.decode_quantum(input, i, output, o, take)? {
346                Ok(indices) => indices,
347                Err(written) => {
348                    return Ok(TranscodeProgress::need_output(
349                        NonZeroUsize::new(written).expect("valid nonempty quantum produces bytes"),
350                        i - input_index,
351                        o - output_index,
352                    ));
353                }
354            };
355            i = next_i;
356            o = next_o;
357        }
358
359        Ok(TranscodeProgress::complete(i - input_index, o - output_index))
360    }
361}
362
363impl Default for Base64Decoder {
364    /// Creates an uninitialized decoder using the strict standard alphabet.
365    #[inline]
366    fn default() -> Self {
367        Self::standard()
368    }
369}
370
371impl TranscodeDecoder for Base64Decoder {
372    /// Domain error type produced when Base64 input cannot be decoded.
373    type DecodeError = MiscCodecError;
374}
375
376impl Transcoder for Base64Decoder {
377    /// Encoded input element type.
378    type Input = u8;
379    /// Decoded output element type.
380    type Output = u8;
381    /// Errors produced by invalid input or invalid stream state.
382    type Error = TranscodeDecodeError<MiscCodecError>;
383
384    /// Returns a checked upper bound for decoding `input_len` encoded bytes.
385    ///
386    /// # Parameters
387    ///
388    /// - `input_len`: Number of encoded input bytes to decode.
389    ///
390    /// # Returns
391    ///
392    /// Returns the maximum number of decoded bytes required for that input
393    /// length.
394    ///
395    /// # Errors
396    ///
397    /// Returns [`CapacityError`] when the output bound overflows `usize`.
398    #[inline]
399    fn max_transcode_output_len(&self, input_len: usize) -> Result<usize, CapacityError> {
400        base64_decode_bound(input_len)
401    }
402
403    /// Starts a fresh stream and clears terminal-padding state.
404    ///
405    /// # Parameters
406    ///
407    /// - `output`: Output buffer available to the reset phase.
408    /// - `output_index`: Starting position in `output`; must not exceed its
409    ///   length.
410    ///
411    /// # Returns
412    ///
413    /// Returns the number of output elements written during reset (zero).
414    ///
415    /// # Errors
416    ///
417    /// Returns an invalid-output-index error when `output_index` exceeds the
418    /// buffer length.
419    fn reset(&mut self, output: &mut [u8], output_index: usize) -> Result<usize, Self::Error> {
420        TranscodeFailure::ensure_output_index(output.len(), output_index)?;
421        self.phase = Base64StreamPhase::Active;
422        self.terminal_padding = false;
423        Ok(0)
424    }
425
426    /// Decodes the visible input without treating its end as source EOF.
427    ///
428    /// # Parameters
429    ///
430    /// - `input`: Visible encoded bytes; unconsumed bytes must be supplied
431    ///   again on a later call.
432    /// - `input_index`: Starting position in `input`; must not exceed its
433    ///   length.
434    /// - `output`: Destination buffer for decoded bytes.
435    /// - `output_index`: Starting position in `output`; must not exceed its
436    ///   length.
437    ///
438    /// # Returns
439    ///
440    /// Returns progress describing consumed input, written output, and whether
441    /// more input or output capacity is needed.
442    ///
443    /// # Errors
444    ///
445    /// Returns invalid-index or lifecycle errors, or a Base64 domain error
446    /// for invalid input. A domain error poisons the stream until reset.
447    fn transcode(
448        &mut self,
449        input: &[u8],
450        input_index: usize,
451        output: &mut [u8],
452        output_index: usize,
453    ) -> Result<TranscodeProgress, Self::Error> {
454        self.decode_segment(input, input_index, output, output_index, false)
455    }
456
457    /// Decodes input after the caller has confirmed source EOF.
458    ///
459    /// # Parameters
460    ///
461    /// - `input`: Visible encoded bytes, including any previously unconsumed
462    ///   tail.
463    /// - `input_index`: Starting position in `input`; must not exceed its
464    ///   length.
465    /// - `output`: Destination buffer for decoded bytes.
466    /// - `output_index`: Starting position in `output`; must not exceed its
467    ///   length.
468    ///
469    /// # Returns
470    ///
471    /// Returns progress describing consumed input, written output, and whether
472    /// more output capacity is needed.
473    ///
474    /// # Errors
475    ///
476    /// Returns invalid-index or lifecycle errors, or a Base64 domain error
477    /// for invalid or incomplete EOF input. A domain error poisons the stream
478    /// until reset.
479    fn transcode_eof(
480        &mut self,
481        input: &[u8],
482        input_index: usize,
483        output: &mut [u8],
484        output_index: usize,
485    ) -> Result<TranscodeProgress, Self::Error> {
486        self.decode_segment(input, input_index, output, output_index, true)
487    }
488
489    /// Marks a successfully decoded stream as finished.
490    ///
491    /// # Parameters
492    ///
493    /// - `output`: Output buffer available to the finish phase.
494    /// - `output_index`: Starting position in `output`; must not exceed its
495    ///   length.
496    ///
497    /// # Returns
498    ///
499    /// Returns the number of output elements written during finish (zero).
500    ///
501    /// # Errors
502    ///
503    /// Returns an invalid-output-index error or a lifecycle error when the
504    /// stream is not ready to finish.
505    fn finish(&mut self, output: &mut [u8], output_index: usize) -> Result<usize, Self::Error> {
506        TranscodeFailure::ensure_output_index(output.len(), output_index)?;
507        self.phase.ensure_finish()?;
508        self.phase = Base64StreamPhase::Finished;
509        Ok(0)
510    }
511}