qubit_codec_misc/base64_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 byte codec.
9
10use ::base64::Engine;
11use qubit_codec::ValueDecoder;
12use qubit_codec::ValueEncoder;
13
14use crate::Base64Decoder;
15use crate::Base64Encoder;
16use crate::MiscCodecError;
17use crate::MiscCodecResult;
18use crate::internal::Base64Config;
19
20/// Encodes and decodes Base64 byte strings.
21///
22/// This facade intentionally remains a whole-value codec backed by the
23/// `base64` crate. Final partial quantum handling and optional `=` padding are
24/// facade/transcoder responsibilities, not part of the low-level quantum codec.
25/// Decoding is strict: each constructor accepts only its configured alphabet
26/// and padding policy, rejects whitespace and non-canonical trailing bits, and
27/// does not permit padded and unpadded partial groups to be interchanged.
28/// Complete three-byte groups such as `TWFu` need no `=` and are accepted by
29/// both padding policies.
30///
31/// # Examples
32///
33/// ```
34/// use qubit_codec_misc::Base64Codec;
35///
36/// let codec = Base64Codec::standard();
37/// assert_eq!(codec.encode(b"hello"), "aGVsbG8=");
38/// ```
39#[must_use]
40#[derive(Debug, Clone, Copy)]
41pub struct Base64Codec {
42 /// Configuration that defines this codec's alphabet and padding policy.
43 config: Base64Config,
44}
45
46impl PartialEq for Base64Codec {
47 #[inline]
48 fn eq(&self, other: &Self) -> bool {
49 self.config == other.config
50 }
51}
52
53impl Eq for Base64Codec {}
54
55impl Base64Codec {
56 /// Creates a standard Base64 codec that requires canonical padding for a
57 /// partial final group. Complete groups need no `=`.
58 ///
59 /// # Returns
60 /// Standard Base64 codec.
61 #[inline]
62 pub fn standard() -> Self {
63 Self {
64 config: Base64Config::Standard,
65 }
66 }
67
68 /// Creates a standard Base64 codec that rejects padding and accepts
69 /// unpadded partial final groups.
70 ///
71 /// # Returns
72 /// Standard no-padding Base64 codec.
73 #[inline]
74 pub fn standard_no_pad() -> Self {
75 Self {
76 config: Base64Config::StandardNoPad,
77 }
78 }
79
80 /// Creates a URL-safe Base64 codec that requires canonical padding for a
81 /// partial final group. Complete groups need no `=`.
82 ///
83 /// # Returns
84 /// URL-safe Base64 codec.
85 #[inline]
86 pub fn url_safe() -> Self {
87 Self {
88 config: Base64Config::UrlSafe,
89 }
90 }
91
92 /// Creates a URL-safe Base64 codec that rejects padding and accepts
93 /// unpadded partial final groups.
94 ///
95 /// # Returns
96 /// URL-safe no-padding Base64 codec.
97 #[inline]
98 pub fn url_safe_no_pad() -> Self {
99 Self {
100 config: Base64Config::UrlSafeNoPad,
101 }
102 }
103
104 /// Encodes bytes into Base64 text.
105 ///
106 /// # Parameters
107 /// - `bytes`: Bytes to encode.
108 ///
109 /// # Returns
110 /// Encoded Base64 text.
111 #[inline]
112 #[must_use]
113 pub fn encode(&self, bytes: &[u8]) -> String {
114 self.config.engine().encode(bytes)
115 }
116
117 /// Decodes Base64 text into bytes.
118 ///
119 /// # Parameters
120 /// - `text`: Base64 text.
121 ///
122 /// # Returns
123 /// Decoded bytes.
124 ///
125 /// # Errors
126 /// Returns [`MiscCodecError::InvalidBase64`] when `text` uses another
127 /// alphabet, violates this codec's padding policy, contains whitespace, or
128 /// has non-canonical trailing bits.
129 #[inline]
130 pub fn decode(&self, text: &str) -> MiscCodecResult<Vec<u8>> {
131 self.config.engine().decode(text).map_err(MiscCodecError::from)
132 }
133
134 /// Creates an independent encoder with this codec's configuration.
135 ///
136 /// The returned encoder does not borrow this codec, uses fixed-size state,
137 /// and must be reset before streaming.
138 ///
139 /// # Returns
140 /// An independent streaming encoder with the same alphabet and padding
141 /// policy.
142 #[inline]
143 pub fn encoder(&self) -> Base64Encoder {
144 Base64Encoder::from_config(self.config)
145 }
146
147 /// Creates an independent decoder with this codec's configuration.
148 ///
149 /// The returned decoder does not borrow this codec, uses fixed-size state,
150 /// and must be reset before streaming.
151 ///
152 /// # Returns
153 /// An independent streaming decoder with the same alphabet and padding
154 /// policy.
155 #[inline]
156 pub fn decoder(&self) -> Base64Decoder {
157 Base64Decoder::from_config(self.config)
158 }
159}
160
161impl Default for Base64Codec {
162 /// Creates a standard Base64 codec with padding.
163 #[inline]
164 fn default() -> Self {
165 Self::standard()
166 }
167}
168
169impl ValueEncoder<[u8]> for Base64Codec {
170 type Error = MiscCodecError;
171 type Output = String;
172
173 /// Encodes bytes into Base64 text without a fallible operation.
174 ///
175 /// # Parameters
176 /// - `input`: Bytes to encode.
177 ///
178 /// # Returns
179 /// Encoded Base64 text.
180 ///
181 /// # Errors
182 /// This implementation does not return an error.
183 #[inline]
184 fn encode(&mut self, input: &[u8]) -> Result<Self::Output, Self::Error> {
185 Ok(Base64Codec::encode(self, input))
186 }
187}
188
189impl ValueDecoder<str> for Base64Codec {
190 type Error = MiscCodecError;
191 type Output = Vec<u8>;
192
193 /// Decodes Base64 text into bytes using this codec's alphabet and padding
194 /// policy.
195 ///
196 /// # Parameters
197 /// - `input`: Base64 text to decode.
198 ///
199 /// # Returns
200 /// Decoded bytes.
201 ///
202 /// # Errors
203 /// Returns [`MiscCodecError::InvalidBase64`] when the input is malformed
204 /// or violates this codec's alphabet or padding policy.
205 #[inline]
206 fn decode(&mut self, input: &str) -> Result<Self::Output, Self::Error> {
207 Base64Codec::decode(self, input)
208 }
209}