#![cfg_attr(not(feature = "std"), no_std)]
#![deny(unsafe_code)]
#![deny(missing_docs)]
#![deny(clippy::all)]
#![deny(clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(all(target_arch = "wasm32", not(feature = "allow-wasm32-best-effort-wipe")))]
compile_error!(
"base64-ng: wasm32 builds use a compiler-fence-only wipe barrier that cannot \
constrain downstream wasm runtime JITs. Enable \
`allow-wasm32-best-effort-wipe` to accept this limitation and use \
caller-owned, platform-approved zeroization for high-assurance wasm deployments."
);
#[cfg(all(
not(miri),
not(feature = "allow-compiler-fence-only-wipe"),
not(any(
target_arch = "aarch64",
target_arch = "arm",
target_arch = "riscv32",
target_arch = "riscv64",
target_arch = "wasm32",
target_arch = "x86",
target_arch = "x86_64",
))
))]
compile_error!(
"base64-ng: this architecture has no native hardware wipe barrier in \
base64-ng. Enable `allow-compiler-fence-only-wipe` only after reviewing \
docs/UNSAFE.md and applying platform-approved memory hygiene controls."
);
mod alphabet;
mod buffers;
mod cleanup;
mod profiles;
pub use alphabet::{
Alphabet, AlphabetError, Bcrypt, Crypt, Standard, UrlSafe, decode_alphabet_byte,
validate_alphabet,
};
pub(crate) use alphabet::{encode_base64_value, encode_base64_value_runtime};
pub use buffers::{DecodedBuffer, EncodedBuffer, ExposedDecodedArray, ExposedEncodedArray};
#[cfg(feature = "alloc")]
pub use buffers::{ExposedSecretString, ExposedSecretVec, SecretBuffer};
pub(crate) use cleanup::{wipe_bytes, wipe_tail};
#[cfg(feature = "alloc")]
pub(crate) use cleanup::{wipe_vec_all, wipe_vec_spare_capacity};
pub use profiles::{BCRYPT, CRYPT, MIME, PEM, PEM_CRLF, Profile};
#[cfg(feature = "simd")]
mod simd;
pub mod runtime;
#[cfg(feature = "stream")]
pub mod stream;
pub mod ct {
use super::{
Alphabet, DecodeError, DecodedBuffer, Standard, UrlSafe, ct_decode_in_place,
ct_decode_slice, ct_decode_slice_staged_clear_tail, ct_decoded_len, ct_validate_decode,
};
use core::marker::PhantomData;
pub const STANDARD: CtEngine<Standard, true> = CtEngine::new();
pub const STANDARD_NO_PAD: CtEngine<Standard, false> = CtEngine::new();
pub const URL_SAFE: CtEngine<UrlSafe, true> = CtEngine::new();
pub const URL_SAFE_NO_PAD: CtEngine<UrlSafe, false> = CtEngine::new();
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct CtEngine<A, const PAD: bool> {
alphabet: PhantomData<A>,
}
impl<A, const PAD: bool> CtEngine<A, PAD>
where
A: Alphabet,
{
#[must_use]
pub const fn new() -> Self {
Self {
alphabet: PhantomData,
}
}
#[must_use]
pub const fn is_padded(&self) -> bool {
PAD
}
pub fn validate_result(&self, input: &[u8]) -> Result<(), DecodeError> {
ct_validate_decode::<A, PAD>(input)
}
#[must_use]
pub fn validate(&self, input: &[u8]) -> bool {
self.validate_result(input).is_ok()
}
pub fn decoded_len(&self, input: &[u8]) -> Result<usize, DecodeError> {
ct_decoded_len::<A, PAD>(input)
}
#[must_use = "handle decode errors; use decode_slice_staged_clear_tail for shared-memory or HSM-style threat models"]
pub fn decode_slice_clear_tail(
&self,
input: &[u8],
output: &mut [u8],
) -> Result<usize, DecodeError> {
let written = match ct_decode_slice::<A, PAD>(input, output) {
Ok(written) => written,
Err(err) => {
crate::wipe_bytes(output);
return Err(err);
}
};
crate::wipe_tail(output, written);
Ok(written)
}
#[must_use = "handle decode errors; staged decode is for shared-memory or HSM-style threat models"]
pub fn decode_slice_staged_clear_tail(
&self,
input: &[u8],
output: &mut [u8],
staging: &mut [u8],
) -> Result<usize, DecodeError> {
ct_decode_slice_staged_clear_tail::<A, PAD>(input, output, staging)
}
pub fn decode_buffer<const CAP: usize>(
&self,
input: &[u8],
) -> Result<DecodedBuffer<CAP>, DecodeError> {
let mut output = DecodedBuffer::new();
let written = match self.decode_slice_clear_tail(input, output.as_mut_capacity()) {
Ok(written) => written,
Err(err) => {
output.clear();
return Err(err);
}
};
output.set_filled(written)?;
Ok(output)
}
pub fn decode_in_place_clear_tail<'a>(
&self,
buffer: &'a mut [u8],
) -> Result<&'a mut [u8], DecodeError> {
let len = match ct_decode_in_place::<A, PAD>(buffer) {
Ok(len) => len,
Err(err) => {
crate::wipe_bytes(buffer);
return Err(err);
}
};
crate::wipe_tail(buffer, len);
Ok(&mut buffer[..len])
}
}
impl<A, const PAD: bool> core::fmt::Display for CtEngine<A, PAD> {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(formatter, "ct padded={PAD}")
}
}
}
#[doc(alias = "ct")]
#[doc(alias = "constant_time")]
#[doc(alias = "sensitive")]
pub const STANDARD: Engine<Standard, true> = Engine::new();
#[doc(alias = "ct")]
#[doc(alias = "constant_time")]
#[doc(alias = "sensitive")]
pub const STANDARD_NO_PAD: Engine<Standard, false> = Engine::new();
#[doc(alias = "ct")]
#[doc(alias = "constant_time")]
#[doc(alias = "sensitive")]
pub const URL_SAFE: Engine<UrlSafe, true> = Engine::new();
#[doc(alias = "ct")]
#[doc(alias = "constant_time")]
#[doc(alias = "sensitive")]
pub const URL_SAFE_NO_PAD: Engine<UrlSafe, false> = Engine::new();
#[doc(alias = "ct")]
#[doc(alias = "constant_time")]
#[doc(alias = "sensitive")]
pub const BCRYPT_NO_PAD: Engine<Bcrypt, false> = Engine::new();
#[doc(alias = "ct")]
#[doc(alias = "constant_time")]
#[doc(alias = "sensitive")]
pub const CRYPT_NO_PAD: Engine<Crypt, false> = Engine::new();
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LineEnding {
Lf,
CrLf,
}
impl LineEnding {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Lf => "LF",
Self::CrLf => "CRLF",
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Lf => "\n",
Self::CrLf => "\r\n",
}
}
#[must_use]
pub const fn as_bytes(self) -> &'static [u8] {
self.as_str().as_bytes()
}
#[must_use]
pub const fn byte_len(self) -> usize {
match self {
Self::Lf => 1,
Self::CrLf => 2,
}
}
}
impl core::fmt::Display for LineEnding {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter.write_str(self.name())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LineWrap {
pub line_len: usize,
pub line_ending: LineEnding,
}
impl LineWrap {
pub const MIME: Self = Self::new(76, LineEnding::CrLf);
pub const PEM: Self = Self::new(64, LineEnding::Lf);
pub const PEM_CRLF: Self = Self::new(64, LineEnding::CrLf);
#[must_use]
pub const fn new(line_len: usize, line_ending: LineEnding) -> Self {
assert!(line_len != 0, "base64 line wrap length must be non-zero");
Self {
line_len,
line_ending,
}
}
#[must_use]
pub const fn checked_new(line_len: usize, line_ending: LineEnding) -> Option<Self> {
if line_len == 0 {
None
} else {
Some(Self::new(line_len, line_ending))
}
}
#[must_use]
pub const fn line_len(self) -> usize {
self.line_len
}
#[must_use]
pub const fn line_ending(self) -> LineEnding {
self.line_ending
}
#[must_use]
pub const fn is_valid(self) -> bool {
self.line_len != 0
}
}
impl core::fmt::Display for LineWrap {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(formatter, "{}:{}", self.line_len, self.line_ending.name())
}
}
pub const fn encoded_len(input_len: usize, padded: bool) -> Result<usize, EncodeError> {
match checked_encoded_len(input_len, padded) {
Some(len) => Ok(len),
None => Err(EncodeError::LengthOverflow),
}
}
pub const fn wrapped_encoded_len(
input_len: usize,
padded: bool,
wrap: LineWrap,
) -> Result<usize, EncodeError> {
if wrap.line_len == 0 {
return Err(EncodeError::InvalidLineWrap { line_len: 0 });
}
let Some(encoded) = checked_encoded_len(input_len, padded) else {
return Err(EncodeError::LengthOverflow);
};
if encoded == 0 {
return Ok(0);
}
let breaks = (encoded - 1) / wrap.line_len;
let Some(line_ending_bytes) = breaks.checked_mul(wrap.line_ending.byte_len()) else {
return Err(EncodeError::LengthOverflow);
};
match encoded.checked_add(line_ending_bytes) {
Some(len) => Ok(len),
None => Err(EncodeError::LengthOverflow),
}
}
#[must_use]
pub const fn checked_wrapped_encoded_len(
input_len: usize,
padded: bool,
wrap: LineWrap,
) -> Option<usize> {
if wrap.line_len == 0 {
return None;
}
let Some(encoded) = checked_encoded_len(input_len, padded) else {
return None;
};
if encoded == 0 {
return Some(0);
}
let breaks = (encoded - 1) / wrap.line_len;
let Some(line_ending_bytes) = breaks.checked_mul(wrap.line_ending.byte_len()) else {
return None;
};
encoded.checked_add(line_ending_bytes)
}
#[must_use]
pub const fn checked_encoded_len(input_len: usize, padded: bool) -> Option<usize> {
let groups = input_len / 3;
if groups > usize::MAX / 4 {
return None;
}
let full = groups * 4;
let rem = input_len % 3;
if rem == 0 {
Some(full)
} else if padded {
full.checked_add(4)
} else {
full.checked_add(rem + 1)
}
}
#[must_use]
pub fn constant_time_eq_fixed_width<const N: usize>(left: &[u8; N], right: &[u8; N]) -> bool {
constant_time_eq_fixed_width_array(left, right)
}
#[must_use]
pub const fn decoded_capacity(encoded_len: usize) -> usize {
let rem = encoded_len % 4;
encoded_len / 4 * 3
+ if rem == 2 {
1
} else if rem == 3 {
2
} else {
0
}
}
pub fn decoded_len(input: &[u8], padded: bool) -> Result<usize, DecodeError> {
if padded {
decoded_len_padded(input)
} else {
decoded_len_unpadded(input)
}
}
#[inline]
pub(crate) const fn ct_mask_bit(bit: u8) -> u8 {
0u8.wrapping_sub(bit & 1)
}
#[inline]
pub(crate) const fn ct_mask_nonzero_u8(value: u8) -> u8 {
let wide = value as u16;
let negative = 0u16.wrapping_sub(wide);
let nonzero = ((wide | negative) >> 8) as u8;
ct_mask_bit(nonzero)
}
#[inline]
pub(crate) const fn ct_mask_eq_u8(left: u8, right: u8) -> u8 {
!ct_mask_nonzero_u8(left ^ right)
}
#[inline]
pub(crate) const fn ct_mask_lt_u8(left: u8, right: u8) -> u8 {
let diff = (left as u16).wrapping_sub(right as u16);
ct_mask_bit((diff >> 8) as u8)
}
#[inline(never)]
fn constant_time_eq_public_len(left: &[u8], right: &[u8]) -> bool {
if left.len() != right.len() {
return false;
}
constant_time_eq_same_len(left, right)
}
#[inline(never)]
fn constant_time_eq_fixed_width_array<const N: usize>(left: &[u8; N], right: &[u8; N]) -> bool {
constant_time_eq_same_len(left, right)
}
#[inline(never)]
#[allow(unsafe_code)]
fn constant_time_eq_same_len(left: &[u8], right: &[u8]) -> bool {
let mut diff = 0u8;
for (left, right) in left.iter().zip(right) {
diff = core::hint::black_box(
core::hint::black_box(diff) | core::hint::black_box(*left ^ *right),
);
diff = unsafe { core::ptr::read_volatile(&raw const diff) };
}
ct_error_gate_barrier(diff, 0);
let result = unsafe { core::ptr::read_volatile(&raw const diff) };
result == 0
}
mod backend {
use super::{
Alphabet, DecodeError, EncodeError, checked_encoded_len, decode_padded, decode_unpadded,
encode_base64_value_runtime,
};
pub(super) fn encode_slice<A, const PAD: bool>(
input: &[u8],
output: &mut [u8],
) -> Result<usize, EncodeError>
where
A: Alphabet,
{
#[cfg(feature = "simd")]
match super::simd::active_backend() {
super::simd::ActiveBackend::Scalar => {}
}
scalar_encode_slice::<A, PAD>(input, output)
}
pub(super) fn decode_slice<A, const PAD: bool>(
input: &[u8],
output: &mut [u8],
) -> Result<usize, DecodeError>
where
A: Alphabet,
{
#[cfg(feature = "simd")]
match super::simd::active_backend() {
super::simd::ActiveBackend::Scalar => {}
}
scalar_decode_slice::<A, PAD>(input, output)
}
#[cfg(test)]
pub(super) fn scalar_reference_encode_slice<A, const PAD: bool>(
input: &[u8],
output: &mut [u8],
) -> Result<usize, EncodeError>
where
A: Alphabet,
{
scalar_encode_slice::<A, PAD>(input, output)
}
#[cfg(test)]
pub(super) fn scalar_reference_decode_slice<A, const PAD: bool>(
input: &[u8],
output: &mut [u8],
) -> Result<usize, DecodeError>
where
A: Alphabet,
{
scalar_decode_slice::<A, PAD>(input, output)
}
fn scalar_encode_slice<A, const PAD: bool>(
input: &[u8],
output: &mut [u8],
) -> Result<usize, EncodeError>
where
A: Alphabet,
{
let required = checked_encoded_len(input.len(), PAD).ok_or(EncodeError::LengthOverflow)?;
if output.len() < required {
return Err(EncodeError::OutputTooSmall {
required,
available: output.len(),
});
}
let mut read = 0;
let mut write = 0;
while read + 3 <= input.len() {
let b0 = input[read];
let b1 = input[read + 1];
let b2 = input[read + 2];
output[write] = encode_base64_value_runtime::<A>(b0 >> 2);
output[write + 1] =
encode_base64_value_runtime::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
output[write + 2] =
encode_base64_value_runtime::<A>(((b1 & 0b0000_1111) << 2) | (b2 >> 6));
output[write + 3] = encode_base64_value_runtime::<A>(b2 & 0b0011_1111);
read += 3;
write += 4;
}
match input.len() - read {
0 => {}
1 => {
let b0 = input[read];
output[write] = encode_base64_value_runtime::<A>(b0 >> 2);
output[write + 1] = encode_base64_value_runtime::<A>((b0 & 0b0000_0011) << 4);
write += 2;
if PAD {
output[write] = b'=';
output[write + 1] = b'=';
write += 2;
}
}
2 => {
let b0 = input[read];
let b1 = input[read + 1];
output[write] = encode_base64_value_runtime::<A>(b0 >> 2);
output[write + 1] =
encode_base64_value_runtime::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
output[write + 2] = encode_base64_value_runtime::<A>((b1 & 0b0000_1111) << 2);
write += 3;
if PAD {
output[write] = b'=';
write += 1;
}
}
_ => unreachable!(),
}
Ok(write)
}
fn scalar_decode_slice<A, const PAD: bool>(
input: &[u8],
output: &mut [u8],
) -> Result<usize, DecodeError>
where
A: Alphabet,
{
if input.is_empty() {
return Ok(0);
}
if PAD {
decode_padded::<A>(input, output)
} else {
decode_unpadded::<A>(input, output)
}
}
}
pub struct Engine<A, const PAD: bool> {
alphabet: core::marker::PhantomData<A>,
}
impl<A, const PAD: bool> Clone for Engine<A, PAD> {
fn clone(&self) -> Self {
*self
}
}
impl<A, const PAD: bool> Copy for Engine<A, PAD> {}
impl<A, const PAD: bool> core::fmt::Debug for Engine<A, PAD> {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter
.debug_struct("Engine")
.field("padded", &PAD)
.finish()
}
}
impl<A, const PAD: bool> core::fmt::Display for Engine<A, PAD> {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(formatter, "padded={PAD}")
}
}
impl<A, const PAD: bool> Default for Engine<A, PAD> {
fn default() -> Self {
Self {
alphabet: core::marker::PhantomData,
}
}
}
impl<A, const PAD: bool> Eq for Engine<A, PAD> {}
impl<A, const PAD: bool> PartialEq for Engine<A, PAD> {
fn eq(&self, _other: &Self) -> bool {
true
}
}
impl<A, const PAD: bool> Engine<A, PAD>
where
A: Alphabet,
{
#[must_use]
pub const fn new() -> Self {
Self {
alphabet: core::marker::PhantomData,
}
}
#[must_use]
pub const fn is_padded(&self) -> bool {
PAD
}
#[must_use]
pub const fn profile(&self) -> Profile<A, PAD> {
Profile::new(*self, None)
}
#[must_use]
pub const fn ct_decoder(&self) -> ct::CtEngine<A, PAD> {
ct::CtEngine::new()
}
#[cfg(feature = "stream")]
#[must_use]
pub fn encoder_writer<W>(&self, inner: W) -> stream::Encoder<W, A, PAD> {
stream::Encoder::new(inner, *self)
}
#[cfg(feature = "stream")]
#[must_use]
pub fn decoder_writer<W>(&self, inner: W) -> stream::Decoder<W, A, PAD> {
stream::Decoder::new(inner, *self)
}
#[cfg(feature = "stream")]
#[must_use]
pub fn encoder_reader<R>(&self, inner: R) -> stream::EncoderReader<R, A, PAD> {
stream::EncoderReader::new(inner, *self)
}
#[cfg(feature = "stream")]
#[must_use]
pub fn decoder_reader<R>(&self, inner: R) -> stream::DecoderReader<R, A, PAD> {
stream::DecoderReader::new(inner, *self)
}
pub const fn encoded_len(&self, input_len: usize) -> Result<usize, EncodeError> {
encoded_len(input_len, PAD)
}
#[must_use]
pub const fn checked_encoded_len(&self, input_len: usize) -> Option<usize> {
checked_encoded_len(input_len, PAD)
}
pub const fn wrapped_encoded_len(
&self,
input_len: usize,
wrap: LineWrap,
) -> Result<usize, EncodeError> {
wrapped_encoded_len(input_len, PAD, wrap)
}
#[must_use]
pub const fn checked_wrapped_encoded_len(
&self,
input_len: usize,
wrap: LineWrap,
) -> Option<usize> {
checked_wrapped_encoded_len(input_len, PAD, wrap)
}
pub fn decoded_len(&self, input: &[u8]) -> Result<usize, DecodeError> {
decoded_len(input, PAD)
}
pub fn decoded_len_legacy(&self, input: &[u8]) -> Result<usize, DecodeError> {
validate_legacy_decode::<A, PAD>(input)
}
pub fn decoded_len_wrapped(&self, input: &[u8], wrap: LineWrap) -> Result<usize, DecodeError> {
validate_wrapped_decode::<A, PAD>(input, wrap)
}
pub fn validate_result(&self, input: &[u8]) -> Result<(), DecodeError> {
validate_decode::<A, PAD>(input).map(|_| ())
}
#[must_use]
pub fn validate(&self, input: &[u8]) -> bool {
self.validate_result(input).is_ok()
}
pub fn validate_legacy_result(&self, input: &[u8]) -> Result<(), DecodeError> {
validate_legacy_decode::<A, PAD>(input).map(|_| ())
}
#[must_use]
pub fn validate_legacy(&self, input: &[u8]) -> bool {
self.validate_legacy_result(input).is_ok()
}
pub fn validate_wrapped_result(&self, input: &[u8], wrap: LineWrap) -> Result<(), DecodeError> {
validate_wrapped_decode::<A, PAD>(input, wrap).map(|_| ())
}
#[must_use]
pub fn validate_wrapped(&self, input: &[u8], wrap: LineWrap) -> bool {
self.validate_wrapped_result(input, wrap).is_ok()
}
#[must_use]
pub const fn encode_array<const INPUT_LEN: usize, const OUTPUT_LEN: usize>(
&self,
input: &[u8; INPUT_LEN],
) -> [u8; OUTPUT_LEN] {
let Some(required) = checked_encoded_len(INPUT_LEN, PAD) else {
panic!("encoded base64 length overflows usize");
};
assert!(
required == OUTPUT_LEN,
"base64 output array has incorrect length"
);
let mut output = [0u8; OUTPUT_LEN];
let mut read = 0;
let mut write = 0;
while INPUT_LEN - read >= 3 {
let b0 = input[read];
let b1 = input[read + 1];
let b2 = input[read + 2];
output[write] = encode_base64_value::<A>(b0 >> 2);
output[write + 1] = encode_base64_value::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
output[write + 2] = encode_base64_value::<A>(((b1 & 0b0000_1111) << 2) | (b2 >> 6));
output[write + 3] = encode_base64_value::<A>(b2 & 0b0011_1111);
read += 3;
write += 4;
}
match INPUT_LEN - read {
0 => {}
1 => {
let b0 = input[read];
output[write] = encode_base64_value::<A>(b0 >> 2);
output[write + 1] = encode_base64_value::<A>((b0 & 0b0000_0011) << 4);
write += 2;
if PAD {
output[write] = b'=';
output[write + 1] = b'=';
}
}
2 => {
let b0 = input[read];
let b1 = input[read + 1];
output[write] = encode_base64_value::<A>(b0 >> 2);
output[write + 1] = encode_base64_value::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
output[write + 2] = encode_base64_value::<A>((b1 & 0b0000_1111) << 2);
if PAD {
output[write + 3] = b'=';
}
}
_ => unreachable!(),
}
output
}
pub fn encode_slice(&self, input: &[u8], output: &mut [u8]) -> Result<usize, EncodeError> {
backend::encode_slice::<A, PAD>(input, output)
}
pub fn encode_slice_wrapped(
&self,
input: &[u8],
output: &mut [u8],
wrap: LineWrap,
) -> Result<usize, EncodeError> {
let required = self.wrapped_encoded_len(input.len(), wrap)?;
if output.len() < required {
return Err(EncodeError::OutputTooSmall {
required,
available: output.len(),
});
}
let encoded_len =
checked_encoded_len(input.len(), PAD).ok_or(EncodeError::LengthOverflow)?;
if encoded_len == 0 {
return Ok(0);
}
let combined_required = match required.checked_add(encoded_len) {
Some(len) => len,
None => usize::MAX,
};
if output.len() < combined_required {
let mut scratch = [0u8; 1024];
let mut input_offset = 0;
let mut output_offset = 0;
let mut column = 0;
while input_offset < input.len() {
let remaining = input.len() - input_offset;
let mut take = remaining.min(768);
if remaining > take {
take -= take % 3;
}
if take == 0 {
take = remaining;
}
let encoded = match self
.encode_slice(&input[input_offset..input_offset + take], &mut scratch)
{
Ok(encoded) => encoded,
Err(err) => {
wipe_bytes(&mut scratch);
return Err(err);
}
};
if let Err(err) = write_wrapped_bytes(
&scratch[..encoded],
output,
&mut output_offset,
&mut column,
wrap,
) {
wipe_bytes(&mut scratch);
return Err(err);
}
wipe_bytes(&mut scratch[..encoded]);
input_offset += take;
}
Ok(output_offset)
} else {
let encoded =
self.encode_slice(input, &mut output[required..required + encoded_len])?;
let mut output_offset = 0;
let mut column = 0;
let mut read = required;
while read < required + encoded {
let byte = output[read];
write_wrapped_byte(byte, output, &mut output_offset, &mut column, wrap)?;
read += 1;
}
wipe_bytes(&mut output[required..required + encoded]);
Ok(output_offset)
}
}
pub fn encode_slice_wrapped_clear_tail(
&self,
input: &[u8],
output: &mut [u8],
wrap: LineWrap,
) -> Result<usize, EncodeError> {
let written = match self.encode_slice_wrapped(input, output, wrap) {
Ok(written) => written,
Err(err) => {
wipe_bytes(output);
return Err(err);
}
};
wipe_tail(output, written);
Ok(written)
}
pub fn encode_wrapped_buffer<const CAP: usize>(
&self,
input: &[u8],
wrap: LineWrap,
) -> Result<EncodedBuffer<CAP>, EncodeError> {
let mut output = EncodedBuffer::new();
let written =
match self.encode_slice_wrapped_clear_tail(input, output.as_mut_capacity(), wrap) {
Ok(written) => written,
Err(err) => {
output.clear();
return Err(err);
}
};
output.set_filled(written)?;
Ok(output)
}
#[cfg(feature = "alloc")]
#[must_use = "for secret-bearing payloads use encode_wrapped_secret, which returns a redacted buffer with drop-time cleanup"]
pub fn encode_wrapped_vec(
&self,
input: &[u8],
wrap: LineWrap,
) -> Result<alloc::vec::Vec<u8>, EncodeError> {
let required = self.wrapped_encoded_len(input.len(), wrap)?;
let mut output = alloc::vec![0; required];
let written = self.encode_slice_wrapped(input, &mut output, wrap)?;
output.truncate(written);
Ok(output)
}
#[cfg(feature = "alloc")]
pub fn encode_wrapped_string(
&self,
input: &[u8],
wrap: LineWrap,
) -> Result<alloc::string::String, EncodeError> {
let output = self.encode_wrapped_vec(input, wrap)?;
match alloc::string::String::from_utf8(output) {
Ok(output) => Ok(output),
Err(_) => unreachable!("base64 encoder produced non-UTF-8 output"),
}
}
#[cfg(feature = "alloc")]
pub fn encode_wrapped_secret(
&self,
input: &[u8],
wrap: LineWrap,
) -> Result<SecretBuffer, EncodeError> {
self.encode_wrapped_vec(input, wrap)
.map(SecretBuffer::from_vec)
}
pub fn encode_slice_clear_tail(
&self,
input: &[u8],
output: &mut [u8],
) -> Result<usize, EncodeError> {
let written = match self.encode_slice(input, output) {
Ok(written) => written,
Err(err) => {
wipe_bytes(output);
return Err(err);
}
};
wipe_tail(output, written);
Ok(written)
}
pub fn encode_buffer<const CAP: usize>(
&self,
input: &[u8],
) -> Result<EncodedBuffer<CAP>, EncodeError> {
let mut output = EncodedBuffer::new();
let written = match self.encode_slice_clear_tail(input, output.as_mut_capacity()) {
Ok(written) => written,
Err(err) => {
output.clear();
return Err(err);
}
};
output.set_filled(written)?;
Ok(output)
}
#[cfg(feature = "alloc")]
#[must_use = "for secret-bearing payloads use encode_secret, which returns a redacted buffer with drop-time cleanup"]
pub fn encode_vec(&self, input: &[u8]) -> Result<alloc::vec::Vec<u8>, EncodeError> {
let required = checked_encoded_len(input.len(), PAD).ok_or(EncodeError::LengthOverflow)?;
let mut output = alloc::vec![0; required];
let written = self.encode_slice(input, &mut output)?;
output.truncate(written);
Ok(output)
}
#[cfg(feature = "alloc")]
pub fn encode_secret(&self, input: &[u8]) -> Result<SecretBuffer, EncodeError> {
self.encode_vec(input).map(SecretBuffer::from_vec)
}
#[cfg(feature = "alloc")]
pub fn encode_string(&self, input: &[u8]) -> Result<alloc::string::String, EncodeError> {
let output = self.encode_vec(input)?;
match alloc::string::String::from_utf8(output) {
Ok(output) => Ok(output),
Err(_) => unreachable!("base64 encoder produced non-UTF-8 output"),
}
}
pub fn encode_in_place<'a>(
&self,
buffer: &'a mut [u8],
input_len: usize,
) -> Result<&'a mut [u8], EncodeError> {
if input_len > buffer.len() {
return Err(EncodeError::InputTooLarge {
input_len,
buffer_len: buffer.len(),
});
}
let required = checked_encoded_len(input_len, PAD).ok_or(EncodeError::LengthOverflow)?;
if buffer.len() < required {
return Err(EncodeError::OutputTooSmall {
required,
available: buffer.len(),
});
}
let mut read = input_len;
let mut write = required;
match input_len % 3 {
0 => {}
1 => {
read -= 1;
let b0 = buffer[read];
if PAD {
write -= 4;
buffer[write] = encode_base64_value_runtime::<A>(b0 >> 2);
buffer[write + 1] = encode_base64_value_runtime::<A>((b0 & 0b0000_0011) << 4);
buffer[write + 2] = b'=';
buffer[write + 3] = b'=';
} else {
write -= 2;
buffer[write] = encode_base64_value_runtime::<A>(b0 >> 2);
buffer[write + 1] = encode_base64_value_runtime::<A>((b0 & 0b0000_0011) << 4);
}
}
2 => {
read -= 2;
let b0 = buffer[read];
let b1 = buffer[read + 1];
if PAD {
write -= 4;
buffer[write] = encode_base64_value_runtime::<A>(b0 >> 2);
buffer[write + 1] =
encode_base64_value_runtime::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
buffer[write + 2] = encode_base64_value_runtime::<A>((b1 & 0b0000_1111) << 2);
buffer[write + 3] = b'=';
} else {
write -= 3;
buffer[write] = encode_base64_value_runtime::<A>(b0 >> 2);
buffer[write + 1] =
encode_base64_value_runtime::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
buffer[write + 2] = encode_base64_value_runtime::<A>((b1 & 0b0000_1111) << 2);
}
}
_ => unreachable!(),
}
while read > 0 {
read -= 3;
write -= 4;
let b0 = buffer[read];
let b1 = buffer[read + 1];
let b2 = buffer[read + 2];
buffer[write] = encode_base64_value_runtime::<A>(b0 >> 2);
buffer[write + 1] =
encode_base64_value_runtime::<A>(((b0 & 0b0000_0011) << 4) | (b1 >> 4));
buffer[write + 2] =
encode_base64_value_runtime::<A>(((b1 & 0b0000_1111) << 2) | (b2 >> 6));
buffer[write + 3] = encode_base64_value_runtime::<A>(b2 & 0b0011_1111);
}
debug_assert_eq!(write, 0);
Ok(&mut buffer[..required])
}
pub fn encode_in_place_clear_tail<'a>(
&self,
buffer: &'a mut [u8],
input_len: usize,
) -> Result<&'a mut [u8], EncodeError> {
let len = match self.encode_in_place(buffer, input_len) {
Ok(encoded) => encoded.len(),
Err(err) => {
wipe_bytes(buffer);
return Err(err);
}
};
wipe_tail(buffer, len);
Ok(&mut buffer[..len])
}
#[must_use = "handle decode errors; use crate::ct for secret-bearing payloads"]
pub fn decode_slice(&self, input: &[u8], output: &mut [u8]) -> Result<usize, DecodeError> {
backend::decode_slice::<A, PAD>(input, output)
}
pub fn decode_slice_clear_tail(
&self,
input: &[u8],
output: &mut [u8],
) -> Result<usize, DecodeError> {
let written = match self.decode_slice(input, output) {
Ok(written) => written,
Err(err) => {
wipe_bytes(output);
return Err(err);
}
};
wipe_tail(output, written);
Ok(written)
}
pub fn decode_buffer<const CAP: usize>(
&self,
input: &[u8],
) -> Result<DecodedBuffer<CAP>, DecodeError> {
let mut output = DecodedBuffer::new();
let written = match self.decode_slice_clear_tail(input, output.as_mut_capacity()) {
Ok(written) => written,
Err(err) => {
output.clear();
return Err(err);
}
};
output.set_filled(written)?;
Ok(output)
}
#[must_use = "handle decode errors; use crate::ct for secret-bearing payloads"]
pub fn decode_slice_legacy(
&self,
input: &[u8],
output: &mut [u8],
) -> Result<usize, DecodeError> {
let required = validate_legacy_decode::<A, PAD>(input)?;
if output.len() < required {
return Err(DecodeError::OutputTooSmall {
required,
available: output.len(),
});
}
decode_legacy_to_slice::<A, PAD>(input, output)
}
pub fn decode_slice_legacy_clear_tail(
&self,
input: &[u8],
output: &mut [u8],
) -> Result<usize, DecodeError> {
let written = match self.decode_slice_legacy(input, output) {
Ok(written) => written,
Err(err) => {
wipe_bytes(output);
return Err(err);
}
};
wipe_tail(output, written);
Ok(written)
}
pub fn decode_buffer_legacy<const CAP: usize>(
&self,
input: &[u8],
) -> Result<DecodedBuffer<CAP>, DecodeError> {
let mut output = DecodedBuffer::new();
let written = match self.decode_slice_legacy_clear_tail(input, output.as_mut_capacity()) {
Ok(written) => written,
Err(err) => {
output.clear();
return Err(err);
}
};
output.set_filled(written)?;
Ok(output)
}
#[must_use = "handle decode errors; use crate::ct for secret-bearing payloads"]
pub fn decode_slice_wrapped(
&self,
input: &[u8],
output: &mut [u8],
wrap: LineWrap,
) -> Result<usize, DecodeError> {
let required = validate_wrapped_decode::<A, PAD>(input, wrap)?;
if output.len() < required {
return Err(DecodeError::OutputTooSmall {
required,
available: output.len(),
});
}
decode_wrapped_to_slice::<A, PAD>(input, output, wrap)
}
pub fn decode_slice_wrapped_clear_tail(
&self,
input: &[u8],
output: &mut [u8],
wrap: LineWrap,
) -> Result<usize, DecodeError> {
let written = match self.decode_slice_wrapped(input, output, wrap) {
Ok(written) => written,
Err(err) => {
wipe_bytes(output);
return Err(err);
}
};
wipe_tail(output, written);
Ok(written)
}
pub fn decode_wrapped_buffer<const CAP: usize>(
&self,
input: &[u8],
wrap: LineWrap,
) -> Result<DecodedBuffer<CAP>, DecodeError> {
let mut output = DecodedBuffer::new();
let written =
match self.decode_slice_wrapped_clear_tail(input, output.as_mut_capacity(), wrap) {
Ok(written) => written,
Err(err) => {
output.clear();
return Err(err);
}
};
output.set_filled(written)?;
Ok(output)
}
#[cfg(feature = "alloc")]
#[must_use = "for secret-bearing payloads use decode_secret, which returns a redacted buffer with drop-time cleanup"]
pub fn decode_vec(&self, input: &[u8]) -> Result<alloc::vec::Vec<u8>, DecodeError> {
let required = validate_decode::<A, PAD>(input)?;
let mut output = alloc::vec![0; required];
let written = match self.decode_slice(input, &mut output) {
Ok(written) => written,
Err(err) => {
wipe_bytes(&mut output);
return Err(err);
}
};
output.truncate(written);
Ok(output)
}
#[cfg(feature = "alloc")]
pub fn decode_secret(&self, input: &[u8]) -> Result<SecretBuffer, DecodeError> {
self.decode_vec(input).map(SecretBuffer::from_vec)
}
#[cfg(feature = "alloc")]
#[must_use = "for secret-bearing payloads use decode_secret_legacy, which returns a redacted buffer with drop-time cleanup"]
pub fn decode_vec_legacy(&self, input: &[u8]) -> Result<alloc::vec::Vec<u8>, DecodeError> {
let required = validate_legacy_decode::<A, PAD>(input)?;
let mut output = alloc::vec![0; required];
let written = match self.decode_slice_legacy(input, &mut output) {
Ok(written) => written,
Err(err) => {
wipe_bytes(&mut output);
return Err(err);
}
};
output.truncate(written);
Ok(output)
}
#[cfg(feature = "alloc")]
pub fn decode_secret_legacy(&self, input: &[u8]) -> Result<SecretBuffer, DecodeError> {
self.decode_vec_legacy(input).map(SecretBuffer::from_vec)
}
#[cfg(feature = "alloc")]
#[must_use = "for secret-bearing payloads use decode_wrapped_secret, which returns a redacted buffer with drop-time cleanup"]
pub fn decode_wrapped_vec(
&self,
input: &[u8],
wrap: LineWrap,
) -> Result<alloc::vec::Vec<u8>, DecodeError> {
let required = validate_wrapped_decode::<A, PAD>(input, wrap)?;
let mut output = alloc::vec![0; required];
let written = match self.decode_slice_wrapped(input, &mut output, wrap) {
Ok(written) => written,
Err(err) => {
wipe_bytes(&mut output);
return Err(err);
}
};
output.truncate(written);
Ok(output)
}
#[cfg(feature = "alloc")]
pub fn decode_wrapped_secret(
&self,
input: &[u8],
wrap: LineWrap,
) -> Result<SecretBuffer, DecodeError> {
self.decode_wrapped_vec(input, wrap)
.map(SecretBuffer::from_vec)
}
pub fn decode_in_place_wrapped<'a>(
&self,
buffer: &'a mut [u8],
wrap: LineWrap,
) -> Result<&'a mut [u8], DecodeError> {
let _required = validate_wrapped_decode::<A, PAD>(buffer, wrap)?;
let compacted = compact_wrapped_input(buffer, wrap)?;
let len = Self::decode_slice_to_start(&mut buffer[..compacted])?;
Ok(&mut buffer[..len])
}
pub fn decode_in_place_wrapped_clear_tail<'a>(
&self,
buffer: &'a mut [u8],
wrap: LineWrap,
) -> Result<&'a mut [u8], DecodeError> {
if let Err(err) = validate_wrapped_decode::<A, PAD>(buffer, wrap) {
wipe_bytes(buffer);
return Err(err);
}
let compacted = match compact_wrapped_input(buffer, wrap) {
Ok(compacted) => compacted,
Err(err) => {
wipe_bytes(buffer);
return Err(err);
}
};
let len = match Self::decode_slice_to_start(&mut buffer[..compacted]) {
Ok(len) => len,
Err(err) => {
wipe_bytes(buffer);
return Err(err);
}
};
wipe_tail(buffer, len);
Ok(&mut buffer[..len])
}
pub fn decode_in_place<'a>(&self, buffer: &'a mut [u8]) -> Result<&'a mut [u8], DecodeError> {
let len = Self::decode_slice_to_start(buffer)?;
Ok(&mut buffer[..len])
}
pub fn decode_in_place_clear_tail<'a>(
&self,
buffer: &'a mut [u8],
) -> Result<&'a mut [u8], DecodeError> {
let len = match Self::decode_slice_to_start(buffer) {
Ok(len) => len,
Err(err) => {
wipe_bytes(buffer);
return Err(err);
}
};
wipe_tail(buffer, len);
Ok(&mut buffer[..len])
}
pub fn decode_in_place_legacy<'a>(
&self,
buffer: &'a mut [u8],
) -> Result<&'a mut [u8], DecodeError> {
let _required = validate_legacy_decode::<A, PAD>(buffer)?;
let mut write = 0;
let mut read = 0;
while read < buffer.len() {
let byte = buffer[read];
if !is_legacy_whitespace(byte) {
buffer[write] = byte;
write += 1;
}
read += 1;
}
let len = Self::decode_slice_to_start(&mut buffer[..write])?;
Ok(&mut buffer[..len])
}
pub fn decode_in_place_legacy_clear_tail<'a>(
&self,
buffer: &'a mut [u8],
) -> Result<&'a mut [u8], DecodeError> {
if let Err(err) = validate_legacy_decode::<A, PAD>(buffer) {
wipe_bytes(buffer);
return Err(err);
}
let mut write = 0;
let mut read = 0;
while read < buffer.len() {
let byte = buffer[read];
if !is_legacy_whitespace(byte) {
buffer[write] = byte;
write += 1;
}
read += 1;
}
let len = match Self::decode_slice_to_start(&mut buffer[..write]) {
Ok(len) => len,
Err(err) => {
wipe_bytes(buffer);
return Err(err);
}
};
wipe_tail(buffer, len);
Ok(&mut buffer[..len])
}
fn decode_slice_to_start(buffer: &mut [u8]) -> Result<usize, DecodeError> {
let _required = validate_decode::<A, PAD>(buffer)?;
let input_len = buffer.len();
let mut read = 0;
let mut write = 0;
while read + 4 <= input_len {
let chunk = read_quad(buffer, read)?;
let available = buffer.len();
let output_tail = buffer.get_mut(write..).ok_or(DecodeError::OutputTooSmall {
required: write,
available,
})?;
let written = decode_chunk::<A, PAD>(chunk, output_tail)
.map_err(|err| err.with_index_offset(read))?;
read += 4;
write += written;
if written < 3 {
if read != input_len {
return Err(DecodeError::InvalidPadding { index: read - 4 });
}
return Ok(write);
}
}
let rem = input_len - read;
if rem == 0 {
return Ok(write);
}
if PAD {
return Err(DecodeError::InvalidLength);
}
let mut tail = [0u8; 3];
tail[..rem].copy_from_slice(&buffer[read..input_len]);
decode_tail_unpadded::<A>(&tail[..rem], &mut buffer[write..])
.map_err(|err| err.with_index_offset(read))
.map(|n| write + n)
}
}
fn write_wrapped_bytes(
input: &[u8],
output: &mut [u8],
output_offset: &mut usize,
column: &mut usize,
wrap: LineWrap,
) -> Result<(), EncodeError> {
for byte in input {
write_wrapped_byte(*byte, output, output_offset, column, wrap)?;
}
Ok(())
}
fn write_wrapped_byte(
byte: u8,
output: &mut [u8],
output_offset: &mut usize,
column: &mut usize,
wrap: LineWrap,
) -> Result<(), EncodeError> {
if *column == wrap.line_len {
let line_ending = wrap.line_ending.as_bytes();
let mut index = 0;
while index < line_ending.len() {
if *output_offset >= output.len() {
return Err(EncodeError::OutputTooSmall {
required: *output_offset + 1,
available: output.len(),
});
}
output[*output_offset] = line_ending[index];
*output_offset += 1;
index += 1;
}
*column = 0;
}
if *output_offset >= output.len() {
return Err(EncodeError::OutputTooSmall {
required: *output_offset + 1,
available: output.len(),
});
}
output[*output_offset] = byte;
*output_offset += 1;
*column += 1;
Ok(())
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EncodeError {
LengthOverflow,
InvalidLineWrap {
line_len: usize,
},
InputTooLarge {
input_len: usize,
buffer_len: usize,
},
OutputTooSmall {
required: usize,
available: usize,
},
}
impl core::fmt::Display for EncodeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::LengthOverflow => f.write_str("base64 output length overflows usize"),
Self::InvalidLineWrap { line_len } => {
write!(f, "base64 line wrap length {line_len} is invalid")
}
Self::InputTooLarge {
input_len,
buffer_len,
} => write!(
f,
"base64 input length {input_len} exceeds buffer length {buffer_len}"
),
Self::OutputTooSmall {
required,
available,
} => write!(
f,
"base64 output buffer too small: required {required}, available {available}"
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for EncodeError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DecodeError {
InvalidInput,
InvalidLength,
InvalidByte {
index: usize,
byte: u8,
},
InvalidPadding {
index: usize,
},
InvalidLineWrap {
index: usize,
},
OutputTooSmall {
required: usize,
available: usize,
},
StagingTooSmall {
required: usize,
available: usize,
},
}
impl core::fmt::Display for DecodeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::InvalidInput => f.write_str("malformed base64 input"),
Self::InvalidLength => f.write_str("invalid base64 input length"),
Self::InvalidByte { index, byte } => {
write!(f, "invalid base64 byte 0x{byte:02x} at index {index}")
}
Self::InvalidPadding { index } => write!(f, "invalid base64 padding at index {index}"),
Self::InvalidLineWrap { index } => {
write!(f, "invalid base64 line wrapping at index {index}")
}
Self::OutputTooSmall {
required,
available,
} => write!(
f,
"base64 decode output buffer too small: required {required}, available {available}"
),
Self::StagingTooSmall {
required,
available,
} => write!(
f,
"base64 decode staging buffer too small: required {required}, available {available}"
),
}
}
}
impl DecodeError {
fn with_index_offset(self, offset: usize) -> Self {
match self {
Self::InvalidByte { index, byte } => Self::InvalidByte {
index: index + offset,
byte,
},
Self::InvalidPadding { index } => Self::InvalidPadding {
index: index + offset,
},
Self::InvalidLineWrap { index } => Self::InvalidLineWrap {
index: index + offset,
},
Self::InvalidInput
| Self::InvalidLength
| Self::OutputTooSmall { .. }
| Self::StagingTooSmall { .. } => self,
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for DecodeError {}
struct LegacyBytes<'a> {
input: &'a [u8],
index: usize,
}
impl<'a> LegacyBytes<'a> {
const fn new(input: &'a [u8]) -> Self {
Self { input, index: 0 }
}
fn next_byte(&mut self) -> Option<(usize, u8)> {
while self.index < self.input.len() {
let index = self.index;
let byte = self.input[index];
self.index += 1;
if !is_legacy_whitespace(byte) {
return Some((index, byte));
}
}
None
}
}
fn validate_legacy_decode<A: Alphabet, const PAD: bool>(
input: &[u8],
) -> Result<usize, DecodeError> {
let mut bytes = LegacyBytes::new(input);
let mut chunk = [0u8; 4];
let mut indexes = [0usize; 4];
let mut chunk_len = 0;
let mut required = 0;
let mut terminal_seen = false;
while let Some((index, byte)) = bytes.next_byte() {
if terminal_seen {
return Err(DecodeError::InvalidPadding { index });
}
chunk[chunk_len] = byte;
indexes[chunk_len] = index;
chunk_len += 1;
if chunk_len == 4 {
let written =
validate_chunk::<A, PAD>(chunk).map_err(|err| map_chunk_error(err, &indexes))?;
required += written;
terminal_seen = written < 3;
chunk_len = 0;
}
}
if chunk_len == 0 {
return Ok(required);
}
if PAD {
return Err(DecodeError::InvalidLength);
}
validate_tail_unpadded::<A>(&chunk[..chunk_len])
.map_err(|err| map_partial_chunk_error(err, &indexes, chunk_len))?;
Ok(required + decoded_capacity(chunk_len))
}
fn decode_legacy_to_slice<A: Alphabet, const PAD: bool>(
input: &[u8],
output: &mut [u8],
) -> Result<usize, DecodeError> {
let mut bytes = LegacyBytes::new(input);
let mut chunk = [0u8; 4];
let mut indexes = [0usize; 4];
let mut chunk_len = 0;
let mut write = 0;
let mut terminal_seen = false;
while let Some((index, byte)) = bytes.next_byte() {
if terminal_seen {
return Err(DecodeError::InvalidPadding { index });
}
chunk[chunk_len] = byte;
indexes[chunk_len] = index;
chunk_len += 1;
if chunk_len == 4 {
let available = output.len();
let output_tail = output.get_mut(write..).ok_or(DecodeError::OutputTooSmall {
required: write,
available,
})?;
let written = decode_chunk::<A, PAD>(chunk, output_tail)
.map_err(|err| map_chunk_error(err, &indexes))?;
write += written;
terminal_seen = written < 3;
chunk_len = 0;
}
}
if chunk_len == 0 {
return Ok(write);
}
if PAD {
return Err(DecodeError::InvalidLength);
}
decode_tail_unpadded::<A>(&chunk[..chunk_len], &mut output[write..])
.map_err(|err| map_partial_chunk_error(err, &indexes, chunk_len))
.map(|n| write + n)
}
struct WrappedBytes<'a> {
input: &'a [u8],
wrap: LineWrap,
index: usize,
line_len: usize,
}
impl<'a> WrappedBytes<'a> {
const fn new(input: &'a [u8], wrap: LineWrap) -> Result<Self, DecodeError> {
if wrap.line_len == 0 {
return Err(DecodeError::InvalidLineWrap { index: 0 });
}
Ok(Self {
input,
wrap,
index: 0,
line_len: 0,
})
}
fn next_byte(&mut self) -> Result<Option<(usize, u8)>, DecodeError> {
loop {
if self.index == self.input.len() {
return Ok(None);
}
if self.starts_with_line_ending() {
let line_end_index = self.index;
if self.line_len == 0 {
return Err(DecodeError::InvalidLineWrap {
index: line_end_index,
});
}
self.index += self.wrap.line_ending.byte_len();
if self.index == self.input.len() {
self.line_len = 0;
return Ok(None);
}
if self.line_len != self.wrap.line_len {
return Err(DecodeError::InvalidLineWrap {
index: line_end_index,
});
}
self.line_len = 0;
continue;
}
let byte = self.input[self.index];
if matches!(byte, b'\r' | b'\n') {
return Err(DecodeError::InvalidLineWrap { index: self.index });
}
self.line_len += 1;
if self.line_len > self.wrap.line_len {
return Err(DecodeError::InvalidLineWrap { index: self.index });
}
let index = self.index;
self.index += 1;
return Ok(Some((index, byte)));
}
}
fn starts_with_line_ending(&self) -> bool {
let line_ending = self.wrap.line_ending.as_bytes();
let Some(end) = self.index.checked_add(line_ending.len()) else {
return false;
};
end <= self.input.len() && &self.input[self.index..end] == line_ending
}
}
fn validate_wrapped_decode<A: Alphabet, const PAD: bool>(
input: &[u8],
wrap: LineWrap,
) -> Result<usize, DecodeError> {
let mut bytes = WrappedBytes::new(input, wrap)?;
let mut chunk = [0u8; 4];
let mut indexes = [0usize; 4];
let mut chunk_len = 0;
let mut required = 0;
let mut terminal_seen = false;
while let Some((index, byte)) = bytes.next_byte()? {
if terminal_seen {
return Err(DecodeError::InvalidPadding { index });
}
chunk[chunk_len] = byte;
indexes[chunk_len] = index;
chunk_len += 1;
if chunk_len == 4 {
let written =
validate_chunk::<A, PAD>(chunk).map_err(|err| map_chunk_error(err, &indexes))?;
required += written;
terminal_seen = written < 3;
chunk_len = 0;
}
}
if chunk_len == 0 {
return Ok(required);
}
if PAD {
return Err(DecodeError::InvalidLength);
}
validate_tail_unpadded::<A>(&chunk[..chunk_len])
.map_err(|err| map_partial_chunk_error(err, &indexes, chunk_len))?;
Ok(required + decoded_capacity(chunk_len))
}
fn decode_wrapped_to_slice<A: Alphabet, const PAD: bool>(
input: &[u8],
output: &mut [u8],
wrap: LineWrap,
) -> Result<usize, DecodeError> {
let mut bytes = WrappedBytes::new(input, wrap)?;
let mut chunk = [0u8; 4];
let mut indexes = [0usize; 4];
let mut chunk_len = 0;
let mut write = 0;
let mut terminal_seen = false;
while let Some((index, byte)) = bytes.next_byte()? {
if terminal_seen {
return Err(DecodeError::InvalidPadding { index });
}
chunk[chunk_len] = byte;
indexes[chunk_len] = index;
chunk_len += 1;
if chunk_len == 4 {
let available = output.len();
let output_tail = output.get_mut(write..).ok_or(DecodeError::OutputTooSmall {
required: write,
available,
})?;
let written = decode_chunk::<A, PAD>(chunk, output_tail)
.map_err(|err| map_chunk_error(err, &indexes))?;
write += written;
terminal_seen = written < 3;
chunk_len = 0;
}
}
if chunk_len == 0 {
return Ok(write);
}
if PAD {
return Err(DecodeError::InvalidLength);
}
decode_tail_unpadded::<A>(&chunk[..chunk_len], &mut output[write..])
.map_err(|err| map_partial_chunk_error(err, &indexes, chunk_len))
.map(|n| write + n)
}
fn compact_wrapped_input(buffer: &mut [u8], wrap: LineWrap) -> Result<usize, DecodeError> {
if !wrap.is_valid() {
return Err(DecodeError::InvalidLineWrap { index: 0 });
}
let line_ending = wrap.line_ending.as_bytes();
let line_ending_len = line_ending.len();
let mut read = 0;
let mut write = 0;
while read < buffer.len() {
let line_end = read + line_ending_len;
if buffer.get(read..line_end) == Some(line_ending) {
read = line_end;
continue;
}
buffer[write] = buffer[read];
write += 1;
read += 1;
}
Ok(write)
}
#[inline]
const fn is_legacy_whitespace(byte: u8) -> bool {
matches!(byte, b' ' | b'\t' | b'\r' | b'\n')
}
fn map_chunk_error(err: DecodeError, indexes: &[usize; 4]) -> DecodeError {
match err {
DecodeError::InvalidByte { index, byte } => DecodeError::InvalidByte {
index: indexes[index],
byte,
},
DecodeError::InvalidPadding { index } => DecodeError::InvalidPadding {
index: indexes[index],
},
DecodeError::InvalidInput
| DecodeError::InvalidLineWrap { .. }
| DecodeError::InvalidLength
| DecodeError::OutputTooSmall { .. }
| DecodeError::StagingTooSmall { .. } => err,
}
}
fn map_partial_chunk_error(err: DecodeError, indexes: &[usize; 4], len: usize) -> DecodeError {
match err {
DecodeError::InvalidByte { index, byte } if index < len => DecodeError::InvalidByte {
index: indexes[index],
byte,
},
DecodeError::InvalidPadding { index } if index < len => DecodeError::InvalidPadding {
index: indexes[index],
},
DecodeError::InvalidByte { .. }
| DecodeError::InvalidPadding { .. }
| DecodeError::InvalidLineWrap { .. }
| DecodeError::InvalidInput
| DecodeError::InvalidLength
| DecodeError::OutputTooSmall { .. }
| DecodeError::StagingTooSmall { .. } => err,
}
}
fn decode_padded<A: Alphabet>(input: &[u8], output: &mut [u8]) -> Result<usize, DecodeError> {
if !input.len().is_multiple_of(4) {
return Err(DecodeError::InvalidLength);
}
let required = decoded_len_padded(input)?;
if output.len() < required {
return Err(DecodeError::OutputTooSmall {
required,
available: output.len(),
});
}
let mut read = 0;
let mut write = 0;
while read < input.len() {
let chunk = read_quad(input, read)?;
let available = output.len();
let output_tail = output.get_mut(write..).ok_or(DecodeError::OutputTooSmall {
required: write,
available,
})?;
let written = decode_chunk::<A, true>(chunk, output_tail)
.map_err(|err| err.with_index_offset(read))?;
read += 4;
write += written;
if written < 3 && read != input.len() {
return Err(DecodeError::InvalidPadding { index: read - 4 });
}
}
Ok(write)
}
fn validate_decode<A: Alphabet, const PAD: bool>(input: &[u8]) -> Result<usize, DecodeError> {
if input.is_empty() {
return Ok(0);
}
if PAD {
validate_padded::<A>(input)
} else {
validate_unpadded::<A>(input)
}
}
fn validate_padded<A: Alphabet>(input: &[u8]) -> Result<usize, DecodeError> {
if !input.len().is_multiple_of(4) {
return Err(DecodeError::InvalidLength);
}
let required = decoded_len_padded(input)?;
let mut read = 0;
while read < input.len() {
let chunk = read_quad(input, read)?;
let written =
validate_chunk::<A, true>(chunk).map_err(|err| err.with_index_offset(read))?;
read += 4;
if written < 3 && read != input.len() {
return Err(DecodeError::InvalidPadding { index: read - 4 });
}
}
Ok(required)
}
fn validate_unpadded<A: Alphabet>(input: &[u8]) -> Result<usize, DecodeError> {
let required = decoded_len_unpadded(input)?;
let mut read = 0;
while read + 4 <= input.len() {
let chunk = read_quad(input, read)?;
validate_chunk::<A, false>(chunk).map_err(|err| err.with_index_offset(read))?;
read += 4;
}
validate_tail_unpadded::<A>(&input[read..]).map_err(|err| err.with_index_offset(read))?;
Ok(required)
}
fn decode_unpadded<A: Alphabet>(input: &[u8], output: &mut [u8]) -> Result<usize, DecodeError> {
let required = decoded_len_unpadded(input)?;
if output.len() < required {
return Err(DecodeError::OutputTooSmall {
required,
available: output.len(),
});
}
let mut read = 0;
let mut write = 0;
while read + 4 <= input.len() {
let chunk = read_quad(input, read)?;
let available = output.len();
let output_tail = output.get_mut(write..).ok_or(DecodeError::OutputTooSmall {
required: write,
available,
})?;
let written = decode_chunk::<A, false>(chunk, output_tail)
.map_err(|err| err.with_index_offset(read))?;
read += 4;
write += written;
}
decode_tail_unpadded::<A>(&input[read..], &mut output[write..])
.map_err(|err| err.with_index_offset(read))
.map(|n| write + n)
}
fn decoded_len_padded(input: &[u8]) -> Result<usize, DecodeError> {
if input.is_empty() {
return Ok(0);
}
if !input.len().is_multiple_of(4) {
return Err(DecodeError::InvalidLength);
}
let Some((&last, before_last_prefix)) = input.split_last() else {
return Ok(0);
};
let Some(&before_last) = before_last_prefix.last() else {
return Err(DecodeError::InvalidLength);
};
let mut padding = 0;
if last == b'=' {
padding += 1;
}
if before_last == b'=' {
padding += 1;
}
if padding == 0
&& let Some(index) = input.iter().position(|byte| *byte == b'=')
{
return Err(DecodeError::InvalidPadding { index });
}
if padding > 0 {
let first_pad = input.len() - padding;
if let Some(index) = input[..first_pad].iter().position(|byte| *byte == b'=') {
return Err(DecodeError::InvalidPadding { index });
}
}
Ok(input.len() / 4 * 3 - padding)
}
fn decoded_len_unpadded(input: &[u8]) -> Result<usize, DecodeError> {
if input.len() % 4 == 1 {
return Err(DecodeError::InvalidLength);
}
if let Some(index) = input.iter().position(|byte| *byte == b'=') {
return Err(DecodeError::InvalidPadding { index });
}
Ok(decoded_capacity(input.len()))
}
fn read_quad(input: &[u8], offset: usize) -> Result<[u8; 4], DecodeError> {
let end = offset.checked_add(4).ok_or(DecodeError::InvalidLength)?;
match input.get(offset..end) {
Some([b0, b1, b2, b3]) => Ok([*b0, *b1, *b2, *b3]),
_ => Err(DecodeError::InvalidLength),
}
}
fn first_padding_index_unchecked(input: [u8; 4]) -> usize {
let [b0, b1, b2, b3] = input;
if b0 == b'=' {
0
} else if b1 == b'=' {
1
} else if b2 == b'=' {
2
} else if b3 == b'=' {
3
} else {
debug_assert!(
false,
"first_padding_index_unchecked called with no padding"
);
4
}
}
fn validate_chunk<A: Alphabet, const PAD: bool>(input: [u8; 4]) -> Result<usize, DecodeError> {
let [b0, b1, b2, b3] = input;
let _v0 = decode_byte::<A>(b0, 0)?;
let v1 = decode_byte::<A>(b1, 1)?;
match (b2, b3) {
(b'=', b'=') if PAD => {
if v1 & 0b0000_1111 != 0 {
return Err(DecodeError::InvalidPadding { index: 1 });
}
Ok(1)
}
(b'=', _) if PAD => Err(DecodeError::InvalidPadding { index: 2 }),
(_, b'=') if PAD => {
let v2 = decode_byte::<A>(b2, 2)?;
if v2 & 0b0000_0011 != 0 {
return Err(DecodeError::InvalidPadding { index: 2 });
}
Ok(2)
}
(b'=', _) | (_, b'=') => Err(DecodeError::InvalidPadding {
index: first_padding_index_unchecked(input),
}),
_ => {
decode_byte::<A>(b2, 2)?;
decode_byte::<A>(b3, 3)?;
Ok(3)
}
}
}
fn decode_chunk<A: Alphabet, const PAD: bool>(
input: [u8; 4],
output: &mut [u8],
) -> Result<usize, DecodeError> {
let [b0, b1, b2, b3] = input;
let v0 = decode_byte::<A>(b0, 0)?;
let v1 = decode_byte::<A>(b1, 1)?;
match (b2, b3) {
(b'=', b'=') if PAD => {
if output.is_empty() {
return Err(DecodeError::OutputTooSmall {
required: 1,
available: output.len(),
});
}
if v1 & 0b0000_1111 != 0 {
return Err(DecodeError::InvalidPadding { index: 1 });
}
output[0] = (v0 << 2) | (v1 >> 4);
Ok(1)
}
(b'=', _) if PAD => Err(DecodeError::InvalidPadding { index: 2 }),
(_, b'=') if PAD => {
if output.len() < 2 {
return Err(DecodeError::OutputTooSmall {
required: 2,
available: output.len(),
});
}
let v2 = decode_byte::<A>(b2, 2)?;
if v2 & 0b0000_0011 != 0 {
return Err(DecodeError::InvalidPadding { index: 2 });
}
output[0] = (v0 << 2) | (v1 >> 4);
output[1] = (v1 << 4) | (v2 >> 2);
Ok(2)
}
(b'=', _) | (_, b'=') => Err(DecodeError::InvalidPadding {
index: first_padding_index_unchecked(input),
}),
_ => {
if output.len() < 3 {
return Err(DecodeError::OutputTooSmall {
required: 3,
available: output.len(),
});
}
let v2 = decode_byte::<A>(b2, 2)?;
let v3 = decode_byte::<A>(b3, 3)?;
output[0] = (v0 << 2) | (v1 >> 4);
output[1] = (v1 << 4) | (v2 >> 2);
output[2] = (v2 << 6) | v3;
Ok(3)
}
}
}
fn validate_tail_unpadded<A: Alphabet>(input: &[u8]) -> Result<(), DecodeError> {
match input {
[] => Ok(()),
[b0, b1] => {
decode_byte::<A>(*b0, 0)?;
let v1 = decode_byte::<A>(*b1, 1)?;
if v1 & 0b0000_1111 != 0 {
return Err(DecodeError::InvalidPadding { index: 1 });
}
Ok(())
}
[b0, b1, b2] => {
decode_byte::<A>(*b0, 0)?;
decode_byte::<A>(*b1, 1)?;
let v2 = decode_byte::<A>(*b2, 2)?;
if v2 & 0b0000_0011 != 0 {
return Err(DecodeError::InvalidPadding { index: 2 });
}
Ok(())
}
_ => Err(DecodeError::InvalidLength),
}
}
fn decode_tail_unpadded<A: Alphabet>(
input: &[u8],
output: &mut [u8],
) -> Result<usize, DecodeError> {
match input {
[] => Ok(0),
[b0, b1] => {
let Some(out0) = output.first_mut() else {
return Err(DecodeError::OutputTooSmall {
required: 1,
available: output.len(),
});
};
let v0 = decode_byte::<A>(*b0, 0)?;
let v1 = decode_byte::<A>(*b1, 1)?;
if v1 & 0b0000_1111 != 0 {
return Err(DecodeError::InvalidPadding { index: 1 });
}
*out0 = (v0 << 2) | (v1 >> 4);
Ok(1)
}
[b0, b1, b2] => {
let available = output.len();
let Some([out0, out1]) = output.get_mut(..2) else {
return Err(DecodeError::OutputTooSmall {
required: 2,
available,
});
};
let v0 = decode_byte::<A>(*b0, 0)?;
let v1 = decode_byte::<A>(*b1, 1)?;
let v2 = decode_byte::<A>(*b2, 2)?;
if v2 & 0b0000_0011 != 0 {
return Err(DecodeError::InvalidPadding { index: 2 });
}
*out0 = (v0 << 2) | (v1 >> 4);
*out1 = (v1 << 4) | (v2 >> 2);
Ok(2)
}
_ => Err(DecodeError::InvalidLength),
}
}
fn decode_byte<A: Alphabet>(byte: u8, index: usize) -> Result<u8, DecodeError> {
A::decode(byte).ok_or(DecodeError::InvalidByte { index, byte })
}
fn ct_decode_slice<A: Alphabet, const PAD: bool>(
input: &[u8],
output: &mut [u8],
) -> Result<usize, DecodeError> {
if input.is_empty() {
return Ok(0);
}
if PAD {
ct_decode_padded::<A>(input, output)
} else {
ct_decode_unpadded::<A>(input, output)
}
}
fn ct_decode_slice_staged_clear_tail<A: Alphabet, const PAD: bool>(
input: &[u8],
output: &mut [u8],
staging: &mut [u8],
) -> Result<usize, DecodeError> {
let required = match ct_decoded_len::<A, PAD>(input) {
Ok(required) => required,
Err(err) => {
wipe_bytes(output);
wipe_bytes(staging);
return Err(err);
}
};
if output.len() < required {
wipe_bytes(output);
wipe_bytes(staging);
return Err(DecodeError::OutputTooSmall {
required,
available: output.len(),
});
}
if staging.len() < required {
wipe_bytes(output);
wipe_bytes(staging);
return Err(DecodeError::StagingTooSmall {
required,
available: staging.len(),
});
}
let written = match ct_decode_slice::<A, PAD>(input, &mut staging[..required]) {
Ok(written) => written,
Err(err) => {
wipe_bytes(output);
wipe_bytes(staging);
return Err(err);
}
};
output[..written].copy_from_slice(&staging[..written]);
wipe_bytes(staging);
wipe_tail(output, written);
Ok(written)
}
fn ct_decode_in_place<A: Alphabet, const PAD: bool>(
buffer: &mut [u8],
) -> Result<usize, DecodeError> {
if buffer.is_empty() {
return Ok(0);
}
if PAD {
ct_decode_padded_in_place::<A>(buffer)
} else {
ct_decode_unpadded_in_place::<A>(buffer)
}
}
#[inline(never)]
#[allow(unsafe_code)]
fn ct_error_gate_barrier(invalid_byte: u8, invalid_padding: u8) {
core::hint::black_box(invalid_byte | invalid_padding);
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
#[cfg(all(not(miri), any(target_arch = "x86", target_arch = "x86_64")))]
{
unsafe {
core::arch::asm!("lfence", options(nostack, preserves_flags, nomem));
}
}
#[cfg(all(not(miri), target_arch = "aarch64"))]
{
unsafe {
core::arch::asm!("isb sy", "hint #20", options(nostack, preserves_flags));
}
}
#[cfg(all(not(miri), target_arch = "arm"))]
{
unsafe {
core::arch::asm!("isb sy", options(nostack, preserves_flags));
}
}
#[cfg(all(not(miri), any(target_arch = "riscv32", target_arch = "riscv64")))]
{
unsafe {
core::arch::asm!("fence rw, rw", options(nostack, preserves_flags));
}
}
}
fn ct_validate_decode<A: Alphabet, const PAD: bool>(input: &[u8]) -> Result<(), DecodeError> {
if input.is_empty() {
return Ok(());
}
if PAD {
ct_validate_padded::<A>(input)
} else {
ct_validate_unpadded::<A>(input)
}
}
fn ct_decoded_len<A: Alphabet, const PAD: bool>(input: &[u8]) -> Result<usize, DecodeError> {
ct_validate_decode::<A, PAD>(input)?;
if input.is_empty() {
return Ok(0);
}
if PAD {
Ok(input.len() / 4 * 3 - ct_padding_len(input))
} else {
let full_quads = input.len() / 4 * 3;
match input.len() % 4 {
0 => Ok(full_quads),
2 => Ok(full_quads + 1),
3 => Ok(full_quads + 2),
_ => Err(DecodeError::InvalidLength),
}
}
}
fn ct_validate_padded<A: Alphabet>(input: &[u8]) -> Result<(), DecodeError> {
if !input.len().is_multiple_of(4) {
return Err(DecodeError::InvalidLength);
}
let padding = ct_padding_len(input);
let mut invalid_byte = 0u8;
let mut invalid_padding = 0u8;
let mut read = 0;
while read + 4 < input.len() {
let [b0, b1, b2, b3] =
read_quad_or_mark_invalid(input, read, &mut invalid_byte, &mut invalid_padding);
let (_, valid0) = ct_decode_alphabet_byte::<A>(b0);
let (_, valid1) = ct_decode_alphabet_byte::<A>(b1);
let (_, valid2) = ct_decode_alphabet_byte::<A>(b2);
let (_, valid3) = ct_decode_alphabet_byte::<A>(b3);
invalid_byte |= !valid0;
invalid_byte |= !valid1;
invalid_byte |= !valid2;
invalid_byte |= !valid3;
invalid_padding |= ct_mask_eq_u8(b2, b'=');
invalid_padding |= ct_mask_eq_u8(b3, b'=');
read += 4;
}
let final_chunk =
read_quad_or_mark_invalid(input, read, &mut invalid_byte, &mut invalid_padding);
let (_, final_invalid_byte, final_invalid_padding, _) =
ct_padded_final_quantum::<A>(final_chunk, padding);
invalid_byte |= final_invalid_byte;
invalid_padding |= final_invalid_padding;
report_ct_error(invalid_byte, invalid_padding)
}
fn ct_validate_unpadded<A: Alphabet>(input: &[u8]) -> Result<(), DecodeError> {
if input.len() % 4 == 1 {
return Err(DecodeError::InvalidLength);
}
let mut invalid_byte = 0u8;
let mut invalid_padding = 0u8;
let mut read = 0;
while read + 4 <= input.len() {
let [b0, b1, b2, b3] =
read_quad_or_mark_invalid(input, read, &mut invalid_byte, &mut invalid_padding);
let (_, valid0) = ct_decode_alphabet_byte::<A>(b0);
let (_, valid1) = ct_decode_alphabet_byte::<A>(b1);
let (_, valid2) = ct_decode_alphabet_byte::<A>(b2);
let (_, valid3) = ct_decode_alphabet_byte::<A>(b3);
invalid_byte |= !valid0;
invalid_byte |= !valid1;
invalid_byte |= !valid2;
invalid_byte |= !valid3;
invalid_padding |= ct_mask_eq_u8(b0, b'=');
invalid_padding |= ct_mask_eq_u8(b1, b'=');
invalid_padding |= ct_mask_eq_u8(b2, b'=');
invalid_padding |= ct_mask_eq_u8(b3, b'=');
read += 4;
}
match read_tail_or_mark_invalid(input, read, &mut invalid_byte, &mut invalid_padding) {
[] => {}
[b0, b1] => {
let (_, valid0) = ct_decode_alphabet_byte::<A>(*b0);
let (v1, valid1) = ct_decode_alphabet_byte::<A>(*b1);
invalid_byte |= !valid0;
invalid_byte |= !valid1;
invalid_padding |= ct_mask_eq_u8(*b0, b'=');
invalid_padding |= ct_mask_eq_u8(*b1, b'=');
invalid_padding |= ct_mask_nonzero_u8(v1 & 0b0000_1111);
}
[b0, b1, b2] => {
let (_, valid0) = ct_decode_alphabet_byte::<A>(*b0);
let (_, valid1) = ct_decode_alphabet_byte::<A>(*b1);
let (v2, valid2) = ct_decode_alphabet_byte::<A>(*b2);
invalid_byte |= !valid0;
invalid_byte |= !valid1;
invalid_byte |= !valid2;
invalid_padding |= ct_mask_eq_u8(*b0, b'=');
invalid_padding |= ct_mask_eq_u8(*b1, b'=');
invalid_padding |= ct_mask_eq_u8(*b2, b'=');
invalid_padding |= ct_mask_nonzero_u8(v2 & 0b0000_0011);
}
_ => {
invalid_byte = 0xff;
invalid_padding = 0xff;
}
}
report_ct_error(invalid_byte, invalid_padding)
}
fn ct_padded_final_quantum<A: Alphabet>(
input: [u8; 4],
padding: usize,
) -> ([u8; 3], u8, u8, usize) {
let [b0, b1, b2, b3] = input;
let (v0, valid0) = ct_decode_alphabet_byte::<A>(b0);
let (v1, valid1) = ct_decode_alphabet_byte::<A>(b1);
let (v2, valid2) = ct_decode_alphabet_byte::<A>(b2);
let (v3, valid3) = ct_decode_alphabet_byte::<A>(b3);
let padding_byte = match padding {
0 => 0,
1 => 1,
2 => 2,
_ => return ([0; 3], 0xff, 0xff, 0),
};
let no_padding = ct_mask_eq_u8(padding_byte, 0);
let one_padding = ct_mask_eq_u8(padding_byte, 1);
let two_padding = ct_mask_eq_u8(padding_byte, 2);
let require_v2 = no_padding | one_padding;
let require_v3 = no_padding;
let invalid_byte = !valid0 | !valid1 | (!valid2 & require_v2) | (!valid3 & require_v3);
let invalid_padding = (ct_mask_nonzero_u8(v1 & 0b0000_1111) & two_padding)
| ((ct_mask_eq_u8(b2, b'=') | ct_mask_nonzero_u8(v2 & 0b0000_0011)) & one_padding)
| ((ct_mask_eq_u8(b2, b'=') | ct_mask_eq_u8(b3, b'=')) & no_padding);
(
[(v0 << 2) | (v1 >> 4), (v1 << 4) | (v2 >> 2), (v2 << 6) | v3],
invalid_byte,
invalid_padding,
3 - padding,
)
}
fn ct_decode_padded<A: Alphabet>(input: &[u8], output: &mut [u8]) -> Result<usize, DecodeError> {
if !input.len().is_multiple_of(4) {
return Err(DecodeError::InvalidLength);
}
let padding = ct_padding_len(input);
let required = input.len() / 4 * 3 - padding;
if output.len() < required {
return Err(DecodeError::OutputTooSmall {
required,
available: output.len(),
});
}
let mut invalid_byte = 0u8;
let mut invalid_padding = 0u8;
let mut write = 0;
let mut read = 0;
while read + 4 < input.len() {
let [b0, b1, b2, b3] =
read_quad_or_mark_invalid(input, read, &mut invalid_byte, &mut invalid_padding);
let (v0, valid0) = ct_decode_alphabet_byte::<A>(b0);
let (v1, valid1) = ct_decode_alphabet_byte::<A>(b1);
let (v2, valid2) = ct_decode_alphabet_byte::<A>(b2);
let (v3, valid3) = ct_decode_alphabet_byte::<A>(b3);
invalid_byte |= !valid0;
invalid_byte |= !valid1;
invalid_byte |= !valid2;
invalid_byte |= !valid3;
invalid_padding |= ct_mask_eq_u8(b2, b'=');
invalid_padding |= ct_mask_eq_u8(b3, b'=');
output[write] = (v0 << 2) | (v1 >> 4);
output[write + 1] = (v1 << 4) | (v2 >> 2);
output[write + 2] = (v2 << 6) | v3;
write += 3;
read += 4;
}
let final_chunk =
read_quad_or_mark_invalid(input, read, &mut invalid_byte, &mut invalid_padding);
let (final_bytes, final_invalid_byte, final_invalid_padding, final_written) =
ct_padded_final_quantum::<A>(final_chunk, padding);
invalid_byte |= final_invalid_byte;
invalid_padding |= final_invalid_padding;
output[write..write + final_written].copy_from_slice(&final_bytes[..final_written]);
write += final_written;
report_ct_error(invalid_byte, invalid_padding)?;
Ok(write)
}
fn ct_decode_padded_in_place<A: Alphabet>(buffer: &mut [u8]) -> Result<usize, DecodeError> {
if !buffer.len().is_multiple_of(4) {
return Err(DecodeError::InvalidLength);
}
let padding = ct_padding_len(buffer);
let required = buffer.len() / 4 * 3 - padding;
if required > buffer.len() {
wipe_bytes(buffer);
return Err(DecodeError::InvalidInput);
}
let mut invalid_byte = 0u8;
let mut invalid_padding = 0u8;
let mut write = 0;
let mut read = 0;
while read + 4 < buffer.len() {
let [b0, b1, b2, b3] =
read_quad_or_mark_invalid(buffer, read, &mut invalid_byte, &mut invalid_padding);
let (v0, valid0) = ct_decode_alphabet_byte::<A>(b0);
let (v1, valid1) = ct_decode_alphabet_byte::<A>(b1);
let (v2, valid2) = ct_decode_alphabet_byte::<A>(b2);
let (v3, valid3) = ct_decode_alphabet_byte::<A>(b3);
invalid_byte |= !valid0;
invalid_byte |= !valid1;
invalid_byte |= !valid2;
invalid_byte |= !valid3;
invalid_padding |= ct_mask_eq_u8(b2, b'=');
invalid_padding |= ct_mask_eq_u8(b3, b'=');
buffer[write] = (v0 << 2) | (v1 >> 4);
buffer[write + 1] = (v1 << 4) | (v2 >> 2);
buffer[write + 2] = (v2 << 6) | v3;
write += 3;
read += 4;
}
let final_chunk =
read_quad_or_mark_invalid(buffer, read, &mut invalid_byte, &mut invalid_padding);
let (final_bytes, final_invalid_byte, final_invalid_padding, final_written) =
ct_padded_final_quantum::<A>(final_chunk, padding);
invalid_byte |= final_invalid_byte;
invalid_padding |= final_invalid_padding;
buffer[write..write + final_written].copy_from_slice(&final_bytes[..final_written]);
write += final_written;
if write != required {
ct_error_gate_barrier(invalid_byte, invalid_padding);
wipe_bytes(buffer);
return Err(DecodeError::InvalidInput);
}
if let Err(err) = report_ct_error(invalid_byte, invalid_padding) {
wipe_bytes(buffer);
return Err(err);
}
Ok(write)
}
fn ct_decode_unpadded<A: Alphabet>(input: &[u8], output: &mut [u8]) -> Result<usize, DecodeError> {
if input.len() % 4 == 1 {
return Err(DecodeError::InvalidLength);
}
let required = decoded_capacity(input.len());
if output.len() < required {
return Err(DecodeError::OutputTooSmall {
required,
available: output.len(),
});
}
let mut invalid_byte = 0u8;
let mut invalid_padding = 0u8;
let mut write = 0;
let mut read = 0;
while read + 4 <= input.len() {
let [b0, b1, b2, b3] =
read_quad_or_mark_invalid(input, read, &mut invalid_byte, &mut invalid_padding);
let (v0, valid0) = ct_decode_alphabet_byte::<A>(b0);
let (v1, valid1) = ct_decode_alphabet_byte::<A>(b1);
let (v2, valid2) = ct_decode_alphabet_byte::<A>(b2);
let (v3, valid3) = ct_decode_alphabet_byte::<A>(b3);
invalid_byte |= !valid0;
invalid_byte |= !valid1;
invalid_byte |= !valid2;
invalid_byte |= !valid3;
invalid_padding |= ct_mask_eq_u8(b0, b'=');
invalid_padding |= ct_mask_eq_u8(b1, b'=');
invalid_padding |= ct_mask_eq_u8(b2, b'=');
invalid_padding |= ct_mask_eq_u8(b3, b'=');
output[write] = (v0 << 2) | (v1 >> 4);
output[write + 1] = (v1 << 4) | (v2 >> 2);
output[write + 2] = (v2 << 6) | v3;
read += 4;
write += 3;
}
match read_tail_or_mark_invalid(input, read, &mut invalid_byte, &mut invalid_padding) {
[] => {}
[b0, b1] => {
let (v0, valid0) = ct_decode_alphabet_byte::<A>(*b0);
let (v1, valid1) = ct_decode_alphabet_byte::<A>(*b1);
invalid_byte |= !valid0;
invalid_byte |= !valid1;
invalid_padding |= ct_mask_eq_u8(*b0, b'=');
invalid_padding |= ct_mask_eq_u8(*b1, b'=');
invalid_padding |= ct_mask_nonzero_u8(v1 & 0b0000_1111);
output[write] = (v0 << 2) | (v1 >> 4);
write += 1;
}
[b0, b1, b2] => {
let (v0, valid0) = ct_decode_alphabet_byte::<A>(*b0);
let (v1, valid1) = ct_decode_alphabet_byte::<A>(*b1);
let (v2, valid2) = ct_decode_alphabet_byte::<A>(*b2);
invalid_byte |= !valid0;
invalid_byte |= !valid1;
invalid_byte |= !valid2;
invalid_padding |= ct_mask_eq_u8(*b0, b'=');
invalid_padding |= ct_mask_eq_u8(*b1, b'=');
invalid_padding |= ct_mask_eq_u8(*b2, b'=');
invalid_padding |= ct_mask_nonzero_u8(v2 & 0b0000_0011);
output[write] = (v0 << 2) | (v1 >> 4);
output[write + 1] = (v1 << 4) | (v2 >> 2);
write += 2;
}
_ => {
invalid_byte = 0xff;
invalid_padding = 0xff;
}
}
report_ct_error(invalid_byte, invalid_padding)?;
Ok(write)
}
fn ct_decode_unpadded_in_place<A: Alphabet>(buffer: &mut [u8]) -> Result<usize, DecodeError> {
if buffer.len() % 4 == 1 {
return Err(DecodeError::InvalidLength);
}
let required = decoded_capacity(buffer.len());
if required > buffer.len() {
wipe_bytes(buffer);
return Err(DecodeError::InvalidInput);
}
let mut invalid_byte = 0u8;
let mut invalid_padding = 0u8;
let mut write = 0;
let mut read = 0;
while read + 4 <= buffer.len() {
let [b0, b1, b2, b3] =
read_quad_or_mark_invalid(buffer, read, &mut invalid_byte, &mut invalid_padding);
let (v0, valid0) = ct_decode_alphabet_byte::<A>(b0);
let (v1, valid1) = ct_decode_alphabet_byte::<A>(b1);
let (v2, valid2) = ct_decode_alphabet_byte::<A>(b2);
let (v3, valid3) = ct_decode_alphabet_byte::<A>(b3);
invalid_byte |= !valid0;
invalid_byte |= !valid1;
invalid_byte |= !valid2;
invalid_byte |= !valid3;
invalid_padding |= ct_mask_eq_u8(b0, b'=');
invalid_padding |= ct_mask_eq_u8(b1, b'=');
invalid_padding |= ct_mask_eq_u8(b2, b'=');
invalid_padding |= ct_mask_eq_u8(b3, b'=');
buffer[write] = (v0 << 2) | (v1 >> 4);
buffer[write + 1] = (v1 << 4) | (v2 >> 2);
buffer[write + 2] = (v2 << 6) | v3;
read += 4;
write += 3;
}
let tail = read_tail_or_mark_invalid(buffer, read, &mut invalid_byte, &mut invalid_padding);
match tail {
[] => {}
[b0, b1] => {
let (v0, valid0) = ct_decode_alphabet_byte::<A>(*b0);
let (v1, valid1) = ct_decode_alphabet_byte::<A>(*b1);
invalid_byte |= !valid0;
invalid_byte |= !valid1;
invalid_padding |= ct_mask_eq_u8(*b0, b'=');
invalid_padding |= ct_mask_eq_u8(*b1, b'=');
invalid_padding |= ct_mask_nonzero_u8(v1 & 0b0000_1111);
buffer[write] = (v0 << 2) | (v1 >> 4);
write += 1;
}
[b0, b1, b2] => {
let (v0, valid0) = ct_decode_alphabet_byte::<A>(*b0);
let (v1, valid1) = ct_decode_alphabet_byte::<A>(*b1);
let (v2, valid2) = ct_decode_alphabet_byte::<A>(*b2);
invalid_byte |= !valid0;
invalid_byte |= !valid1;
invalid_byte |= !valid2;
invalid_padding |= ct_mask_eq_u8(*b0, b'=');
invalid_padding |= ct_mask_eq_u8(*b1, b'=');
invalid_padding |= ct_mask_eq_u8(*b2, b'=');
invalid_padding |= ct_mask_nonzero_u8(v2 & 0b0000_0011);
buffer[write] = (v0 << 2) | (v1 >> 4);
buffer[write + 1] = (v1 << 4) | (v2 >> 2);
write += 2;
}
_ => {
invalid_byte = 0xff;
invalid_padding = 0xff;
}
}
if write != required {
ct_error_gate_barrier(invalid_byte, invalid_padding);
wipe_bytes(buffer);
return Err(DecodeError::InvalidInput);
}
if let Err(err) = report_ct_error(invalid_byte, invalid_padding) {
wipe_bytes(buffer);
return Err(err);
}
Ok(write)
}
fn read_tail(input: &[u8], offset: usize) -> Result<&[u8], DecodeError> {
input.get(offset..).ok_or(DecodeError::InvalidLength)
}
fn read_quad_or_mark_invalid(
input: &[u8],
offset: usize,
invalid_byte: &mut u8,
invalid_padding: &mut u8,
) -> [u8; 4] {
if let Ok(quad) = read_quad(input, offset) {
quad
} else {
debug_assert!(
false,
"read_quad failed inside length-validated constant-time decode loop"
);
*invalid_byte = 0xff;
*invalid_padding = 0xff;
[0; 4]
}
}
fn read_tail_or_mark_invalid<'a>(
input: &'a [u8],
offset: usize,
invalid_byte: &mut u8,
invalid_padding: &mut u8,
) -> &'a [u8] {
if let Ok(tail) = read_tail(input, offset) {
tail
} else {
debug_assert!(
false,
"read_tail failed inside length-validated constant-time decode loop"
);
*invalid_byte = 0xff;
*invalid_padding = 0xff;
&[]
}
}
#[inline(never)]
#[allow(unsafe_code)]
fn ct_decode_alphabet_byte<A: Alphabet>(byte: u8) -> (u8, u8) {
let mut decoded = 0u8;
let mut valid = 0u8;
let mut candidate = 0u8;
while candidate < 64 {
let matches = core::hint::black_box(ct_mask_eq_u8(
core::hint::black_box(byte),
core::hint::black_box(A::ENCODE[candidate as usize]),
));
decoded = core::hint::black_box(
core::hint::black_box(decoded) | core::hint::black_box(candidate & matches),
);
decoded = unsafe { core::ptr::read_volatile(&raw const decoded) };
valid =
core::hint::black_box(core::hint::black_box(valid) | core::hint::black_box(matches));
valid = unsafe { core::ptr::read_volatile(&raw const valid) };
candidate += 1;
}
(decoded, valid)
}
fn ct_padding_len(input: &[u8]) -> usize {
let Some((&last, before_last_prefix)) = input.split_last() else {
return 0;
};
let Some(&before_last) = before_last_prefix.last() else {
return 0;
};
usize::from(ct_mask_eq_u8(last, b'=') & 1) + usize::from(ct_mask_eq_u8(before_last, b'=') & 1)
}
fn report_ct_error(invalid_byte: u8, invalid_padding: u8) -> Result<(), DecodeError> {
ct_error_gate_barrier(invalid_byte, invalid_padding);
if (invalid_byte | invalid_padding) != 0 {
Err(DecodeError::InvalidInput)
} else {
Ok(())
}
}
#[cfg(kani)]
mod kani_proofs {
use super::{
STANDARD, Standard, checked_encoded_len, ct, decode_byte, decode_chunk,
decode_tail_unpadded, decoded_capacity, validate_tail_unpadded,
};
#[kani::proof]
fn checked_encoded_len_is_bounded_for_small_inputs() {
let len = usize::from(kani::any::<u8>());
let padded = kani::any::<bool>();
let encoded = checked_encoded_len(len, padded).expect("u8 input length cannot overflow");
assert!(encoded >= len);
assert!(encoded <= len / 3 * 4 + 4);
}
#[kani::proof]
fn decoded_capacity_is_bounded_for_small_inputs() {
let len = usize::from(kani::any::<u8>());
let capacity = decoded_capacity(len);
assert!(capacity <= len / 4 * 3 + 2);
}
#[kani::proof]
#[kani::unwind(3)]
fn standard_in_place_decode_returns_prefix_within_buffer() {
let mut buffer = kani::any::<[u8; 8]>();
let result = STANDARD.decode_in_place(&mut buffer);
if let Ok(decoded) = result {
assert!(decoded.len() <= 8);
}
}
#[kani::proof]
#[kani::unwind(3)]
fn standard_decode_slice_returns_written_within_output() {
let input = kani::any::<[u8; 4]>();
let mut output = kani::any::<[u8; 3]>();
let result = STANDARD.decode_slice(&input, &mut output);
if let Ok(written) = result {
assert!(written <= output.len());
}
}
#[kani::proof]
#[kani::unwind(3)]
fn standard_decode_chunk_returns_written_within_output() {
let input = kani::any::<[u8; 4]>();
let mut output = kani::any::<[u8; 3]>();
let result = decode_chunk::<Standard, true>(input, &mut output);
if let Ok(written) = result {
assert!(written <= output.len());
assert!(written <= 3);
}
}
#[kani::proof]
#[kani::unwind(3)]
fn standard_decode_chunk_bit_packing_matches_decoded_values() {
let input = kani::any::<[u8; 4]>();
let mut output = kani::any::<[u8; 3]>();
let result = decode_chunk::<Standard, true>(input, &mut output);
if let Ok(written) = result {
let v0 = decode_byte::<Standard>(input[0], 0).expect("successful chunk has v0");
let v1 = decode_byte::<Standard>(input[1], 1).expect("successful chunk has v1");
assert!(output[0] == ((v0 << 2) | (v1 >> 4)));
if written >= 2 {
let v2 = decode_byte::<Standard>(input[2], 2).expect("successful chunk has v2");
assert!(output[1] == ((v1 << 4) | (v2 >> 2)));
}
if written == 3 {
let v2 = decode_byte::<Standard>(input[2], 2).expect("successful chunk has v2");
let v3 = decode_byte::<Standard>(input[3], 3).expect("successful chunk has v3");
assert!(output[2] == ((v2 << 6) | v3));
}
}
}
#[kani::proof]
#[kani::unwind(3)]
fn standard_validate_tail_unpadded_accepts_or_rejects_without_panic() {
let input = kani::any::<[u8; 3]>();
let len = usize::from(kani::any::<u8>() % 4);
let result = validate_tail_unpadded::<Standard>(&input[..len]);
if result.is_ok() {
assert!(len == 0 || len == 2 || len == 3);
}
}
#[kani::proof]
#[kani::unwind(3)]
fn standard_decode_two_byte_tail_returns_written_within_output() {
let input = kani::any::<[u8; 2]>();
let mut output = kani::any::<[u8; 1]>();
let result = decode_tail_unpadded::<Standard>(&input, &mut output);
if let Ok(written) = result {
assert!(written <= output.len());
assert!(written == 1);
}
}
#[kani::proof]
#[kani::unwind(3)]
fn standard_decode_three_byte_tail_returns_written_within_output() {
let input = kani::any::<[u8; 3]>();
let mut output = kani::any::<[u8; 2]>();
let result = decode_tail_unpadded::<Standard>(&input, &mut output);
if let Ok(written) = result {
assert!(written <= output.len());
assert!(written == 2);
}
}
#[kani::proof]
#[kani::unwind(3)]
fn standard_decode_slice_clear_tail_clears_output_on_error() {
let input = kani::any::<[u8; 4]>();
let mut output = kani::any::<[u8; 3]>();
let result = STANDARD.decode_slice_clear_tail(&input, &mut output);
if result.is_err() {
assert!(output.iter().all(|byte| *byte == 0));
}
}
#[kani::proof]
#[kani::unwind(3)]
fn standard_encode_slice_returns_written_within_output() {
let input = kani::any::<[u8; 3]>();
let mut output = kani::any::<[u8; 4]>();
let result = STANDARD.encode_slice(&input, &mut output);
if let Ok(written) = result {
assert!(written <= output.len());
}
}
#[kani::proof]
#[kani::unwind(4)]
fn standard_encode_in_place_returns_prefix_within_buffer() {
let mut buffer = kani::any::<[u8; 8]>();
let input_len = usize::from(kani::any::<u8>() % 9);
let result = STANDARD.encode_in_place(&mut buffer, input_len);
if let Ok(encoded) = result {
assert!(encoded.len() <= 8);
}
}
#[kani::proof]
#[kani::unwind(3)]
fn standard_clear_tail_decode_clears_buffer_on_error() {
let mut buffer = kani::any::<[u8; 4]>();
let result = STANDARD.decode_in_place_clear_tail(&mut buffer);
if result.is_err() {
assert!(buffer.iter().all(|byte| *byte == 0));
}
}
#[kani::proof]
#[kani::unwind(3)]
fn ct_standard_decode_slice_returns_written_within_output() {
let input = kani::any::<[u8; 4]>();
let mut output = kani::any::<[u8; 3]>();
let result = ct::STANDARD.decode_slice_clear_tail(&input, &mut output);
if let Ok(written) = result {
assert!(written <= output.len());
}
}
#[kani::proof]
#[kani::unwind(3)]
fn ct_standard_decode_slice_clear_tail_clears_output_on_error() {
let input = kani::any::<[u8; 4]>();
let mut output = kani::any::<[u8; 3]>();
let result = ct::STANDARD.decode_slice_clear_tail(&input, &mut output);
if result.is_err() {
assert!(output.iter().all(|byte| *byte == 0));
}
}
#[kani::proof]
#[kani::unwind(3)]
fn ct_standard_decode_in_place_clear_tail_clears_buffer_on_error() {
let mut buffer = kani::any::<[u8; 4]>();
let result = ct::STANDARD.decode_in_place_clear_tail(&mut buffer);
if result.is_err() {
assert!(buffer.iter().all(|byte| *byte == 0));
}
}
#[kani::proof]
#[kani::unwind(3)]
fn ct_standard_validate_matches_decode_for_one_quantum() {
let input = kani::any::<[u8; 4]>();
let mut output = kani::any::<[u8; 3]>();
let validate_ok = ct::STANDARD.validate_result(&input).is_ok();
let decode_ok = ct::STANDARD
.decode_slice_clear_tail(&input, &mut output)
.is_ok();
assert!(validate_ok == decode_ok);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fill_pattern(output: &mut [u8], seed: usize) {
for (index, byte) in output.iter_mut().enumerate() {
let value = (index * 73 + seed * 19) % 256;
*byte = u8::try_from(value).unwrap();
}
}
fn assert_encode_backend_matches_scalar<A, const PAD: bool>(input: &[u8])
where
A: Alphabet,
{
let engine = Engine::<A, PAD>::new();
let mut dispatched = [0x55; 256];
let mut scalar = [0xaa; 256];
let dispatched_result = engine.encode_slice(input, &mut dispatched);
let scalar_result = backend::scalar_reference_encode_slice::<A, PAD>(input, &mut scalar);
assert_eq!(dispatched_result, scalar_result);
if let Ok(written) = dispatched_result {
assert_eq!(&dispatched[..written], &scalar[..written]);
}
let required = checked_encoded_len(input.len(), PAD).unwrap();
if required > 0 {
let mut dispatched_short = [0x55; 256];
let mut scalar_short = [0xaa; 256];
let available = required - 1;
assert_eq!(
engine.encode_slice(input, &mut dispatched_short[..available]),
backend::scalar_reference_encode_slice::<A, PAD>(
input,
&mut scalar_short[..available],
)
);
}
}
fn assert_decode_backend_matches_scalar<A, const PAD: bool>(input: &[u8])
where
A: Alphabet,
{
let engine = Engine::<A, PAD>::new();
let mut dispatched = [0x55; 128];
let mut scalar = [0xaa; 128];
let dispatched_result = engine.decode_slice(input, &mut dispatched);
let scalar_result = backend::scalar_reference_decode_slice::<A, PAD>(input, &mut scalar);
assert_eq!(dispatched_result, scalar_result);
if let Ok(written) = dispatched_result {
assert_eq!(&dispatched[..written], &scalar[..written]);
if written > 0 {
let mut dispatched_short = [0x55; 128];
let mut scalar_short = [0xaa; 128];
let available = written - 1;
assert_eq!(
engine.decode_slice(input, &mut dispatched_short[..available]),
backend::scalar_reference_decode_slice::<A, PAD>(
input,
&mut scalar_short[..available],
)
);
}
}
}
fn assert_backend_round_trip_matches_scalar<A, const PAD: bool>(input: &[u8])
where
A: Alphabet,
{
assert_encode_backend_matches_scalar::<A, PAD>(input);
let mut encoded = [0; 256];
let encoded_len =
backend::scalar_reference_encode_slice::<A, PAD>(input, &mut encoded).unwrap();
assert_decode_backend_matches_scalar::<A, PAD>(&encoded[..encoded_len]);
}
fn assert_standard_decode_chunk_matches_input(input: &[u8]) {
let mut encoded = [0u8; 4];
let encoded_len = STANDARD.encode_slice(input, &mut encoded).unwrap();
assert_eq!(encoded_len, 4);
let chunk = [encoded[0], encoded[1], encoded[2], encoded[3]];
let mut decoded = [0u8; 3];
let decoded_len = decode_chunk::<Standard, true>(chunk, &mut decoded).unwrap();
assert_eq!(decoded_len, input.len());
assert_eq!(&decoded[..decoded_len], input);
}
#[test]
fn backend_dispatch_matches_scalar_reference_for_canonical_inputs() {
let mut input = [0; 128];
for input_len in 0..=input.len() {
fill_pattern(&mut input[..input_len], input_len);
let input = &input[..input_len];
assert_backend_round_trip_matches_scalar::<Standard, true>(input);
assert_backend_round_trip_matches_scalar::<Standard, false>(input);
assert_backend_round_trip_matches_scalar::<UrlSafe, true>(input);
assert_backend_round_trip_matches_scalar::<UrlSafe, false>(input);
}
}
#[test]
fn backend_dispatch_matches_scalar_reference_for_malformed_inputs() {
for input in [
&b"Z"[..],
b"====",
b"AA=A",
b"Zh==",
b"Zm9=",
b"Zm9v$g==",
b"Zm9vZh==",
] {
assert_decode_backend_matches_scalar::<Standard, true>(input);
}
for input in [&b"Z"[..], b"AA=A", b"Zh", b"Zm9", b"Zm9vYg$"] {
assert_decode_backend_matches_scalar::<Standard, false>(input);
}
assert_decode_backend_matches_scalar::<UrlSafe, true>(b"AA+A");
assert_decode_backend_matches_scalar::<UrlSafe, false>(b"AA/A");
assert_decode_backend_matches_scalar::<Standard, true>(b"AA-A");
assert_decode_backend_matches_scalar::<Standard, false>(b"AA_A");
}
#[test]
fn decode_chunk_bit_packing_matches_exhaustive_small_inputs() {
for byte in u8::MIN..=u8::MAX {
assert_standard_decode_chunk_matches_input(&[byte]);
}
for first in u8::MIN..=u8::MAX {
for second in u8::MIN..=u8::MAX {
assert_standard_decode_chunk_matches_input(&[first, second]);
}
}
}
#[test]
fn decode_chunk_bit_packing_matches_representative_full_quanta() {
const SAMPLES: [u8; 16] = [
0, 1, 2, 15, 16, 31, 32, 63, 64, 95, 127, 128, 191, 192, 254, 255,
];
for first in SAMPLES {
for second in SAMPLES {
for third in SAMPLES {
assert_standard_decode_chunk_matches_input(&[first, second, third]);
}
}
}
}
#[test]
fn ct_padded_final_quantum_fails_closed_for_invalid_padding_count() {
let (_, invalid_byte, invalid_padding, written) =
ct_padded_final_quantum::<Standard>(*b"ABCD", 3);
assert_ne!(invalid_byte, 0);
assert_ne!(invalid_padding, 0);
assert_eq!(written, 0);
assert_eq!(
report_ct_error(invalid_byte, invalid_padding),
Err(DecodeError::InvalidInput)
);
}
#[cfg(feature = "simd")]
#[test]
fn simd_dispatch_scaffold_keeps_scalar_active() {
assert_eq!(simd::active_backend(), simd::ActiveBackend::Scalar);
let _candidate = simd::detected_candidate();
}
#[test]
fn encodes_standard_vectors() {
let vectors = [
(&b""[..], &b""[..]),
(&b"f"[..], &b"Zg=="[..]),
(&b"fo"[..], &b"Zm8="[..]),
(&b"foo"[..], &b"Zm9v"[..]),
(&b"foob"[..], &b"Zm9vYg=="[..]),
(&b"fooba"[..], &b"Zm9vYmE="[..]),
(&b"foobar"[..], &b"Zm9vYmFy"[..]),
];
for (input, expected) in vectors {
let mut output = [0u8; 16];
let written = STANDARD.encode_slice(input, &mut output).unwrap();
assert_eq!(&output[..written], expected);
}
}
#[test]
fn decodes_standard_vectors() {
let vectors = [
(&b""[..], &b""[..]),
(&b"Zg=="[..], &b"f"[..]),
(&b"Zm8="[..], &b"fo"[..]),
(&b"Zm9v"[..], &b"foo"[..]),
(&b"Zm9vYg=="[..], &b"foob"[..]),
(&b"Zm9vYmE="[..], &b"fooba"[..]),
(&b"Zm9vYmFy"[..], &b"foobar"[..]),
];
for (input, expected) in vectors {
let mut output = [0u8; 16];
let written = STANDARD.decode_slice(input, &mut output).unwrap();
assert_eq!(&output[..written], expected);
}
}
#[test]
fn supports_unpadded_url_safe() {
let mut encoded = [0u8; 16];
let written = URL_SAFE_NO_PAD
.encode_slice(b"\xfb\xff", &mut encoded)
.unwrap();
assert_eq!(&encoded[..written], b"-_8");
let mut decoded = [0u8; 2];
let written = URL_SAFE_NO_PAD
.decode_slice(&encoded[..written], &mut decoded)
.unwrap();
assert_eq!(&decoded[..written], b"\xfb\xff");
}
#[test]
fn decodes_in_place() {
let mut buffer = *b"Zm9vYmFy";
let decoded = STANDARD_NO_PAD.decode_in_place(&mut buffer).unwrap();
assert_eq!(decoded, b"foobar");
}
#[test]
fn rejects_non_canonical_padding_bits() {
let mut output = [0u8; 4];
assert_eq!(
STANDARD.decode_slice(b"Zh==", &mut output),
Err(DecodeError::InvalidPadding { index: 1 })
);
assert_eq!(
STANDARD.decode_slice(b"Zm9=", &mut output),
Err(DecodeError::InvalidPadding { index: 2 })
);
}
}