//! Conversion formats use super::*; use std::{ ops::Deref, borrow::Borrow, fmt, error, convert::TryFrom, }; use regex::{ Regex, }; //TODO: Mayb use base65536 for URL stuffs? idk.. const BASE64_VALIDATE_RE_STR: &'static str = r#"^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"#; const MOD_BASE64_VALIDATE_RE_STR: &'static str = r#"^(?:[-A-Za-z0-9_]{4})*(?:[-_A-Za-z0-9]{2}~~|[-_A-Za-z0-9]{3}~)?$"#; lazy_static!{ static ref BASE64_CONV_TABLE: smallmap::Map = smallmap![ {'/' => '_'}, {'+' => '-'}, {'=' => '~'}, ]; static ref BASE64_CONV_TABLE_REV: smallmap::Map = BASE64_CONV_TABLE.clone().reverse(); static ref BASE64_VALIDATE_REGEX: Regex = Regex::new(BASE64_VALIDATE_RE_STR).expect("Failed to compile base64 validation regex"); static ref MOD_BASE64_VALIDATE_REGEX: Regex = Regex::new(MOD_BASE64_VALIDATE_RE_STR).expect("Failed to compile modified base64 validation regex"); } /// An error in base64 or modified base64 encoding/decoding. #[derive(Debug)] pub struct Base64Error(T); impl Base64Error { /// The invalid value of this error pub fn value(&self) -> &T { &self.0 } /// Consume into the invalid value from this instance #[inline] pub fn into_inner(self) -> T { self.0 } } impl error::Error for Base64Error where T: AsRef + fmt::Debug{} impl fmt::Display for Base64Error where T: AsRef { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "invalid base64: {:?}", self.0.as_ref()) } } /// A string of modified base64, appropriate for URL paths etc. #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct ModifiedBase64String(String); impl fmt::Display for ModifiedBase64String { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } impl ModifiedBase64String { #[inline(always)] fn from_base64_unchecked(string: &str) -> Self { Self(conv_str(&BASE64_CONV_TABLE, string).collect()) } /// Consume into a normal base64 string pub fn into_base64(self) -> String { conv_char_iter(&BASE64_CONV_TABLE_REV, self.0.chars()).collect() } /// Consume into the inner modified base64 string pub fn into_string(self) -> String { self.0 } /// Create an instance from a modified base64 string pub fn new(from: String) -> Result> { if MOD_BASE64_VALIDATE_REGEX.is_match(from.as_str()) { Ok(Self(from)) } else { Err(Base64Error(from)) } } /// Try to convert a base64 string into a modified base64 string pub fn try_from_base64>(base64: T) -> Result> { let string = base64.as_ref(); if BASE64_VALIDATE_REGEX.is_match(string) { Ok(Self::from_base64_unchecked(string)) } else { Err(Base64Error(base64)) } } /// As a string slice #[inline(always)] pub fn as_str(&self) -> &str { self.0.as_str() } /// Encode from a slice pub fn encode(slice: impl AsRef<[u8]>) -> Self { Self::from_base64_unchecked(base64::encode(slice.as_ref()).as_str()) } /// Consume into decoded bytes, write those bytes into the provided buffer pub fn decode(self, output: &mut [u8]) -> usize { base64::decode_config_slice(self.into_base64(), base64::STANDARD, output).expect("modified base64 string contained invalid formatted data") } /// Consume into decoded bytes, return those bytes as a new `Vec` pub fn decode_new(self) -> Vec { base64::decode(self.into_base64()).expect("modified base64 string contained invalid formatted data") } } impl Deref for ModifiedBase64String { type Target = str; #[inline] fn deref(&self) -> &Self::Target { self.as_str() } } impl AsRef for ModifiedBase64String { #[inline] fn as_ref(&self) -> &str { self.as_str() } } impl TryFrom for ModifiedBase64String { type Error = Base64Error; #[inline] fn try_from(from: String) -> Result { Self::try_from_base64(from) } } impl From for String { #[inline] fn from(from: ModifiedBase64String) -> Self { from.into_base64() } } /// Convert this string with a specified char map. Returns a `char` yielding iterator. #[inline] pub fn conv_str<'a, 'b>(table: &'b smallmap::Map, string: &'a (impl AsRef + ?Sized)) -> CharSubstituteIter<'b, std::str::Chars<'a>> where 'b: 'a { conv_char_iter(table, string.as_ref().chars()) } /// Convert this iterator of chars with this char swapping map #[inline] pub fn conv_char_iter(table: &smallmap::Map, iter: I) -> CharSubstituteIter where I: IntoIterator, T: From + smallmap::Collapse, char: Borrow { iter.replace_chars(table) } #[cfg(test)] mod tests { #[test] fn mod_base64_enc_dec() { let mut value = [0u8; 512]; getrandom::getrandom(&mut value[..]).expect("setup failed"); let md = super::ModifiedBase64String::encode(&value[..]); println!("e-md: {:?}", md); let ou = md.decode_new(); assert_eq!(&ou[..], &value[..]); } }