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    /// [`crate::ct`] with a matching unwrapped engine for constant-time-oriented
192    /// secret decoding.
193    #[must_use = "handle decode errors; use crate::ct for secret-bearing payloads"]
194    pub fn decode_slice(&self, input: &[u8], output: &mut [u8]) -> Result<usize, DecodeError> {
195        match self.wrap {
196            Some(wrap) => self.engine.decode_slice_wrapped(input, output, wrap),
197            None => self.engine.decode_slice(input, output),
198        }
199    }
200
201    /// Decodes `input` into `output` and clears all bytes after the decoded
202    /// prefix.
203    pub fn decode_slice_clear_tail(
204        &self,
205        input: &[u8],
206        output: &mut [u8],
207    ) -> Result<usize, DecodeError> {
208        match self.wrap {
209            Some(wrap) => self
210                .engine
211                .decode_slice_wrapped_clear_tail(input, output, wrap),
212            None => self.engine.decode_slice_clear_tail(input, output),
213        }
214    }
215
216    /// Decodes `input` into a stack-backed buffer according to this profile.
217    ///
218    /// This is useful for short decoded values where heap allocation is
219    /// unnecessary. If decoding fails, the internal backing array is cleared
220    /// before the error is returned.
221    pub fn decode_buffer<const CAP: usize>(
222        &self,
223        input: &[u8],
224    ) -> Result<DecodedBuffer<CAP>, DecodeError> {
225        let mut output = DecodedBuffer::new();
226        let written = match self.decode_slice_clear_tail(input, output.as_mut_capacity()) {
227            Ok(written) => written,
228            Err(err) => {
229                output.clear();
230                return Err(err);
231            }
232        };
233        output.set_filled(written)?;
234        Ok(output)
235    }
236
237    /// Decodes `buffer` in place according to this profile.
238    ///
239    /// For wrapped profiles, configured line endings are compacted out before
240    /// decoding. If validation fails, the buffer contents are unspecified.
241    /// On success, bytes after the returned decoded prefix may retain compacted
242    /// encoded input. Use [`Self::decode_in_place_clear_tail`] when the buffer
243    /// may be reused or freed without a caller-managed wipe.
244    ///
245    /// # Security
246    ///
247    /// Profile in-place decoders use the normal strict decode path. They may
248    /// branch or return early based on malformed input, padding position,
249    /// wrapping, and output capacity in order to return precise
250    /// [`DecodeError`] diagnostics. Do not use this method for token
251    /// comparison, key-material decoding, or secret-bearing validation where
252    /// malformed-input timing matters.
253    ///
254    /// # Examples
255    ///
256    /// ```
257    /// use base64_ng::{LineEnding, LineWrap, Profile, STANDARD};
258    ///
259    /// let profile = Profile::new(STANDARD, Some(LineWrap::new(4, LineEnding::Lf)));
260    /// let mut buffer = *b"aGVs\nbG8=";
261    /// let decoded = profile.decode_in_place(&mut buffer).unwrap();
262    ///
263    /// assert_eq!(decoded, b"hello");
264    /// ```
265    pub fn decode_in_place<'a>(&self, buffer: &'a mut [u8]) -> Result<&'a mut [u8], DecodeError> {
266        match self.wrap {
267            Some(wrap) => self.engine.decode_in_place_wrapped(buffer, wrap),
268            None => self.engine.decode_in_place(buffer),
269        }
270    }
271
272    /// Decodes `buffer` in place according to this profile and clears all
273    /// bytes after the decoded prefix.
274    ///
275    /// If validation or decoding fails, the entire buffer is cleared before the
276    /// error is returned.
277    ///
278    /// # Examples
279    ///
280    /// ```
281    /// use base64_ng::{LineEnding, LineWrap, Profile, STANDARD};
282    ///
283    /// let profile = Profile::new(STANDARD, Some(LineWrap::new(4, LineEnding::Lf)));
284    /// let mut buffer = *b"aGVs\nbG8=";
285    /// let len = profile.decode_in_place_clear_tail(&mut buffer).unwrap().len();
286    ///
287    /// assert_eq!(&buffer[..len], b"hello");
288    /// assert!(buffer[len..].iter().all(|byte| *byte == 0));
289    /// ```
290    pub fn decode_in_place_clear_tail<'a>(
291        &self,
292        buffer: &'a mut [u8],
293    ) -> Result<&'a mut [u8], DecodeError> {
294        match self.wrap {
295            Some(wrap) => self.engine.decode_in_place_wrapped_clear_tail(buffer, wrap),
296            None => self.engine.decode_in_place_clear_tail(buffer),
297        }
298    }
299
300    /// Encodes `input` into a newly allocated byte vector.
301    #[cfg(feature = "alloc")]
302    #[must_use = "for secret-bearing payloads use encode_secret, which returns a redacted buffer with drop-time cleanup"]
303    pub fn encode_vec(&self, input: &[u8]) -> Result<alloc::vec::Vec<u8>, EncodeError> {
304        match self.wrap {
305            Some(wrap) => self.engine.encode_wrapped_vec(input, wrap),
306            None => self.engine.encode_vec(input),
307        }
308    }
309
310    /// Encodes `input` into a redacted owned secret buffer.
311    #[cfg(feature = "alloc")]
312    pub fn encode_secret(&self, input: &[u8]) -> Result<SecretBuffer, EncodeError> {
313        self.encode_vec(input).map(SecretBuffer::from_vec)
314    }
315
316    /// Encodes `input` into a newly allocated UTF-8 string.
317    #[cfg(feature = "alloc")]
318    pub fn encode_string(&self, input: &[u8]) -> Result<alloc::string::String, EncodeError> {
319        match self.wrap {
320            Some(wrap) => self.engine.encode_wrapped_string(input, wrap),
321            None => self.engine.encode_string(input),
322        }
323    }
324
325    /// Decodes `input` into a newly allocated byte vector.
326    #[cfg(feature = "alloc")]
327    #[must_use = "for secret-bearing payloads use decode_secret, which returns a redacted buffer with drop-time cleanup"]
328    pub fn decode_vec(&self, input: &[u8]) -> Result<alloc::vec::Vec<u8>, DecodeError> {
329        match self.wrap {
330            Some(wrap) => self.engine.decode_wrapped_vec(input, wrap),
331            None => self.engine.decode_vec(input),
332        }
333    }
334
335    /// Decodes `input` into a redacted owned secret buffer.
336    ///
337    /// # Security
338    ///
339    /// This uses the profile's normal strict decoder, not the
340    /// constant-time-oriented [`crate::ct`] module. It may branch or return
341    /// early on malformed input and reports localized decode errors. For
342    /// secret-bearing payloads where malformed-input timing matters, use the
343    /// matching [`crate::ct::CtEngine`] explicitly and wrap successful output in
344    /// [`SecretBuffer`].
345    #[cfg(feature = "alloc")]
346    pub fn decode_secret(&self, input: &[u8]) -> Result<SecretBuffer, DecodeError> {
347        self.decode_vec(input).map(SecretBuffer::from_vec)
348    }
349}
350
351impl<A, const PAD: bool> Default for Profile<A, PAD>
352where
353    A: Alphabet,
354{
355    fn default() -> Self {
356        Self::new(Engine::new(), None)
357    }
358}
359
360impl<A, const PAD: bool> core::fmt::Display for Profile<A, PAD>
361where
362    A: Alphabet,
363{
364    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
365        match self.wrap {
366            Some(wrap) => write!(formatter, "padded={PAD} wrap={wrap}"),
367            None => write!(formatter, "padded={PAD} wrap=none"),
368        }
369    }
370}
371
372impl<A, const PAD: bool> From<Engine<A, PAD>> for Profile<A, PAD>
373where
374    A: Alphabet,
375{
376    fn from(engine: Engine<A, PAD>) -> Self {
377        Self::new(engine, None)
378    }
379}
380
381/// MIME Base64 profile: standard alphabet, padding, 76-column CRLF wrapping.
382///
383/// This profile uses the default strict decoder and is not a constant-time
384/// token validator or key-material decoder. Use
385/// [`ct::STANDARD`](crate::ct::STANDARD) with an application-level wrapping
386/// policy for sensitive fixed-shape protocols.
387#[doc(alias = "ct")]
388#[doc(alias = "constant_time")]
389#[doc(alias = "sensitive")]
390pub const MIME: Profile<Standard, true> = Profile::new(STANDARD, Some(LineWrap::MIME));
391
392/// PEM Base64 profile: standard alphabet, padding, 64-column LF wrapping.
393///
394/// This profile uses the default strict decoder and is not a constant-time
395/// token validator or key-material decoder. Use
396/// [`ct::STANDARD`](crate::ct::STANDARD) with an application-level wrapping
397/// policy for sensitive fixed-shape protocols.
398#[doc(alias = "ct")]
399#[doc(alias = "constant_time")]
400#[doc(alias = "sensitive")]
401pub const PEM: Profile<Standard, true> = Profile::new(STANDARD, Some(LineWrap::PEM));
402
403/// PEM Base64 profile with CRLF line endings.
404///
405/// This profile uses the default strict decoder and is not a constant-time
406/// token validator or key-material decoder. Use
407/// [`ct::STANDARD`](crate::ct::STANDARD) with an application-level wrapping
408/// policy for sensitive fixed-shape protocols.
409#[doc(alias = "ct")]
410#[doc(alias = "constant_time")]
411#[doc(alias = "sensitive")]
412pub const PEM_CRLF: Profile<Standard, true> = Profile::new(STANDARD, Some(LineWrap::PEM_CRLF));
413
414/// bcrypt-style no-padding Base64 profile.
415///
416/// This profile carries the bcrypt alphabet and no padding. It does not parse
417/// complete bcrypt password-hash strings. Its default strict decoder is not a
418/// constant-time token validator or key-material decoder; use
419/// [`Profile::engine`] with [`Engine::ct_decoder`] for the matching
420/// constant-time-oriented decoder when timing posture matters.
421#[doc(alias = "ct")]
422#[doc(alias = "constant_time")]
423#[doc(alias = "sensitive")]
424pub const BCRYPT: Profile<Bcrypt, false> = Profile::new(BCRYPT_NO_PAD, None);
425
426/// Unix `crypt(3)`-style no-padding Base64 profile.
427///
428/// This profile carries the `crypt(3)` alphabet and no padding. It does not
429/// parse complete password-hash strings. Its default strict decoder is not a
430/// constant-time token validator or key-material decoder; use
431/// [`Profile::engine`] with [`Engine::ct_decoder`] for the matching
432/// constant-time-oriented decoder when timing posture matters.
433#[doc(alias = "ct")]
434#[doc(alias = "constant_time")]
435#[doc(alias = "sensitive")]
436pub const CRYPT: Profile<Crypt, false> = Profile::new(CRYPT_NO_PAD, None);