use crate::sexpr::SExprError;
#[must_use]
pub fn encode(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push(nibble(b >> 4));
out.push(nibble(b & 0xF));
}
out
}
pub fn decode(s: &str) -> Result<Vec<u8>, SExprError> {
if s.len() % 2 != 0 {
return Err(SExprError::Parse("hex string must have even length".into()));
}
let mut out = Vec::with_capacity(s.len() / 2);
for chunk in s.as_bytes().chunks(2) {
let pair =
std::str::from_utf8(chunk).map_err(|_| SExprError::Parse("non-ascii in hex".into()))?;
let byte =
u8::from_str_radix(pair, 16).map_err(|e| SExprError::Parse(format!("bad hex: {e}")))?;
out.push(byte);
}
Ok(out)
}
pub(crate) fn nibble(n: u8) -> char {
match n {
0..=9 => (b'0' + n) as char,
10..=15 => (b'a' + (n - 10)) as char,
_ => unreachable!("nibble must be 0..=15"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_empty_is_empty() {
assert_eq!(encode(&[]), "");
}
#[test]
fn encode_is_lowercase() {
assert_eq!(encode(&[0xde, 0xad, 0xbe, 0xef]), "deadbeef");
}
#[test]
fn encode_length_is_twice_input() {
for n in 0..16 {
let bytes: Vec<u8> = (0..n).collect();
assert_eq!(encode(&bytes).len(), n as usize * 2);
}
}
#[test]
fn decode_empty_is_empty_vec() {
assert!(decode("").unwrap().is_empty());
}
#[test]
fn decode_rejects_odd_length() {
let err = decode("abc").unwrap_err();
assert!(format!("{err:?}").contains("even length"));
}
#[test]
fn decode_rejects_non_ascii() {
let err = decode("aña").unwrap_err();
assert!(
format!("{err:?}").contains("non-ascii"),
"unexpected: {err:?}"
);
}
#[test]
fn decode_rejects_non_hex_digits() {
let err = decode("ag").unwrap_err();
assert!(format!("{err:?}").contains("bad hex"));
}
#[test]
fn decode_accepts_uppercase_mixed_case() {
assert_eq!(decode("DEADBEEF").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
assert_eq!(decode("DeadBeef").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
}
#[test]
fn roundtrip_every_byte_value() {
for b in 0u8..=255u8 {
let enc = encode(&[b]);
assert_eq!(enc.len(), 2);
let dec = decode(&enc).unwrap();
assert_eq!(dec, vec![b], "failed for byte 0x{b:02x}");
}
}
#[test]
fn roundtrip_larger_buffer() {
let original: Vec<u8> = (0..=255).chain((0..=255).rev()).collect();
let enc = encode(&original);
assert_eq!(enc.len(), original.len() * 2);
let dec = decode(&enc).unwrap();
assert_eq!(dec, original);
}
#[test]
fn nibble_boundaries() {
assert_eq!(nibble(0), '0');
assert_eq!(nibble(9), '9');
assert_eq!(nibble(10), 'a');
assert_eq!(nibble(15), 'f');
}
#[test]
#[should_panic(expected = "nibble must be 0..=15")]
fn nibble_panics_above_range() {
let _ = nibble(16);
}
}