use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum HexError {
#[error("hex: input has odd length {0}")]
OddLength(usize),
#[error("hex: invalid character at index {0}")]
InvalidCharacter(usize),
}
#[must_use]
pub fn encode(bytes: &[u8]) -> String {
hex::encode(bytes)
}
pub fn decode(s: &str) -> Result<Vec<u8>, HexError> {
hex::decode(s).map_err(|e| match e {
hex::FromHexError::OddLength => HexError::OddLength(s.len()),
hex::FromHexError::InvalidHexCharacter { index, .. } => HexError::InvalidCharacter(index),
hex::FromHexError::InvalidStringLength => HexError::OddLength(s.len()),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encodes_lowercase_no_prefix_no_separators() {
assert_eq!(encode(&[0x00, 0xab, 0xff, 0x10]), "00abff10");
}
#[test]
fn empty_round_trips() {
assert_eq!(encode(&[]), "");
assert_eq!(decode("").unwrap(), Vec::<u8>::new());
}
#[test]
fn round_trips_full_byte_range() {
let all: Vec<u8> = (0u16..=255).map(|b| b as u8).collect();
assert_eq!(decode(&encode(&all)).unwrap(), all);
}
#[test]
fn decode_is_case_insensitive() {
assert_eq!(decode("00ABFF").unwrap(), decode("00abff").unwrap());
assert_eq!(decode("DeadBeef").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
}
#[test]
fn rejects_odd_length() {
assert_eq!(decode("abc"), Err(HexError::OddLength(3)));
assert_eq!(decode("f"), Err(HexError::OddLength(1)));
}
#[test]
fn rejects_non_hex_characters() {
assert_eq!(decode("0g"), Err(HexError::InvalidCharacter(1)));
assert_eq!(decode("zz"), Err(HexError::InvalidCharacter(0)));
assert_eq!(decode("0xff"), Err(HexError::InvalidCharacter(1)));
}
}