Skip to main content

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}