Skip to main content

qubit_codec_misc/
base64_quantum_codec.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//! Base64 quantum codec.
9
10use core::num::NonZeroUsize;
11
12use base64::DecodeError;
13use qubit_codec::Codec;
14use qubit_codec::DecodeFailure;
15use qubit_utils::nonzero;
16
17use crate::MiscCodecError;
18use crate::MiscCodecResult;
19use crate::misc_error::map_misc_decode_failure_with_consumed;
20
21/// Encodes and decodes one complete Base64 quantum.
22///
23/// A quantum maps exactly three raw bytes to four Base64 units. It does not
24/// handle final short input groups or `=` padding; callers that process streams
25/// must finalize those cases in a transcoder or facade layer.
26///
27/// # Examples
28///
29/// ```
30/// use qubit_codec::Codec;
31/// use qubit_codec_misc::Base64QuantumCodec;
32///
33/// let mut codec = Base64QuantumCodec::standard();
34/// let mut encoded = [0; 4];
35/// // SAFETY: `encoded` has room for the four units written by one quantum.
36/// let written = unsafe { codec.encode(b"Man", &mut encoded, 0) }
37///     .expect("three bytes can be encoded");
38/// assert_eq!(written, 4);
39/// assert_eq!(&encoded, b"TWFu");
40/// ```
41#[must_use]
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct Base64QuantumCodec {
44    /// Selects the URL-safe alphabet when set; otherwise the standard alphabet
45    /// is used.
46    url_safe: bool,
47}
48
49impl Base64QuantumCodec {
50    /// Creates a standard-alphabet Base64 quantum codec.
51    ///
52    /// # Returns
53    /// Standard Base64 quantum codec.
54    #[inline]
55    pub fn standard() -> Self {
56        Self { url_safe: false }
57    }
58
59    /// Creates a URL-safe-alphabet Base64 quantum codec.
60    ///
61    /// # Returns
62    /// URL-safe Base64 quantum codec.
63    #[inline]
64    pub fn url_safe() -> Self {
65        Self { url_safe: true }
66    }
67
68    /// Selects the alphabet for this quantum codec.
69    ///
70    /// # Returns
71    /// The 64-byte alphabet used for encoding.
72    #[must_use]
73    #[inline]
74    fn alphabet(&self) -> &'static [u8; 64] {
75        if self.url_safe {
76            b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
77        } else {
78            b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
79        }
80    }
81
82    /// Converts one Base64 unit to a sextet.
83    ///
84    /// # Parameters
85    /// - `unit`: Encoded Base64 unit.
86    /// - `index`: Unit index in the original input.
87    ///
88    /// # Returns
89    /// Sextet value.
90    ///
91    /// # Errors
92    /// Returns [`MiscCodecError::InvalidBase64`] with an
93    /// [`base64::DecodeError::InvalidByte`] source when `unit` is not valid for
94    /// this quantum codec's alphabet. The index is relative to the original
95    /// input slice.
96    fn decode_unit(&self, unit: u8, index: usize) -> MiscCodecResult<u8> {
97        match unit {
98            b'A'..=b'Z' => Ok(unit - b'A'),
99            b'a'..=b'z' => Ok(unit - b'a' + 26),
100            b'0'..=b'9' => Ok(unit - b'0' + 52),
101            b'+' if !self.url_safe => Ok(62),
102            b'/' if !self.url_safe => Ok(63),
103            b'-' if self.url_safe => Ok(62),
104            b'_' if self.url_safe => Ok(63),
105            _ => Err(MiscCodecError::from(DecodeError::InvalidByte(index, unit))),
106        }
107    }
108}
109
110impl Default for Base64QuantumCodec {
111    /// Creates a standard-alphabet Base64 quantum codec.
112    ///
113    /// # Returns
114    /// A codec configured to use the standard Base64 alphabet.
115    #[inline]
116    fn default() -> Self {
117        Self::standard()
118    }
119}
120
121impl Codec for Base64QuantumCodec {
122    /// Raw value represented by one complete Base64 quantum.
123    type Value = [u8; 3];
124    /// Encoded unit used by the Base64 alphabet.
125    type Unit = u8;
126    /// Error reported when an encoded unit is outside the selected alphabet.
127    type DecodeError = MiscCodecError;
128    /// Error type exposed by the codec's encoding operation.
129    type EncodeError = MiscCodecError;
130
131    const MIN_UNITS_PER_VALUE: usize = 4;
132    const MAX_ENCODE_UNITS_PER_VALUE: usize = 4;
133
134    const MAX_DECODE_UNITS_PER_VALUE: usize = 4;
135
136    /// Decodes one complete four-unit Base64 quantum.
137    ///
138    /// # Parameters
139    /// - `input`: Encoded Base64 units containing the quantum.
140    /// - `input_index`: Index of the first unit in the quantum.
141    ///
142    /// # Returns
143    /// The decoded three-byte value and the number of consumed input units.
144    ///
145    /// # Errors
146    /// Returns a decode failure with [`MiscCodecError::InvalidBase64`] when any
147    /// unit is not valid for the selected alphabet. Its input index is relative
148    /// to `input`, and the failure reports that the complete four-unit quantum
149    /// was consumed.
150    ///
151    /// # Safety
152    /// The caller must ensure that four units starting at `input_index` are
153    /// readable in `input`.
154    unsafe fn decode(
155        &mut self,
156        input: &[u8],
157        input_index: usize,
158    ) -> Result<([u8; 3], NonZeroUsize), DecodeFailure<Self::DecodeError>> {
159        debug_assert!(input_index + 4 <= input.len());
160
161        let first = self
162            .decode_unit(input[input_index], input_index)
163            .map_err(|error| map_misc_decode_failure_with_consumed(error, nonzero(4)))?;
164        let second = self
165            .decode_unit(input[input_index + 1], input_index + 1)
166            .map_err(|error| map_misc_decode_failure_with_consumed(error, nonzero(4)))?;
167        let third = self
168            .decode_unit(input[input_index + 2], input_index + 2)
169            .map_err(|error| map_misc_decode_failure_with_consumed(error, nonzero(4)))?;
170        let fourth = self
171            .decode_unit(input[input_index + 3], input_index + 3)
172            .map_err(|error| map_misc_decode_failure_with_consumed(error, nonzero(4)))?;
173        Ok((
174            [
175                (first << 2) | (second >> 4),
176                (second << 4) | (third >> 2),
177                (third << 6) | fourth,
178            ],
179            nonzero(4),
180        ))
181    }
182
183    /// Encodes one complete three-byte Base64 quantum.
184    ///
185    /// # Parameters
186    /// - `value`: Three raw bytes to encode.
187    /// - `output`: Destination buffer for four encoded units.
188    /// - `output_index`: Index of the first destination unit to write.
189    ///
190    /// # Returns
191    /// The number of output units written, which is always four.
192    ///
193    /// # Errors
194    /// This implementation does not return an error.
195    ///
196    /// # Safety
197    /// The caller must ensure that four units starting at `output_index` are
198    /// writable in `output`.
199    unsafe fn encode(
200        &mut self,
201        value: &[u8; 3],
202        output: &mut [u8],
203        output_index: usize,
204    ) -> Result<usize, Self::EncodeError> {
205        debug_assert!(output_index + 4 <= output.len());
206
207        let alphabet = self.alphabet();
208        output[output_index] = alphabet[(value[0] >> 2) as usize];
209        output[output_index + 1] = alphabet[(((value[0] & 0x03) << 4) | (value[1] >> 4)) as usize];
210        output[output_index + 2] = alphabet[(((value[1] & 0x0f) << 2) | (value[2] >> 6)) as usize];
211        output[output_index + 3] = alphabet[(value[2] & 0x3f) as usize];
212        Ok(4)
213    }
214}