fashex 0.0.10

Hexadecimal string encoding and decoding with best-effort SIMD acceleration.
Documentation
//! [`Hex`]

use crate::buffer::{OwnedBuf, OwnedUninitBuf as _, StringBuf};
use crate::error::InvalidInput;
use crate::{decode, encode};

trait HexPriv {}

#[allow(private_bounds, reason = "Sealed trait.")]
/// A trait for encoding / decoding a hexadecimal string from / into the raw
/// bytes.
///
/// ```rust
/// use fashex::Hex;
/// let bytes = &[0xde, 0xad, 0xbe, 0xef];
///
/// assert_eq!(bytes.encode_hex::<String, false>().unwrap(), "deadbeef");
/// assert_eq!(bytes.encode_hex::<String, true>().unwrap(), "DEADBEEF");
/// ```
///
/// ```rust
/// use fashex::Hex;
/// let bytes = b"deadbeef";
///
/// assert_eq!(
///     &bytes.decode_hex::<Vec<u8>>().unwrap()[..],
///     &[0xde, 0xad, 0xbe, 0xef]
/// );
/// ```
pub trait Hex: HexPriv {
    /// [`encode()`] the current raw bytes slice into a hexadecimal string.
    ///
    /// ## Errors
    ///
    /// 1. The target buffer type has a capacity limit, and cannot accommodate
    ///    the encoded output.
    fn encode_hex<O: StringBuf, const UPPER: bool>(&self) -> Result<O, InvalidInput>;

    /// [`decode()`] the current hexadecimal string into raw bytes.
    ///
    /// ## Errors
    ///
    /// 1. The target buffer type has a capacity limit, and cannot accommodate
    ///    the decoded output.
    /// 1. The input string is not a valid hexadecimal string (e.g., has an odd
    ///    length, or contains non-hex chars, etc).
    fn decode_hex<O: OwnedBuf>(&self) -> Result<O, InvalidInput>;
}

impl<T: AsRef<[u8]>> HexPriv for T {}

impl<T: AsRef<[u8]>> Hex for T {
    fn encode_hex<O: StringBuf, const UPPER: bool>(&self) -> Result<O, InvalidInput> {
        let this = self.as_ref();

        let mut buf = O::Buf::new_uninit_slice(this.len() * 2)?;

        encode::<UPPER>(this, buf.as_uninit())?;

        #[allow(unsafe_code, reason = "`encode` has fully initialized the buffer.")]
        let buf = unsafe { buf.assume_init() };

        #[allow(
            unsafe_code,
            reason = "Hexadecimal output contains only ASCII bytes, which are valid UTF-8."
        )]
        let buf = unsafe { O::from_utf8_unchecked(buf) };

        Ok(buf)
    }

    fn decode_hex<O: OwnedBuf>(&self) -> Result<O, InvalidInput> {
        let this = self.as_ref();

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

        let mut buf = O::new_uninit_slice(this.len() / 2)?;

        decode(this, buf.as_uninit())?;

        #[allow(unsafe_code, reason = "`decode` has fully initialized the buffer.")]
        let buf = unsafe { buf.assume_init() };

        Ok(buf)
    }
}

#[cfg(test)]
mod smoking {
    use alloc::boxed::Box;
    use alloc::rc::Rc;
    use alloc::string::String;
    use alloc::sync::Arc;
    use alloc::vec::Vec;

    use super::*;

    #[test]
    fn test_encode() {
        let bytes = &[0xde, 0xad, 0xbe, 0xef];

        macro_rules! test {
            ($ty:ty, $expected:expr) => {
                assert!(
                    bytes
                        .encode_hex::<$ty, false>()
                        .unwrap()
                        .eq_ignore_ascii_case($expected)
                );
            };
        }

        test!([u8; 8], b"deadbeef");
        test!(Vec<u8>, b"deadbeef");
        test!(Box<[u8]>, b"deadbeef");
        test!(Arc<[u8]>, b"deadbeef");
        test!(Rc<[u8]>, b"deadbeef");
        test!(String, "deadbeef");
        test!(Box<str>, "deadbeef");
        test!(Arc<str>, "deadbeef");
        test!(Rc<str>, "deadbeef");
    }

    #[test]
    fn test_decode() {
        let hex = b"deadbeef";

        macro_rules! test {
            ($ty:ty, $expected:expr) => {
                assert_eq!(&hex.decode_hex::<$ty>().unwrap()[..], $expected);
            };
        }

        test!([u8; 4], b"\xde\xad\xbe\xef");
        test!(Vec<u8>, b"\xde\xad\xbe\xef");
        test!(Box<[u8]>, b"\xde\xad\xbe\xef");
        test!(Arc<[u8]>, b"\xde\xad\xbe\xef");
        test!(Rc<[u8]>, b"\xde\xad\xbe\xef");
    }
}