use std::fmt;
#[derive(Debug, PartialEq, Eq)]
pub enum HexError {
OddLength,
InvalidChar(char),
}
impl fmt::Display for HexError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HexError::OddLength => f.write_str("odd number of hex digits"),
HexError::InvalidChar(c) => write!(f, "invalid hex character: {c:?}"),
}
}
}
impl std::error::Error for HexError {}
pub fn encode(bytes: &[u8]) -> String {
bytes.iter().flat_map(|b| nibble_to_hex(b >> 4, false).chain(nibble_to_hex(b & 0xf, false))).collect()
}
pub fn encode_upper(bytes: &[u8]) -> String {
bytes.iter().flat_map(|b| nibble_to_hex(b >> 4, true).chain(nibble_to_hex(b & 0xf, true))).collect()
}
fn nibble_to_hex(nibble: u8, upper: bool) -> std::array::IntoIter<char, 1> {
let ch = match nibble {
0..=9 => b'0' + nibble,
_ if upper => b'A' + nibble - 10,
_ => b'a' + nibble - 10,
};
[ch as char].into_iter()
}
pub fn decode(hex: &str) -> Result<Vec<u8>, HexError> {
let hex = hex.strip_prefix("0x").or_else(|| hex.strip_prefix("0X")).unwrap_or(hex);
if !hex.len().is_multiple_of(2) {
return Err(HexError::OddLength);
}
hex.as_bytes()
.chunks(2)
.map(|pair| {
let hi = from_hex_char(pair[0] as char)?;
let lo = from_hex_char(pair[1] as char)?;
Ok((hi << 4) | lo)
})
.collect()
}
fn from_hex_char(c: char) -> Result<u8, HexError> {
match c {
'0'..='9' => Ok(c as u8 - b'0'),
'a'..='f' => Ok(c as u8 - b'a' + 10),
'A'..='F' => Ok(c as u8 - b'A' + 10),
_ => Err(HexError::InvalidChar(c)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_empty() {
assert_eq!(encode(b""), "");
}
#[test]
fn encode_single_byte() {
assert_eq!(encode(b"\xff"), "ff");
assert_eq!(encode(b"\x00"), "00");
}
#[test]
fn encode_lowercase() {
assert_eq!(encode(b"\xde\xad\xbe\xef"), "deadbeef");
}
#[test]
fn encode_upper_uppercase() {
assert_eq!(encode_upper(b"\xde\xad\xbe\xef"), "DEADBEEF");
}
#[test]
fn encode_roundtrip() {
let original = b"Hello, world!\x00\xff";
assert_eq!(decode(&encode(original)).unwrap(), original);
}
#[test]
fn decode_empty() {
assert_eq!(decode("").unwrap(), b"");
}
#[test]
fn decode_lowercase() {
assert_eq!(decode("deadbeef").unwrap(), b"\xde\xad\xbe\xef");
}
#[test]
fn decode_uppercase() {
assert_eq!(decode("DEADBEEF").unwrap(), b"\xde\xad\xbe\xef");
}
#[test]
fn decode_mixed_case() {
assert_eq!(decode("DeAdBeEf").unwrap(), b"\xde\xad\xbe\xef");
}
#[test]
fn decode_0x_prefix() {
assert_eq!(decode("0xdeadbeef").unwrap(), b"\xde\xad\xbe\xef");
assert_eq!(decode("0Xdeadbeef").unwrap(), b"\xde\xad\xbe\xef");
}
#[test]
fn decode_odd_length_errors() {
assert_eq!(decode("abc").unwrap_err(), HexError::OddLength);
}
#[test]
fn decode_invalid_char_errors() {
assert!(matches!(decode("zz").unwrap_err(), HexError::InvalidChar('z')));
}
#[test]
fn encode_all_bytes() {
let all: Vec<u8> = (0u8..=255).collect();
let hex = encode(&all);
assert_eq!(hex.len(), 512);
assert_eq!(decode(&hex).unwrap(), all);
}
}