const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
#[must_use]
pub fn to_lower_hex(bytes: &[u8]) -> String {
let mut hex = String::with_capacity(bytes.len() * 2);
for &byte in bytes {
push_lower_hex_byte(&mut hex, byte);
}
hex
}
pub fn push_lower_hex_byte(output: &mut String, byte: u8) {
for nybble in [byte >> 4, byte & 0x0f] {
if let Some(digit) = HEX_DIGITS.get(usize::from(nybble)).copied() {
output.push(char::from(digit));
}
}
}
#[cfg(test)]
#[path = "hex_property_tests.rs"]
mod property_tests;
#[cfg(test)]
mod tests {
use super::{push_lower_hex_byte, to_lower_hex};
use rstest::rstest;
#[rstest]
#[case(&[], "")]
#[case(&[0x00], "00")]
#[case(&[0xff], "ff")]
#[case(&[0x0f, 0xf0], "0ff0")]
#[case(b"\xde\xad\xbe\xef", "deadbeef")]
fn encodes_known_vectors(#[case] bytes: &[u8], #[case] expected: &str) {
assert_eq!(to_lower_hex(bytes), expected);
}
#[rstest]
fn every_byte_round_trips() {
for byte in u8::MIN..=u8::MAX {
let encoded = to_lower_hex(&[byte]);
assert_eq!(
encoded.len(),
2,
"byte {byte:#04x} did not render two digits"
);
assert!(
encoded
.chars()
.all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c)),
"byte {byte:#04x} rendered non-lowercase-hex characters: {encoded}"
);
let decoded =
u8::from_str_radix(&encoded, 16).expect("encoded byte should parse as hex");
assert_eq!(decoded, byte);
}
}
#[rstest]
fn push_matches_whole_slice_encoding() {
let bytes: Vec<u8> = (u8::MIN..=u8::MAX).collect();
let mut pushed = String::new();
for byte in &bytes {
push_lower_hex_byte(&mut pushed, *byte);
}
assert_eq!(pushed, to_lower_hex(&bytes));
}
}