use std::{
fmt::{Debug, Display},
str,
};
use serde::{de::Visitor, Deserialize, Serialize};
pub const HEX_DIGITS: &str = "0123456789abcdef";
#[expect(clippy::char_lit_as_u8)]
pub const fn parse_ascii_hex_digit(ch: char) -> Option<u8> {
match ch {
i if i.is_ascii_digit() => Some(i as u8 - ('0' as u8)),
i if i.is_ascii_hexdigit() => Some(10 + i.to_ascii_lowercase() as u8 - ('a' as u8)),
_ => None,
}
}
mod test_hex_digit_parsing {
use super::parse_ascii_hex_digit;
const _: () = assert!(parse_ascii_hex_digit('g').is_none());
const _: () = assert!(parse_ascii_hex_digit('ß').is_none());
const _: () = assert!(matches!(parse_ascii_hex_digit('0'), Some(0)));
const _: () = assert!(matches!(parse_ascii_hex_digit('1'), Some(1)));
const _: () = assert!(matches!(parse_ascii_hex_digit('2'), Some(2)));
const _: () = assert!(matches!(parse_ascii_hex_digit('3'), Some(3)));
const _: () = assert!(matches!(parse_ascii_hex_digit('4'), Some(4)));
const _: () = assert!(matches!(parse_ascii_hex_digit('5'), Some(5)));
const _: () = assert!(matches!(parse_ascii_hex_digit('6'), Some(6)));
const _: () = assert!(matches!(parse_ascii_hex_digit('7'), Some(7)));
const _: () = assert!(matches!(parse_ascii_hex_digit('8'), Some(8)));
const _: () = assert!(matches!(parse_ascii_hex_digit('9'), Some(9)));
const _: () = assert!(matches!(parse_ascii_hex_digit('A'), Some(10)));
const _: () = assert!(matches!(parse_ascii_hex_digit('B'), Some(11)));
const _: () = assert!(matches!(parse_ascii_hex_digit('C'), Some(12)));
const _: () = assert!(matches!(parse_ascii_hex_digit('D'), Some(13)));
const _: () = assert!(matches!(parse_ascii_hex_digit('E'), Some(14)));
const _: () = assert!(matches!(parse_ascii_hex_digit('F'), Some(15)));
const _: () = assert!(matches!(parse_ascii_hex_digit('a'), Some(10)));
const _: () = assert!(matches!(parse_ascii_hex_digit('b'), Some(11)));
const _: () = assert!(matches!(parse_ascii_hex_digit('c'), Some(12)));
const _: () = assert!(matches!(parse_ascii_hex_digit('d'), Some(13)));
const _: () = assert!(matches!(parse_ascii_hex_digit('e'), Some(14)));
const _: () = assert!(matches!(parse_ascii_hex_digit('f'), Some(15)));
use super::HEX_DIGITS;
#[allow(dead_code)]
const fn verify_hex_digits() {
let mut i = 0;
loop {
assert!(HEX_DIGITS.len() == 16);
let ch = HEX_DIGITS.as_bytes()[i] as char;
if let Some(val) = parse_ascii_hex_digit(ch) {
assert!(val as usize == i);
} else {
panic!("could not parse")
}
assert!(ch.is_ascii_hexdigit());
assert!(ch.is_ascii_digit() || ch.is_ascii_lowercase());
i += 1;
if i == HEX_DIGITS.len() {
break;
}
}
}
const _: () = verify_hex_digits();
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct OwnedHexStr<const LEN: usize, const HLEN: usize> {
hex_str: [u8; HLEN],
}
impl<const LEN: usize, const HLEN: usize> OwnedHexStr<LEN, HLEN> {
const __76560: () = assert!(2 * LEN == HLEN);
#[expect(clippy::char_lit_as_u8)]
pub const fn from_bytes(value: [u8; LEN]) -> Self {
let mut hex_str = [0; HLEN];
let mut i = 0;
loop {
hex_str[i * 2] = match value[i] / 16 {
i if i < 10 => '0' as u8 + i,
i => 'a' as u8 - 10 + i,
};
hex_str[i * 2 + 1] = match value[i] % 16 {
i if i < 10 => '0' as u8 + i,
i => 'a' as u8 - 10 + i,
};
i += 1;
if i == LEN {
break;
}
}
OwnedHexStr { hex_str }
}
#[expect(clippy::char_lit_as_u8)]
#[allow(clippy::wrong_self_convention)]
pub const fn into_original_bytes(&self) -> [u8; LEN] {
let mut bytes = [0; LEN];
let mut i = 0;
loop {
let upper: u8 = match self.hex_str[i * 2] {
i if i.is_ascii_digit() => i - ('0' as u8),
i => 10 + i - ('a' as u8),
};
let lower: u8 = match self.hex_str[i * 2 + 1] {
i if i.is_ascii_digit() => i - ('0' as u8),
i => 10 + i - ('a' as u8),
};
bytes[i] = (upper << 4) + lower;
i += 1;
if i == LEN {
break;
}
}
bytes
}
#[allow(dead_code)]
pub(crate) const fn from_hex_str(string: &str) -> Self {
let string = string.as_bytes();
assert!(HLEN == string.len());
let mut hex_str = [0; HLEN];
let mut i = 0;
loop {
assert!(string[i].is_ascii_hexdigit());
hex_str[i] = string[i];
i += 1;
if i == HLEN {
break;
}
}
OwnedHexStr { hex_str }
}
pub const fn as_str(&self) -> &str {
#[cfg(debug_assertions)]
{
assert!(self.hex_str.is_ascii());
let mut i = 0;
loop {
assert!(self.hex_str[i].is_ascii_hexdigit());
i += 1;
if i == HLEN {
break;
}
}
}
unsafe { str::from_utf8_unchecked(&self.hex_str) }
}
#[expect(clippy::needless_lifetimes)]
#[allow(unused)]
pub fn chars<'a>(&'a self) -> impl DoubleEndedIterator<Item = char> + 'a {
self.as_str().chars()
}
}
impl<I: Into<[u8; LEN]>, const LEN: usize, const HLEN: usize> From<I> for OwnedHexStr<LEN, HLEN> {
fn from(value: I) -> Self {
Self::from_bytes(value.into())
}
}
mod test {
use super::OwnedHexStr;
use const_format::assertcp_eq;
macro_rules! owned_hex_str {
($arr:expr) => {{
assertcp_eq!(
OwnedHexStr::<{ $arr.len() }, { $arr.len() * 2 }>::from_bytes($arr)
.into_original_bytes()[0],
$arr[0]
);
OwnedHexStr::<{ $arr.len() }, { $arr.len() * 2 }>::from_bytes($arr)
}};
}
assertcp_eq!(owned_hex_str!([175, 254]).as_str(), "affe");
assertcp_eq!(owned_hex_str!([222, 173]).as_str(), "dead");
assertcp_eq!(owned_hex_str!([42, 233]).as_str(), "2ae9");
assertcp_eq!(owned_hex_str!([0, 255]).as_str(), "00ff");
assertcp_eq!(owned_hex_str!([10, 9]).as_str(), "0a09");
assertcp_eq!(owned_hex_str!([16, 15]).as_str(), "100f");
}
impl<const LEN: usize, const HLEN: usize> AsRef<str> for OwnedHexStr<LEN, HLEN> {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl<const LEN: usize, const HLEN: usize> Display for OwnedHexStr<LEN, HLEN> {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.as_str(), fmt)
}
}
impl<const LEN: usize, const HLEN: usize> Debug for OwnedHexStr<LEN, HLEN> {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(self.as_str(), fmt)
}
}
impl<const LEN: usize, const HLEN: usize> Serialize for OwnedHexStr<LEN, HLEN> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de, const LEN: usize, const HLEN: usize> Deserialize<'de> for OwnedHexStr<LEN, HLEN> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct VisitorImpl<const LEN: usize, const HLEN: usize>;
#[expect(clippy::needless_lifetimes)]
impl<'de, const LEN: usize, const HLEN: usize> Visitor<'de> for VisitorImpl<LEN, HLEN> {
type Value = OwnedHexStr<LEN, HLEN>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "a string containing {} hex digits", HLEN)
}
fn visit_str<E>(self, val: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if val.len() != HLEN {
Err(E::custom(format!(
"Expected length {HLEN} got length {}",
val.len()
)))
} else if !val.is_ascii() {
Err(E::custom("Expected ASCII string"))
} else if let Some(ch) = val.chars().find(|ch| !ch.is_ascii_hexdigit()) {
Err(E::custom(format!("Expected hex digit, got {ch:?}")))
} else {
let mut hex_str = [0; HLEN];
for (i, b) in val.bytes().enumerate() {
hex_str[i] = b;
}
Ok(OwnedHexStr { hex_str })
}
}
}
deserializer.deserialize_string(VisitorImpl::<LEN, HLEN> {})
}
}