use base64::{engine::general_purpose::STANDARD as Base64Engine, Engine};
use xso::error::Error;
pub trait Codec {
type Decoded;
fn decode(s: &str) -> Result<Self::Decoded, Error>;
fn encode(decoded: &Self::Decoded) -> Option<String>;
}
pub struct Text;
impl Codec for Text {
type Decoded = String;
fn decode(s: &str) -> Result<String, Error> {
Ok(s.to_owned())
}
fn encode(decoded: &String) -> Option<String> {
Some(decoded.to_owned())
}
}
pub struct OptionalCodec<T: Codec>(std::marker::PhantomData<T>);
impl<T> Codec for OptionalCodec<T>
where
T: Codec,
{
type Decoded = Option<T::Decoded>;
fn decode(s: &str) -> Result<Option<T::Decoded>, Error> {
if s.is_empty() {
return Ok(None);
}
Ok(Some(T::decode(s)?))
}
fn encode(decoded: &Option<T::Decoded>) -> Option<String> {
decoded.as_ref().and_then(T::encode)
}
}
pub struct Trimmed<T: Codec>(std::marker::PhantomData<T>);
impl<T> Codec for Trimmed<T>
where
T: Codec,
{
type Decoded = T::Decoded;
fn decode(s: &str) -> Result<T::Decoded, Error> {
match s.trim() {
"" => Err(Error::Other(
"The text in the element's text node was empty after trimming.",
)),
trimmed => T::decode(trimmed),
}
}
fn encode(decoded: &T::Decoded) -> Option<String> {
T::encode(decoded)
}
}
pub struct Base64;
impl Codec for Base64 {
type Decoded = Vec<u8>;
fn decode(s: &str) -> Result<Vec<u8>, Error> {
Base64Engine.decode(s).map_err(Error::text_parse_error)
}
fn encode(decoded: &Vec<u8>) -> Option<String> {
Some(Base64Engine.encode(decoded))
}
}
pub struct WhitespaceAwareBase64;
impl Codec for WhitespaceAwareBase64 {
type Decoded = Vec<u8>;
fn decode(s: &str) -> Result<Self::Decoded, Error> {
let s: String = s
.chars()
.filter(|ch| *ch != ' ' && *ch != '\n' && *ch != '\t')
.collect();
Base64Engine.decode(s).map_err(Error::text_parse_error)
}
fn encode(decoded: &Self::Decoded) -> Option<String> {
Some(Base64Engine.encode(decoded))
}
}
pub struct FixedHex<const N: usize>;
impl<const N: usize> Codec for FixedHex<N> {
type Decoded = [u8; N];
fn decode(s: &str) -> Result<Self::Decoded, Error> {
if s.len() != 2 * N {
return Err(Error::Other("Invalid length"));
}
let mut bytes = [0u8; N];
for i in 0..N {
bytes[i] =
u8::from_str_radix(&s[2 * i..2 * i + 2], 16).map_err(Error::text_parse_error)?;
}
Ok(bytes)
}
fn encode(decoded: &Self::Decoded) -> Option<String> {
let mut bytes = String::with_capacity(N * 2);
for byte in decoded {
bytes.extend(format!("{:02x}", byte).chars());
}
Some(bytes)
}
}
pub struct ColonSeparatedHex;
impl Codec for ColonSeparatedHex {
type Decoded = Vec<u8>;
fn decode(s: &str) -> Result<Self::Decoded, Error> {
let mut bytes = vec![];
for i in 0..(1 + s.len()) / 3 {
let byte =
u8::from_str_radix(&s[3 * i..3 * i + 2], 16).map_err(Error::text_parse_error)?;
if 3 * i + 2 < s.len() {
assert_eq!(&s[3 * i + 2..3 * i + 3], ":");
}
bytes.push(byte);
}
Ok(bytes)
}
fn encode(decoded: &Self::Decoded) -> Option<String> {
let mut bytes = vec![];
for byte in decoded {
bytes.push(format!("{:02X}", byte));
}
Some(bytes.join(":"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fixed_hex() {
let value = [0x01, 0xfe, 0xef];
let hex = FixedHex::<3>::decode("01feEF").unwrap();
assert_eq!(&hex, &value);
let hex = FixedHex::<3>::encode(&value).unwrap();
assert_eq!(hex, "01feef");
let err = FixedHex::<3>::decode("01feEF01").unwrap_err();
assert_eq!(err.to_string(), "Invalid length");
let err = FixedHex::<3>::decode("01fe").unwrap_err();
assert_eq!(err.to_string(), "Invalid length");
let err = FixedHex::<3>::decode("01feE").unwrap_err();
assert_eq!(err.to_string(), "Invalid length");
let err = FixedHex::<3>::decode("0:f:EF").unwrap_err();
assert_eq!(
err.to_string(),
"text parse error: invalid digit found in string"
);
let err = FixedHex::<3>::decode("01defg").unwrap_err();
assert_eq!(
err.to_string(),
"text parse error: invalid digit found in string"
);
}
}