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 redacted owned secret buffer.
315 #[cfg(feature = "alloc")]
316 pub fn encode_secret(&self, input: &[u8]) -> Result<SecretBuffer, EncodeError> {
317 self.encode_vec(input).map(SecretBuffer::from_vec)
318 }
319
320 /// Encodes `input` into a newly allocated UTF-8 string.
321 #[cfg(feature = "alloc")]
322 pub fn encode_string(&self, input: &[u8]) -> Result<alloc::string::String, EncodeError> {
323 match self.wrap {
324 Some(wrap) => self.engine.encode_wrapped_string(input, wrap),
325 None => self.engine.encode_string(input),
326 }
327 }
328
329 /// Decodes `input` into a newly allocated byte vector.
330 #[cfg(feature = "alloc")]
331 #[must_use = "for secret-bearing payloads use decode_secret, which returns a redacted buffer with drop-time cleanup"]
332 pub fn decode_vec(&self, input: &[u8]) -> Result<alloc::vec::Vec<u8>, DecodeError> {
333 match self.wrap {
334 Some(wrap) => self.engine.decode_wrapped_vec(input, wrap),
335 None => self.engine.decode_vec(input),
336 }
337 }
338
339 /// Decodes `input` into a redacted owned secret buffer.
340 ///
341 /// # Security
342 ///
343 /// This uses the profile's normal strict decoder, not the
344 /// constant-time-oriented [`crate::ct`] module. It may branch or return
345 /// early on malformed input and reports localized decode errors. For
346 /// secret-bearing payloads where malformed-input timing matters, use the
347 /// matching [`crate::ct::CtEngine`] explicitly and wrap successful output in
348 /// [`SecretBuffer`].
349 #[cfg(feature = "alloc")]
350 pub fn decode_secret(&self, input: &[u8]) -> Result<SecretBuffer, DecodeError> {
351 self.decode_vec(input).map(SecretBuffer::from_vec)
352 }
353}
354
355impl<A, const PAD: bool> Default for Profile<A, PAD>
356where
357 A: Alphabet,
358{
359 fn default() -> Self {
360 Self::new(Engine::new(), None)
361 }
362}
363
364impl<A, const PAD: bool> core::fmt::Display for Profile<A, PAD>
365where
366 A: Alphabet,
367{
368 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
369 match self.wrap {
370 Some(wrap) => write!(formatter, "padded={PAD} wrap={wrap}"),
371 None => write!(formatter, "padded={PAD} wrap=none"),
372 }
373 }
374}
375
376impl<A, const PAD: bool> From<Engine<A, PAD>> for Profile<A, PAD>
377where
378 A: Alphabet,
379{
380 fn from(engine: Engine<A, PAD>) -> Self {
381 Self::new(engine, None)
382 }
383}
384
385/// MIME Base64 profile: standard alphabet, padding, 76-column CRLF wrapping.
386///
387/// This profile uses the default strict decoder and is not a constant-time
388/// token validator or key-material decoder. Use
389/// [`ct::STANDARD`](crate::ct::STANDARD) with an application-level wrapping
390/// policy for sensitive fixed-shape protocols.
391#[doc(alias = "ct")]
392#[doc(alias = "constant_time")]
393#[doc(alias = "sensitive")]
394pub const MIME: Profile<Standard, true> = Profile::new(STANDARD, Some(LineWrap::MIME));
395
396/// PEM Base64 profile: standard alphabet, padding, 64-column LF wrapping.
397///
398/// This profile uses the default strict decoder and is not a constant-time
399/// token validator or key-material decoder. Use
400/// [`ct::STANDARD`](crate::ct::STANDARD) with an application-level wrapping
401/// policy for sensitive fixed-shape protocols.
402#[doc(alias = "ct")]
403#[doc(alias = "constant_time")]
404#[doc(alias = "sensitive")]
405pub const PEM: Profile<Standard, true> = Profile::new(STANDARD, Some(LineWrap::PEM));
406
407/// PEM Base64 profile with CRLF line endings.
408///
409/// This profile uses the default strict decoder and is not a constant-time
410/// token validator or key-material decoder. Use
411/// [`ct::STANDARD`](crate::ct::STANDARD) with an application-level wrapping
412/// policy for sensitive fixed-shape protocols.
413#[doc(alias = "ct")]
414#[doc(alias = "constant_time")]
415#[doc(alias = "sensitive")]
416pub const PEM_CRLF: Profile<Standard, true> = Profile::new(STANDARD, Some(LineWrap::PEM_CRLF));
417
418/// bcrypt-style no-padding Base64 profile.
419///
420/// This profile carries the bcrypt alphabet and no padding. It does not parse
421/// complete bcrypt password-hash strings. Its default strict decoder is not a
422/// constant-time token validator or key-material decoder; use
423/// [`Profile::engine`] with [`Engine::ct_decoder`] for the matching
424/// constant-time-oriented decoder when timing posture matters.
425#[doc(alias = "ct")]
426#[doc(alias = "constant_time")]
427#[doc(alias = "sensitive")]
428pub const BCRYPT: Profile<Bcrypt, false> = Profile::new(BCRYPT_NO_PAD, None);
429
430/// Unix `crypt(3)`-style no-padding Base64 profile.
431///
432/// This profile carries the `crypt(3)` alphabet and no padding. It does not
433/// parse complete password-hash strings. Its default strict decoder is not a
434/// constant-time token validator or key-material decoder; use
435/// [`Profile::engine`] with [`Engine::ct_decoder`] for the matching
436/// constant-time-oriented decoder when timing posture matters.
437#[doc(alias = "ct")]
438#[doc(alias = "constant_time")]
439#[doc(alias = "sensitive")]
440pub const CRYPT: Profile<Crypt, false> = Profile::new(CRYPT_NO_PAD, None);