use std::fmt::Write as _;
#[must_use]
pub fn to_hex(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
let _ = write!(s, "{b:02x}");
}
s
}
pub fn from_hex(s: &str) -> Result<Vec<u8>, String> {
let bytes = s.as_bytes();
if !bytes.len().is_multiple_of(2) {
return Err("odd-length hex string".to_owned());
}
fn nibble(b: u8) -> Result<u8, String> {
match b {
b'0'..=b'9' => Ok(b - b'0'),
b'a'..=b'f' => Ok(b - b'a' + 10),
b'A'..=b'F' => Ok(b - b'A' + 10),
_ => Err("invalid hex digit".to_owned()),
}
}
bytes
.chunks_exact(2)
.map(|pair| Ok((nibble(pair[0])? << 4) | nibble(pair[1])?))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hex_round_trips() {
let bytes = vec![0x00, 0x0f, 0x10, 0xff, 0xa5];
let hex = to_hex(&bytes);
assert_eq!(hex, "000f10ffa5");
assert_eq!(from_hex(&hex).unwrap(), bytes);
}
#[test]
fn from_hex_accepts_uppercase() {
assert_eq!(from_hex("A5FF").unwrap(), vec![0xa5, 0xff]);
}
#[test]
fn from_hex_errors_on_odd_length() {
assert!(from_hex("abc").is_err());
}
#[test]
fn from_hex_errors_on_invalid_digit() {
assert!(from_hex("zz").is_err());
}
#[test]
fn from_hex_errors_on_even_byte_multibyte_input() {
let res = from_hex("é");
assert!(res.is_err(), "multibyte input must error, not panic");
}
}