base64_ng/buffers/encoded.rs
1use crate::{EncodeError, STANDARD, constant_time_eq_public_len, wipe_bytes, wipe_tail};
2
3/// Stack-backed encoded Base64 output.
4///
5/// This type is intended for short values where heap allocation would be
6/// unnecessary but manually sizing and passing a separate output slice is
7/// noisy. Its visible bytes are produced by crate encoders, so [`Self::as_str`]
8/// can return `&str` without exposing a fallible UTF-8 conversion to callers.
9/// [`core::fmt::Display`] intentionally writes the full encoded text; use
10/// `SecretBuffer` for encoded secrets that may reach logs or error messages.
11///
12/// The backing array is cleared when the value is dropped. This is best-effort
13/// data-retention reduction and is not a formal zeroization guarantee.
14///
15/// On `wasm32` targets, the wipe barrier uses only a compiler fence. The wasm
16/// runtime JIT may still optimize or retain cleared bytes in ways this crate
17/// cannot control. `wasm32` builds fail closed by default; enable
18/// `allow-wasm32-best-effort-wipe` only when the deployment explicitly accepts
19/// this limitation and applies its own memory strategy around stack-backed
20/// buffers.
21pub struct EncodedBuffer<const CAP: usize> {
22 bytes: [u8; CAP],
23 len: usize,
24}
25
26/// Owned stack array extracted from [`EncodedBuffer`].
27///
28/// This wrapper keeps the extracted encoded bytes on the crate's best-effort
29/// drop-time cleanup path. Use
30/// [`Self::into_exposed_unprotected_array_caller_must_zeroize`] only when a
31/// bare array is unavoidable and the caller will handle cleanup.
32pub struct ExposedEncodedArray<const CAP: usize> {
33 bytes: [u8; CAP],
34 len: usize,
35}
36
37impl<const CAP: usize> ExposedEncodedArray<CAP> {
38 /// Wraps an encoded backing array and visible length.
39 ///
40 /// # Panics
41 ///
42 /// Panics if `len` is greater than `CAP`.
43 #[must_use]
44 pub const fn from_array(bytes: [u8; CAP], len: usize) -> Self {
45 assert!(len <= CAP, "visible length exceeds array capacity");
46 Self { bytes, len }
47 }
48
49 /// Returns the visible encoded bytes.
50 #[must_use]
51 pub fn as_bytes(&self) -> &[u8] {
52 &self.bytes[..self.len]
53 }
54
55 /// Returns the number of visible encoded bytes.
56 #[must_use]
57 pub const fn len(&self) -> usize {
58 self.len
59 }
60
61 /// Returns whether there are no visible encoded bytes.
62 #[must_use]
63 pub const fn is_empty(&self) -> bool {
64 self.len == 0
65 }
66
67 /// Returns the backing array capacity.
68 #[must_use]
69 pub const fn capacity(&self) -> usize {
70 CAP
71 }
72
73 /// Consumes the wrapper and returns a bare array plus visible length.
74 ///
75 /// This is an unprotected escape hatch. The returned array will not be
76 /// cleared by this crate on drop. Callers must clear it with their own
77 /// approved zeroization policy.
78 ///
79 /// # Security
80 ///
81 /// Treat this as a cleanup-boundary API. Failing to clear the returned
82 /// array leaves the encoded bytes in ordinary caller-owned memory until
83 /// overwritten by later stack or heap activity.
84 #[must_use = "caller must zeroize the returned array"]
85 pub fn into_exposed_unprotected_array_caller_must_zeroize(mut self) -> ([u8; CAP], usize) {
86 let len = self.len;
87 self.len = 0;
88 (core::mem::replace(&mut self.bytes, [0u8; CAP]), len)
89 }
90}
91
92impl<const CAP: usize> Drop for ExposedEncodedArray<CAP> {
93 fn drop(&mut self) {
94 wipe_bytes(&mut self.bytes);
95 self.len = 0;
96 }
97}
98
99impl<const CAP: usize> core::fmt::Debug for ExposedEncodedArray<CAP> {
100 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
101 formatter
102 .debug_struct("ExposedEncodedArray")
103 .field("bytes", &"<redacted>")
104 .field("len", &self.len)
105 .field("capacity", &CAP)
106 .finish()
107 }
108}
109
110impl<const CAP: usize> EncodedBuffer<CAP> {
111 /// Creates an empty encoded buffer.
112 #[must_use]
113 pub const fn new() -> Self {
114 Self {
115 bytes: [0u8; CAP],
116 len: 0,
117 }
118 }
119
120 /// Returns the full backing array as an output slice for crate-internal
121 /// encode paths.
122 pub(crate) fn as_mut_capacity(&mut self) -> &mut [u8] {
123 &mut self.bytes
124 }
125
126 /// Sets the visible length after a crate-internal encode path succeeds.
127 pub(crate) fn set_filled(&mut self, written: usize) -> Result<(), EncodeError> {
128 debug_assert!(
129 written <= CAP,
130 "encoder wrote past stack-backed buffer capacity"
131 );
132 if written > CAP {
133 self.clear();
134 return Err(EncodeError::OutputTooSmall {
135 required: written,
136 available: CAP,
137 });
138 }
139 self.len = written;
140 Ok(())
141 }
142
143 /// Returns the number of visible encoded bytes.
144 #[must_use]
145 pub const fn len(&self) -> usize {
146 self.len
147 }
148
149 /// Returns whether the buffer has no visible encoded bytes.
150 #[must_use]
151 pub const fn is_empty(&self) -> bool {
152 self.len == 0
153 }
154
155 /// Returns whether the visible encoded bytes fill the stack backing array.
156 #[must_use]
157 pub const fn is_full(&self) -> bool {
158 self.len == CAP
159 }
160
161 /// Returns the stack capacity in bytes.
162 #[must_use]
163 pub const fn capacity(&self) -> usize {
164 CAP
165 }
166
167 /// Returns the number of unused bytes in the stack backing array.
168 #[must_use]
169 pub const fn remaining_capacity(&self) -> usize {
170 CAP - self.len
171 }
172
173 /// Returns the visible encoded bytes.
174 #[must_use]
175 pub fn as_bytes(&self) -> &[u8] {
176 &self.bytes[..self.len]
177 }
178
179 /// Returns the visible encoded bytes as UTF-8 text.
180 ///
181 /// Encoded Base64 output is produced as ASCII by this crate, so this
182 /// method should not fail unless an internal invariant has been broken.
183 /// It is provided for callers that prefer a fallible accessor over
184 /// [`Self::as_str`].
185 pub fn as_utf8(&self) -> Result<&str, core::str::Utf8Error> {
186 core::str::from_utf8(self.as_bytes())
187 }
188
189 /// Returns the visible encoded bytes as UTF-8.
190 ///
191 /// # Panics
192 ///
193 /// Panics only if the crate's internal invariant is broken and the buffer
194 /// contains non-UTF-8 bytes.
195 #[must_use]
196 pub fn as_str(&self) -> &str {
197 match self.as_utf8() {
198 Ok(output) => output,
199 Err(_) => unreachable!("base64 encoder produced non-UTF-8 output"),
200 }
201 }
202
203 /// Compares this encoded output to `other` without short-circuiting on the
204 /// first differing byte.
205 ///
206 /// Length and the final equality result remain public. Different lengths
207 /// return `false` immediately; use this helper only when the compared
208 /// lengths are public protocol facts or have been normalized by the
209 /// caller. For equal-length inputs, this helper scans every byte before
210 /// returning. It is constant-time-oriented best effort, not a formal
211 /// cryptographic constant-time guarantee. This comparison is deliberately
212 /// explicit: redacted buffer types do not implement [`PartialEq`] because
213 /// `==` would make a best-effort helper look like a formal token/MAC
214 /// comparison primitive.
215 ///
216 /// Do not use this helper as the sole MAC, bearer-token, password-hash, or
217 /// authentication-secret comparison primitive in high-assurance systems.
218 /// Applications that can admit dependencies should use a reviewed
219 /// constant-time comparison primitive, such as `subtle`, at the protocol
220 /// boundary.
221 #[doc(alias = "constant_time_eq")]
222 #[must_use]
223 pub fn constant_time_eq_public_len(&self, other: &[u8]) -> bool {
224 constant_time_eq_public_len(self.as_bytes(), other)
225 }
226
227 /// Consumes the wrapper and returns the backing array plus visible length
228 /// inside a drop-wiping exposed wrapper.
229 ///
230 /// This is an explicit escape hatch for no-alloc interop with APIs that
231 /// require ownership of a fixed array. The returned
232 /// [`ExposedEncodedArray`] remains redacted by formatting and clears its
233 /// backing array on drop.
234 #[must_use]
235 pub fn into_exposed_array(mut self) -> ExposedEncodedArray<CAP> {
236 let len = self.len;
237 self.len = 0;
238 ExposedEncodedArray::from_array(core::mem::replace(&mut self.bytes, [0u8; CAP]), len)
239 }
240
241 /// Clears the visible bytes and the full backing array.
242 pub fn clear(&mut self) {
243 wipe_bytes(&mut self.bytes);
244 self.len = 0;
245 }
246
247 /// Clears bytes after the visible prefix.
248 pub fn clear_tail(&mut self) {
249 wipe_tail(&mut self.bytes, self.len);
250 }
251}
252
253impl<const CAP: usize> AsRef<[u8]> for EncodedBuffer<CAP> {
254 fn as_ref(&self) -> &[u8] {
255 self.as_bytes()
256 }
257}
258
259impl<const CAP: usize> Clone for EncodedBuffer<CAP> {
260 /// Clones the visible encoded bytes into a second stack-backed buffer.
261 ///
262 /// Security note: cloning duplicates the visible bytes in memory. Both the
263 /// original and the clone must be dropped or explicitly cleared before the
264 /// duplicated bytes are gone on the crate's best-effort cleanup path. The
265 /// compiler may also create temporary stack copies while performing the
266 /// copy; those intermediates are outside this crate's cleanup boundary.
267 /// Avoid cloning encoded secret material; use `SecretBuffer` when redacted
268 /// formatting and heap-owned secret handling are required.
269 fn clone(&self) -> Self {
270 let mut output = Self::new();
271 output.bytes[..self.len].copy_from_slice(self.as_bytes());
272 output.len = self.len;
273 output
274 }
275}
276
277impl<const CAP: usize> core::fmt::Debug for EncodedBuffer<CAP> {
278 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
279 formatter
280 .debug_struct("EncodedBuffer")
281 .field("bytes", &"<redacted>")
282 .field("len", &self.len)
283 .field("capacity", &CAP)
284 .finish()
285 }
286}
287
288impl<const CAP: usize> core::fmt::Display for EncodedBuffer<CAP> {
289 /// Writes the full Base64 text.
290 ///
291 /// Security note: this is intentionally not redacted. Do not use
292 /// `EncodedBuffer` for encoded secrets that may reach logs or error
293 /// messages; use `SecretBuffer` for redacted formatting.
294 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
295 formatter.write_str(self.as_str())
296 }
297}
298
299impl<const CAP: usize> Default for EncodedBuffer<CAP> {
300 fn default() -> Self {
301 Self::new()
302 }
303}
304
305impl<const CAP: usize> Drop for EncodedBuffer<CAP> {
306 fn drop(&mut self) {
307 self.clear();
308 }
309}
310
311impl<const CAP: usize> TryFrom<&[u8]> for EncodedBuffer<CAP> {
312 type Error = EncodeError;
313
314 /// Encodes bytes into strict standard padded Base64 in a stack-backed
315 /// buffer.
316 ///
317 /// Use [`crate::Engine::encode_buffer`] or [`crate::Profile::encode_buffer`] when a
318 /// different alphabet, padding mode, or line-wrapping profile is required.
319 fn try_from(input: &[u8]) -> Result<Self, Self::Error> {
320 STANDARD.encode_buffer(input)
321 }
322}
323
324impl<const CAP: usize, const N: usize> TryFrom<&[u8; N]> for EncodedBuffer<CAP> {
325 type Error = EncodeError;
326
327 /// Encodes a byte array into strict standard padded Base64 in a
328 /// stack-backed buffer.
329 ///
330 /// Use [`crate::Engine::encode_buffer`] or [`crate::Profile::encode_buffer`] when a
331 /// different alphabet, padding mode, or line-wrapping profile is required.
332 fn try_from(input: &[u8; N]) -> Result<Self, Self::Error> {
333 Self::try_from(&input[..])
334 }
335}
336
337impl<const CAP: usize> TryFrom<&str> for EncodedBuffer<CAP> {
338 type Error = EncodeError;
339
340 /// Encodes UTF-8 text bytes into strict standard padded Base64 in a
341 /// stack-backed buffer.
342 ///
343 /// This treats the string as raw input bytes. Use
344 /// [`crate::Engine::encode_buffer`] or [`crate::Profile::encode_buffer`] when a
345 /// different alphabet, padding mode, or line-wrapping profile is required.
346 fn try_from(input: &str) -> Result<Self, Self::Error> {
347 Self::try_from(input.as_bytes())
348 }
349}