base64_ng/lib.rs
1#![cfg_attr(not(feature = "std"), no_std)]
2#![deny(unsafe_code)]
3#![deny(missing_docs)]
4#![deny(clippy::all)]
5#![deny(clippy::pedantic)]
6#![allow(clippy::missing_errors_doc)]
7
8//! `base64-ng` is a `no_std`-first Base64 encoder and decoder.
9//!
10//! This initial release provides strict scalar RFC 4648-style behavior and
11//! caller-owned output buffers. Future SIMD fast paths, including AVX, NEON,
12//! and wasm `simd128` candidates, will be required to match this scalar module
13//! byte-for-byte.
14//!
15//! # Examples
16//!
17//! Encode and decode with caller-owned buffers:
18//!
19//! ```
20//! use base64_ng::{STANDARD, checked_encoded_len};
21//!
22//! let input = b"hello";
23//! const ENCODED_CAPACITY: usize = match checked_encoded_len(5, true) {
24//! Some(len) => len,
25//! None => panic!("encoded length overflow"),
26//! };
27//! let mut encoded = [0u8; ENCODED_CAPACITY];
28//! let encoded_len = STANDARD.encode_slice(input, &mut encoded).unwrap();
29//! assert_eq!(&encoded[..encoded_len], b"aGVsbG8=");
30//!
31//! let mut decoded = [0u8; 5];
32//! let decoded_len = STANDARD.decode_slice(&encoded, &mut decoded).unwrap();
33//! assert_eq!(&decoded[..decoded_len], input);
34//! ```
35//!
36//! Use the URL-safe no-padding engine:
37//!
38//! ```
39//! use base64_ng::URL_SAFE_NO_PAD;
40//!
41//! let mut encoded = [0u8; 3];
42//! let encoded_len = URL_SAFE_NO_PAD.encode_slice(b"\xfb\xff", &mut encoded).unwrap();
43//! assert_eq!(&encoded[..encoded_len], b"-_8");
44//! ```
45//!
46//! # Sensitive Decode Policy
47//!
48//! The default engines such as [`STANDARD`] and [`URL_SAFE_NO_PAD`] are strict
49//! scalar encoders/decoders with localized diagnostics. They are not
50//! constant-time token validators or key-material decoders: strict decode and
51//! validation may branch or return early based on malformed input. Use
52//! [`ct::STANDARD`], [`ct::URL_SAFE_NO_PAD`], or [`Engine::ct_decoder`] for
53//! secret-bearing payloads where decode timing posture matters more than exact
54//! error indexes.
55//!
56//! Recommended heap-owning pattern for secret-bearing standard Base64:
57//!
58//! ```
59//! # #[cfg(feature = "alloc")]
60//! # {
61//! use base64_ng::ct;
62//!
63//! let expected = b"session-key";
64//! let decoded = ct::STANDARD.decode_secret(b"c2Vzc2lvbi1rZXk=").unwrap();
65//!
66//! assert!(decoded.constant_time_eq_public_len(expected));
67//! # }
68//! ```
69//!
70//! For shared-memory, enclave-adjacent, HSM-style, or multi-principal
71//! deployments where even transient writes into caller-owned output are
72//! unacceptable, use [`ct::CtEngine::decode_slice_staged_clear_tail`] with a
73//! private staging buffer.
74//!
75//! # Zeroization Caveat
76//!
77//! Cleanup APIs and redacted buffers use dependency-free best-effort wiping:
78//! byte-wise volatile zero writes followed by an architecture-gated inline
79//! assembly barrier plus a hardware store-ordering fence where stable Rust
80//! supports it, and a compiler fence on all targets. This resists common
81//! compiler dead-store elimination and orders the issued zero stores on native
82//! supported architectures, but it is not a formal zeroization guarantee and
83//! cannot clear historical copies, registers, cache lines, write buffers, swap,
84//! hibernation images, core dumps, cold-boot remanence, or OS-level memory
85//! snapshots.
86//! High-assurance applications should apply their own approved zeroization
87//! policy to caller-owned buffers at the protocol boundary. Architectures
88//! without a native wipe barrier fail closed by default unless
89//! `allow-compiler-fence-only-wipe` is enabled after platform review. On
90//! `wasm32`, the wipe barrier is compiler-fence-only and cannot constrain
91//! downstream wasm runtime JITs. For that reason, `wasm32` builds fail closed
92//! by default. Enable `allow-wasm32-best-effort-wipe` only when the deployment
93//! explicitly accepts compiler-fence-only cleanup and applies its own memory
94//! strategy.
95
96#[cfg(feature = "alloc")]
97extern crate alloc;
98
99#[cfg(all(target_arch = "wasm32", not(feature = "allow-wasm32-best-effort-wipe")))]
100compile_error!(
101 "base64-ng: wasm32 builds use a compiler-fence-only wipe barrier that cannot \
102 constrain downstream wasm runtime JITs. Enable \
103 `allow-wasm32-best-effort-wipe` to accept this limitation and use \
104 caller-owned, platform-approved zeroization for high-assurance wasm deployments."
105);
106
107#[cfg(all(
108 not(miri),
109 not(feature = "allow-compiler-fence-only-wipe"),
110 not(any(
111 target_arch = "aarch64",
112 target_arch = "arm",
113 target_arch = "riscv32",
114 target_arch = "riscv64",
115 target_arch = "wasm32",
116 target_arch = "x86",
117 target_arch = "x86_64",
118 ))
119))]
120compile_error!(
121 "base64-ng: this architecture has no native hardware wipe barrier in \
122 base64-ng. Enable `allow-compiler-fence-only-wipe` only after reviewing \
123 docs/UNSAFE.md and applying platform-approved memory hygiene controls."
124);
125
126mod alphabet;
127mod buffers;
128mod cleanup;
129pub mod ct;
130mod errors;
131mod length;
132mod profiles;
133mod scalar;
134mod wrap;
135
136pub use alphabet::{
137 Alphabet, AlphabetError, Bcrypt, Crypt, Standard, UrlSafe, decode_alphabet_byte,
138 validate_alphabet,
139};
140pub(crate) use alphabet::{encode_base64_value, encode_base64_value_runtime};
141pub use buffers::{DecodedBuffer, EncodedBuffer, ExposedDecodedArray, ExposedEncodedArray};
142#[cfg(feature = "alloc")]
143pub use buffers::{ExposedSecretString, ExposedSecretVec, SecretBuffer};
144pub(crate) use cleanup::{wipe_bytes, wipe_tail};
145#[cfg(feature = "alloc")]
146pub(crate) use cleanup::{wipe_vec_all, wipe_vec_spare_capacity};
147pub(crate) use ct::{
148 constant_time_eq_fixed_width_array, constant_time_eq_public_len, ct_mask_eq_u8, ct_mask_lt_u8,
149};
150#[cfg(test)]
151pub(crate) use ct::{ct_padded_final_quantum, report_ct_error};
152pub use errors::{DecodeError, EncodeError};
153pub use length::{
154 LineEnding, LineWrap, checked_encoded_len, checked_wrapped_encoded_len, decoded_capacity,
155 decoded_len, encoded_len, wrapped_encoded_len,
156};
157pub(crate) use length::{decoded_len_padded, decoded_len_unpadded};
158pub use profiles::{BCRYPT, CRYPT, MIME, PEM, PEM_CRLF, Profile};
159#[cfg(kani)]
160pub(crate) use scalar::decode_byte;
161pub(crate) use scalar::{
162 decode_chunk, decode_tail_unpadded, read_quad, validate_chunk, validate_decode,
163 validate_tail_unpadded,
164};
165pub(crate) use wrap::{
166 compact_wrapped_input, decode_legacy_to_slice, decode_wrapped_to_slice, is_legacy_whitespace,
167 validate_legacy_decode, validate_wrapped_decode, write_wrapped_byte, write_wrapped_bytes,
168};
169
170#[cfg(feature = "simd")]
171mod simd;
172
173/// Runtime backend reporting for security-sensitive deployments.
174///
175/// This module does not enable acceleration. It exposes the backend posture so
176/// callers can log, assert, or audit whether execution is scalar-only or merely
177/// detecting future SIMD candidates.
178pub mod runtime;
179
180#[cfg(feature = "stream")]
181pub mod stream;
182
183/// Standard Base64 engine with padding.
184///
185/// This default strict engine is not a constant-time token validator or
186/// key-material decoder. Use [`ct::STANDARD`] or [`Engine::ct_decoder`] for the
187/// matching constant-time-oriented decoder when timing posture matters.
188#[doc(alias = "ct")]
189#[doc(alias = "constant_time")]
190#[doc(alias = "sensitive")]
191pub const STANDARD: Engine<Standard, true> = Engine::new();
192
193/// Standard Base64 engine without padding.
194///
195/// This default strict engine is not a constant-time token validator or
196/// key-material decoder. Use [`ct::STANDARD_NO_PAD`] or [`Engine::ct_decoder`]
197/// for the matching constant-time-oriented decoder when timing posture
198/// matters.
199#[doc(alias = "ct")]
200#[doc(alias = "constant_time")]
201#[doc(alias = "sensitive")]
202pub const STANDARD_NO_PAD: Engine<Standard, false> = Engine::new();
203
204/// URL-safe Base64 engine with padding.
205///
206/// This default strict engine is not a constant-time token validator or
207/// key-material decoder. Use [`ct::URL_SAFE`] or [`Engine::ct_decoder`] for the
208/// matching constant-time-oriented decoder when timing posture matters.
209#[doc(alias = "ct")]
210#[doc(alias = "constant_time")]
211#[doc(alias = "sensitive")]
212pub const URL_SAFE: Engine<UrlSafe, true> = Engine::new();
213
214/// URL-safe Base64 engine without padding.
215///
216/// This default strict engine is not a constant-time token validator or
217/// key-material decoder. Use [`ct::URL_SAFE_NO_PAD`] or [`Engine::ct_decoder`]
218/// for the matching constant-time-oriented decoder when timing posture
219/// matters.
220#[doc(alias = "ct")]
221#[doc(alias = "constant_time")]
222#[doc(alias = "sensitive")]
223pub const URL_SAFE_NO_PAD: Engine<UrlSafe, false> = Engine::new();
224
225/// bcrypt-style Base64 engine without padding.
226///
227/// This uses the bcrypt alphabet with the crate's normal Base64 bit packing.
228/// It does not parse complete bcrypt password-hash strings. This default strict
229/// engine is not a constant-time token validator or key-material decoder; use
230/// [`Engine::ct_decoder`] for the matching constant-time-oriented decoder when
231/// timing posture matters.
232#[doc(alias = "ct")]
233#[doc(alias = "constant_time")]
234#[doc(alias = "sensitive")]
235pub const BCRYPT_NO_PAD: Engine<Bcrypt, false> = Engine::new();
236
237/// Unix `crypt(3)`-style Base64 engine without padding.
238///
239/// This uses the `crypt(3)` alphabet with the crate's normal Base64 bit
240/// packing. It does not parse complete password-hash strings. This default
241/// strict engine is not a constant-time token validator or key-material
242/// decoder; use [`Engine::ct_decoder`] for the matching constant-time-oriented
243/// decoder when timing posture matters.
244#[doc(alias = "ct")]
245#[doc(alias = "constant_time")]
246#[doc(alias = "sensitive")]
247pub const CRYPT_NO_PAD: Engine<Crypt, false> = Engine::new();
248
249/// Encodes `input` as strict standard padded Base64.
250///
251/// This is a convenience wrapper around [`Engine::encode_string`] on
252/// [`STANDARD`] for callers migrating from simpler Base64 APIs. It requires
253/// the `alloc` feature because it returns an owned string.
254///
255/// # Examples
256///
257/// ```
258/// assert_eq!(base64_ng::encode(b"hello").unwrap(), "aGVsbG8=");
259/// ```
260#[cfg(feature = "alloc")]
261pub fn encode(input: &[u8]) -> Result<alloc::string::String, EncodeError> {
262 STANDARD.encode_string(input)
263}
264
265/// Decodes strict standard padded Base64 into an owned byte vector.
266///
267/// This is a convenience wrapper around [`Engine::decode_vec`] on
268/// [`STANDARD`].
269/// It uses the normal strict decoder, not the [`crate::ct`] module, and may
270/// branch or return early on malformed input. For secret-bearing payloads where
271/// malformed-input timing matters, use
272/// [`crate::ct::CtEngine::decode_secret`] through [`crate::ct::STANDARD`]
273/// instead.
274///
275/// # Examples
276///
277/// ```
278/// assert_eq!(base64_ng::decode("aGVsbG8=").unwrap(), b"hello");
279/// ```
280#[cfg(feature = "alloc")]
281#[must_use = "handle decode errors; use crate::ct for secret-bearing payloads"]
282pub fn decode(input: impl AsRef<[u8]>) -> Result<alloc::vec::Vec<u8>, DecodeError> {
283 STANDARD.decode_vec(input.as_ref())
284}
285
286/// Compares two fixed-width byte arrays without a length-mismatch branch.
287///
288/// Use this helper when the value length itself should not be represented as a
289/// timing-distinct branch in the comparison API. The array length `N` is a
290/// compile-time public type fact, and the helper scans exactly `N` bytes before
291/// returning. The final equality result remains public. This is still a
292/// dependency-free, constant-time-oriented best-effort helper, not a formally
293/// verified cryptographic comparison primitive.
294///
295/// # Examples
296///
297/// ```
298/// use base64_ng::constant_time_eq_fixed_width;
299///
300/// assert!(constant_time_eq_fixed_width(b"token", b"token"));
301/// assert!(!constant_time_eq_fixed_width(b"token", b"Token"));
302/// ```
303#[must_use]
304pub fn constant_time_eq_fixed_width<const N: usize>(left: &[u8; N], right: &[u8; N]) -> bool {
305 constant_time_eq_fixed_width_array(left, right)
306}
307
308/// Compares two byte slices with a public length-mismatch branch.
309///
310/// Equal-length inputs are scanned fully before returning. Different lengths
311/// return `false` immediately because length is treated as public. This is a
312/// dependency-free, constant-time-oriented best-effort helper, not a formally
313/// verified cryptographic MAC, password, or bearer-token comparison primitive.
314///
315/// # Security
316///
317/// This helper is intended to avoid ordinary early-exit equality on values
318/// whose length is public. It is not a formal constant-time guarantee and
319/// should not be the sole primitive admitted at MAC, password, or bearer-token
320/// protocol boundaries in high-assurance systems. Use a reviewed comparison
321/// primitive at that boundary when your dependency policy allows one.
322///
323/// # Examples
324///
325/// ```
326/// assert!(base64_ng::constant_time_eq(b"token", b"token"));
327/// assert!(!base64_ng::constant_time_eq(b"token", b"Token"));
328/// assert!(!base64_ng::constant_time_eq(b"token", b"token2"));
329/// ```
330#[must_use]
331pub fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
332 constant_time_eq_public_len(left, right)
333}
334
335/// A zero-sized Base64 engine parameterized by alphabet and padding policy.
336pub struct Engine<A, const PAD: bool> {
337 alphabet: core::marker::PhantomData<A>,
338}
339
340impl<A, const PAD: bool> Clone for Engine<A, PAD> {
341 fn clone(&self) -> Self {
342 *self
343 }
344}
345
346impl<A, const PAD: bool> Copy for Engine<A, PAD> {}
347
348impl<A, const PAD: bool> core::fmt::Debug for Engine<A, PAD> {
349 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
350 formatter
351 .debug_struct("Engine")
352 .field("padded", &PAD)
353 .finish()
354 }
355}
356
357impl<A, const PAD: bool> core::fmt::Display for Engine<A, PAD> {
358 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
359 write!(formatter, "padded={PAD}")
360 }
361}
362
363impl<A, const PAD: bool> Default for Engine<A, PAD> {
364 fn default() -> Self {
365 Self {
366 alphabet: core::marker::PhantomData,
367 }
368 }
369}
370
371impl<A, const PAD: bool> Eq for Engine<A, PAD> {}
372
373impl<A, const PAD: bool> PartialEq for Engine<A, PAD> {
374 fn eq(&self, _other: &Self) -> bool {
375 true
376 }
377}
378
379impl<A, const PAD: bool> Engine<A, PAD>
380where
381 A: Alphabet,
382{
383 /// Creates a new engine value.
384 #[must_use]
385 pub const fn new() -> Self {
386 Self {
387 alphabet: core::marker::PhantomData,
388 }
389 }
390
391 /// Returns whether this engine uses padded Base64.
392 #[must_use]
393 pub const fn is_padded(&self) -> bool {
394 PAD
395 }
396
397 /// Returns this engine as an unwrapped profile.
398 ///
399 /// Use [`Profile::new`] or [`Profile::checked_new`] when a strict
400 /// line-wrapping policy should travel with the profile.
401 #[must_use]
402 pub const fn profile(&self) -> Profile<A, PAD> {
403 Profile::new(*self, None)
404 }
405
406 /// Returns the matching constant-time-oriented decoder for this engine's
407 /// alphabet and padding policy.
408 ///
409 /// The returned decoder is still an explicit opt-in to the [`ct`] module's
410 /// slower, opaque-error, constant-time-oriented scalar path.
411 #[must_use]
412 pub const fn ct_decoder(&self) -> ct::CtEngine<A, PAD> {
413 ct::CtEngine::new()
414 }
415
416 /// Wraps a `std::io::Write` value in a streaming Base64 encoder.
417 ///
418 /// This is a convenience constructor for [`stream::Encoder::new`] that
419 /// keeps the selected engine attached to the call site.
420 ///
421 /// ```
422 /// use std::io::Write;
423 /// use base64_ng::STANDARD;
424 ///
425 /// let mut encoder = STANDARD.encoder_writer(Vec::new());
426 /// encoder.write_all(b"hello").unwrap();
427 /// assert_eq!(encoder.finish().unwrap(), b"aGVsbG8=");
428 /// ```
429 #[cfg(feature = "stream")]
430 #[must_use]
431 pub fn encoder_writer<W>(&self, inner: W) -> stream::Encoder<W, A, PAD> {
432 stream::Encoder::new(inner, *self)
433 }
434
435 /// Wraps a `std::io::Write` value in a streaming Base64 decoder.
436 ///
437 /// This is a convenience constructor for [`stream::Decoder::new`] that
438 /// keeps the selected engine attached to the call site.
439 ///
440 /// ```
441 /// use std::io::Write;
442 /// use base64_ng::STANDARD;
443 ///
444 /// let mut decoder = STANDARD.decoder_writer(Vec::new());
445 /// decoder.write_all(b"aGVsbG8=").unwrap();
446 /// assert_eq!(decoder.finish().unwrap(), b"hello");
447 /// ```
448 ///
449 /// # Security
450 ///
451 /// Streaming decoders use the normal strict decode path, not the
452 /// [`crate::ct`] module. Do not use this adapter for secret-bearing
453 /// payloads when malformed-input timing matters.
454 #[cfg(feature = "stream")]
455 #[must_use]
456 pub fn decoder_writer<W>(&self, inner: W) -> stream::Decoder<W, A, PAD> {
457 stream::Decoder::new(inner, *self)
458 }
459
460 /// Wraps a `std::io::Read` value in a streaming Base64 encoder.
461 ///
462 /// This is a convenience constructor for [`stream::EncoderReader::new`]
463 /// that keeps the selected engine attached to the call site.
464 ///
465 /// ```
466 /// use std::io::Read;
467 /// use base64_ng::STANDARD;
468 ///
469 /// let mut reader = STANDARD.encoder_reader(&b"hello"[..]);
470 /// let mut encoded = String::new();
471 /// reader.read_to_string(&mut encoded).unwrap();
472 /// assert_eq!(encoded, "aGVsbG8=");
473 /// ```
474 #[cfg(feature = "stream")]
475 #[must_use]
476 pub fn encoder_reader<R>(&self, inner: R) -> stream::EncoderReader<R, A, PAD> {
477 stream::EncoderReader::new(inner, *self)
478 }
479
480 /// Wraps a `std::io::Read` value in a streaming Base64 decoder.
481 ///
482 /// This is a convenience constructor for [`stream::DecoderReader::new`]
483 /// that keeps the selected engine attached to the call site.
484 ///
485 /// ```
486 /// use std::io::Read;
487 /// use base64_ng::STANDARD;
488 ///
489 /// let mut reader = STANDARD.decoder_reader(&b"aGVsbG8="[..]);
490 /// let mut decoded = Vec::new();
491 /// reader.read_to_end(&mut decoded).unwrap();
492 /// assert_eq!(decoded, b"hello");
493 /// ```
494 ///
495 /// # Security
496 ///
497 /// Streaming decoder readers use the normal strict decode path, not the
498 /// [`crate::ct`] module. Do not use this adapter for secret-bearing
499 /// payloads when malformed-input timing matters.
500 #[cfg(feature = "stream")]
501 #[must_use]
502 pub fn decoder_reader<R>(&self, inner: R) -> stream::DecoderReader<R, A, PAD> {
503 stream::DecoderReader::new(inner, *self)
504 }
505
506 /// Returns the encoded length for this engine's padding policy.
507 pub const fn encoded_len(&self, input_len: usize) -> Result<usize, EncodeError> {
508 encoded_len(input_len, PAD)
509 }
510
511 /// Returns the encoded length for this engine, or `None` on overflow.
512 #[must_use]
513 pub const fn checked_encoded_len(&self, input_len: usize) -> Option<usize> {
514 checked_encoded_len(input_len, PAD)
515 }
516
517 /// Returns the encoded length after applying a line wrapping policy.
518 ///
519 /// The returned length includes inserted line endings but does not include
520 /// a trailing line ending after the final encoded line.
521 pub const fn wrapped_encoded_len(
522 &self,
523 input_len: usize,
524 wrap: LineWrap,
525 ) -> Result<usize, EncodeError> {
526 wrapped_encoded_len(input_len, PAD, wrap)
527 }
528
529 /// Returns the encoded length after line wrapping, or `None` on overflow or
530 /// invalid line wrapping.
531 #[must_use]
532 pub const fn checked_wrapped_encoded_len(
533 &self,
534 input_len: usize,
535 wrap: LineWrap,
536 ) -> Option<usize> {
537 checked_wrapped_encoded_len(input_len, PAD, wrap)
538 }
539
540 /// Returns the exact decoded length implied by input length and padding.
541 ///
542 /// This validates padding placement and impossible lengths, but it does not
543 /// validate alphabet membership or non-canonical trailing bits.
544 pub fn decoded_len(&self, input: &[u8]) -> Result<usize, DecodeError> {
545 decoded_len(input, PAD)
546 }
547
548 /// Returns the exact decoded length for the explicit legacy profile.
549 ///
550 /// The legacy profile ignores ASCII space, tab, carriage return, and line
551 /// feed bytes before applying the same alphabet, padding, and canonical-bit
552 /// checks as strict decoding.
553 pub fn decoded_len_legacy(&self, input: &[u8]) -> Result<usize, DecodeError> {
554 validate_legacy_decode::<A, PAD>(input)
555 }
556
557 /// Returns the exact decoded length for a line-wrapped profile.
558 ///
559 /// The wrapped profile accepts only the configured line ending. Non-final
560 /// lines must contain exactly `wrap.line_len` encoded bytes; the final line
561 /// may be shorter. A single trailing line ending after the final line is
562 /// accepted.
563 pub fn decoded_len_wrapped(&self, input: &[u8], wrap: LineWrap) -> Result<usize, DecodeError> {
564 validate_wrapped_decode::<A, PAD>(input, wrap)
565 }
566
567 /// Validates strict Base64 input without writing decoded bytes.
568 ///
569 /// This applies the same alphabet, padding, and canonical-bit checks as
570 /// [`Self::decode_slice`]. Use this method when malformed-input
571 /// diagnostics matter; use [`Self::validate`] when a boolean is enough.
572 /// This default validator is not constant-time; use
573 /// [`crate::ct::CtEngine::validate_result`] through [`Self::ct_decoder`]
574 /// for secret-bearing payloads where timing posture matters.
575 ///
576 /// # Examples
577 ///
578 /// ```
579 /// use base64_ng::STANDARD;
580 ///
581 /// STANDARD.validate_result(b"aGVsbG8=").unwrap();
582 /// assert!(STANDARD.validate_result(b"aGVsbG8").is_err());
583 /// ```
584 pub fn validate_result(&self, input: &[u8]) -> Result<(), DecodeError> {
585 validate_decode::<A, PAD>(input).map(|_| ())
586 }
587
588 /// Returns whether `input` is valid strict Base64 for this engine.
589 ///
590 /// This is a convenience wrapper around [`Self::validate_result`] and is
591 /// not constant-time. Use [`crate::ct::CtEngine::validate`] through
592 /// [`Self::ct_decoder`] for secret-bearing payloads where timing posture
593 /// matters.
594 ///
595 /// # Examples
596 ///
597 /// ```
598 /// use base64_ng::URL_SAFE_NO_PAD;
599 ///
600 /// assert!(URL_SAFE_NO_PAD.validate(b"-_8"));
601 /// assert!(!URL_SAFE_NO_PAD.validate(b"+/8"));
602 /// ```
603 #[must_use]
604 pub fn validate(&self, input: &[u8]) -> bool {
605 self.validate_result(input).is_ok()
606 }
607
608 /// Validates input using the explicit legacy whitespace profile.
609 ///
610 /// ASCII space, tab, carriage return, and line feed bytes are ignored
611 /// before applying the same alphabet, padding, and canonical-bit checks as
612 /// strict decoding.
613 ///
614 /// # Examples
615 ///
616 /// ```
617 /// use base64_ng::STANDARD;
618 ///
619 /// STANDARD.validate_legacy_result(b" aG\r\nVsbG8= ").unwrap();
620 /// assert!(STANDARD.validate_legacy_result(b" aG-=").is_err());
621 /// ```
622 pub fn validate_legacy_result(&self, input: &[u8]) -> Result<(), DecodeError> {
623 validate_legacy_decode::<A, PAD>(input).map(|_| ())
624 }
625
626 /// Returns whether `input` is valid for the explicit legacy whitespace
627 /// profile.
628 ///
629 /// This is a convenience wrapper around [`Self::validate_legacy_result`].
630 ///
631 /// # Examples
632 ///
633 /// ```
634 /// use base64_ng::STANDARD;
635 ///
636 /// assert!(STANDARD.validate_legacy(b" aG\r\nVsbG8= "));
637 /// assert!(!STANDARD.validate_legacy(b"aG-V"));
638 /// ```
639 #[must_use]
640 pub fn validate_legacy(&self, input: &[u8]) -> bool {
641 self.validate_legacy_result(input).is_ok()
642 }
643
644 /// Validates input using a strict line-wrapped profile.
645 ///
646 /// This is stricter than [`Self::validate_legacy_result`]: it accepts only
647 /// the configured line ending and enforces the configured line length for
648 /// every non-final line.
649 ///
650 /// # Examples
651 ///
652 /// ```
653 /// use base64_ng::{LineEnding, LineWrap, STANDARD};
654 ///
655 /// let wrap = LineWrap::new(4, LineEnding::Lf);
656 /// STANDARD.validate_wrapped_result(b"aGVs\nbG8=", wrap).unwrap();
657 /// assert!(STANDARD.validate_wrapped_result(b"aG\nVsbG8=", wrap).is_err());
658 /// ```
659 pub fn validate_wrapped_result(&self, input: &[u8], wrap: LineWrap) -> Result<(), DecodeError> {
660 validate_wrapped_decode::<A, PAD>(input, wrap).map(|_| ())
661 }
662
663 /// Returns whether `input` is valid for a strict line-wrapped profile.
664 ///
665 /// This is a convenience wrapper around [`Self::validate_wrapped_result`].
666 ///
667 /// # Examples
668 ///
669 /// ```
670 /// use base64_ng::{LineEnding, LineWrap, STANDARD};
671 ///
672 /// let wrap = LineWrap::new(4, LineEnding::Lf);
673 /// assert!(STANDARD.validate_wrapped(b"aGVs\nbG8=", wrap));
674 /// assert!(!STANDARD.validate_wrapped(b"aG\nVsbG8=", wrap));
675 /// ```
676 #[must_use]
677 pub fn validate_wrapped(&self, input: &[u8], wrap: LineWrap) -> bool {
678 self.validate_wrapped_result(input, wrap).is_ok()
679 }
680
681 /// Encodes a fixed-size input into a fixed-size output array in const contexts.
682 ///
683 /// Stable Rust does not yet allow this API to return an array whose length
684 /// is computed from `INPUT_LEN` directly. Instead, the caller supplies the
685 /// output length through the destination type and this function panics
686 /// during const evaluation if the length is wrong.
687 ///
688 /// # Panics
689 ///
690 /// Panics if `OUTPUT_LEN` is not exactly the encoded length for `INPUT_LEN`
691 /// and this engine's padding policy, or if that length overflows `usize`.
692 ///
693 /// # Examples
694 ///
695 /// ```
696 /// use base64_ng::{STANDARD, URL_SAFE_NO_PAD};
697 ///
698 /// const HELLO: [u8; 8] = STANDARD.encode_array(b"hello");
699 /// const URL_SAFE: [u8; 3] = URL_SAFE_NO_PAD.encode_array(b"\xfb\xff");
700 ///
701 /// assert_eq!(&HELLO, b"aGVsbG8=");
702 /// assert_eq!(&URL_SAFE, b"-_8");
703 /// ```
704 ///
705 /// Incorrect output lengths fail during const evaluation:
706 ///
707 /// ```compile_fail
708 /// use base64_ng::STANDARD;
709 ///
710 /// const TOO_SHORT: [u8; 7] = STANDARD.encode_array(b"hello");
711 /// ```
712 #[must_use]
713 pub const fn encode_array<const INPUT_LEN: usize, const OUTPUT_LEN: usize>(
714 &self,
715 input: &[u8; INPUT_LEN],
716 ) -> [u8; OUTPUT_LEN] {
717 let Some(required) = checked_encoded_len(INPUT_LEN, PAD) else {
718 panic!("encoded base64 length overflows usize");
719 };
720 assert!(
721 required == OUTPUT_LEN,
722 "base64 output array has incorrect length"
723 );
724
725 let mut output = [0u8; OUTPUT_LEN];
726 let mut read = 0;
727 let mut write = 0;
728 while INPUT_LEN - read >= 3 {
729 let b0 = input[read];
730 let b1 = input[read + 1];
731 let b2 = input[read + 2];
732
733 output[write] = encode_base64_value::<A>(b0 >> 2);
734 output[write + 1] = encode_base64_value::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
735 output[write + 2] = encode_base64_value::<A>(((b1 & 0b0000_1111) << 2) | (b2 >> 6));
736 output[write + 3] = encode_base64_value::<A>(b2 & 0b0011_1111);
737
738 read += 3;
739 write += 4;
740 }
741
742 match INPUT_LEN - read {
743 0 => {}
744 1 => {
745 let b0 = input[read];
746 output[write] = encode_base64_value::<A>(b0 >> 2);
747 output[write + 1] = encode_base64_value::<A>((b0 & 0b0000_0011) << 4);
748 write += 2;
749 if PAD {
750 output[write] = b'=';
751 output[write + 1] = b'=';
752 }
753 }
754 2 => {
755 let b0 = input[read];
756 let b1 = input[read + 1];
757 output[write] = encode_base64_value::<A>(b0 >> 2);
758 output[write + 1] = encode_base64_value::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
759 output[write + 2] = encode_base64_value::<A>((b1 & 0b0000_1111) << 2);
760 if PAD {
761 output[write + 3] = b'=';
762 }
763 }
764 _ => unreachable!(),
765 }
766
767 output
768 }
769
770 /// Encodes `input` into `output`, returning the number of bytes written.
771 pub fn encode_slice(&self, input: &[u8], output: &mut [u8]) -> Result<usize, EncodeError> {
772 scalar::encode_slice::<A, PAD>(input, output)
773 }
774
775 /// Encodes `input` into `output` with line wrapping.
776 ///
777 /// The wrapping policy inserts line endings between encoded lines and does
778 /// not append a trailing line ending after the final line.
779 ///
780 /// # Examples
781 ///
782 /// ```
783 /// use base64_ng::{LineEnding, LineWrap, STANDARD};
784 ///
785 /// let wrap = LineWrap::new(4, LineEnding::Lf);
786 /// let mut output = [0u8; 9];
787 /// let written = STANDARD
788 /// .encode_slice_wrapped(b"hello", &mut output, wrap)
789 /// .unwrap();
790 ///
791 /// assert_eq!(&output[..written], b"aGVs\nbG8=");
792 /// ```
793 pub fn encode_slice_wrapped(
794 &self,
795 input: &[u8],
796 output: &mut [u8],
797 wrap: LineWrap,
798 ) -> Result<usize, EncodeError> {
799 let required = self.wrapped_encoded_len(input.len(), wrap)?;
800 if output.len() < required {
801 return Err(EncodeError::OutputTooSmall {
802 required,
803 available: output.len(),
804 });
805 }
806
807 let encoded_len =
808 checked_encoded_len(input.len(), PAD).ok_or(EncodeError::LengthOverflow)?;
809 if encoded_len == 0 {
810 return Ok(0);
811 }
812
813 // If the temporary in-buffer layout size overflows, fall back to the
814 // fixed scratch buffer path rather than relying on saturated arithmetic.
815 let combined_required = match required.checked_add(encoded_len) {
816 Some(len) => len,
817 None => usize::MAX,
818 };
819 if output.len() < combined_required {
820 let mut scratch = [0u8; 1024];
821 let mut input_offset = 0;
822 let mut output_offset = 0;
823 let mut column = 0;
824
825 while input_offset < input.len() {
826 let remaining = input.len() - input_offset;
827 let mut take = remaining.min(768);
828 if remaining > take {
829 take -= take % 3;
830 }
831 if take == 0 {
832 take = remaining;
833 }
834
835 let encoded = match self
836 .encode_slice(&input[input_offset..input_offset + take], &mut scratch)
837 {
838 Ok(encoded) => encoded,
839 Err(err) => {
840 wipe_bytes(&mut scratch);
841 return Err(err);
842 }
843 };
844 if let Err(err) = write_wrapped_bytes(
845 &scratch[..encoded],
846 output,
847 &mut output_offset,
848 &mut column,
849 wrap,
850 ) {
851 wipe_bytes(&mut scratch);
852 return Err(err);
853 }
854 wipe_bytes(&mut scratch[..encoded]);
855 input_offset += take;
856 }
857
858 Ok(output_offset)
859 } else {
860 let encoded =
861 self.encode_slice(input, &mut output[required..required + encoded_len])?;
862 let mut output_offset = 0;
863 let mut column = 0;
864 let mut read = required;
865 while read < required + encoded {
866 let byte = output[read];
867 write_wrapped_byte(byte, output, &mut output_offset, &mut column, wrap)?;
868 read += 1;
869 }
870 wipe_bytes(&mut output[required..required + encoded]);
871 Ok(output_offset)
872 }
873 }
874
875 /// Encodes `input` with line wrapping and clears all bytes after the
876 /// encoded prefix.
877 ///
878 /// If encoding fails, the entire output buffer is cleared before the error
879 /// is returned.
880 pub fn encode_slice_wrapped_clear_tail(
881 &self,
882 input: &[u8],
883 output: &mut [u8],
884 wrap: LineWrap,
885 ) -> Result<usize, EncodeError> {
886 let written = match self.encode_slice_wrapped(input, output, wrap) {
887 Ok(written) => written,
888 Err(err) => {
889 wipe_bytes(output);
890 return Err(err);
891 }
892 };
893 wipe_tail(output, written);
894 Ok(written)
895 }
896
897 /// Encodes `input` with line wrapping into a stack-backed buffer.
898 ///
899 /// This is useful for MIME/PEM-style protocols where heap allocation is
900 /// unnecessary. If encoding fails, the internal backing array is cleared
901 /// before the error is returned.
902 pub fn encode_wrapped_buffer<const CAP: usize>(
903 &self,
904 input: &[u8],
905 wrap: LineWrap,
906 ) -> Result<EncodedBuffer<CAP>, EncodeError> {
907 let mut output = EncodedBuffer::new();
908 let written =
909 match self.encode_slice_wrapped_clear_tail(input, output.as_mut_capacity(), wrap) {
910 Ok(written) => written,
911 Err(err) => {
912 output.clear();
913 return Err(err);
914 }
915 };
916 output.set_filled(written)?;
917 Ok(output)
918 }
919
920 /// Encodes `input` with line wrapping into a newly allocated byte vector.
921 #[cfg(feature = "alloc")]
922 #[must_use = "for secret-bearing payloads use encode_wrapped_secret, which returns a redacted buffer with drop-time cleanup"]
923 pub fn encode_wrapped_vec(
924 &self,
925 input: &[u8],
926 wrap: LineWrap,
927 ) -> Result<alloc::vec::Vec<u8>, EncodeError> {
928 let required = self.wrapped_encoded_len(input.len(), wrap)?;
929 let mut output = alloc::vec![0; required];
930 let written = self.encode_slice_wrapped(input, &mut output, wrap)?;
931 output.truncate(written);
932 Ok(output)
933 }
934
935 /// Encodes `input` with line wrapping into a newly allocated UTF-8 string.
936 #[cfg(feature = "alloc")]
937 pub fn encode_wrapped_string(
938 &self,
939 input: &[u8],
940 wrap: LineWrap,
941 ) -> Result<alloc::string::String, EncodeError> {
942 let output = self.encode_wrapped_vec(input, wrap)?;
943 match alloc::string::String::from_utf8(output) {
944 Ok(output) => Ok(output),
945 Err(_) => unreachable!("base64 encoder produced non-UTF-8 output"),
946 }
947 }
948
949 /// Encodes `input` with line wrapping into a redacted owned secret buffer.
950 ///
951 /// This is useful when the wrapped encoded representation itself is
952 /// sensitive and should not be accidentally logged through formatting.
953 #[cfg(feature = "alloc")]
954 pub fn encode_wrapped_secret(
955 &self,
956 input: &[u8],
957 wrap: LineWrap,
958 ) -> Result<SecretBuffer, EncodeError> {
959 self.encode_wrapped_vec(input, wrap)
960 .map(SecretBuffer::from_vec)
961 }
962
963 /// Encodes `input` into `output` and clears all bytes after the encoded
964 /// prefix.
965 ///
966 /// If encoding fails, the entire output buffer is cleared before the error
967 /// is returned.
968 ///
969 /// # Examples
970 ///
971 /// ```
972 /// use base64_ng::STANDARD;
973 ///
974 /// let mut output = [0xff; 12];
975 /// let written = STANDARD
976 /// .encode_slice_clear_tail(b"hello", &mut output)
977 /// .unwrap();
978 ///
979 /// assert_eq!(&output[..written], b"aGVsbG8=");
980 /// assert!(output[written..].iter().all(|byte| *byte == 0));
981 /// ```
982 pub fn encode_slice_clear_tail(
983 &self,
984 input: &[u8],
985 output: &mut [u8],
986 ) -> Result<usize, EncodeError> {
987 let written = match self.encode_slice(input, output) {
988 Ok(written) => written,
989 Err(err) => {
990 wipe_bytes(output);
991 return Err(err);
992 }
993 };
994 wipe_tail(output, written);
995 Ok(written)
996 }
997
998 /// Encodes `input` into a stack-backed buffer.
999 ///
1000 /// This helper is useful for short values where callers want the
1001 /// convenience of an owned result without enabling `alloc`.
1002 ///
1003 /// # Examples
1004 ///
1005 /// ```
1006 /// use base64_ng::STANDARD;
1007 ///
1008 /// let encoded = STANDARD.encode_buffer::<8>(b"hello").unwrap();
1009 ///
1010 /// assert_eq!(encoded.as_str(), "aGVsbG8=");
1011 /// ```
1012 pub fn encode_buffer<const CAP: usize>(
1013 &self,
1014 input: &[u8],
1015 ) -> Result<EncodedBuffer<CAP>, EncodeError> {
1016 let mut output = EncodedBuffer::new();
1017 let written = match self.encode_slice_clear_tail(input, output.as_mut_capacity()) {
1018 Ok(written) => written,
1019 Err(err) => {
1020 output.clear();
1021 return Err(err);
1022 }
1023 };
1024 output.set_filled(written)?;
1025 Ok(output)
1026 }
1027
1028 /// Encodes `input` into a newly allocated byte vector.
1029 #[cfg(feature = "alloc")]
1030 #[must_use = "for secret-bearing payloads use encode_secret, which returns a redacted buffer with drop-time cleanup"]
1031 pub fn encode_vec(&self, input: &[u8]) -> Result<alloc::vec::Vec<u8>, EncodeError> {
1032 let required = checked_encoded_len(input.len(), PAD).ok_or(EncodeError::LengthOverflow)?;
1033 let mut output = alloc::vec![0; required];
1034 let written = self.encode_slice(input, &mut output)?;
1035 output.truncate(written);
1036 Ok(output)
1037 }
1038
1039 /// Encodes `input` into a redacted owned secret buffer.
1040 ///
1041 /// This is useful when the encoded representation itself is sensitive and
1042 /// should not be accidentally logged through formatting.
1043 #[cfg(feature = "alloc")]
1044 pub fn encode_secret(&self, input: &[u8]) -> Result<SecretBuffer, EncodeError> {
1045 self.encode_vec(input).map(SecretBuffer::from_vec)
1046 }
1047
1048 /// Encodes `input` into a newly allocated UTF-8 string.
1049 ///
1050 /// Base64 output is ASCII by construction. This helper is available with
1051 /// the `alloc` feature and has the same encoding semantics as
1052 /// [`Self::encode_slice`].
1053 ///
1054 /// # Examples
1055 ///
1056 /// ```
1057 /// use base64_ng::{STANDARD, URL_SAFE_NO_PAD};
1058 ///
1059 /// assert_eq!(STANDARD.encode_string(b"hello").unwrap(), "aGVsbG8=");
1060 /// assert_eq!(URL_SAFE_NO_PAD.encode_string(b"\xfb\xff").unwrap(), "-_8");
1061 /// ```
1062 #[cfg(feature = "alloc")]
1063 pub fn encode_string(&self, input: &[u8]) -> Result<alloc::string::String, EncodeError> {
1064 let output = self.encode_vec(input)?;
1065 match alloc::string::String::from_utf8(output) {
1066 Ok(output) => Ok(output),
1067 Err(_) => unreachable!("base64 encoder produced non-UTF-8 output"),
1068 }
1069 }
1070
1071 /// Encodes the first `input_len` bytes of `buffer` in place.
1072 ///
1073 /// The buffer must have enough spare capacity for the encoded output. The
1074 /// implementation writes from right to left, so unread input bytes are not
1075 /// overwritten before they are encoded.
1076 ///
1077 /// # Panics
1078 ///
1079 /// Panics only if an internal right-to-left encode invariant is violated.
1080 /// This indicates a bug in `base64-ng`; valid or malformed caller input is
1081 /// reported through [`EncodeError`] instead.
1082 ///
1083 /// # Examples
1084 ///
1085 /// ```
1086 /// use base64_ng::STANDARD;
1087 ///
1088 /// let mut buffer = [0u8; 8];
1089 /// buffer[..5].copy_from_slice(b"hello");
1090 /// let encoded = STANDARD.encode_in_place(&mut buffer, 5).unwrap();
1091 /// assert_eq!(encoded, b"aGVsbG8=");
1092 /// ```
1093 pub fn encode_in_place<'a>(
1094 &self,
1095 buffer: &'a mut [u8],
1096 input_len: usize,
1097 ) -> Result<&'a mut [u8], EncodeError> {
1098 if input_len > buffer.len() {
1099 return Err(EncodeError::InputTooLarge {
1100 input_len,
1101 buffer_len: buffer.len(),
1102 });
1103 }
1104
1105 let required = checked_encoded_len(input_len, PAD).ok_or(EncodeError::LengthOverflow)?;
1106 if buffer.len() < required {
1107 return Err(EncodeError::OutputTooSmall {
1108 required,
1109 available: buffer.len(),
1110 });
1111 }
1112
1113 let mut read = input_len;
1114 let mut write = required;
1115
1116 match input_len % 3 {
1117 0 => {}
1118 1 => {
1119 read -= 1;
1120 let b0 = buffer[read];
1121 if PAD {
1122 write -= 4;
1123 buffer[write] = encode_base64_value_runtime::<A>(b0 >> 2);
1124 buffer[write + 1] = encode_base64_value_runtime::<A>((b0 & 0b0000_0011) << 4);
1125 buffer[write + 2] = b'=';
1126 buffer[write + 3] = b'=';
1127 } else {
1128 write -= 2;
1129 buffer[write] = encode_base64_value_runtime::<A>(b0 >> 2);
1130 buffer[write + 1] = encode_base64_value_runtime::<A>((b0 & 0b0000_0011) << 4);
1131 }
1132 }
1133 2 => {
1134 read -= 2;
1135 let b0 = buffer[read];
1136 let b1 = buffer[read + 1];
1137 if PAD {
1138 write -= 4;
1139 buffer[write] = encode_base64_value_runtime::<A>(b0 >> 2);
1140 buffer[write + 1] =
1141 encode_base64_value_runtime::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
1142 buffer[write + 2] = encode_base64_value_runtime::<A>((b1 & 0b0000_1111) << 2);
1143 buffer[write + 3] = b'=';
1144 } else {
1145 write -= 3;
1146 buffer[write] = encode_base64_value_runtime::<A>(b0 >> 2);
1147 buffer[write + 1] =
1148 encode_base64_value_runtime::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
1149 buffer[write + 2] = encode_base64_value_runtime::<A>((b1 & 0b0000_1111) << 2);
1150 }
1151 }
1152 _ => unreachable!(),
1153 }
1154
1155 while read > 0 {
1156 read -= 3;
1157 write -= 4;
1158 let b0 = buffer[read];
1159 let b1 = buffer[read + 1];
1160 let b2 = buffer[read + 2];
1161
1162 buffer[write] = encode_base64_value_runtime::<A>(b0 >> 2);
1163 buffer[write + 1] =
1164 encode_base64_value_runtime::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
1165 buffer[write + 2] =
1166 encode_base64_value_runtime::<A>(((b1 & 0b0000_1111) << 2) | (b2 >> 6));
1167 buffer[write + 3] = encode_base64_value_runtime::<A>(b2 & 0b0011_1111);
1168 }
1169
1170 // The right-to-left loop consumes exactly three input bytes for every
1171 // four output bytes. If this invariant changes, returning a shifted
1172 // slice would silently corrupt the in-place output.
1173 assert_eq!(
1174 write, 0,
1175 "encode_in_place invariant violated: right-to-left loop did not complete"
1176 );
1177 Ok(&mut buffer[..required])
1178 }
1179
1180 /// Encodes the first `input_len` bytes of `buffer` in place and clears all
1181 /// bytes after the encoded prefix.
1182 ///
1183 /// If encoding fails because `input_len` is too large, the output buffer is
1184 /// too small, or the encoded length overflows `usize`, the entire buffer is
1185 /// cleared before the error is returned.
1186 ///
1187 /// # Examples
1188 ///
1189 /// ```
1190 /// use base64_ng::STANDARD;
1191 ///
1192 /// let mut buffer = [0xff; 12];
1193 /// buffer[..5].copy_from_slice(b"hello");
1194 /// let encoded = STANDARD.encode_in_place_clear_tail(&mut buffer, 5).unwrap();
1195 /// assert_eq!(encoded, b"aGVsbG8=");
1196 /// ```
1197 pub fn encode_in_place_clear_tail<'a>(
1198 &self,
1199 buffer: &'a mut [u8],
1200 input_len: usize,
1201 ) -> Result<&'a mut [u8], EncodeError> {
1202 let len = match self.encode_in_place(buffer, input_len) {
1203 Ok(encoded) => encoded.len(),
1204 Err(err) => {
1205 wipe_bytes(buffer);
1206 return Err(err);
1207 }
1208 };
1209 wipe_tail(buffer, len);
1210 Ok(&mut buffer[..len])
1211 }
1212
1213 /// Decodes `input` into `output`, returning the number of bytes written.
1214 ///
1215 /// This is strict decoding. Whitespace, mixed alphabets, malformed padding,
1216 /// and trailing non-padding data are rejected.
1217 ///
1218 /// # Security
1219 ///
1220 /// This default scalar decoder prioritizes strict validation, exact error
1221 /// reporting, and ordinary throughput. It may branch or return early based
1222 /// on byte validity, malformed input, padding position, and output
1223 /// capacity. It also reports exact failure positions and invalid byte
1224 /// values through [`DecodeError`]. Do not use this method for token
1225 /// comparison, key-material decoding, or secret-bearing validation where
1226 /// malformed-input timing matters. Use [`crate::ct`],
1227 /// [`crate::ct::STANDARD`], [`crate::ct::URL_SAFE_NO_PAD`], or
1228 /// [`Self::ct_decoder`] with `decode_slice_clear_tail` for
1229 /// constant-time-oriented secret decoding.
1230 #[must_use = "handle decode errors; use crate::ct for secret-bearing payloads"]
1231 pub fn decode_slice(&self, input: &[u8], output: &mut [u8]) -> Result<usize, DecodeError> {
1232 scalar::decode_slice::<A, PAD>(input, output)
1233 }
1234
1235 /// Decodes `input` into `output` and clears all bytes after the decoded
1236 /// prefix.
1237 ///
1238 /// If decoding fails, the entire output buffer is cleared before the error
1239 /// is returned.
1240 ///
1241 /// # Examples
1242 ///
1243 /// ```
1244 /// use base64_ng::STANDARD;
1245 ///
1246 /// let mut output = [0xff; 8];
1247 /// let written = STANDARD
1248 /// .decode_slice_clear_tail(b"aGk=", &mut output)
1249 /// .unwrap();
1250 ///
1251 /// assert_eq!(&output[..written], b"hi");
1252 /// assert!(output[written..].iter().all(|byte| *byte == 0));
1253 /// ```
1254 pub fn decode_slice_clear_tail(
1255 &self,
1256 input: &[u8],
1257 output: &mut [u8],
1258 ) -> Result<usize, DecodeError> {
1259 let written = match self.decode_slice(input, output) {
1260 Ok(written) => written,
1261 Err(err) => {
1262 wipe_bytes(output);
1263 return Err(err);
1264 }
1265 };
1266 wipe_tail(output, written);
1267 Ok(written)
1268 }
1269
1270 /// Decodes `input` into a stack-backed buffer.
1271 ///
1272 /// This helper is useful for short decoded values where callers want the
1273 /// convenience of an owned result without enabling `alloc`.
1274 ///
1275 /// # Examples
1276 ///
1277 /// ```
1278 /// use base64_ng::STANDARD;
1279 ///
1280 /// let decoded = STANDARD.decode_buffer::<5>(b"aGVsbG8=").unwrap();
1281 ///
1282 /// assert_eq!(decoded.as_bytes(), b"hello");
1283 /// ```
1284 pub fn decode_buffer<const CAP: usize>(
1285 &self,
1286 input: &[u8],
1287 ) -> Result<DecodedBuffer<CAP>, DecodeError> {
1288 let mut output = DecodedBuffer::new();
1289 let written = match self.decode_slice_clear_tail(input, output.as_mut_capacity()) {
1290 Ok(written) => written,
1291 Err(err) => {
1292 output.clear();
1293 return Err(err);
1294 }
1295 };
1296 output.set_filled(written)?;
1297 Ok(output)
1298 }
1299
1300 /// Decodes `input` using the explicit legacy whitespace profile.
1301 ///
1302 /// ASCII space, tab, carriage return, and line feed bytes are ignored.
1303 /// Alphabet selection, padding placement, trailing data after padding, and
1304 /// non-canonical trailing bits remain strict.
1305 ///
1306 /// # Security
1307 ///
1308 /// This method uses the normal strict decode path after legacy whitespace
1309 /// handling. It may branch or return early based on malformed input and is
1310 /// not a constant-time token validator or key-material decoder. Use
1311 /// [`crate::ct`] for secret-bearing payloads.
1312 #[must_use = "handle decode errors; use crate::ct for secret-bearing payloads"]
1313 pub fn decode_slice_legacy(
1314 &self,
1315 input: &[u8],
1316 output: &mut [u8],
1317 ) -> Result<usize, DecodeError> {
1318 let required = validate_legacy_decode::<A, PAD>(input)?;
1319 if output.len() < required {
1320 return Err(DecodeError::OutputTooSmall {
1321 required,
1322 available: output.len(),
1323 });
1324 }
1325 decode_legacy_to_slice::<A, PAD>(input, output)
1326 }
1327
1328 /// Decodes `input` using the explicit legacy whitespace profile and clears
1329 /// all bytes after the decoded prefix.
1330 ///
1331 /// If validation or decoding fails, the entire output buffer is cleared
1332 /// before the error is returned.
1333 ///
1334 /// # Examples
1335 ///
1336 /// ```
1337 /// use base64_ng::STANDARD;
1338 ///
1339 /// let mut output = [0xff; 8];
1340 /// let written = STANDARD
1341 /// .decode_slice_legacy_clear_tail(b" aG\r\nk= ", &mut output)
1342 /// .unwrap();
1343 ///
1344 /// assert_eq!(&output[..written], b"hi");
1345 /// assert!(output[written..].iter().all(|byte| *byte == 0));
1346 /// ```
1347 pub fn decode_slice_legacy_clear_tail(
1348 &self,
1349 input: &[u8],
1350 output: &mut [u8],
1351 ) -> Result<usize, DecodeError> {
1352 let written = match self.decode_slice_legacy(input, output) {
1353 Ok(written) => written,
1354 Err(err) => {
1355 wipe_bytes(output);
1356 return Err(err);
1357 }
1358 };
1359 wipe_tail(output, written);
1360 Ok(written)
1361 }
1362
1363 /// Decodes `input` into a stack-backed buffer using the explicit legacy
1364 /// whitespace profile.
1365 ///
1366 /// ASCII space, tab, carriage return, and line feed bytes are ignored.
1367 /// Alphabet selection, padding placement, trailing data after padding, and
1368 /// non-canonical trailing bits remain strict. If decoding fails, the
1369 /// internal backing array is cleared before the error is returned.
1370 pub fn decode_buffer_legacy<const CAP: usize>(
1371 &self,
1372 input: &[u8],
1373 ) -> Result<DecodedBuffer<CAP>, DecodeError> {
1374 let mut output = DecodedBuffer::new();
1375 let written = match self.decode_slice_legacy_clear_tail(input, output.as_mut_capacity()) {
1376 Ok(written) => written,
1377 Err(err) => {
1378 output.clear();
1379 return Err(err);
1380 }
1381 };
1382 output.set_filled(written)?;
1383 Ok(output)
1384 }
1385
1386 /// Decodes `input` using a strict line-wrapped profile.
1387 ///
1388 /// The wrapped profile accepts only the configured line ending. Non-final
1389 /// lines must contain exactly `wrap.line_len` encoded bytes; the final line
1390 /// may be shorter. A single trailing line ending after the final line is
1391 /// accepted.
1392 ///
1393 /// # Security
1394 ///
1395 /// This method uses the normal strict decode path after line-profile
1396 /// validation. It may branch or return early based on malformed input and
1397 /// is not a constant-time token validator or key-material decoder. Use
1398 /// [`crate::ct`] for secret-bearing payloads.
1399 #[must_use = "handle decode errors; use crate::ct for secret-bearing payloads"]
1400 pub fn decode_slice_wrapped(
1401 &self,
1402 input: &[u8],
1403 output: &mut [u8],
1404 wrap: LineWrap,
1405 ) -> Result<usize, DecodeError> {
1406 let required = validate_wrapped_decode::<A, PAD>(input, wrap)?;
1407 if output.len() < required {
1408 return Err(DecodeError::OutputTooSmall {
1409 required,
1410 available: output.len(),
1411 });
1412 }
1413 decode_wrapped_to_slice::<A, PAD>(input, output, wrap)
1414 }
1415
1416 /// Decodes `input` using a strict line-wrapped profile and clears all bytes
1417 /// after the decoded prefix.
1418 ///
1419 /// If validation or decoding fails, the entire output buffer is cleared
1420 /// before the error is returned.
1421 pub fn decode_slice_wrapped_clear_tail(
1422 &self,
1423 input: &[u8],
1424 output: &mut [u8],
1425 wrap: LineWrap,
1426 ) -> Result<usize, DecodeError> {
1427 let written = match self.decode_slice_wrapped(input, output, wrap) {
1428 Ok(written) => written,
1429 Err(err) => {
1430 wipe_bytes(output);
1431 return Err(err);
1432 }
1433 };
1434 wipe_tail(output, written);
1435 Ok(written)
1436 }
1437
1438 /// Decodes `input` using a strict line-wrapped profile into a stack-backed
1439 /// buffer.
1440 ///
1441 /// The wrapped profile accepts only the configured line ending. Non-final
1442 /// lines must contain exactly `wrap.line_len` encoded bytes; the final line
1443 /// may be shorter. A single trailing line ending after the final line is
1444 /// accepted. If decoding fails, the internal backing array is cleared
1445 /// before the error is returned.
1446 pub fn decode_wrapped_buffer<const CAP: usize>(
1447 &self,
1448 input: &[u8],
1449 wrap: LineWrap,
1450 ) -> Result<DecodedBuffer<CAP>, DecodeError> {
1451 let mut output = DecodedBuffer::new();
1452 let written =
1453 match self.decode_slice_wrapped_clear_tail(input, output.as_mut_capacity(), wrap) {
1454 Ok(written) => written,
1455 Err(err) => {
1456 output.clear();
1457 return Err(err);
1458 }
1459 };
1460 output.set_filled(written)?;
1461 Ok(output)
1462 }
1463
1464 /// Decodes `input` into a newly allocated byte vector.
1465 ///
1466 /// This is strict decoding with the same semantics as [`Self::decode_slice`].
1467 #[cfg(feature = "alloc")]
1468 #[must_use = "for secret-bearing payloads use decode_secret, which returns a redacted buffer with drop-time cleanup"]
1469 pub fn decode_vec(&self, input: &[u8]) -> Result<alloc::vec::Vec<u8>, DecodeError> {
1470 let required = validate_decode::<A, PAD>(input)?;
1471 let mut output = alloc::vec![0; required];
1472 let written = match self.decode_slice(input, &mut output) {
1473 Ok(written) => written,
1474 Err(err) => {
1475 wipe_bytes(&mut output);
1476 return Err(err);
1477 }
1478 };
1479 output.truncate(written);
1480 Ok(output)
1481 }
1482
1483 /// Decodes `input` into a redacted owned secret buffer.
1484 ///
1485 /// On malformed input, the intermediate output buffer is cleared before the
1486 /// error is returned by [`Self::decode_vec`].
1487 #[cfg(feature = "alloc")]
1488 pub fn decode_secret(&self, input: &[u8]) -> Result<SecretBuffer, DecodeError> {
1489 self.decode_vec(input).map(SecretBuffer::from_vec)
1490 }
1491
1492 /// Decodes `input` into a newly allocated byte vector using the explicit
1493 /// legacy whitespace profile.
1494 #[cfg(feature = "alloc")]
1495 #[must_use = "for secret-bearing payloads use decode_secret_legacy, which returns a redacted buffer with drop-time cleanup"]
1496 pub fn decode_vec_legacy(&self, input: &[u8]) -> Result<alloc::vec::Vec<u8>, DecodeError> {
1497 let required = validate_legacy_decode::<A, PAD>(input)?;
1498 let mut output = alloc::vec![0; required];
1499 let written = match self.decode_slice_legacy(input, &mut output) {
1500 Ok(written) => written,
1501 Err(err) => {
1502 wipe_bytes(&mut output);
1503 return Err(err);
1504 }
1505 };
1506 output.truncate(written);
1507 Ok(output)
1508 }
1509
1510 /// Decodes `input` into a redacted owned secret buffer using the explicit
1511 /// legacy whitespace profile.
1512 ///
1513 /// ASCII space, tab, carriage return, and line feed bytes are ignored.
1514 /// Alphabet selection, padding placement, trailing data after padding, and
1515 /// non-canonical trailing bits remain strict.
1516 #[cfg(feature = "alloc")]
1517 pub fn decode_secret_legacy(&self, input: &[u8]) -> Result<SecretBuffer, DecodeError> {
1518 self.decode_vec_legacy(input).map(SecretBuffer::from_vec)
1519 }
1520
1521 /// Decodes line-wrapped input into a newly allocated byte vector.
1522 #[cfg(feature = "alloc")]
1523 #[must_use = "for secret-bearing payloads use decode_wrapped_secret, which returns a redacted buffer with drop-time cleanup"]
1524 pub fn decode_wrapped_vec(
1525 &self,
1526 input: &[u8],
1527 wrap: LineWrap,
1528 ) -> Result<alloc::vec::Vec<u8>, DecodeError> {
1529 let required = validate_wrapped_decode::<A, PAD>(input, wrap)?;
1530 let mut output = alloc::vec![0; required];
1531 let written = match self.decode_slice_wrapped(input, &mut output, wrap) {
1532 Ok(written) => written,
1533 Err(err) => {
1534 wipe_bytes(&mut output);
1535 return Err(err);
1536 }
1537 };
1538 output.truncate(written);
1539 Ok(output)
1540 }
1541
1542 /// Decodes line-wrapped input into a redacted owned secret buffer.
1543 ///
1544 /// The wrapped profile accepts only the configured line ending. Non-final
1545 /// lines must contain exactly `wrap.line_len` encoded bytes; the final line
1546 /// may be shorter. A single trailing line ending after the final line is
1547 /// accepted.
1548 #[cfg(feature = "alloc")]
1549 pub fn decode_wrapped_secret(
1550 &self,
1551 input: &[u8],
1552 wrap: LineWrap,
1553 ) -> Result<SecretBuffer, DecodeError> {
1554 self.decode_wrapped_vec(input, wrap)
1555 .map(SecretBuffer::from_vec)
1556 }
1557
1558 /// Decodes `buffer` in place using a strict line-wrapped profile.
1559 ///
1560 /// The wrapped profile accepts only the configured line ending. Non-final
1561 /// lines must contain exactly `wrap.line_len` encoded bytes; the final line
1562 /// may be shorter. A single trailing line ending after the final line is
1563 /// accepted.
1564 ///
1565 /// # Security
1566 ///
1567 /// This method compacts line endings in place before decoding. If
1568 /// validation or decoding fails, the buffer contents are unspecified and
1569 /// may contain a compacted encoded prefix. On success, bytes after the
1570 /// returned decoded prefix may retain the compacted encoded representation.
1571 /// Use
1572 /// [`Self::decode_in_place_wrapped_clear_tail`] when the buffer may be
1573 /// reused or freed without a caller-managed wipe; treat that clear-tail
1574 /// variant as the default for secret-bearing wrapped payloads.
1575 ///
1576 /// # Examples
1577 ///
1578 /// ```
1579 /// use base64_ng::{LineEnding, LineWrap, STANDARD};
1580 ///
1581 /// let mut buffer = *b"aGVs\nbG8=";
1582 /// let decoded = STANDARD
1583 /// .decode_in_place_wrapped(&mut buffer, LineWrap::new(4, LineEnding::Lf))
1584 /// .unwrap();
1585 ///
1586 /// assert_eq!(decoded, b"hello");
1587 /// ```
1588 pub fn decode_in_place_wrapped<'a>(
1589 &self,
1590 buffer: &'a mut [u8],
1591 wrap: LineWrap,
1592 ) -> Result<&'a mut [u8], DecodeError> {
1593 let _required = validate_wrapped_decode::<A, PAD>(buffer, wrap)?;
1594 let compacted = compact_wrapped_input(buffer, wrap)?;
1595 let len = Self::decode_slice_to_start(&mut buffer[..compacted])?;
1596 Ok(&mut buffer[..len])
1597 }
1598
1599 /// Decodes `buffer` in place using a strict line-wrapped profile and clears
1600 /// all bytes after the decoded prefix.
1601 ///
1602 /// If validation or decoding fails, the entire buffer is cleared before the
1603 /// error is returned.
1604 ///
1605 /// # Examples
1606 ///
1607 /// ```
1608 /// use base64_ng::{LineEnding, LineWrap, STANDARD};
1609 ///
1610 /// let mut buffer = *b"aGVs\nbG8=";
1611 /// let len = STANDARD
1612 /// .decode_in_place_wrapped_clear_tail(&mut buffer, LineWrap::new(4, LineEnding::Lf))
1613 /// .unwrap()
1614 /// .len();
1615 ///
1616 /// assert_eq!(&buffer[..len], b"hello");
1617 /// assert!(buffer[len..].iter().all(|byte| *byte == 0));
1618 /// ```
1619 pub fn decode_in_place_wrapped_clear_tail<'a>(
1620 &self,
1621 buffer: &'a mut [u8],
1622 wrap: LineWrap,
1623 ) -> Result<&'a mut [u8], DecodeError> {
1624 if let Err(err) = validate_wrapped_decode::<A, PAD>(buffer, wrap) {
1625 wipe_bytes(buffer);
1626 return Err(err);
1627 }
1628
1629 let compacted = match compact_wrapped_input(buffer, wrap) {
1630 Ok(compacted) => compacted,
1631 Err(err) => {
1632 wipe_bytes(buffer);
1633 return Err(err);
1634 }
1635 };
1636
1637 let len = match Self::decode_slice_to_start(&mut buffer[..compacted]) {
1638 Ok(len) => len,
1639 Err(err) => {
1640 wipe_bytes(buffer);
1641 return Err(err);
1642 }
1643 };
1644 wipe_tail(buffer, len);
1645 Ok(&mut buffer[..len])
1646 }
1647
1648 /// Decodes the buffer in place and returns the decoded prefix.
1649 ///
1650 /// On success, bytes after the returned decoded prefix may retain encoded
1651 /// input bytes. Use [`Self::decode_in_place_clear_tail`] when the buffer
1652 /// may be reused or freed without a caller-managed wipe.
1653 ///
1654 /// # Security
1655 ///
1656 /// This default scalar decoder prioritizes strict validation, exact error
1657 /// reporting, and ordinary throughput. It may branch or return early based
1658 /// on malformed input and reports exact failure positions and invalid byte
1659 /// values through [`DecodeError`]. Do not use this method for token
1660 /// comparison, key-material decoding, or secret-bearing validation where
1661 /// malformed-input timing matters.
1662 ///
1663 /// # Examples
1664 ///
1665 /// ```
1666 /// use base64_ng::STANDARD_NO_PAD;
1667 ///
1668 /// let mut buffer = *b"Zm9vYmFy";
1669 /// let decoded = STANDARD_NO_PAD.decode_in_place(&mut buffer).unwrap();
1670 /// assert_eq!(decoded, b"foobar");
1671 /// ```
1672 pub fn decode_in_place<'a>(&self, buffer: &'a mut [u8]) -> Result<&'a mut [u8], DecodeError> {
1673 let len = Self::decode_slice_to_start(buffer)?;
1674 Ok(&mut buffer[..len])
1675 }
1676
1677 /// Decodes the buffer in place and clears all bytes after the decoded prefix.
1678 ///
1679 /// If decoding fails, the entire buffer is cleared before the error is
1680 /// returned. Use this variant when the encoded or partially decoded data is
1681 /// sensitive and the caller wants best-effort cleanup without adding a
1682 /// dependency.
1683 ///
1684 /// # Examples
1685 ///
1686 /// ```
1687 /// use base64_ng::STANDARD;
1688 ///
1689 /// let mut buffer = *b"aGk=";
1690 /// let decoded = STANDARD.decode_in_place_clear_tail(&mut buffer).unwrap();
1691 /// assert_eq!(decoded, b"hi");
1692 /// ```
1693 pub fn decode_in_place_clear_tail<'a>(
1694 &self,
1695 buffer: &'a mut [u8],
1696 ) -> Result<&'a mut [u8], DecodeError> {
1697 let len = match Self::decode_slice_to_start(buffer) {
1698 Ok(len) => len,
1699 Err(err) => {
1700 wipe_bytes(buffer);
1701 return Err(err);
1702 }
1703 };
1704 wipe_tail(buffer, len);
1705 Ok(&mut buffer[..len])
1706 }
1707
1708 /// Decodes `buffer` in place using the explicit legacy whitespace profile.
1709 ///
1710 /// Ignored whitespace is compacted out before decoding. If validation
1711 /// fails, the buffer contents are unspecified. On success, bytes after the
1712 /// returned decoded prefix may retain the compacted encoded
1713 /// representation. Use [`Self::decode_in_place_legacy_clear_tail`] when the
1714 /// buffer may be reused or freed without a caller-managed wipe.
1715 pub fn decode_in_place_legacy<'a>(
1716 &self,
1717 buffer: &'a mut [u8],
1718 ) -> Result<&'a mut [u8], DecodeError> {
1719 let _required = validate_legacy_decode::<A, PAD>(buffer)?;
1720 let mut write = 0;
1721 let mut read = 0;
1722 while read < buffer.len() {
1723 let byte = buffer[read];
1724 if !is_legacy_whitespace(byte) {
1725 buffer[write] = byte;
1726 write += 1;
1727 }
1728 read += 1;
1729 }
1730 let len = Self::decode_slice_to_start(&mut buffer[..write])?;
1731 Ok(&mut buffer[..len])
1732 }
1733
1734 /// Decodes `buffer` in place using the explicit legacy whitespace profile
1735 /// and clears all bytes after the decoded prefix.
1736 ///
1737 /// If validation or decoding fails, the entire buffer is cleared before the
1738 /// error is returned.
1739 pub fn decode_in_place_legacy_clear_tail<'a>(
1740 &self,
1741 buffer: &'a mut [u8],
1742 ) -> Result<&'a mut [u8], DecodeError> {
1743 if let Err(err) = validate_legacy_decode::<A, PAD>(buffer) {
1744 wipe_bytes(buffer);
1745 return Err(err);
1746 }
1747
1748 let mut write = 0;
1749 let mut read = 0;
1750 while read < buffer.len() {
1751 let byte = buffer[read];
1752 if !is_legacy_whitespace(byte) {
1753 buffer[write] = byte;
1754 write += 1;
1755 }
1756 read += 1;
1757 }
1758
1759 let len = match Self::decode_slice_to_start(&mut buffer[..write]) {
1760 Ok(len) => len,
1761 Err(err) => {
1762 wipe_bytes(buffer);
1763 return Err(err);
1764 }
1765 };
1766 wipe_tail(buffer, len);
1767 Ok(&mut buffer[..len])
1768 }
1769
1770 fn decode_slice_to_start(buffer: &mut [u8]) -> Result<usize, DecodeError> {
1771 let _required = validate_decode::<A, PAD>(buffer)?;
1772 let input_len = buffer.len();
1773 let mut read = 0;
1774 let mut write = 0;
1775 while read + 4 <= input_len {
1776 let chunk = read_quad(buffer, read)?;
1777 let available = buffer.len();
1778 let output_tail = buffer.get_mut(write..).ok_or(DecodeError::OutputTooSmall {
1779 required: write,
1780 available,
1781 })?;
1782 let written = decode_chunk::<A, PAD>(chunk, output_tail)
1783 .map_err(|err| err.with_index_offset(read))?;
1784 read += 4;
1785 write += written;
1786 if written < 3 {
1787 if read != input_len {
1788 return Err(DecodeError::InvalidPadding { index: read - 4 });
1789 }
1790 return Ok(write);
1791 }
1792 }
1793
1794 let rem = input_len - read;
1795 if rem == 0 {
1796 return Ok(write);
1797 }
1798 if PAD {
1799 return Err(DecodeError::InvalidLength);
1800 }
1801 let mut tail = [0u8; 3];
1802 tail[..rem].copy_from_slice(&buffer[read..input_len]);
1803 decode_tail_unpadded::<A>(&tail[..rem], &mut buffer[write..])
1804 .map_err(|err| err.with_index_offset(read))
1805 .map(|n| write + n)
1806 }
1807}
1808
1809#[cfg(kani)]
1810mod kani_proofs {
1811 use super::{
1812 STANDARD, Standard, checked_encoded_len, ct, decode_byte, decode_chunk,
1813 decode_tail_unpadded, decoded_capacity, validate_tail_unpadded,
1814 };
1815
1816 #[kani::proof]
1817 fn checked_encoded_len_is_bounded_for_small_inputs() {
1818 let len = usize::from(kani::any::<u8>());
1819 let padded = kani::any::<bool>();
1820 let encoded = checked_encoded_len(len, padded).expect("u8 input length cannot overflow");
1821
1822 assert!(encoded >= len);
1823 assert!(encoded <= len / 3 * 4 + 4);
1824 }
1825
1826 #[kani::proof]
1827 fn decoded_capacity_is_bounded_for_small_inputs() {
1828 let len = usize::from(kani::any::<u8>());
1829 let capacity = decoded_capacity(len);
1830
1831 assert!(capacity <= len / 4 * 3 + 2);
1832 }
1833
1834 #[kani::proof]
1835 #[kani::unwind(70)]
1836 fn standard_in_place_decode_returns_prefix_within_buffer() {
1837 let mut buffer = kani::any::<[u8; 8]>();
1838 let result = STANDARD.decode_in_place(&mut buffer);
1839
1840 if let Ok(decoded) = result {
1841 assert!(decoded.len() <= 8);
1842 }
1843 }
1844
1845 #[kani::proof]
1846 #[kani::unwind(70)]
1847 fn standard_decode_slice_returns_written_within_output() {
1848 let input = kani::any::<[u8; 4]>();
1849 let mut output = kani::any::<[u8; 3]>();
1850 let result = STANDARD.decode_slice(&input, &mut output);
1851
1852 if let Ok(written) = result {
1853 assert!(written <= output.len());
1854 }
1855 }
1856
1857 #[kani::proof]
1858 #[kani::unwind(70)]
1859 fn standard_decode_chunk_returns_written_within_output() {
1860 let input = kani::any::<[u8; 4]>();
1861 let mut output = kani::any::<[u8; 3]>();
1862 let result = decode_chunk::<Standard, true>(input, &mut output);
1863
1864 if let Ok(written) = result {
1865 assert!(written <= output.len());
1866 assert!(written <= 3);
1867 }
1868 }
1869
1870 #[kani::proof]
1871 #[kani::unwind(70)]
1872 fn standard_decode_chunk_bit_packing_matches_decoded_values() {
1873 let input = kani::any::<[u8; 4]>();
1874 let mut output = kani::any::<[u8; 3]>();
1875 let result = decode_chunk::<Standard, true>(input, &mut output);
1876
1877 if let Ok(written) = result {
1878 let v0 = decode_byte::<Standard>(input[0], 0).expect("successful chunk has v0");
1879 let v1 = decode_byte::<Standard>(input[1], 1).expect("successful chunk has v1");
1880
1881 assert!(output[0] == ((v0 << 2) | (v1 >> 4)));
1882
1883 if written >= 2 {
1884 let v2 = decode_byte::<Standard>(input[2], 2).expect("successful chunk has v2");
1885 assert!(output[1] == ((v1 << 4) | (v2 >> 2)));
1886 }
1887
1888 if written == 3 {
1889 let v2 = decode_byte::<Standard>(input[2], 2).expect("successful chunk has v2");
1890 let v3 = decode_byte::<Standard>(input[3], 3).expect("successful chunk has v3");
1891 assert!(output[2] == ((v2 << 6) | v3));
1892 }
1893 }
1894 }
1895
1896 #[kani::proof]
1897 #[kani::unwind(70)]
1898 fn standard_validate_tail_unpadded_accepts_or_rejects_without_panic() {
1899 let input = kani::any::<[u8; 3]>();
1900 let len = usize::from(kani::any::<u8>() % 4);
1901 let result = validate_tail_unpadded::<Standard>(&input[..len]);
1902
1903 if result.is_ok() {
1904 assert!(len == 0 || len == 2 || len == 3);
1905 }
1906 }
1907
1908 #[kani::proof]
1909 #[kani::unwind(70)]
1910 fn standard_decode_two_byte_tail_returns_written_within_output() {
1911 let input = kani::any::<[u8; 2]>();
1912 let mut output = kani::any::<[u8; 1]>();
1913 let result = decode_tail_unpadded::<Standard>(&input, &mut output);
1914
1915 if let Ok(written) = result {
1916 assert!(written <= output.len());
1917 assert!(written == 1);
1918 }
1919 }
1920
1921 #[kani::proof]
1922 #[kani::unwind(70)]
1923 fn standard_decode_three_byte_tail_returns_written_within_output() {
1924 let input = kani::any::<[u8; 3]>();
1925 let mut output = kani::any::<[u8; 2]>();
1926 let result = decode_tail_unpadded::<Standard>(&input, &mut output);
1927
1928 if let Ok(written) = result {
1929 assert!(written <= output.len());
1930 assert!(written == 2);
1931 }
1932 }
1933
1934 #[kani::proof]
1935 #[kani::unwind(70)]
1936 fn standard_decode_slice_clear_tail_clears_output_on_error() {
1937 let input = kani::any::<[u8; 4]>();
1938 let mut output = kani::any::<[u8; 3]>();
1939 let result = STANDARD.decode_slice_clear_tail(&input, &mut output);
1940
1941 if result.is_err() {
1942 assert!(output.iter().all(|byte| *byte == 0));
1943 }
1944 }
1945
1946 #[kani::proof]
1947 #[kani::unwind(70)]
1948 fn standard_encode_slice_returns_written_within_output() {
1949 let input = kani::any::<[u8; 3]>();
1950 let mut output = kani::any::<[u8; 4]>();
1951 let result = STANDARD.encode_slice(&input, &mut output);
1952
1953 if let Ok(written) = result {
1954 assert!(written <= output.len());
1955 }
1956 }
1957
1958 #[kani::proof]
1959 #[kani::unwind(70)]
1960 fn standard_encode_in_place_returns_prefix_within_buffer() {
1961 let mut buffer = kani::any::<[u8; 8]>();
1962 let input_len = usize::from(kani::any::<u8>() % 9);
1963 let result = STANDARD.encode_in_place(&mut buffer, input_len);
1964
1965 if let Ok(encoded) = result {
1966 assert!(encoded.len() <= 8);
1967 }
1968 }
1969
1970 #[kani::proof]
1971 #[kani::unwind(70)]
1972 fn standard_clear_tail_decode_clears_buffer_on_error() {
1973 let mut buffer = kani::any::<[u8; 4]>();
1974 let result = STANDARD.decode_in_place_clear_tail(&mut buffer);
1975
1976 if result.is_err() {
1977 assert!(buffer.iter().all(|byte| *byte == 0));
1978 }
1979 }
1980
1981 #[kani::proof]
1982 #[kani::unwind(70)]
1983 fn ct_standard_decode_slice_returns_written_within_output() {
1984 let input = kani::any::<[u8; 4]>();
1985 let mut output = kani::any::<[u8; 3]>();
1986 let result = ct::STANDARD.decode_slice_clear_tail(&input, &mut output);
1987
1988 if let Ok(written) = result {
1989 assert!(written <= output.len());
1990 }
1991 }
1992
1993 #[kani::proof]
1994 #[kani::unwind(70)]
1995 fn ct_standard_decode_slice_clear_tail_clears_output_on_error() {
1996 let input = kani::any::<[u8; 4]>();
1997 let mut output = kani::any::<[u8; 3]>();
1998 let result = ct::STANDARD.decode_slice_clear_tail(&input, &mut output);
1999
2000 if result.is_err() {
2001 assert!(output.iter().all(|byte| *byte == 0));
2002 }
2003 }
2004
2005 #[kani::proof]
2006 #[kani::unwind(70)]
2007 fn ct_standard_decode_in_place_clear_tail_clears_buffer_on_error() {
2008 let mut buffer = kani::any::<[u8; 4]>();
2009 let result = ct::STANDARD.decode_in_place_clear_tail(&mut buffer);
2010
2011 if result.is_err() {
2012 assert!(buffer.iter().all(|byte| *byte == 0));
2013 }
2014 }
2015
2016 #[kani::proof]
2017 #[kani::unwind(70)]
2018 fn ct_standard_validate_matches_decode_for_one_quantum() {
2019 let input = kani::any::<[u8; 4]>();
2020 let mut output = kani::any::<[u8; 3]>();
2021
2022 let validate_ok = ct::STANDARD.validate_result(&input).is_ok();
2023 let decode_ok = ct::STANDARD
2024 .decode_slice_clear_tail(&input, &mut output)
2025 .is_ok();
2026
2027 assert!(validate_ok == decode_ok);
2028 }
2029}
2030
2031#[cfg(test)]
2032mod tests {
2033 use super::*;
2034
2035 fn fill_pattern(output: &mut [u8], seed: usize) {
2036 for (index, byte) in output.iter_mut().enumerate() {
2037 let value = (index * 73 + seed * 19) % 256;
2038 *byte = u8::try_from(value).unwrap();
2039 }
2040 }
2041
2042 fn assert_encode_backend_matches_scalar<A, const PAD: bool>(input: &[u8])
2043 where
2044 A: Alphabet,
2045 {
2046 let engine = Engine::<A, PAD>::new();
2047 let mut dispatched = [0x55; 256];
2048 let mut scalar = [0xaa; 256];
2049
2050 let dispatched_result = engine.encode_slice(input, &mut dispatched);
2051 let scalar_result = scalar::scalar_reference_encode_slice::<A, PAD>(input, &mut scalar);
2052
2053 assert_eq!(dispatched_result, scalar_result);
2054 if let Ok(written) = dispatched_result {
2055 assert_eq!(&dispatched[..written], &scalar[..written]);
2056 }
2057
2058 let required = checked_encoded_len(input.len(), PAD).unwrap();
2059 if required > 0 {
2060 let mut dispatched_short = [0x55; 256];
2061 let mut scalar_short = [0xaa; 256];
2062 let available = required - 1;
2063
2064 assert_eq!(
2065 engine.encode_slice(input, &mut dispatched_short[..available]),
2066 scalar::scalar_reference_encode_slice::<A, PAD>(
2067 input,
2068 &mut scalar_short[..available],
2069 )
2070 );
2071 }
2072 }
2073
2074 fn assert_decode_backend_matches_scalar<A, const PAD: bool>(input: &[u8])
2075 where
2076 A: Alphabet,
2077 {
2078 let engine = Engine::<A, PAD>::new();
2079 let mut dispatched = [0x55; 128];
2080 let mut scalar = [0xaa; 128];
2081
2082 let dispatched_result = engine.decode_slice(input, &mut dispatched);
2083 let scalar_result = scalar::scalar_reference_decode_slice::<A, PAD>(input, &mut scalar);
2084
2085 assert_eq!(dispatched_result, scalar_result);
2086 if let Ok(written) = dispatched_result {
2087 assert_eq!(&dispatched[..written], &scalar[..written]);
2088
2089 if written > 0 {
2090 let mut dispatched_short = [0x55; 128];
2091 let mut scalar_short = [0xaa; 128];
2092 let available = written - 1;
2093
2094 assert_eq!(
2095 engine.decode_slice(input, &mut dispatched_short[..available]),
2096 scalar::scalar_reference_decode_slice::<A, PAD>(
2097 input,
2098 &mut scalar_short[..available],
2099 )
2100 );
2101 }
2102 }
2103 }
2104
2105 fn assert_backend_round_trip_matches_scalar<A, const PAD: bool>(input: &[u8])
2106 where
2107 A: Alphabet,
2108 {
2109 assert_encode_backend_matches_scalar::<A, PAD>(input);
2110
2111 let mut encoded = [0; 256];
2112 let encoded_len =
2113 scalar::scalar_reference_encode_slice::<A, PAD>(input, &mut encoded).unwrap();
2114 assert_decode_backend_matches_scalar::<A, PAD>(&encoded[..encoded_len]);
2115 }
2116
2117 fn assert_standard_decode_chunk_matches_input(input: &[u8]) {
2118 let mut encoded = [0u8; 4];
2119 let encoded_len = STANDARD.encode_slice(input, &mut encoded).unwrap();
2120 assert_eq!(encoded_len, 4);
2121
2122 let chunk = [encoded[0], encoded[1], encoded[2], encoded[3]];
2123 let mut decoded = [0u8; 3];
2124 let decoded_len = decode_chunk::<Standard, true>(chunk, &mut decoded).unwrap();
2125
2126 assert_eq!(decoded_len, input.len());
2127 assert_eq!(&decoded[..decoded_len], input);
2128 }
2129
2130 #[test]
2131 fn backend_dispatch_matches_scalar_reference_for_canonical_inputs() {
2132 let mut input = [0; 128];
2133
2134 for input_len in 0..=input.len() {
2135 fill_pattern(&mut input[..input_len], input_len);
2136 let input = &input[..input_len];
2137
2138 assert_backend_round_trip_matches_scalar::<Standard, true>(input);
2139 assert_backend_round_trip_matches_scalar::<Standard, false>(input);
2140 assert_backend_round_trip_matches_scalar::<UrlSafe, true>(input);
2141 assert_backend_round_trip_matches_scalar::<UrlSafe, false>(input);
2142 }
2143 }
2144
2145 #[test]
2146 fn backend_dispatch_matches_scalar_reference_for_malformed_inputs() {
2147 for input in [
2148 &b"Z"[..],
2149 b"====",
2150 b"AA=A",
2151 b"Zh==",
2152 b"Zm9=",
2153 b"Zm9v$g==",
2154 b"Zm9vZh==",
2155 ] {
2156 assert_decode_backend_matches_scalar::<Standard, true>(input);
2157 }
2158
2159 for input in [&b"Z"[..], b"AA=A", b"Zh", b"Zm9", b"Zm9vYg$"] {
2160 assert_decode_backend_matches_scalar::<Standard, false>(input);
2161 }
2162
2163 assert_decode_backend_matches_scalar::<UrlSafe, true>(b"AA+A");
2164 assert_decode_backend_matches_scalar::<UrlSafe, false>(b"AA/A");
2165 assert_decode_backend_matches_scalar::<Standard, true>(b"AA-A");
2166 assert_decode_backend_matches_scalar::<Standard, false>(b"AA_A");
2167 }
2168
2169 #[test]
2170 fn decode_chunk_bit_packing_matches_exhaustive_small_inputs() {
2171 for byte in u8::MIN..=u8::MAX {
2172 assert_standard_decode_chunk_matches_input(&[byte]);
2173 }
2174
2175 for first in u8::MIN..=u8::MAX {
2176 for second in u8::MIN..=u8::MAX {
2177 assert_standard_decode_chunk_matches_input(&[first, second]);
2178 }
2179 }
2180 }
2181
2182 #[test]
2183 fn decode_chunk_bit_packing_matches_representative_full_quanta() {
2184 const SAMPLES: [u8; 16] = [
2185 0, 1, 2, 15, 16, 31, 32, 63, 64, 95, 127, 128, 191, 192, 254, 255,
2186 ];
2187
2188 for first in SAMPLES {
2189 for second in SAMPLES {
2190 for third in SAMPLES {
2191 assert_standard_decode_chunk_matches_input(&[first, second, third]);
2192 }
2193 }
2194 }
2195 }
2196
2197 #[test]
2198 fn ct_padded_final_quantum_fails_closed_for_invalid_padding_count() {
2199 let (_, invalid_byte, invalid_padding, written) =
2200 ct_padded_final_quantum::<Standard>(*b"ABCD", 3);
2201
2202 assert_ne!(invalid_byte, 0);
2203 assert_ne!(invalid_padding, 0);
2204 assert_eq!(written, 0);
2205 assert_eq!(
2206 report_ct_error(invalid_byte, invalid_padding),
2207 Err(DecodeError::InvalidInput)
2208 );
2209 }
2210
2211 #[cfg(feature = "simd")]
2212 #[test]
2213 fn simd_dispatch_scaffold_keeps_scalar_active() {
2214 assert_eq!(simd::active_backend(), simd::ActiveBackend::Scalar);
2215 let _candidate = simd::detected_candidate();
2216 }
2217
2218 #[test]
2219 fn encodes_standard_vectors() {
2220 let vectors = [
2221 (&b""[..], &b""[..]),
2222 (&b"f"[..], &b"Zg=="[..]),
2223 (&b"fo"[..], &b"Zm8="[..]),
2224 (&b"foo"[..], &b"Zm9v"[..]),
2225 (&b"foob"[..], &b"Zm9vYg=="[..]),
2226 (&b"fooba"[..], &b"Zm9vYmE="[..]),
2227 (&b"foobar"[..], &b"Zm9vYmFy"[..]),
2228 ];
2229 for (input, expected) in vectors {
2230 let mut output = [0u8; 16];
2231 let written = STANDARD.encode_slice(input, &mut output).unwrap();
2232 assert_eq!(&output[..written], expected);
2233 }
2234 }
2235
2236 #[test]
2237 fn decodes_standard_vectors() {
2238 let vectors = [
2239 (&b""[..], &b""[..]),
2240 (&b"Zg=="[..], &b"f"[..]),
2241 (&b"Zm8="[..], &b"fo"[..]),
2242 (&b"Zm9v"[..], &b"foo"[..]),
2243 (&b"Zm9vYg=="[..], &b"foob"[..]),
2244 (&b"Zm9vYmE="[..], &b"fooba"[..]),
2245 (&b"Zm9vYmFy"[..], &b"foobar"[..]),
2246 ];
2247 for (input, expected) in vectors {
2248 let mut output = [0u8; 16];
2249 let written = STANDARD.decode_slice(input, &mut output).unwrap();
2250 assert_eq!(&output[..written], expected);
2251 }
2252 }
2253
2254 #[test]
2255 fn supports_unpadded_url_safe() {
2256 let mut encoded = [0u8; 16];
2257 let written = URL_SAFE_NO_PAD
2258 .encode_slice(b"\xfb\xff", &mut encoded)
2259 .unwrap();
2260 assert_eq!(&encoded[..written], b"-_8");
2261
2262 let mut decoded = [0u8; 2];
2263 let written = URL_SAFE_NO_PAD
2264 .decode_slice(&encoded[..written], &mut decoded)
2265 .unwrap();
2266 assert_eq!(&decoded[..written], b"\xfb\xff");
2267 }
2268
2269 #[test]
2270 fn decodes_in_place() {
2271 let mut buffer = *b"Zm9vYmFy";
2272 let decoded = STANDARD_NO_PAD.decode_in_place(&mut buffer).unwrap();
2273 assert_eq!(decoded, b"foobar");
2274 }
2275
2276 #[test]
2277 fn rejects_non_canonical_padding_bits() {
2278 let mut output = [0u8; 4];
2279 assert_eq!(
2280 STANDARD.decode_slice(b"Zh==", &mut output),
2281 Err(DecodeError::InvalidPadding { index: 1 })
2282 );
2283 assert_eq!(
2284 STANDARD.decode_slice(b"Zm9=", &mut output),
2285 Err(DecodeError::InvalidPadding { index: 2 })
2286 );
2287 }
2288}