fashex 0.0.7

Hexadecimal string encoding and decoding with best-effort SIMD acceleration.
Documentation
//! [`FromHex`] / [`ToHex`] traits and their implementations.

#[cfg(feature = "alloc")]
use alloc::boxed::Box;
#[cfg(feature = "alloc")]
use alloc::rc::Rc;
#[cfg(feature = "alloc")]
use alloc::string::String;
#[cfg(feature = "alloc")]
use alloc::sync::Arc;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
use core::mem::MaybeUninit;

use crate::decode;
#[cfg(feature = "alloc")]
use crate::encode;
use crate::error::InvalidInput;

trait FromHexPriv {}

#[allow(private_bounds, reason = "Sealed trait.")]
/// [`decode()`], but for those who prefer `T::decode(bytes)` over
/// `decode(bytes, buffer)`.
pub trait FromHex: FromHexPriv + Sized {
    /// See [`decode()`].
    fn decode<T: AsRef<[u8]>>(bytes: T) -> Result<Self, InvalidInput>;
}

#[cfg(feature = "alloc")]
impl FromHexPriv for Vec<u8> {}

#[cfg(feature = "alloc")]
impl FromHex for Vec<u8> {
    fn decode<T: AsRef<[u8]>>(bytes: T) -> Result<Self, InvalidInput> {
        let bytes = bytes.as_ref();

        if bytes.len() % 2 != 0 {
            return Err(InvalidInput);
        }

        let mut buf = Self::with_capacity(bytes.len() / 2);

        decode(bytes, &mut buf)?;

        Ok(buf)
    }
}

#[cfg(feature = "alloc")]
impl FromHexPriv for Box<[u8]> {}

#[cfg(feature = "alloc")]
impl FromHex for Box<[u8]> {
    fn decode<T: AsRef<[u8]>>(bytes: T) -> Result<Self, InvalidInput> {
        let bytes = bytes.as_ref();

        if bytes.len() % 2 != 0 {
            return Err(InvalidInput);
        }

        let mut buf = Self::new_uninit_slice(bytes.len() / 2);

        decode(bytes, &mut buf[..])?;

        #[allow(unsafe_code, reason = "XXX")]
        let buf = unsafe { buf.assume_init() };

        Ok(buf)
    }
}

#[cfg(feature = "alloc")]
impl FromHexPriv for Arc<[u8]> {}

#[cfg(feature = "alloc")]
impl FromHex for Arc<[u8]> {
    fn decode<T: AsRef<[u8]>>(bytes: T) -> Result<Self, InvalidInput> {
        let bytes = bytes.as_ref();

        if bytes.len() % 2 != 0 {
            return Err(InvalidInput);
        }

        let mut buf = Self::new_uninit_slice(bytes.len() / 2);

        decode(bytes, Arc::make_mut(&mut buf))?;

        #[allow(unsafe_code, reason = "XXX")]
        let buf = unsafe { buf.assume_init() };

        Ok(buf)
    }
}

#[cfg(feature = "alloc")]
impl FromHexPriv for Rc<[u8]> {}

#[cfg(feature = "alloc")]
impl FromHex for Rc<[u8]> {
    fn decode<T: AsRef<[u8]>>(bytes: T) -> Result<Self, InvalidInput> {
        let bytes = bytes.as_ref();

        if bytes.len() % 2 != 0 {
            return Err(InvalidInput);
        }

        let mut buf = Self::new_uninit_slice(bytes.len() / 2);

        decode(bytes, Rc::make_mut(&mut buf))?;

        #[allow(unsafe_code, reason = "XXX")]
        let buf = unsafe { buf.assume_init() };

        Ok(buf)
    }
}

impl<const N: usize> FromHexPriv for [u8; N] {}

impl<const N: usize> FromHex for [u8; N] {
    fn decode<T: AsRef<[u8]>>(bytes: T) -> Result<Self, InvalidInput> {
        let bytes = bytes.as_ref();

        if bytes.len() % 2 != 0 {
            return Err(InvalidInput);
        }

        if bytes.len() / 2 != N {
            return Err(InvalidInput);
        }

        let mut buf = [MaybeUninit::uninit(); N];

        decode(bytes, &mut buf)?;

        #[allow(unsafe_code, reason = "XXX")]
        // SAFETY: the buffer is fully initialized with decoded bytes.
        let buf = unsafe { *((&raw const buf).cast::<[u8; N]>()) };

        Ok(buf)
    }
}

#[cfg(feature = "alloc")]
trait ToHexPriv {}

#[cfg(feature = "alloc")]
#[allow(private_bounds, reason = "Sealed trait.")]
/// [`encode()`], but for those who prefer `bytes.encode()` over `encode(bytes,
/// buffer)`.
pub trait ToHex: ToHexPriv {
    /// See [`encode()`].
    fn encode<const UPPER: bool>(&self) -> String;
}

#[cfg(feature = "alloc")]
impl<T> ToHexPriv for T where T: AsRef<[u8]> + ?Sized {}

#[cfg(feature = "alloc")]
impl<T> ToHex for T
where
    T: AsRef<[u8]> + ?Sized,
{
    fn encode<const UPPER: bool>(&self) -> String {
        let this = self.as_ref();

        let mut buf = Vec::with_capacity(this.len() * 2);

        let _ = encode::<Vec<u8>, UPPER>(this, &mut buf);

        #[allow(unsafe_code, reason = "XXX")]
        unsafe {
            String::from_utf8_unchecked(buf)
        }
    }
}

#[cfg(test)]
mod smoking {
    use super::*;

    #[test]
    fn test_decoding() {
        macro_rules! test {
            ($expr:expr, $expected:expr) => {
                assert_eq!(Vec::decode($expr).unwrap(), $expected);
                assert_eq!(&*Box::<[u8]>::decode($expr).unwrap(), $expected);
                assert_eq!(&*Arc::<[u8]>::decode($expr).unwrap(), $expected);
                assert_eq!(&*Rc::<[u8]>::decode($expr).unwrap(), $expected);
                assert_eq!(&<[u8; $expected.len()]>::decode($expr).unwrap(), $expected);
            };
        }

        test!("", b"");
        test!("68656c6c6f20776f726c64", b"hello world");
        test!("68656C6C6F20776F726C64", b"hello world");
        test!("68656c6C6f20776F726C64", b"hello world");

        macro_rules! test_should_fail {
            ($expr:expr) => {
                assert!(Vec::decode($expr).is_err());
                assert!(Box::<[u8]>::decode($expr).is_err());
                assert!(Arc::<[u8]>::decode($expr).is_err());
                assert!(Rc::<[u8]>::decode($expr).is_err());
                assert!(<[u8; $expr.len() / 2]>::decode($expr).is_err());
            };
        }

        test_should_fail!("68656c6c6f20776f726c6");
        test_should_fail!("68656x6c6f20776f726c64");

        assert!(<[u8; 2]>::decode("68656c6c6f20776f726c64").is_err());
    }

    #[test]
    fn test_encoding() {
        macro_rules! test {
            ($expr:expr, $lower:expr, $upper:expr) => {
                assert_eq!($expr.encode::<false>(), $lower);
                assert_eq!($expr.encode::<true>(), $upper);
            };
        }

        test!(b"", "", "");
        test!(
            b"hello world",
            "68656c6c6f20776f726c64",
            "68656C6C6F20776F726C64"
        );
    }
}