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 #[inline]
101 pub(crate) fn from_config(config: Base64Config) -> Self {
102 Self {
103 config,
104 phase: Base64StreamPhase::Uninitialized,
105 tail: [0; 2],
106 tail_len: 0,
107 }
108 }
109
110 /// Creates a strict standard Base64 encoder with padding.
111 ///
112 /// The encoder requires `reset` before streaming and does not allocate.
113 #[inline]
114 pub fn standard() -> Self {
115 Self::from_config(Base64Config::Standard)
116 }
117
118 /// Creates a strict standard Base64 encoder without padding.
119 ///
120 /// The encoder requires `reset` before streaming and does not allocate.
121 #[inline]
122 pub fn standard_no_pad() -> Self {
123 Self::from_config(Base64Config::StandardNoPad)
124 }
125
126 /// Creates a strict URL-safe Base64 encoder with padding.
127 ///
128 /// The encoder requires `reset` before streaming and does not allocate.
129 #[inline]
130 pub fn url_safe() -> Self {
131 Self::from_config(Base64Config::UrlSafe)
132 }
133
134 /// Creates a strict URL-safe Base64 encoder without padding.
135 ///
136 /// The encoder requires `reset` before streaming and does not allocate.
137 #[inline]
138 pub fn url_safe_no_pad() -> Self {
139 Self::from_config(Base64Config::UrlSafeNoPad)
140 }
141}
142
143impl Default for Base64Encoder {
144 #[inline]
145 fn default() -> Self {
146 Self::standard()
147 }
148}
149
150impl TranscodeEncoder for Base64Encoder {
151 type EncodeError = Infallible;
152}
153
154impl Transcoder for Base64Encoder {
155 type Input = u8;
156 type Output = u8;
157 type Error = TranscodeEncodeError<Infallible, u8>;
158
159 fn max_transcode_output_len(&self, input_len: usize) -> Result<usize, CapacityError> {
160 crate::internal::base64_encode_bound(input_len)
161 }
162
163 fn max_finish_output_len(&self) -> Result<usize, CapacityError> {
164 Ok(if self.config.padded() { 4 } else { 3 })
165 }
166
167 fn reset(&mut self, output: &mut [u8], output_index: usize) -> Result<usize, Self::Error> {
168 TranscodeFailure::ensure_output_index(output.len(), output_index)?;
169 self.tail = [0; 2];
170 self.tail_len = 0;
171 self.phase = Base64StreamPhase::Active;
172 Ok(0)
173 }
174
175 fn transcode(
176 &mut self,
177 input: &[u8],
178 input_index: usize,
179 output: &mut [u8],
180 output_index: usize,
181 ) -> Result<TranscodeProgress, Self::Error> {
182 TranscodeFailure::ensure_transcode_indices(input.len(), input_index, output.len(), output_index)?;
183 self.phase.ensure_transcode()?;
184 let mut i = input_index;
185 let mut o = output_index;
186 while i < input.len() {
187 if self.tail_len > 0 {
188 while self.tail_len < 2 && i < input.len() {
189 self.tail[self.tail_len] = input[i];
190 self.tail_len += 1;
191 i += 1;
192 }
193 if i == input.len() {
194 break;
195 }
196 if output.len() - o < 4 {
197 return Ok(TranscodeProgress::need_output(
198 NonZeroUsize::new(4).expect("four is nonzero"),
199 i - input_index,
200 o - output_index,
201 ));
202 }
203 let quantum = [self.tail[0], self.tail[1], input[i]];
204 let written = self
205 .config
206 .engine()
207 .encode_slice(quantum, &mut output[o..o + 4])
208 .expect("four output bytes fit one full quantum");
209 debug_assert_eq!(written, 4);
210 self.tail_len = 0;
211 i += 1;
212 o += written;
213 continue;
214 }
215 let groups = ((input.len() - i) / 3).min((output.len() - o) / 4);
216 if groups > 0 {
217 let consumed = groups * 3;
218 let emitted = groups * 4;
219 let written = self
220 .config
221 .engine()
222 .encode_slice(&input[i..i + consumed], &mut output[o..o + emitted])
223 .expect("preflight reserves four bytes per full quantum");
224 debug_assert_eq!(written, emitted);
225 i += consumed;
226 o += written;
227 continue;
228 }
229 let retained = (input.len() - i).min(2);
230 self.tail[..retained].copy_from_slice(&input[i..i + retained]);
231 self.tail_len = retained;
232 i += retained;
233 }
234 Ok(TranscodeProgress::complete(i - input_index, o - output_index))
235 }
236
237 fn finish(&mut self, output: &mut [u8], output_index: usize) -> Result<usize, Self::Error> {
238 TranscodeFailure::ensure_output_index(output.len(), output_index)?;
239 self.phase.ensure_finish()?;
240 let needed = if self.tail_len == 0 {
241 0
242 } else if self.config.padded() {
243 4
244 } else {
245 self.tail_len + 1
246 };
247 TranscodeFailure::ensure_output_capacity(output.len(), output_index, needed)?;
248 let written = self
249 .config
250 .engine()
251 .encode_slice(
252 &self.tail[..self.tail_len],
253 &mut output[output_index..output_index + needed],
254 )
255 .expect("preflight reserves the exact final encoded length");
256 self.tail = [0; 2];
257 self.tail_len = 0;
258 self.phase = Base64StreamPhase::Finished;
259 Ok(written)
260 }
261}