use core::{array, convert, fmt};
#[cfg(any(feature = "std", test))]
use std::error;
const ALPHABET_LEN: usize = 64;
pub(crate) const PADDING_SYMBOL: Symbol = Symbol(b'=');
#[derive(Clone, Eq, PartialEq)]
pub struct Alphabet {
pub(crate) symbols: [u8; ALPHABET_LEN],
pub(crate) padding: Symbol,
}
impl Alphabet {
const fn from_str_unchecked(alphabet: &str, padding: Symbol) -> Self {
let mut symbols = [0_u8; ALPHABET_LEN];
let source_bytes = alphabet.as_bytes();
let mut index = 0;
while index < ALPHABET_LEN {
symbols[index] = source_bytes[index];
index += 1;
}
Self { symbols, padding }
}
pub const fn new(alphabet: &str) -> Result<Self, ParseAlphabetError> {
Self::new_with_padding(alphabet, PADDING_SYMBOL)
}
pub const fn new_with_padding(
alphabet: &str,
padding: Symbol,
) -> Result<Self, ParseAlphabetError> {
let bytes = alphabet.as_bytes();
if bytes.len() != ALPHABET_LEN {
return Err(ParseAlphabetError::InvalidLength);
}
{
let mut index = 0;
while index < ALPHABET_LEN {
let byte = bytes[index];
if !is_valid_b64_symbol(byte) {
return Err(ParseAlphabetError::UnprintableByte(byte));
}
if byte == padding.as_u8() {
return Err(ParseAlphabetError::ReservedByte(byte));
}
let mut probe_index = 0;
while probe_index < ALPHABET_LEN {
if probe_index != index && byte == bytes[probe_index] {
return Err(ParseAlphabetError::DuplicatedByte(byte));
}
probe_index += 1;
}
index += 1;
}
}
Ok(Self::from_str_unchecked(alphabet, padding))
}
#[must_use]
pub fn as_str(&self) -> &str {
core::str::from_utf8(&self.symbols).unwrap()
}
pub fn symbols(&self) -> [Symbol; ALPHABET_LEN] {
array::from_fn(|i| {
Symbol(self.symbols[i])
})
}
pub fn padding(&self) -> Symbol {
self.padding
}
}
impl fmt::Debug for Alphabet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Alphabet {{ symbols: {:?}, padding: '{:?}' }}",
self.as_str(),
self.padding
)
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Symbol(u8);
impl Symbol {
pub const fn new(symbol: u8) -> Option<Self> {
if is_valid_b64_symbol(symbol) {
Some(Self(symbol))
} else {
None
}
}
pub const fn as_u8(&self) -> u8 {
self.0
}
pub fn as_char(&self) -> char {
char::from(self.0)
}
}
impl From<Symbol> for u8 {
fn from(value: Symbol) -> Self {
value.0
}
}
impl fmt::Debug for Symbol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_char())
}
}
pub(crate) const fn is_valid_b64_symbol(byte: u8) -> bool {
byte >= 32_u8 && byte <= 126_u8
}
impl convert::TryFrom<&str> for Alphabet {
type Error = ParseAlphabetError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Debug, Eq, PartialEq)]
pub enum ParseAlphabetError {
InvalidLength,
DuplicatedByte(u8),
UnprintableByte(u8),
ReservedByte(u8),
}
impl fmt::Display for ParseAlphabetError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidLength => write!(f, "Invalid length - must be 64 bytes"),
Self::DuplicatedByte(b) => write!(f, "Duplicated byte: {:#04x}", b),
Self::UnprintableByte(b) => write!(f, "Unprintable byte: {:#04x}", b),
Self::ReservedByte(b) => write!(f, "Reserved byte: {:#04x}", b),
}
}
}
#[cfg(any(feature = "std", test))]
impl error::Error for ParseAlphabetError {}
pub const STANDARD: Alphabet = Alphabet::from_str_unchecked(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
PADDING_SYMBOL,
);
pub const URL_SAFE: Alphabet = Alphabet::from_str_unchecked(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",
PADDING_SYMBOL,
);
pub const CRYPT: Alphabet = Alphabet::from_str_unchecked(
"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
PADDING_SYMBOL,
);
pub const BCRYPT: Alphabet = Alphabet::from_str_unchecked(
"./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
PADDING_SYMBOL,
);
pub const IMAP_MUTF7: Alphabet = Alphabet::from_str_unchecked(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,",
PADDING_SYMBOL,
);
pub const BIN_HEX: Alphabet = Alphabet::from_str_unchecked(
"!\"#$%&'()*+,-012345689@ABCDEFGHIJKLMNPQRSTUVXYZ[`abcdefhijklmpqr",
PADDING_SYMBOL,
);
#[cfg(test)]
mod tests {
use crate::alphabet::*;
use core::convert::TryFrom as _;
#[test]
fn detects_duplicate_start() {
assert_eq!(
ParseAlphabetError::DuplicatedByte(b'A'),
Alphabet::new("AACDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
.unwrap_err()
);
}
#[test]
fn detects_duplicate_end() {
assert_eq!(
ParseAlphabetError::DuplicatedByte(b'/'),
Alphabet::new("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789//")
.unwrap_err()
);
}
#[test]
fn detects_duplicate_middle() {
assert_eq!(
ParseAlphabetError::DuplicatedByte(b'Z'),
Alphabet::new("ABCDEFGHIJKLMNOPQRSTUVWXYZZbcdefghijklmnopqrstuvwxyz0123456789+/")
.unwrap_err()
);
}
#[test]
fn detects_length() {
assert_eq!(
ParseAlphabetError::InvalidLength,
Alphabet::new(
"xxxxxxxxxABCDEFGHIJKLMNOPQRSTUVWXYZZbcdefghijklmnopqrstuvwxyz0123456789+/",
)
.unwrap_err()
);
}
#[test]
fn detects_padding() {
assert_eq!(
ParseAlphabetError::ReservedByte(b'='),
Alphabet::new("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=")
.unwrap_err()
);
}
#[test]
fn detects_unprintable() {
assert_eq!(
ParseAlphabetError::UnprintableByte(0xc),
Alphabet::new("\x0cBCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
.unwrap_err()
);
}
#[test]
fn same_as_unchecked() {
assert_eq!(
STANDARD,
Alphabet::try_from("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
.unwrap()
);
}
#[test]
fn str_same_as_input() {
let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let a = Alphabet::try_from(alphabet).unwrap();
assert_eq!(alphabet, a.as_str())
}
#[test]
fn symbol_matches_char_for_all_valid_symbols() {
for symbol in (0..=u8::MAX).filter_map(Symbol::new) {
let bytes = &[symbol.as_u8()];
let s = std::str::from_utf8(bytes).unwrap();
assert_eq!(1, s.chars().count());
let char = s.chars().next().unwrap();
assert_eq!(char, symbol.as_char());
}
}
#[test]
fn alphabet_debug() {
assert_eq!(
r##"Alphabet { symbols: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", padding: '=' }"##,
format!("{STANDARD:?}")
);
}
#[test]
fn alphabet_symbols() {
assert_eq!(
STANDARD.as_str(),
STANDARD
.symbols()
.iter()
.map(|s| s.as_char())
.collect::<String>()
);
}
}