Skip to main content

base64_ng/
profiles.rs

1//! Named Base64 profiles with optional strict line wrapping.
2
3use crate::{
4    Alphabet, BCRYPT_NO_PAD, Bcrypt, CRYPT_NO_PAD, Crypt, DecodeError, DecodedBuffer, EncodeError,
5    EncodedBuffer, Engine, LineEnding, LineWrap, STANDARD, Standard, checked_encoded_len,
6    checked_wrapped_encoded_len, encoded_len, wrapped_encoded_len,
7};
8
9#[cfg(feature = "alloc")]
10use crate::SecretBuffer;
11
12/// A named Base64 profile with an engine and optional strict line wrapping.
13///
14/// Profiles are convenience values for protocol-shaped Base64. They keep the
15/// same strict alphabet, padding, canonical-bit, and output-buffer rules as
16/// [`Engine`], while carrying the wrapping policy for MIME/PEM-like formats.
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub struct Profile<A, const PAD: bool> {
19    engine: Engine<A, PAD>,
20    wrap: Option<LineWrap>,
21}
22
23impl<A, const PAD: bool> Profile<A, PAD>
24where
25    A: Alphabet,
26{
27    /// Creates a profile from an engine and optional strict line wrapping.
28    #[must_use]
29    pub const fn new(engine: Engine<A, PAD>, wrap: Option<LineWrap>) -> Self {
30        Self { engine, wrap }
31    }
32
33    /// Creates a profile, returning `None` when the wrapping policy is invalid.
34    ///
35    /// This is useful when a profile is assembled from configuration or other
36    /// untrusted metadata. Use [`Self::new`] for compile-time constants where
37    /// the wrapping policy is known to be valid.
38    #[must_use]
39    pub const fn checked_new(engine: Engine<A, PAD>, wrap: Option<LineWrap>) -> Option<Self> {
40        match wrap {
41            Some(wrap) if !wrap.is_valid() => None,
42            _ => Some(Self::new(engine, wrap)),
43        }
44    }
45
46    /// Returns whether this profile can be used by encoders and decoders.
47    #[must_use]
48    pub const fn is_valid(&self) -> bool {
49        match self.wrap {
50            Some(wrap) => wrap.is_valid(),
51            None => true,
52        }
53    }
54
55    /// Returns the underlying engine.
56    #[must_use]
57    pub const fn engine(&self) -> Engine<A, PAD> {
58        self.engine
59    }
60
61    /// Returns whether this profile uses padded Base64.
62    #[must_use]
63    pub const fn is_padded(&self) -> bool {
64        PAD
65    }
66
67    /// Returns whether this profile carries a strict line-wrapping policy.
68    #[must_use]
69    pub const fn is_wrapped(&self) -> bool {
70        self.wrap.is_some()
71    }
72
73    /// Returns the strict wrapping policy carried by this profile, if any.
74    #[must_use]
75    pub const fn line_wrap(&self) -> Option<LineWrap> {
76        self.wrap
77    }
78
79    /// Returns the encoded line length for wrapped profiles.
80    #[must_use]
81    pub const fn line_len(&self) -> Option<usize> {
82        match self.wrap {
83            Some(wrap) => Some(wrap.line_len()),
84            None => None,
85        }
86    }
87
88    /// Returns the line ending for wrapped profiles.
89    #[must_use]
90    pub const fn line_ending(&self) -> Option<LineEnding> {
91        match self.wrap {
92            Some(wrap) => Some(wrap.line_ending()),
93            None => None,
94        }
95    }
96
97    /// Returns the encoded length for this profile.
98    pub const fn encoded_len(&self, input_len: usize) -> Result<usize, EncodeError> {
99        match self.wrap {
100            Some(wrap) => wrapped_encoded_len(input_len, PAD, wrap),
101            None => encoded_len(input_len, PAD),
102        }
103    }
104
105    /// Returns the encoded length for this profile, or `None` on overflow or
106    /// invalid line wrapping.
107    #[must_use]
108    pub const fn checked_encoded_len(&self, input_len: usize) -> Option<usize> {
109        match self.wrap {
110            Some(wrap) => checked_wrapped_encoded_len(input_len, PAD, wrap),
111            None => checked_encoded_len(input_len, PAD),
112        }
113    }
114
115    /// Returns the exact decoded length for this profile.
116    pub fn decoded_len(&self, input: &[u8]) -> Result<usize, DecodeError> {
117        match self.wrap {
118            Some(wrap) => self.engine.decoded_len_wrapped(input, wrap),
119            None => self.engine.decoded_len(input),
120        }
121    }
122
123    /// Validates input according to this profile without writing decoded bytes.
124    pub fn validate_result(&self, input: &[u8]) -> Result<(), DecodeError> {
125        match self.wrap {
126            Some(wrap) => self.engine.validate_wrapped_result(input, wrap),
127            None => self.engine.validate_result(input),
128        }
129    }
130
131    /// Returns whether `input` is valid for this profile.
132    #[must_use]
133    pub fn validate(&self, input: &[u8]) -> bool {
134        self.validate_result(input).is_ok()
135    }
136
137    /// Encodes `input` into `output` according to this profile.
138    pub fn encode_slice(&self, input: &[u8], output: &mut [u8]) -> Result<usize, EncodeError> {
139        match self.wrap {
140            Some(wrap) => self.engine.encode_slice_wrapped(input, output, wrap),
141            None => self.engine.encode_slice(input, output),
142        }
143    }
144
145    /// Encodes `input` into `output` and clears all bytes after the encoded
146    /// prefix.
147    pub fn encode_slice_clear_tail(
148        &self,
149        input: &[u8],
150        output: &mut [u8],
151    ) -> Result<usize, EncodeError> {
152        match self.wrap {
153            Some(wrap) => self
154                .engine
155                .encode_slice_wrapped_clear_tail(input, output, wrap),
156            None => self.engine.encode_slice_clear_tail(input, output),
157        }
158    }
159
160    /// Encodes `input` into a stack-backed buffer.
161    ///
162    /// This is useful for short values where heap allocation is unnecessary.
163    /// If encoding fails, the internal backing array is cleared before the
164    /// error is returned.
165    pub fn encode_buffer<const CAP: usize>(
166        &self,
167        input: &[u8],
168    ) -> Result<EncodedBuffer<CAP>, EncodeError> {
169        let mut output = EncodedBuffer::new();
170        let written = match self.encode_slice_clear_tail(input, output.as_mut_capacity()) {
171            Ok(written) => written,
172            Err(err) => {
173                output.clear();
174                return Err(err);
175            }
176        };
177        output.set_filled(written)?;
178        Ok(output)
179    }
180
181    /// Decodes `input` into `output` according to this profile.
182    ///
183    /// # Security
184    ///
185    /// Profile decoders use the normal strict decode path. They may branch or
186    /// return early based on malformed input, padding position, wrapping, and
187    /// output capacity in order to return precise [`DecodeError`] diagnostics,
188    /// including exact invalid-byte values and positions.
189    /// Do not use this method for token comparison, key-material decoding, or
190    /// secret-bearing validation where malformed-input timing matters. Use
191    /// [`DecodeError::kind`] instead of logging full strict errors when input
192    /// may be secret-bearing or secret-adjacent. Use
193    /// [`crate::ct`] with a matching unwrapped engine for constant-time-oriented
194    /// secret decoding.
195    #[must_use = "handle decode errors; use crate::ct for secret-bearing payloads"]
196    pub fn decode_slice(&self, input: &[u8], output: &mut [u8]) -> Result<usize, DecodeError> {
197        match self.wrap {
198            Some(wrap) => self.engine.decode_slice_wrapped(input, output, wrap),
199            None => self.engine.decode_slice(input, output),
200        }
201    }
202
203    /// Decodes `input` into `output` and clears all bytes after the decoded
204    /// prefix.
205    pub fn decode_slice_clear_tail(
206        &self,
207        input: &[u8],
208        output: &mut [u8],
209    ) -> Result<usize, DecodeError> {
210        match self.wrap {
211            Some(wrap) => self
212                .engine
213                .decode_slice_wrapped_clear_tail(input, output, wrap),
214            None => self.engine.decode_slice_clear_tail(input, output),
215        }
216    }
217
218    /// Decodes `input` into a stack-backed buffer according to this profile.
219    ///
220    /// This is useful for short decoded values where heap allocation is
221    /// unnecessary. If decoding fails, the internal backing array is cleared
222    /// before the error is returned.
223    pub fn decode_buffer<const CAP: usize>(
224        &self,
225        input: &[u8],
226    ) -> Result<DecodedBuffer<CAP>, DecodeError> {
227        let mut output = DecodedBuffer::new();
228        let written = match self.decode_slice_clear_tail(input, output.as_mut_capacity()) {
229            Ok(written) => written,
230            Err(err) => {
231                output.clear();
232                return Err(err);
233            }
234        };
235        output.set_filled(written)?;
236        Ok(output)
237    }
238
239    /// Decodes `buffer` in place according to this profile.
240    ///
241    /// For wrapped profiles, configured line endings are compacted out before
242    /// decoding. If validation fails, the buffer contents are unspecified.
243    /// On success, bytes after the returned decoded prefix may retain compacted
244    /// encoded input. Use [`Self::decode_in_place_clear_tail`] when the buffer
245    /// may be reused or freed without a caller-managed wipe.
246    ///
247    /// # Security
248    ///
249    /// Profile in-place decoders use the normal strict decode path. They may
250    /// branch or return early based on malformed input, padding position,
251    /// wrapping, and output capacity in order to return precise
252    /// [`DecodeError`] diagnostics. Do not use this method for token
253    /// comparison, key-material decoding, or secret-bearing validation where
254    /// malformed-input timing matters. Use [`DecodeError::kind`] instead of
255    /// logging full strict errors when input may be secret-bearing or
256    /// secret-adjacent.
257    ///
258    /// # Examples
259    ///
260    /// ```
261    /// use base64_ng::{LineEnding, LineWrap, Profile, STANDARD};
262    ///
263    /// let profile = Profile::new(STANDARD, Some(LineWrap::new(4, LineEnding::Lf)));
264    /// let mut buffer = *b"aGVs\nbG8=";
265    /// let decoded = profile.decode_in_place(&mut buffer).unwrap();
266    ///
267    /// assert_eq!(decoded, b"hello");
268    /// ```
269    pub fn decode_in_place<'a>(&self, buffer: &'a mut [u8]) -> Result<&'a mut [u8], DecodeError> {
270        match self.wrap {
271            Some(wrap) => self.engine.decode_in_place_wrapped(buffer, wrap),
272            None => self.engine.decode_in_place(buffer),
273        }
274    }
275
276    /// Decodes `buffer` in place according to this profile and clears all
277    /// bytes after the decoded prefix.
278    ///
279    /// If validation or decoding fails, the entire buffer is cleared before the
280    /// error is returned.
281    ///
282    /// # Examples
283    ///
284    /// ```
285    /// use base64_ng::{LineEnding, LineWrap, Profile, STANDARD};
286    ///
287    /// let profile = Profile::new(STANDARD, Some(LineWrap::new(4, LineEnding::Lf)));
288    /// let mut buffer = *b"aGVs\nbG8=";
289    /// let len = profile.decode_in_place_clear_tail(&mut buffer).unwrap().len();
290    ///
291    /// assert_eq!(&buffer[..len], b"hello");
292    /// assert!(buffer[len..].iter().all(|byte| *byte == 0));
293    /// ```
294    pub fn decode_in_place_clear_tail<'a>(
295        &self,
296        buffer: &'a mut [u8],
297    ) -> Result<&'a mut [u8], DecodeError> {
298        match self.wrap {
299            Some(wrap) => self.engine.decode_in_place_wrapped_clear_tail(buffer, wrap),
300            None => self.engine.decode_in_place_clear_tail(buffer),
301        }
302    }
303
304    /// Encodes `input` into a newly allocated byte vector.
305    #[cfg(feature = "alloc")]
306    #[must_use = "for secret-bearing payloads use encode_secret, which returns a redacted buffer with drop-time cleanup"]
307    pub fn encode_vec(&self, input: &[u8]) -> Result<alloc::vec::Vec<u8>, EncodeError> {
308        match self.wrap {
309            Some(wrap) => self.engine.encode_wrapped_vec(input, wrap),
310            None => self.engine.encode_vec(input),
311        }
312    }
313
314    /// Encodes `input` into a newly allocated byte vector.
315    ///
316    /// This is a convenience wrapper around [`Self::encode_vec`] for ordinary
317    /// byte-to-Base64 paths where encoding failure would indicate an internal
318    /// length/allocation invariant failure rather than invalid input.
319    ///
320    /// Prefer [`Self::encode_vec`] when handling untrusted length metadata,
321    /// constrained allocation environments, or code paths that must return a
322    /// recoverable error instead of panicking.
323    ///
324    /// # Panics
325    ///
326    /// Panics if [`Self::encode_vec`] returns an error. This includes encoded
327    /// length overflow; on 32-bit targets, inputs larger than roughly 1.5 GiB
328    /// can overflow the encoded length. For attacker-controlled or externally
329    /// sized buffers, use [`Self::encode_vec`], which returns a recoverable
330    /// [`EncodeError::LengthOverflow`].
331    #[cfg(feature = "alloc")]
332    #[must_use]
333    pub fn encode_vec_infallible(&self, input: &[u8]) -> alloc::vec::Vec<u8> {
334        self.encode_vec(input)
335            .expect("base64-ng profile encode_vec failed for byte input")
336    }
337
338    /// Encodes `input` into a redacted owned secret buffer.
339    #[cfg(feature = "alloc")]
340    pub fn encode_secret(&self, input: &[u8]) -> Result<SecretBuffer, EncodeError> {
341        self.encode_vec(input).map(SecretBuffer::from_vec)
342    }
343
344    /// Encodes `input` into a newly allocated UTF-8 string.
345    #[cfg(feature = "alloc")]
346    pub fn encode_string(&self, input: &[u8]) -> Result<alloc::string::String, EncodeError> {
347        match self.wrap {
348            Some(wrap) => self.engine.encode_wrapped_string(input, wrap),
349            None => self.engine.encode_string(input),
350        }
351    }
352
353    /// Encodes `input` into a newly allocated UTF-8 string.
354    ///
355    /// This is a convenience wrapper around [`Self::encode_string`] for
356    /// ordinary byte-to-Base64 paths where encoding failure would indicate an
357    /// internal length/allocation invariant failure rather than invalid input.
358    ///
359    /// Prefer [`Self::encode_string`] when handling untrusted length metadata,
360    /// constrained allocation environments, or code paths that must return a
361    /// recoverable error instead of panicking.
362    ///
363    /// # Panics
364    ///
365    /// Panics if [`Self::encode_string`] returns an error. This includes
366    /// encoded length overflow; on 32-bit targets, inputs larger than roughly
367    /// 1.5 GiB can overflow the encoded length. For attacker-controlled or
368    /// externally sized buffers, use [`Self::encode_string`], which returns a
369    /// recoverable [`EncodeError::LengthOverflow`].
370    #[cfg(feature = "alloc")]
371    #[must_use]
372    pub fn encode_string_infallible(&self, input: &[u8]) -> alloc::string::String {
373        self.encode_string(input)
374            .expect("base64-ng profile encode_string failed for byte input")
375    }
376
377    /// Decodes `input` into a newly allocated byte vector.
378    #[cfg(feature = "alloc")]
379    #[must_use = "for secret-bearing payloads use decode_secret, which returns a redacted buffer with drop-time cleanup"]
380    pub fn decode_vec(&self, input: &[u8]) -> Result<alloc::vec::Vec<u8>, DecodeError> {
381        match self.wrap {
382            Some(wrap) => self.engine.decode_wrapped_vec(input, wrap),
383            None => self.engine.decode_vec(input),
384        }
385    }
386
387    /// Decodes `input` into a redacted owned secret buffer.
388    ///
389    /// # Security
390    ///
391    /// This uses the profile's normal strict decoder, not the
392    /// constant-time-oriented [`crate::ct`] module. It may branch or return
393    /// early on malformed input and reports localized decode errors. For
394    /// secret-bearing payloads where malformed-input timing matters, use the
395    /// matching [`crate::ct::CtEngine`] explicitly and wrap successful output in
396    /// [`SecretBuffer`].
397    #[cfg(feature = "alloc")]
398    pub fn decode_secret(&self, input: &[u8]) -> Result<SecretBuffer, DecodeError> {
399        self.decode_vec(input).map(SecretBuffer::from_vec)
400    }
401}
402
403impl<A, const PAD: bool> Default for Profile<A, PAD>
404where
405    A: Alphabet,
406{
407    fn default() -> Self {
408        Self::new(Engine::new(), None)
409    }
410}
411
412impl<A, const PAD: bool> core::fmt::Display for Profile<A, PAD>
413where
414    A: Alphabet,
415{
416    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
417        match self.wrap {
418            Some(wrap) => write!(formatter, "padded={PAD} wrap={wrap}"),
419            None => write!(formatter, "padded={PAD} wrap=none"),
420        }
421    }
422}
423
424impl<A, const PAD: bool> From<Engine<A, PAD>> for Profile<A, PAD>
425where
426    A: Alphabet,
427{
428    fn from(engine: Engine<A, PAD>) -> Self {
429        Self::new(engine, None)
430    }
431}
432
433/// MIME Base64 profile: standard alphabet, padding, 76-column CRLF wrapping.
434///
435/// This profile uses the default strict decoder and is not a constant-time
436/// token validator or key-material decoder. Use
437/// [`ct::STANDARD`](crate::ct::STANDARD) with an application-level wrapping
438/// policy for sensitive fixed-shape protocols.
439#[doc(alias = "ct")]
440#[doc(alias = "constant_time")]
441#[doc(alias = "sensitive")]
442pub const MIME: Profile<Standard, true> = Profile::new(STANDARD, Some(LineWrap::MIME));
443
444/// PEM Base64 profile: standard alphabet, padding, 64-column LF wrapping.
445///
446/// This profile uses the default strict decoder and is not a constant-time
447/// token validator or key-material decoder. Use
448/// [`ct::STANDARD`](crate::ct::STANDARD) with an application-level wrapping
449/// policy for sensitive fixed-shape protocols.
450#[doc(alias = "ct")]
451#[doc(alias = "constant_time")]
452#[doc(alias = "sensitive")]
453pub const PEM: Profile<Standard, true> = Profile::new(STANDARD, Some(LineWrap::PEM));
454
455/// PEM Base64 profile with CRLF line endings.
456///
457/// This profile uses the default strict decoder and is not a constant-time
458/// token validator or key-material decoder. Use
459/// [`ct::STANDARD`](crate::ct::STANDARD) with an application-level wrapping
460/// policy for sensitive fixed-shape protocols.
461#[doc(alias = "ct")]
462#[doc(alias = "constant_time")]
463#[doc(alias = "sensitive")]
464pub const PEM_CRLF: Profile<Standard, true> = Profile::new(STANDARD, Some(LineWrap::PEM_CRLF));
465
466/// bcrypt-style no-padding Base64 profile.
467///
468/// This profile carries the bcrypt alphabet and no padding. It does not parse
469/// complete bcrypt password-hash strings. Its default strict decoder is not a
470/// constant-time token validator or key-material decoder; use
471/// [`Profile::engine`] with [`Engine::ct_decoder`] for the matching
472/// constant-time-oriented decoder when timing posture matters.
473#[doc(alias = "ct")]
474#[doc(alias = "constant_time")]
475#[doc(alias = "sensitive")]
476pub const BCRYPT: Profile<Bcrypt, false> = Profile::new(BCRYPT_NO_PAD, None);
477
478/// Unix `crypt(3)`-style no-padding Base64 profile.
479///
480/// This profile carries the `crypt(3)` alphabet and no padding. It does not
481/// parse complete password-hash strings. Its default strict decoder is not a
482/// constant-time token validator or key-material decoder; use
483/// [`Profile::engine`] with [`Engine::ct_decoder`] for the matching
484/// constant-time-oriented decoder when timing posture matters.
485#[doc(alias = "ct")]
486#[doc(alias = "constant_time")]
487#[doc(alias = "sensitive")]
488pub const CRYPT: Profile<Crypt, false> = Profile::new(CRYPT_NO_PAD, None);