Skip to main content

qubit_codec_misc/
base64_encoder.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 encoder configuration and state.
9
10use core::convert::Infallible;
11use core::num::NonZeroUsize;
12
13use base64::Engine;
14use qubit_codec::CapacityError;
15use qubit_codec::TranscodeEncodeError;
16use qubit_codec::TranscodeEncoder;
17use qubit_codec::TranscodeFailure;
18use qubit_codec::TranscodeProgress;
19use qubit_codec::Transcoder;
20
21use crate::internal::Base64Config;
22use crate::internal::Base64StreamPhase;
23
24/// Encodes bytes incrementally using a strict Base64 configuration.
25///
26/// The encoder uses fixed-size state and does not allocate. Call `reset`
27/// before streaming. Input and output buffers remain caller-owned. It may
28/// retain up to two consumed input bytes until a complete quantum is available
29/// or `finish` encodes the final partial quantum. Retained bytes are already
30/// included in the reported input consumption and must not be submitted again.
31/// If output capacity is insufficient, use the returned progress to retry with
32/// the unconsumed input and a larger output buffer. `NeedOutput.required` is
33/// the minimum total capacity from the advanced output position
34/// (`output_index + progress.written()`) for the next step. When retaining the
35/// output buffer, its length must cover that position plus `required`; when
36/// switching to an empty buffer at index zero, provide at least `required`
37/// slots.
38///
39/// # Examples
40///
41/// ```
42/// use qubit_codec::Transcoder;
43/// use qubit_codec_misc::Base64Encoder;
44///
45/// let mut encoder = Base64Encoder::standard();
46/// let mut output = [0; 4];
47/// encoder.reset(&mut output, 0).unwrap();
48/// let progress = encoder.transcode(b"M", 0, &mut output, 0).unwrap();
49/// assert_eq!(progress.read(), 1);
50/// assert_eq!(progress.written(), 0);
51/// let written = encoder.finish(&mut output, 0).unwrap();
52/// assert_eq!(&output[..written], b"TQ==");
53/// ```
54///
55/// Retrying after output pressure must advance both indices. The encoder has
56/// already consumed the first five bytes of `ManMan`, so only the final `n`
57/// is submitted again:
58///
59/// ```
60/// use qubit_codec::TranscodeStatus;
61/// use qubit_codec::Transcoder;
62/// use qubit_codec_misc::Base64Encoder;
63///
64/// let mut encoder = Base64Encoder::standard();
65/// encoder.reset(&mut [], 0).unwrap();
66/// let mut output = [0; 4];
67/// let first = encoder.transcode(b"ManMan", 0, &mut output, 0).unwrap();
68/// assert_eq!((first.read(), first.written()), (5, 4));
69/// assert_eq!(&output, b"TWFu");
70/// let required = match first.status() {
71///     TranscodeStatus::NeedOutput { required } => required.get(),
72///     status => panic!("expected NeedOutput, got {status:?}"),
73/// };
74/// let mut larger = [0; 8];
75/// larger[..4].copy_from_slice(&output);
76/// let next = encoder.transcode(b"ManMan", first.read(), &mut larger, first.written()).unwrap();
77/// assert_eq!(next.read(), 1);
78/// assert_eq!(next.written(), 4);
79/// assert!(required <= larger.len() - first.written());
80/// assert_eq!(encoder.finish(&mut larger, first.written() + next.written()).unwrap(), 0);
81/// assert_eq!(&larger, b"TWFuTWFu");
82/// ```
83#[must_use]
84#[derive(Debug)]
85pub struct Base64Encoder {
86    /// Strict alphabet and padding policy used for encoding.
87    pub(crate) config: Base64Config,
88    /// Lifecycle state; streaming requires reset and finish is terminal.
89    pub(crate) phase: Base64StreamPhase,
90    /// Up to two input bytes already reported as consumed but not yet encoded.
91    pub(crate) tail: [u8; 2],
92    /// Number of valid bytes in `tail`, always in `0..=2`.
93    pub(crate) tail_len: usize,
94}
95
96impl Base64Encoder {
97    /// Creates an encoder for `config` in the uninitialized phase.
98    ///
99    /// Callers must reset the returned encoder before streaming.
100    ///
101    /// # Parameters
102    ///
103    /// - `config`: Selects the Base64 alphabet and padding policy.
104    ///
105    /// # Returns
106    ///
107    /// An encoder in the uninitialized phase, with no retained input bytes.
108    #[inline]
109    pub(crate) fn from_config(config: Base64Config) -> Self {
110        Self {
111            config,
112            phase: Base64StreamPhase::Uninitialized,
113            tail: [0; 2],
114            tail_len: 0,
115        }
116    }
117
118    /// Creates a strict standard Base64 encoder with padding.
119    ///
120    /// The encoder requires `reset` before streaming and does not allocate.
121    #[inline]
122    pub fn standard() -> Self {
123        Self::from_config(Base64Config::Standard)
124    }
125
126    /// Creates a strict standard Base64 encoder without padding.
127    ///
128    /// The encoder requires `reset` before streaming and does not allocate.
129    #[inline]
130    pub fn standard_no_pad() -> Self {
131        Self::from_config(Base64Config::StandardNoPad)
132    }
133
134    /// Creates a strict URL-safe Base64 encoder with padding.
135    ///
136    /// The encoder requires `reset` before streaming and does not allocate.
137    #[inline]
138    pub fn url_safe() -> Self {
139        Self::from_config(Base64Config::UrlSafe)
140    }
141
142    /// Creates a strict URL-safe Base64 encoder without padding.
143    ///
144    /// The encoder requires `reset` before streaming and does not allocate.
145    #[inline]
146    pub fn url_safe_no_pad() -> Self {
147        Self::from_config(Base64Config::UrlSafeNoPad)
148    }
149}
150
151impl Default for Base64Encoder {
152    /// Creates the default strict standard Base64 encoder with padding.
153    ///
154    /// The encoder must be reset before streaming.
155    #[inline]
156    fn default() -> Self {
157        Self::standard()
158    }
159}
160
161impl TranscodeEncoder for Base64Encoder {
162    /// The encoder cannot fail while converting an input byte.
163    type EncodeError = Infallible;
164}
165
166impl Transcoder for Base64Encoder {
167    /// Base64 encoding consumes bytes.
168    type Input = u8;
169    /// Base64 encoding produces bytes.
170    type Output = u8;
171    /// Reports streaming state or output-capacity failures; encoding itself is
172    /// infallible.
173    type Error = TranscodeEncodeError<Infallible, u8>;
174
175    /// Returns an upper bound for encoding `input_len` input bytes.
176    ///
177    /// # Parameters
178    ///
179    /// - `input_len`: Number of input bytes to encode.
180    ///
181    /// # Returns
182    ///
183    /// The maximum encoded length, or a capacity error if that length
184    /// overflows.
185    fn max_transcode_output_len(&self, input_len: usize) -> Result<usize, CapacityError> {
186        crate::internal::base64_encode_bound(input_len)
187    }
188
189    /// Returns the maximum output needed to encode the retained final bytes.
190    ///
191    /// # Returns
192    ///
193    /// Four bytes for padded output, or three bytes for unpadded output.
194    ///
195    /// # Errors
196    ///
197    /// This method always returns `Ok`; its constant bound cannot overflow.
198    fn max_finish_output_len(&self) -> Result<usize, CapacityError> {
199        Ok(if self.config.padded() { 4 } else { 3 })
200    }
201
202    /// Starts a new stream and validates the output position.
203    ///
204    /// Any bytes retained from an earlier stream are discarded.
205    ///
206    /// # Parameters
207    ///
208    /// - `output`: Caller-owned output storage; it is not modified.
209    /// - `output_index`: Position at which subsequent output will be written.
210    ///
211    /// # Returns
212    ///
213    /// Zero bytes written.
214    ///
215    /// # Errors
216    ///
217    /// Returns an error if `output_index` is outside `output`.
218    fn reset(&mut self, output: &mut [u8], output_index: usize) -> Result<usize, Self::Error> {
219        TranscodeFailure::ensure_output_index(output.len(), output_index)?;
220        self.tail = [0; 2];
221        self.tail_len = 0;
222        self.phase = Base64StreamPhase::Active;
223        Ok(0)
224    }
225
226    /// Encodes complete Base64 quanta and retains up to two trailing bytes.
227    ///
228    /// Retained bytes count as consumed; resuming must use the returned input
229    /// position so those bytes are not submitted twice. On output pressure,
230    /// continue with the reported indices and sufficient output capacity.
231    ///
232    /// # Parameters
233    ///
234    /// - `input`: Caller-owned input bytes.
235    /// - `input_index`: Position of the first input byte to consume.
236    /// - `output`: Caller-owned destination storage.
237    /// - `output_index`: Position of the first output byte to write.
238    ///
239    /// # Returns
240    ///
241    /// Progress containing the input consumed, output written, and whether
242    /// more output capacity is required.
243    ///
244    /// # Errors
245    ///
246    /// Returns an error for invalid indices or when the stream has not been
247    /// reset or has already been finished.
248    fn transcode(
249        &mut self,
250        input: &[u8],
251        input_index: usize,
252        output: &mut [u8],
253        output_index: usize,
254    ) -> Result<TranscodeProgress, Self::Error> {
255        TranscodeFailure::ensure_transcode_indices(input.len(), input_index, output.len(), output_index)?;
256        self.phase.ensure_transcode()?;
257        let mut i = input_index;
258        let mut o = output_index;
259        while i < input.len() {
260            if self.tail_len > 0 {
261                while self.tail_len < 2 && i < input.len() {
262                    self.tail[self.tail_len] = input[i];
263                    self.tail_len += 1;
264                    i += 1;
265                }
266                if i == input.len() {
267                    break;
268                }
269                if output.len() - o < 4 {
270                    return Ok(TranscodeProgress::need_output(
271                        NonZeroUsize::new(4).expect("four is nonzero"),
272                        i - input_index,
273                        o - output_index,
274                    ));
275                }
276                let quantum = [self.tail[0], self.tail[1], input[i]];
277                let written = self
278                    .config
279                    .engine()
280                    .encode_slice(quantum, &mut output[o..o + 4])
281                    .expect("four output bytes fit one full quantum");
282                debug_assert_eq!(written, 4);
283                self.tail_len = 0;
284                i += 1;
285                o += written;
286                continue;
287            }
288            let groups = ((input.len() - i) / 3).min((output.len() - o) / 4);
289            if groups > 0 {
290                let consumed = groups * 3;
291                let emitted = groups * 4;
292                let written = self
293                    .config
294                    .engine()
295                    .encode_slice(&input[i..i + consumed], &mut output[o..o + emitted])
296                    .expect("preflight reserves four bytes per full quantum");
297                debug_assert_eq!(written, emitted);
298                i += consumed;
299                o += written;
300                continue;
301            }
302            let retained = (input.len() - i).min(2);
303            self.tail[..retained].copy_from_slice(&input[i..i + retained]);
304            self.tail_len = retained;
305            i += retained;
306        }
307        Ok(TranscodeProgress::complete(i - input_index, o - output_index))
308    }
309
310    /// Encodes any retained final bytes and marks the stream as finished.
311    ///
312    /// # Parameters
313    ///
314    /// - `output`: Caller-owned destination storage.
315    /// - `output_index`: Position at which final encoded bytes are written.
316    ///
317    /// # Returns
318    ///
319    /// The number of bytes written for the final partial quantum.
320    ///
321    /// # Errors
322    ///
323    /// Returns an error for an invalid output index, insufficient capacity, or
324    /// a stream that was not reset or was already finished.
325    fn finish(&mut self, output: &mut [u8], output_index: usize) -> Result<usize, Self::Error> {
326        TranscodeFailure::ensure_output_index(output.len(), output_index)?;
327        self.phase.ensure_finish()?;
328        let needed = if self.tail_len == 0 {
329            0
330        } else if self.config.padded() {
331            4
332        } else {
333            self.tail_len + 1
334        };
335        TranscodeFailure::ensure_output_capacity(output.len(), output_index, needed)?;
336        let written = self
337            .config
338            .engine()
339            .encode_slice(
340                &self.tail[..self.tail_len],
341                &mut output[output_index..output_index + needed],
342            )
343            .expect("preflight reserves the exact final encoded length");
344        self.tail = [0; 2];
345        self.tail_len = 0;
346        self.phase = Base64StreamPhase::Finished;
347        Ok(written)
348    }
349}