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