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);