#![allow(clippy::tabs_in_doc_comments)]
#![deny(missing_docs)]
#![no_std]
extern crate alloc;
#[cfg(test)] mod test;
use core::{convert::TryInto, mem, result::Result as CoreResult};
use alloc::{string::String, vec::Vec};
#[cfg(feature = "serde")] use serde::{de::Error as DeError, Deserialize, Deserializer};
pub type Result<T> = CoreResult<T, Error>;
pub type Bytes = Vec<u8>;
pub type Hex = String;
pub trait TryFromHex
where
Self: Sized,
{
fn try_from_hex<H>(hex: H) -> Result<Self>
where
H: AsRef<str>;
}
macro_rules! impl_num_from_hex {
($t:ty) => {
impl TryFromHex for $t {
fn try_from_hex<H>(hex: H) -> Result<Self>
where
H: AsRef<str>,
{
let hex = hex.as_ref().trim_start_matches("0x");
Self::from_str_radix(hex, 16).map_err(Error::ParseIntError)
}
}
};
}
impl_num_from_hex!(isize);
impl_num_from_hex!(i8);
impl_num_from_hex!(i16);
impl_num_from_hex!(i32);
impl_num_from_hex!(i64);
impl_num_from_hex!(i128);
impl_num_from_hex!(usize);
impl_num_from_hex!(u8);
impl_num_from_hex!(u16);
impl_num_from_hex!(u32);
impl_num_from_hex!(u64);
impl_num_from_hex!(u128);
#[derive(Debug, PartialEq, Eq)]
pub enum Error {
InvalidLength,
InvalidChar {
char: char,
index: usize,
},
MismatchedLength {
expect: usize,
},
ParseIntError(core::num::ParseIntError),
}
pub fn slice2array<const N: usize, T>(slice: &[T]) -> Result<[T; N]>
where
T: Copy,
{
slice.try_into().map_err(|_| Error::MismatchedLength { expect: N })
}
pub fn slice2array_unchecked<const N: usize, T>(slice: &[T]) -> [T; N]
where
T: Copy,
{
slice2array(slice).unwrap()
}
pub fn slice_n_into<const N: usize, T, V>(slice: &[T]) -> Result<V>
where
T: Copy,
V: From<[T; N]>,
{
Ok(slice2array(slice)?.into())
}
pub fn slice_n_into_unchecked<const N: usize, T, V>(slice: &[T]) -> V
where
T: Copy,
V: From<[T; N]>,
{
slice2array_unchecked(slice).into()
}
pub fn vec2array<const N: usize, T>(vec: Vec<T>) -> Result<[T; N]> {
vec.try_into().map_err(|_| Error::MismatchedLength { expect: N })
}
pub fn vec2array_unchecked<const N: usize, T>(vec: Vec<T>) -> [T; N] {
vec2array(vec).unwrap()
}
pub fn vec_n_into<const N: usize, T, V>(vec: Vec<T>) -> Result<V>
where
V: From<[T; N]>,
{
Ok(vec2array(vec)?.into())
}
pub fn vec_n_into_unchecked<const N: usize, T, V>(vec: Vec<T>) -> V
where
V: From<[T; N]>,
{
vec2array_unchecked(vec).into()
}
pub fn hex_bytes2hex_str(bytes: &[u8]) -> Result<&str> {
for (i, byte) in bytes.iter().enumerate().skip(if bytes.starts_with(b"0x") { 2 } else { 0 }) {
if !is_hex_ascii(byte) {
Err(Error::InvalidChar { char: *byte as _, index: i })?;
}
}
Ok(unsafe {
#[allow(clippy::transmute_bytes_to_str)]
mem::transmute(bytes)
})
}
pub unsafe fn hex_bytes2hex_str_unchecked(bytes: &[u8]) -> &str {
#[allow(clippy::transmute_bytes_to_str)]
mem::transmute(bytes)
}
pub fn bytes2hex<B>(prefix: &str, bytes: B) -> Hex
where
B: AsRef<[u8]>,
{
let bytes = bytes.as_ref();
let mut hex = Hex::with_capacity(prefix.len() + bytes.len() * 2);
prefix.chars().for_each(|byte| hex.push(byte));
bytes.iter().for_each(|byte| {
hex.push(char::from_digit((byte >> 4) as _, 16).unwrap());
hex.push(char::from_digit((byte & 0xf) as _, 16).unwrap());
});
hex
}
pub fn hex2array<H, const N: usize>(hex: H) -> Result<[u8; N]>
where
H: AsRef<str>,
{
vec2array(hex2bytes(hex.as_ref())?)
}
pub fn hex2array_unchecked<H, const N: usize>(hex: H) -> [u8; N]
where
H: AsRef<str>,
{
hex2bytes_unchecked(hex).try_into().unwrap()
}
pub fn hex2bytes<H>(hex: H) -> Result<Bytes>
where
H: AsRef<str>,
{
let hex = strip_0x(hex.as_ref());
if hex.len() % 2 != 0 {
Err(Error::InvalidLength)?;
}
let mut bytes = Bytes::new();
for i in (0..hex.len()).step_by(2) {
let Some(str) = hex.get(i..i + 2) else {
Err(Error::InvalidChar { char: hex.as_bytes()[i] as _, index: i })?
};
match u8::from_str_radix(str, 16) {
Ok(byte_) => bytes.push(byte_),
Err(e) => {
if !is_hex_ascii(&hex.as_bytes()[i]) {
Err(Error::InvalidChar { char: hex.as_bytes()[i] as _, index: i })?;
}
if !is_hex_ascii(&hex.as_bytes()[i + 1]) {
Err(Error::InvalidChar { char: hex.as_bytes()[i + 1] as _, index: i + 1 })?;
}
Err(Error::ParseIntError(e))?;
},
}
}
Ok(bytes)
}
pub fn hex2bytes_unchecked<H>(hex: H) -> Bytes
where
H: AsRef<str>,
{
let hex = strip_0x(hex.as_ref());
(0..hex.len()).step_by(2).map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap()).collect()
}
pub fn hex2slice<H>(hex: H, slice: &mut [u8]) -> Result<&[u8]>
where
H: AsRef<str>,
{
let hex = strip_0x(hex.as_ref());
if hex.len() % 2 != 0 {
Err(Error::InvalidLength)?;
}
let expected_len = hex.len() >> 1;
if expected_len != slice.len() {
Err(Error::MismatchedLength { expect: expected_len })?;
}
for (byte, i) in slice.iter_mut().zip((0..hex.len()).step_by(2)) {
let Some(str) = hex.get(i..i + 2) else {
Err(Error::InvalidChar { char: hex.as_bytes()[i] as _, index: i })?
};
match u8::from_str_radix(str, 16) {
Ok(byte_) => *byte = byte_,
Err(e) => {
if !is_hex_ascii(&hex.as_bytes()[i]) {
Err(Error::InvalidChar { char: hex.as_bytes()[i] as _, index: i })?;
}
if !is_hex_ascii(&hex.as_bytes()[i + 1]) {
Err(Error::InvalidChar { char: hex.as_bytes()[i + 1] as _, index: i + 1 })?;
}
Err(Error::ParseIntError(e))?;
},
}
}
Ok(slice)
}
pub fn hex2slice_unchecked<H>(hex: H, slice: &mut [u8]) -> &[u8]
where
H: AsRef<str>,
{
let hex = strip_0x(hex.as_ref());
slice.iter_mut().zip((0..hex.len()).step_by(2)).for_each(|(byte, i)| {
*byte = u8::from_str_radix(&hex[i..i + 2], 16)
.expect("radix is always 16 which will never fail this; qed");
});
slice
}
pub fn hex_into<H, T>(hex: H) -> Result<T>
where
H: AsRef<str>,
T: From<Bytes>,
{
Ok(hex2bytes(hex.as_ref())?.into())
}
pub fn hex_into_unchecked<H, T>(hex: H) -> T
where
H: AsRef<str>,
T: From<Bytes>,
{
hex2bytes_unchecked(hex).into()
}
pub fn hex_n_into<H, T, const N: usize>(hex: H) -> Result<T>
where
H: AsRef<str>,
T: From<[u8; N]>,
{
Ok(hex2array(hex)?.into())
}
pub fn hex_n_into_unchecked<H, T, const N: usize>(hex: H) -> T
where
H: AsRef<str>,
T: From<[u8; N]>,
{
hex2array_unchecked(hex).into()
}
#[cfg(feature = "serde")]
pub fn hex_deserialize_into<'de, D, T>(hex: D) -> CoreResult<T, D::Error>
where
D: Deserializer<'de>,
T: From<Bytes>,
{
Ok(hex2bytes_unchecked(<&str>::deserialize(hex)?).into())
}
#[cfg(feature = "serde")]
pub fn hex_deserialize_n_into<'de, D, T, const N: usize>(hex: D) -> CoreResult<T, D::Error>
where
D: Deserializer<'de>,
T: From<[u8; N]>,
{
Ok(hex2array_unchecked(<&str>::deserialize(hex)?).into())
}
#[cfg(feature = "serde")]
pub fn de_hex2num<'de, D, T>(hex: D) -> CoreResult<T, D::Error>
where
D: Deserializer<'de>,
T: TryFromHex,
{
let hex = <&str>::deserialize(hex)?;
T::try_from_hex(hex).map_err(|_| D::Error::custom(alloc::format!("Invalid hex str `{}`", hex)))
}
#[cfg(feature = "serde")]
pub fn de_hex2bytes<'de, D>(hex: D) -> CoreResult<Bytes, D::Error>
where
D: Deserializer<'de>,
{
let hex = <&str>::deserialize(hex)?;
hex2bytes(hex).map_err(|_| D::Error::custom(alloc::format!("Invalid hex str `{}`", hex)))
}
fn strip_0x(hex: &str) -> &str {
if let Some(hex) = hex.strip_prefix("0x") {
hex
} else {
hex
}
}
fn is_hex_ascii(byte: &u8) -> bool {
let byte = byte | 0b10_0000;
matches!(byte, b'0'..=b'9' | b'a'..=b'f')
}