use arrow::datatypes::ArrowNativeType;
use crate::Result;
use crate::error::_internal_err;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HexCase {
Lower,
Upper,
}
const LOWER_DIGITS: &[u8; 16] = b"0123456789abcdef";
const UPPER_DIGITS: &[u8; 16] = b"0123456789ABCDEF";
const LOOKUP_LOWER: [[u8; 2]; 256] = build_lookup(LOWER_DIGITS);
const LOOKUP_UPPER: [[u8; 2]; 256] = build_lookup(UPPER_DIGITS);
const fn build_lookup(digits: &[u8; 16]) -> [[u8; 2]; 256] {
let mut table = [[0u8; 2]; 256];
let mut i = 0;
while i < 256 {
table[i][0] = digits[i >> 4];
table[i][1] = digits[i & 0xF];
i += 1;
}
table
}
impl HexCase {
#[inline]
const fn lookup(self) -> &'static [[u8; 2]; 256] {
match self {
HexCase::Lower => &LOOKUP_LOWER,
HexCase::Upper => &LOOKUP_UPPER,
}
}
#[inline]
const fn digits(self) -> &'static [u8; 16] {
match self {
HexCase::Lower => LOWER_DIGITS,
HexCase::Upper => UPPER_DIGITS,
}
}
}
pub trait ToHex: ArrowNativeType {
fn write_hex(self, case: HexCase, buf: &mut [u8; 16]) -> &[u8];
}
macro_rules! impl_to_hex_signed {
($ty:ty) => {
impl ToHex for $ty {
#[inline(always)]
fn write_hex(self, case: HexCase, buf: &mut [u8; 16]) -> &[u8] {
encode_u64(self as i64 as u64, case, buf)
}
}
};
}
macro_rules! impl_to_hex_unsigned {
($ty:ty) => {
impl ToHex for $ty {
#[inline(always)]
fn write_hex(self, case: HexCase, buf: &mut [u8; 16]) -> &[u8] {
encode_u64(self as u64, case, buf)
}
}
};
}
impl_to_hex_signed!(i8);
impl_to_hex_signed!(i16);
impl_to_hex_signed!(i32);
impl_to_hex_signed!(i64);
impl_to_hex_unsigned!(u8);
impl_to_hex_unsigned!(u16);
impl_to_hex_unsigned!(u32);
impl_to_hex_unsigned!(u64);
#[inline(always)]
pub fn encode_bytes_into(bytes: &[u8], case: HexCase, out: &mut Vec<u8>) {
let lookup = case.lookup();
for &byte in bytes {
out.extend_from_slice(&lookup[byte as usize]);
}
}
#[inline(always)]
pub fn encode_bytes_to_slice(bytes: &[u8], case: HexCase, out: &mut [u8]) -> Result<()> {
let expected = bytes.len() * 2;
if out.len() != expected {
return _internal_err!(
"hex output buffer is {} bytes, expected {expected}",
out.len()
);
}
let lookup = case.lookup();
for (&b, chunk) in bytes.iter().zip(out.chunks_exact_mut(2)) {
chunk.copy_from_slice(&lookup[b as usize]);
}
Ok(())
}
#[inline]
pub fn encode_bytes(bytes: &[u8], case: HexCase) -> String {
let mut out = Vec::with_capacity(bytes.len() * 2);
encode_bytes_into(bytes, case, &mut out);
unsafe { String::from_utf8_unchecked(out) }
}
#[inline(always)]
pub fn encode_u64(v: u64, case: HexCase, buf: &mut [u8; 16]) -> &[u8] {
let start = write_digits(v, case, buf);
&buf[start..]
}
#[inline(always)]
fn write_digits(v: u64, case: HexCase, buf: &mut [u8; 16]) -> usize {
if v == 0 {
buf[15] = b'0';
return 15;
}
let lookup = case.lookup();
let mut pos = 16;
let mut rest = v;
while rest >= 0x10 {
pos -= 2;
let pair = lookup[(rest & 0xFF) as usize];
buf[pos] = pair[0];
buf[pos + 1] = pair[1];
rest >>= 8;
}
if rest > 0 {
pos -= 1;
buf[pos] = case.digits()[rest as usize];
}
pos
}
#[cfg(test)]
mod tests {
use super::*;
fn hex_u64(v: u64, case: HexCase) -> String {
let mut buf = [0u8; 16];
String::from_utf8(encode_u64(v, case, &mut buf).to_vec()).unwrap()
}
#[test]
fn encode_u64_zero() {
assert_eq!(hex_u64(0, HexCase::Lower), "0");
assert_eq!(hex_u64(0, HexCase::Upper), "0");
}
#[test]
fn encode_u64_single_nibble() {
for v in 1..=0xFu64 {
assert_eq!(hex_u64(v, HexCase::Lower), format!("{v:x}"));
assert_eq!(hex_u64(v, HexCase::Upper), format!("{v:X}"));
}
}
#[test]
fn encode_u64_digit_count_boundaries() {
for v in [
0x10u64,
0xFF,
0x100,
0xFFF,
0x1000,
0xFFFFF,
0xFFFF_FFFF,
0x1_0000_0000,
] {
assert_eq!(hex_u64(v, HexCase::Lower), format!("{v:x}"));
assert_eq!(hex_u64(v, HexCase::Upper), format!("{v:X}"));
}
}
#[test]
fn encode_u64_max() {
assert_eq!(hex_u64(u64::MAX, HexCase::Lower), "ffffffffffffffff");
assert_eq!(hex_u64(u64::MAX, HexCase::Upper), "FFFFFFFFFFFFFFFF");
}
#[test]
fn encode_u64_signed_is_twos_complement() {
assert_eq!(hex_u64(-1i64 as u64, HexCase::Lower), "ffffffffffffffff");
assert_eq!(hex_u64(i64::MIN as u64, HexCase::Upper), "8000000000000000");
}
#[test]
fn encode_bytes_empty() {
assert_eq!(encode_bytes(&[], HexCase::Lower), "");
assert_eq!(encode_bytes(&[], HexCase::Upper), "");
}
#[test]
fn encode_bytes_examples() {
assert_eq!(encode_bytes(&[0x00], HexCase::Lower), "00");
assert_eq!(encode_bytes(&[0xAB], HexCase::Lower), "ab");
assert_eq!(encode_bytes(&[0xAB], HexCase::Upper), "AB");
assert_eq!(
encode_bytes(&[0xde, 0xad, 0xbe, 0xef], HexCase::Lower),
"deadbeef"
);
assert_eq!(
encode_bytes(&[0xde, 0xad, 0xbe, 0xef], HexCase::Upper),
"DEADBEEF"
);
}
#[test]
fn encode_bytes_covers_every_byte_value() {
let bytes: Vec<u8> = (0..=255u8).collect();
let expected: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
assert_eq!(encode_bytes(&bytes, HexCase::Lower), expected);
let expected: String = bytes.iter().map(|b| format!("{b:02X}")).collect();
assert_eq!(encode_bytes(&bytes, HexCase::Upper), expected);
}
#[test]
fn encode_bytes_into_appends_without_clearing() {
let mut out = b"prefix-".to_vec();
encode_bytes_into(&[0x01, 0x02], HexCase::Lower, &mut out);
assert_eq!(out, b"prefix-0102");
}
#[test]
fn encode_u64_reused_buffer_leaks_no_stale_digits() {
let mut buf = [0u8; 16];
assert_eq!(
encode_u64(u64::MAX, HexCase::Lower, &mut buf),
b"ffffffffffffffff"
);
assert_eq!(encode_u64(0, HexCase::Lower, &mut buf), b"0");
assert_eq!(encode_u64(0xAB, HexCase::Lower, &mut buf), b"ab");
}
#[test]
fn encode_bytes_to_slice_empty() -> Result<()> {
let mut out: [u8; 0] = [];
encode_bytes_to_slice(&[], HexCase::Lower, &mut out)?;
assert_eq!(out, [] as [u8; 0]);
Ok(())
}
#[test]
fn encode_bytes_to_slice_examples() -> Result<()> {
let mut out = [0u8; 8];
encode_bytes_to_slice(&[0xde, 0xad, 0xbe, 0xef], HexCase::Lower, &mut out)?;
assert_eq!(&out, b"deadbeef");
let mut out = [0u8; 8];
encode_bytes_to_slice(&[0xde, 0xad, 0xbe, 0xef], HexCase::Upper, &mut out)?;
assert_eq!(&out, b"DEADBEEF");
Ok(())
}
#[test]
fn encode_bytes_to_slice_agrees_with_encode_bytes() -> Result<()> {
let bytes: Vec<u8> = (0..=255u8).collect();
for case in [HexCase::Lower, HexCase::Upper] {
let mut out = vec![0u8; bytes.len() * 2];
encode_bytes_to_slice(&bytes, case, &mut out)?;
assert_eq!(String::from_utf8(out).unwrap(), encode_bytes(&bytes, case));
}
Ok(())
}
#[test]
fn encode_bytes_to_slice_rejects_wrong_length() {
let mut short = [0u8; 6];
let err =
encode_bytes_to_slice(&[0xde, 0xad, 0xbe, 0xef], HexCase::Lower, &mut short)
.unwrap_err();
assert!(
err.message()
.contains("hex output buffer is 6 bytes, expected 8"),
"unexpected message: {err}"
);
let mut long = [0u8; 10];
assert!(
encode_bytes_to_slice(&[0xde, 0xad, 0xbe, 0xef], HexCase::Lower, &mut long)
.is_err()
);
}
}