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