use core::fmt;
const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
const HEX_DECODE: [u8; 256] = build_hex_decode_table();
const fn build_hex_decode_table() -> [u8; 256] {
let mut table = [255u8; 256];
let mut i: u8 = 0;
while i <= 9 {
table[(b'0' + i) as usize] = i;
i += 1;
}
i = 0;
while i < 6 {
table[(b'a' + i) as usize] = 10 + i;
table[(b'A' + i) as usize] = 10 + i;
i += 1;
}
table
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HexDecodeErrorKind {
OddLength,
InvalidCharacter,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HexDecodeError {
kind: HexDecodeErrorKind,
}
impl HexDecodeError {
const fn new(kind: HexDecodeErrorKind) -> Self {
Self { kind }
}
}
impl fmt::Display for HexDecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind {
HexDecodeErrorKind::OddLength => write!(f, "hex: odd-length input"),
HexDecodeErrorKind::InvalidCharacter => write!(f, "hex: invalid character in input"),
}
}
}
impl std::error::Error for HexDecodeError {}
#[must_use]
pub fn hex_encode(input: &[u8]) -> String {
let mut out = Vec::with_capacity(input.len() * 2);
for &byte in input {
out.push(HEX_DIGITS[(byte >> 4) as usize]);
out.push(HEX_DIGITS[(byte & 0x0F) as usize]);
}
#[allow(clippy::expect_used)]
String::from_utf8(out).expect("hex output is always valid ASCII")
}
const HEX_DIGITS_UPPER: &[u8; 16] = b"0123456789ABCDEF";
#[must_use]
pub fn hex_encode_upper(input: &[u8]) -> String {
let mut out = Vec::with_capacity(input.len() * 2);
for &byte in input {
out.push(HEX_DIGITS_UPPER[(byte >> 4) as usize]);
out.push(HEX_DIGITS_UPPER[(byte & 0x0F) as usize]);
}
#[allow(clippy::expect_used)]
String::from_utf8(out).expect("hex output is always valid ASCII")
}
pub fn hex_decode(input: &str) -> Result<Vec<u8>, HexDecodeError> {
let bytes = input.as_bytes();
if bytes.len() % 2 != 0 {
return Err(HexDecodeError::new(HexDecodeErrorKind::OddLength));
}
let mut out = Vec::with_capacity(bytes.len() / 2);
for pair in bytes.chunks_exact(2) {
let high = decode_nibble(pair[0])?;
let low = decode_nibble(pair[1])?;
out.push((high << 4) | low);
}
Ok(out)
}
#[inline]
fn decode_nibble(byte: u8) -> Result<u8, HexDecodeError> {
let val = HEX_DECODE[byte as usize];
if val == 255 {
return Err(HexDecodeError::new(HexDecodeErrorKind::InvalidCharacter));
}
Ok(val)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_empty() {
assert_eq!(hex_encode(b""), "");
}
#[test]
fn encode_single_byte() {
assert_eq!(hex_encode(&[0x00]), "00");
assert_eq!(hex_encode(&[0xFF]), "ff");
assert_eq!(hex_encode(&[0xAB]), "ab");
}
#[test]
fn encode_multi_byte() {
assert_eq!(hex_encode(&[0xDE, 0xAD, 0xBE, 0xEF]), "deadbeef");
}
#[test]
fn encode_all_bytes() {
let input: Vec<u8> = (0..=255).collect();
let encoded = hex_encode(&input);
assert_eq!(encoded.len(), 512);
assert!(encoded.starts_with("00"));
assert!(encoded.ends_with("ff"));
}
#[test]
fn encode_produces_lowercase() {
let result = hex_encode(&[0xCA, 0xFE]);
assert_eq!(result, "cafe");
assert_eq!(result, result.to_lowercase());
}
#[test]
fn encode_upper_empty() {
assert_eq!(hex_encode_upper(b""), "");
}
#[test]
fn encode_upper_single_byte() {
assert_eq!(hex_encode_upper(&[0x00]), "00");
assert_eq!(hex_encode_upper(&[0xFF]), "FF");
assert_eq!(hex_encode_upper(&[0xAB]), "AB");
}
#[test]
fn encode_upper_multi_byte() {
assert_eq!(hex_encode_upper(&[0xDE, 0xAD, 0xBE, 0xEF]), "DEADBEEF");
}
#[test]
fn encode_upper_produces_uppercase() {
let result = hex_encode_upper(&[0xca, 0xfe]);
assert_eq!(result, "CAFE");
assert_eq!(result, result.to_uppercase());
}
#[test]
fn decode_empty() {
assert_eq!(hex_decode("").unwrap(), b"");
}
#[test]
fn decode_single_byte() {
assert_eq!(hex_decode("00").unwrap(), &[0x00]);
assert_eq!(hex_decode("ff").unwrap(), &[0xFF]);
assert_eq!(hex_decode("ab").unwrap(), &[0xAB]);
}
#[test]
fn decode_multi_byte() {
assert_eq!(hex_decode("deadbeef").unwrap(), &[0xDE, 0xAD, 0xBE, 0xEF]);
}
#[test]
fn decode_case_insensitive() {
assert_eq!(hex_decode("DEADBEEF").unwrap(), &[0xDE, 0xAD, 0xBE, 0xEF]);
assert_eq!(hex_decode("DeAdBeEf").unwrap(), &[0xDE, 0xAD, 0xBE, 0xEF]);
}
#[test]
fn round_trip_all_bytes() {
let data: Vec<u8> = (0..=255).collect();
let encoded = hex_encode(&data);
let decoded = hex_decode(&encoded).unwrap();
assert_eq!(decoded, data);
}
#[test]
fn round_trip_short_lengths() {
for len in 0..=32_u8 {
let data: Vec<u8> = (0..len).collect();
let encoded = hex_encode(&data);
let decoded = hex_decode(&encoded).unwrap();
assert_eq!(decoded, data, "round-trip failed for length {len}");
}
}
#[test]
fn decode_rejects_odd_length() {
let err = hex_decode("abc").unwrap_err();
assert_eq!(err, HexDecodeError::new(HexDecodeErrorKind::OddLength));
}
#[test]
fn decode_rejects_single_char() {
let err = hex_decode("a").unwrap_err();
assert_eq!(err, HexDecodeError::new(HexDecodeErrorKind::OddLength));
}
#[test]
fn decode_rejects_invalid_character() {
let err = hex_decode("zz").unwrap_err();
assert_eq!(
err,
HexDecodeError::new(HexDecodeErrorKind::InvalidCharacter)
);
}
#[test]
fn decode_rejects_non_hex_characters() {
assert!(hex_decode("gg").is_err());
assert!(hex_decode("0x").is_err());
assert!(hex_decode(" a").is_err());
assert!(hex_decode("a!").is_err());
}
#[test]
fn error_display_messages() {
let odd = HexDecodeError::new(HexDecodeErrorKind::OddLength);
assert_eq!(odd.to_string(), "hex: odd-length input");
let invalid = HexDecodeError::new(HexDecodeErrorKind::InvalidCharacter);
assert_eq!(invalid.to_string(), "hex: invalid character in input");
}
#[test]
fn error_implements_std_error() {
let err: Box<dyn std::error::Error> =
Box::new(HexDecodeError::new(HexDecodeErrorKind::InvalidCharacter));
let _ = err.to_string();
}
}