#![expect(
clippy::doc_paragraphs_missing_punctuation,
reason = "false positive for crate documentation having image links"
)]
#![cfg_attr(
all(
test,
target_os = "macos",
any(
target_pointer_width = "16",
target_pointer_width = "32",
target_pointer_width = "64"
)
),
allow(linker_info, reason = "getrandom causes linker-info to fire on macos")
)]
#![no_std]
#![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(any(doc, feature = "alloc"))]
extern crate alloc;
#[cfg(test)]
mod tests;
#[cfg(any(doc, feature = "alloc"))]
use alloc::{collections::TryReserveError, string::String, vec::Vec};
use core::{
error::Error,
fmt::{self, Display, Formatter, Write},
hint::cold_path,
mem,
};
#[expect(
non_camel_case_types,
reason = "want to use a variant as close to what the value is"
)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[repr(u8)]
pub enum Alphabet {
#[default]
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z,
a,
b,
c,
d,
e,
f,
g,
h,
i,
j,
k,
l,
m,
n,
o,
p,
q,
r,
s,
t,
u,
v,
w,
x,
y,
z,
Zero,
One,
Two,
Three,
Four,
Five,
Six,
Seven,
Eight,
Nine,
Hyphen,
Underscore,
}
const ASCII: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
const CHARS: &[char; 64] = &[
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4',
'5', '6', '7', '8', '9', '-', '_',
];
const FROM_ASCII: &[Option<Alphabet>; 256] = &[
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
Some(Alphabet::Hyphen),
None,
None,
Some(Alphabet::Zero),
Some(Alphabet::One),
Some(Alphabet::Two),
Some(Alphabet::Three),
Some(Alphabet::Four),
Some(Alphabet::Five),
Some(Alphabet::Six),
Some(Alphabet::Seven),
Some(Alphabet::Eight),
Some(Alphabet::Nine),
None,
None,
None,
None,
None,
None,
None,
Some(Alphabet::A),
Some(Alphabet::B),
Some(Alphabet::C),
Some(Alphabet::D),
Some(Alphabet::E),
Some(Alphabet::F),
Some(Alphabet::G),
Some(Alphabet::H),
Some(Alphabet::I),
Some(Alphabet::J),
Some(Alphabet::K),
Some(Alphabet::L),
Some(Alphabet::M),
Some(Alphabet::N),
Some(Alphabet::O),
Some(Alphabet::P),
Some(Alphabet::Q),
Some(Alphabet::R),
Some(Alphabet::S),
Some(Alphabet::T),
Some(Alphabet::U),
Some(Alphabet::V),
Some(Alphabet::W),
Some(Alphabet::X),
Some(Alphabet::Y),
Some(Alphabet::Z),
None,
None,
None,
None,
Some(Alphabet::Underscore),
None,
Some(Alphabet::a),
Some(Alphabet::b),
Some(Alphabet::c),
Some(Alphabet::d),
Some(Alphabet::e),
Some(Alphabet::f),
Some(Alphabet::g),
Some(Alphabet::h),
Some(Alphabet::i),
Some(Alphabet::j),
Some(Alphabet::k),
Some(Alphabet::l),
Some(Alphabet::m),
Some(Alphabet::n),
Some(Alphabet::o),
Some(Alphabet::p),
Some(Alphabet::q),
Some(Alphabet::r),
Some(Alphabet::s),
Some(Alphabet::t),
Some(Alphabet::u),
Some(Alphabet::v),
Some(Alphabet::w),
Some(Alphabet::x),
Some(Alphabet::y),
Some(Alphabet::z),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
];
impl Alphabet {
#[expect(unsafe_code, reason = "comment justifies correctness")]
#[expect(clippy::as_conversions, reason = "comment justifies correctness")]
#[inline]
#[must_use]
pub const fn from_u8(b: u8) -> Option<Self> {
if b <= Self::Underscore as u8 {
Some(unsafe { mem::transmute::<u8, Self>(b) })
} else {
None
}
}
#[expect(clippy::as_conversions, reason = "comment justifies correctness")]
#[inline]
#[must_use]
pub const fn to_u8(self) -> u8 {
self as u8
}
#[expect(
clippy::as_conversions,
clippy::indexing_slicing,
reason = "comments justify correctness"
)]
#[inline]
#[must_use]
pub const fn to_ascii(self) -> u8 {
ASCII[self.to_u8() as usize]
}
#[expect(
clippy::as_conversions,
clippy::indexing_slicing,
reason = "comments justify correctness"
)]
#[inline]
#[must_use]
pub const fn from_ascii(ascii: u8) -> Option<Self> {
FROM_ASCII[ascii as usize]
}
#[expect(
clippy::as_conversions,
clippy::indexing_slicing,
reason = "comments justify correctness"
)]
#[inline]
#[must_use]
pub const fn to_char(self) -> char {
CHARS[self.to_u8() as usize]
}
#[expect(
clippy::as_conversions,
clippy::cast_possible_truncation,
reason = "comments justify correctness"
)]
#[inline]
#[must_use]
pub const fn from_char(c: char) -> Option<Self> {
let code_point = c as u32;
if code_point < 256 {
Self::from_ascii(code_point as u8)
} else {
None
}
}
}
impl Display for Alphabet {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_char(self.to_char())
}
}
impl From<Alphabet> for u8 {
#[inline]
fn from(value: Alphabet) -> Self {
value.to_u8()
}
}
impl From<Alphabet> for u16 {
#[inline]
fn from(value: Alphabet) -> Self {
Self::from(value.to_u8())
}
}
impl From<Alphabet> for u32 {
#[inline]
fn from(value: Alphabet) -> Self {
Self::from(value.to_u8())
}
}
impl From<Alphabet> for u64 {
#[inline]
fn from(value: Alphabet) -> Self {
Self::from(value.to_u8())
}
}
impl From<Alphabet> for u128 {
#[inline]
fn from(value: Alphabet) -> Self {
Self::from(value.to_u8())
}
}
impl From<Alphabet> for char {
#[inline]
fn from(value: Alphabet) -> Self {
value.to_char()
}
}
pub const MAX_ENCODE_INPUT_LEN: usize = decode_len(usize::MAX).unwrap();
#[expect(
clippy::arithmetic_side_effects,
clippy::integer_division,
clippy::integer_division_remainder_used,
reason = "proof and comment justifies their correctness"
)]
#[inline]
#[must_use]
pub const fn encode_len_checked(input_length: usize) -> Option<usize> {
if input_length <= MAX_ENCODE_INPUT_LEN {
Some(((input_length / 3) << 2) + ((input_length % 3) << 2).div_ceil(3))
} else {
cold_path();
None
}
}
#[expect(clippy::unwrap_used, reason = "comment justifies correctness")]
#[inline]
#[must_use]
pub const fn encode_len(input_length: usize) -> usize {
encode_len_checked(input_length).unwrap()
}
macro_rules! impossible {
( $( $x:literal)? ) => {
{
cold_path();
$(
panic!($x);
)?
}
};
}
#[expect(unsafe_code, reason = "comments justify correctness")]
#[expect(
clippy::missing_asserts_for_indexing,
reason = "trust the compiler to already optimize since we match on the length"
)]
#[expect(
clippy::arithmetic_side_effects,
clippy::as_conversions,
clippy::indexing_slicing,
reason = "comments justify correctness"
)]
#[inline]
pub const fn encode_buffer_checked<'a>(input: &[u8], output: &'a mut [u8]) -> Option<&'a mut str> {
let final_len = encode_len(input.len());
if output.len() >= final_len {
let (mut chunks, rem) = input.as_chunks::<3>();
let (mut fst, mut snd, mut third);
let mut output_idx = 0;
while let [first, ref rest @ ..] = *chunks {
(fst, snd, third) = (first[0], first[1], first[2]);
output[output_idx] = ASCII[(fst >> 2) as usize];
output_idx += 1;
output[output_idx] = ASCII[(((fst & 3) << 4) | (snd >> 4)) as usize];
output_idx += 1;
output[output_idx] = ASCII[(((snd & 15) << 2) | (third >> 6)) as usize];
output_idx += 1;
output[output_idx] = ASCII[(third & 63) as usize];
output_idx += 1;
chunks = rest;
}
match rem.len() {
0 => {}
1 => {
fst = rem[0];
output[output_idx] = ASCII[(fst >> 2) as usize];
output[output_idx + 1] = ASCII[((fst & 3) << 4) as usize];
}
2 => {
(fst, snd) = (rem[0], rem[1]);
output[output_idx] = ASCII[(fst >> 2) as usize];
output[output_idx + 1] = ASCII[(((fst & 3) << 4) | (snd >> 4)) as usize];
output[output_idx + 2] = ASCII[((snd & 15) << 2) as usize];
}
_ => impossible!("there is a bug in core::slice::as_chunks"),
}
let val = unsafe { output.split_at_mut_unchecked(final_len) }.0;
Some(unsafe { str::from_utf8_unchecked_mut(val) })
} else {
cold_path();
None
}
}
#[expect(clippy::unwrap_used, reason = "comment justifies correctness")]
#[inline]
pub const fn encode_buffer<'a>(input: &[u8], output: &'a mut [u8]) -> &'a mut str {
encode_buffer_checked(input, output).unwrap()
}
#[cfg(feature = "alloc")]
#[expect(unsafe_code, reason = "comment justifies correctness")]
#[inline]
pub fn try_encode(input: &[u8]) -> Result<String, TryReserveError> {
let mut output = Vec::new();
let len = encode_len(input.len());
output.try_reserve_exact(len).map(|()| {
output.resize(len, 0);
_ = encode_buffer(input, output.as_mut_slice());
unsafe { String::from_utf8_unchecked(output) }
})
}
#[cfg(feature = "alloc")]
#[expect(
clippy::unwrap_used,
reason = "purpose of function is to panic on allocation failure"
)]
#[inline]
#[must_use]
pub fn encode(input: &[u8]) -> String {
try_encode(input).unwrap()
}
#[expect(unsafe_code, reason = "comment justifies correctness")]
#[expect(
clippy::arithmetic_side_effects,
clippy::as_conversions,
clippy::indexing_slicing,
reason = "comments justify correctness"
)]
#[inline]
pub fn encode_write<W: Write>(mut input: &[u8], mut writer: W) -> fmt::Result {
const MAX_BUFFER_LEN: usize = 1024;
const _: () = {
const MAX_LEN: usize = i16::MAX as usize;
assert!(
4 <= MAX_BUFFER_LEN && MAX_BUFFER_LEN < MAX_LEN && MAX_BUFFER_LEN.is_power_of_two(),
"encode_write::MAX_BUFFER_LEN must be a power of two less than i16::MAX but at least as large as 4"
);
};
const INPUT_LEN: usize = decode_len(MAX_BUFFER_LEN).unwrap();
let mut buffer = [0; MAX_BUFFER_LEN];
let len = encode_len(input.len());
if len <= MAX_BUFFER_LEN {
writer.write_str(encode_buffer(input, &mut buffer[..len]))
} else {
let mut counter = 0;
let term = len >> MAX_BUFFER_LEN.trailing_zeros();
let mut input_buffer;
while counter < term {
input_buffer = unsafe { input.split_at_unchecked(INPUT_LEN) };
writer.write_str(encode_buffer(input_buffer.0, buffer.as_mut_slice()))?;
input = input_buffer.1;
counter += 1;
}
writer.write_str(encode_buffer(input, &mut buffer[..encode_len(input.len())]))
}
}
#[cfg(feature = "alloc")]
#[expect(unsafe_code, reason = "comment justifies correctness")]
#[expect(
clippy::arithmetic_side_effects,
clippy::indexing_slicing,
reason = "comments justify correctness"
)]
#[inline]
pub fn try_encode_append<'a>(
input: &[u8],
s: &'a mut String,
) -> Result<&'a mut str, TryReserveError> {
let additional_len = encode_len(input.len());
s.try_reserve_exact(additional_len).map(|()| {
let utf8 = unsafe { s.as_mut_vec() };
let original_len = utf8.len();
utf8.resize(original_len + additional_len, 0);
encode_buffer(input, &mut utf8[original_len..])
})
}
#[cfg(feature = "alloc")]
#[inline]
pub fn try_encode_append_only(input: &[u8], s: &mut String) -> Result<(), TryReserveError> {
try_encode_append(input, s).map(|_| ())
}
#[cfg(feature = "alloc")]
#[expect(
clippy::unwrap_used,
reason = "purpose of this function is to panic on allocation failure"
)]
#[inline]
pub fn encode_append<'a>(input: &[u8], s: &'a mut String) -> &'a mut str {
try_encode_append(input, s).unwrap()
}
#[cfg(feature = "alloc")]
#[inline]
pub fn encode_append_only(input: &[u8], s: &mut String) {
_ = encode_append(input, s);
}
#[expect(
clippy::arithmetic_side_effects,
reason = "proof and comment justifies their correctness"
)]
#[inline]
#[must_use]
pub const fn decode_len(input_length: usize) -> Option<usize> {
let rem = input_length & 3;
if rem == 1 {
None
} else {
Some((3 * (input_length >> 2)) + ((3 * rem) >> 2))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DecodeErr {
EncodedLen,
BufferLen,
TrailingBits,
InvalidByte,
#[cfg(feature = "alloc")]
TryReserve(TryReserveError),
}
#[cfg(not(feature = "alloc"))]
impl Copy for DecodeErr {}
impl Display for DecodeErr {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
Self::EncodedLen => f.write_str("length of encoded data was invalid"),
Self::BufferLen => {
f.write_str("length of the output buffer is too small to contain the decoded data")
}
Self::TrailingBits => {
f.write_str("encoded data contained trailing bits that were not zero")
}
Self::InvalidByte => f.write_str("encoded data contained an invalid byte"),
#[cfg(feature = "alloc")]
Self::TryReserve(ref err) => err.fmt(f),
}
}
}
impl Error for DecodeErr {}
#[expect(unsafe_code, reason = "comment justifies correctness")]
#[expect(
clippy::missing_asserts_for_indexing,
reason = "trust the compiler to already optimize since we match on the length"
)]
#[expect(
clippy::arithmetic_side_effects,
clippy::indexing_slicing,
reason = "comments justify correctness"
)]
#[expect(clippy::redundant_else, reason = "prefer the elses")]
#[inline]
pub const fn decode_buffer<'a>(
input: &[u8],
output: &'a mut [u8],
) -> Result<&'a mut [u8], DecodeErr> {
let len = input.len();
if let Some(output_len) = decode_len(len) {
if output.len() >= output_len {
let mut output_idx = 0;
let (mut chunks, rem) = input.as_chunks::<4>();
let (mut snd, mut third);
while let [first, ref rest @ ..] = *chunks {
if let Some(base64_fst) = Alphabet::from_ascii(first[0])
&& let Some(base64_snd) = Alphabet::from_ascii(first[1])
&& let Some(base64_third) = Alphabet::from_ascii(first[2])
&& let Some(base64_fourth) = Alphabet::from_ascii(first[3])
{
(snd, third) = (base64_snd.to_u8(), base64_third.to_u8());
output[output_idx] = (base64_fst.to_u8() << 2) | (snd >> 4);
output_idx += 1;
output[output_idx] = (snd << 4) | (third >> 2);
output_idx += 1;
output[output_idx] = (third << 6) | base64_fourth.to_u8();
output_idx += 1;
chunks = rest;
} else {
return Err(DecodeErr::InvalidByte);
}
}
match rem.len() {
0 => {}
1 => impossible!("there is a bug in base64url_nopad::decode_len"),
2 => {
if let Some(base64_fst) = Alphabet::from_ascii(rem[0])
&& let Some(base64_snd) = Alphabet::from_ascii(rem[1])
{
snd = base64_snd.to_u8();
if snd.trailing_zeros() < 4 {
cold_path();
return Err(DecodeErr::TrailingBits);
} else {
output[output_idx] = (base64_fst.to_u8() << 2) | (snd >> 4);
}
} else {
cold_path();
return Err(DecodeErr::InvalidByte);
}
}
3 => {
if let Some(base64_fst) = Alphabet::from_ascii(rem[0])
&& let Some(base64_snd) = Alphabet::from_ascii(rem[1])
&& let Some(base64_third) = Alphabet::from_ascii(rem[2])
{
(snd, third) = (base64_snd.to_u8(), base64_third.to_u8());
if third.trailing_zeros() < 2 {
cold_path();
return Err(DecodeErr::TrailingBits);
} else {
output[output_idx] = (base64_fst.to_u8() << 2) | (snd >> 4);
output[output_idx + 1] = (snd << 4) | (third >> 2);
}
} else {
cold_path();
return Err(DecodeErr::InvalidByte);
}
}
_ => impossible!("there is a bug in core::slice::as_chunks"),
}
Ok(unsafe { output.split_at_mut_unchecked(output_len) }.0)
} else {
cold_path();
Err(DecodeErr::BufferLen)
}
} else {
Err(DecodeErr::EncodedLen)
}
}
#[cfg(feature = "alloc")]
#[inline]
pub fn decode(input: &[u8]) -> Result<Vec<u8>, DecodeErr> {
decode_len(input.len())
.ok_or(DecodeErr::EncodedLen)
.and_then(|capacity| {
let mut buffer = Vec::new();
buffer
.try_reserve_exact(capacity)
.map_err(DecodeErr::TryReserve)
.and_then(|()| {
buffer.resize(capacity, 0);
if let Err(e) = decode_buffer(input, buffer.as_mut_slice()) {
Err(e)
} else {
Ok(buffer)
}
})
})
}
#[expect(
clippy::missing_asserts_for_indexing,
reason = "trust the compiler to already optimize since we match on the length"
)]
#[expect(clippy::indexing_slicing, reason = "comments justify correctness")]
#[inline]
pub const fn validate_encoded_data(input: &[u8]) -> Result<(), DecodeErr> {
let (mut chunks, rem) = input.as_chunks::<4>();
while let [first, ref rest @ ..] = *chunks {
if Alphabet::from_ascii(first[0]).is_some()
&& Alphabet::from_ascii(first[1]).is_some()
&& Alphabet::from_ascii(first[2]).is_some()
&& Alphabet::from_ascii(first[3]).is_some()
{
chunks = rest;
} else {
return Err(DecodeErr::InvalidByte);
}
}
match rem.len() {
0 => Ok(()),
1 => {
cold_path();
Err(DecodeErr::EncodedLen)
}
2 => {
if Alphabet::from_ascii(rem[0]).is_some()
&& let Some(base64_snd) = Alphabet::from_ascii(rem[1])
{
if base64_snd.to_u8().trailing_zeros() < 4 {
cold_path();
Err(DecodeErr::TrailingBits)
} else {
Ok(())
}
} else {
cold_path();
Err(DecodeErr::InvalidByte)
}
}
3 => {
if Alphabet::from_ascii(rem[0]).is_some()
&& Alphabet::from_ascii(rem[1]).is_some()
&& let Some(base64_third) = Alphabet::from_ascii(rem[2])
{
if base64_third.to_u8().trailing_zeros() < 2 {
cold_path();
Err(DecodeErr::TrailingBits)
} else {
Ok(())
}
} else {
cold_path();
Err(DecodeErr::InvalidByte)
}
}
_ => impossible!("there is a bug in core::slice::as_chunks"),
}
}
#[inline]
pub const fn encode_buffer_exact(input: &[u8], output: &mut [u8]) {
assert!(
output.len() == encode_len(input.len()),
"encode_buffer_exact must be passed an output buffer whose length is exactly the length needed to encode the data"
);
_ = encode_buffer(input, output);
}
#[expect(
clippy::panic,
clippy::panic_in_result_fn,
reason = "purpose of this function is to panic when output does not have the exact length needed"
)]
#[inline]
pub const fn decode_buffer_exact(input: &[u8], output: &mut [u8]) -> Result<(), DecodeErr> {
let output_len = output.len();
match decode_buffer(input, output) {
Ok(v) => {
assert!(
v.len() == output_len,
"decode_buffer_exact must be passed an output buffer whose length is exactly the length needed to decode the data"
);
Ok(())
}
Err(e) => {
if matches!(e, DecodeErr::BufferLen) {
cold_path();
panic!(
"decode_buffer_exact must be passed an output buffer whose length is exactly the length needed to decode the data"
);
} else {
Err(e)
}
}
}
}