Hex Encode And Decode

Convert bytes to lowercase hex and back, accepting an optional prefix and embedded whitespace while rejecting invalid digits.

RustData Integrity

Code

snippet
fn encode_hex(bytes: &[u8]) -> String {
    const DIGITS: &[u8; 16] = b"0123456789abcdef";
    let mut output = String::with_capacity(bytes.len() * 2);
    for &byte in bytes {
        output.push(DIGITS[(byte >> 4) as usize] as char);
        output.push(DIGITS[(byte & 0x0f) as usize] as char);
    }
    output
}

fn hex_value(byte: u8) -> Result<u8, String> {
    match byte {
        b'0'..=b'9' => Ok(byte - b'0'),
        b'a'..=b'f' => Ok(byte - b'a' + 10),
        b'A'..=b'F' => Ok(byte - b'A' + 10),
        _ => Err(format!("invalid hex digit: {}", byte as char)),
    }
}

fn decode_hex(input: &str) -> Result<Vec<u8>, String> {
    let trimmed = input.trim();
    let cleaned = trimmed
        .strip_prefix("0x")
        .or_else(|| trimmed.strip_prefix("0X"))
        .unwrap_or(trimmed);
    let digits: Vec<u8> = cleaned
        .bytes()
        .filter(|byte| !byte.is_ascii_whitespace())
        .collect();
    if digits.len() % 2 != 0 {
        return Err("hex input must have an even number of digits".to_string());
    }
    let mut output = Vec::with_capacity(digits.len() / 2);
    for pair in digits.chunks(2) {
        let high = hex_value(pair[0])?;
        let low = hex_value(pair[1])?;
        output.push((high << 4) | low);
    }
    Ok(output)
}

fn main() {
    let payload = b"hello";
    let encoded = encode_hex(payload);
    println!("encoded: {encoded}");
    println!(
        "decoded: {:?}",
        decode_hex(&encoded).map(|bytes| String::from_utf8_lossy(&bytes).into_owned())
    );
    println!("prefixed: {:?}", decode_hex("0xDE AD BE EF"));
    println!("odd length: {}", decode_hex("abc").is_err());
    println!("bad digit: {}", decode_hex("zz").is_err());
}