#[derive(Debug)]
pub(crate) enum HexError {
Length,
NotHex,
}
pub(crate) fn decode_exact(s: &str, out: &mut [u8]) -> Result<(), HexError> {
let s = s.as_bytes();
if s.len() != out.len() * 2 {
return Err(HexError::Length);
}
let (pairs, _remainder) = s.as_chunks::<2>();
for (byte, pair) in out.iter_mut().zip(pairs) {
*byte = digit(pair[0])? * 16 + digit(pair[1])?;
}
Ok(())
}
pub(crate) fn decode_vec(s: &str) -> Result<Vec<u8>, HexError> {
let s = s.as_bytes();
if !s.len().is_multiple_of(2) {
return Err(HexError::Length);
}
let (pairs, _remainder) = s.as_chunks::<2>();
pairs
.iter()
.map(|pair| Ok(digit(pair[0])? * 16 + digit(pair[1])?))
.collect()
}
fn digit(b: u8) -> Result<u8, HexError> {
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(HexError::NotHex),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_multibyte_character_is_an_error_rather_than_a_panic() {
let s = format!("{}\u{041e}b", "a".repeat(61));
assert_eq!(s.len(), 64, "the byte-length check must be the passing one");
let mut out = [0u8; 32];
assert!(matches!(decode_exact(&s, &mut out), Err(HexError::NotHex)));
assert!(matches!(decode_vec(&s), Err(HexError::NotHex)));
}
#[test]
fn ordinary_hex_still_decodes_in_both_cases() {
let mut out = [0u8; 4];
assert!(decode_exact("DeadBeef", &mut out).is_ok());
assert_eq!(out, [0xde, 0xad, 0xbe, 0xef]);
assert_eq!(decode_vec("00ff10").unwrap(), vec![0x00, 0xff, 0x10]);
assert_eq!(decode_vec("").unwrap(), Vec::<u8>::new());
}
#[test]
fn lengths_and_non_digits_are_told_apart() {
let mut out = [0u8; 2];
assert!(matches!(decode_exact("aabb", &mut out), Ok(())));
assert!(matches!(
decode_exact("aa", &mut out),
Err(HexError::Length)
));
assert!(matches!(
decode_exact("aabbcc", &mut out),
Err(HexError::Length)
));
assert!(matches!(
decode_exact("aagg", &mut out),
Err(HexError::NotHex)
));
assert!(matches!(decode_vec("abc"), Err(HexError::Length)));
}
}