Skip to main content

loonfs_api/
hex.rs

1//! Lowercase hexadecimal encoding shared by wire and durable codecs.
2
3use thiserror::Error;
4
5/// Describes why bytes cannot be decoded from the durable lowercase hexadecimal form.
6#[derive(Debug, Clone, PartialEq, Eq, Error)]
7pub enum HexDecodeError {
8    /// Reports an input that ends after the high nibble of its final byte.
9    #[error("odd hex length {length}")]
10    OddLength {
11        /// Number of ASCII bytes in the rejected input.
12        length: usize,
13    },
14    /// Reports an input byte outside `0`–`9` and `a`–`f`.
15    #[error("invalid hex byte {byte:#04x}")]
16    InvalidByte {
17        /// First byte that violated the lowercase hexadecimal alphabet.
18        byte: u8,
19    },
20}
21
22/// Encodes bytes using the lowercase hexadecimal alphabet expected by durable codecs.
23pub fn hex_encode_bytes(bytes: &[u8]) -> String {
24    const HEX: &[u8; 16] = b"0123456789abcdef";
25    let mut encoded = String::with_capacity(bytes.len() * 2);
26    for byte in bytes {
27        encoded.push(char::from(HEX[(byte >> 4) as usize]));
28        encoded.push(char::from(HEX[(byte & 0x0f) as usize]));
29    }
30    encoded
31}
32
33/// Decodes lowercase hex. Errors carry no input bytes; callers name the
34/// field they were decoding.
35pub fn hex_decode_bytes(encoded: &str) -> Result<Vec<u8>, HexDecodeError> {
36    fn nibble(byte: u8) -> Result<u8, HexDecodeError> {
37        match byte {
38            b'0'..=b'9' => Ok(byte - b'0'),
39            b'a'..=b'f' => Ok(byte - b'a' + 10),
40            _ => Err(HexDecodeError::InvalidByte { byte }),
41        }
42    }
43
44    let bytes = encoded.as_bytes();
45    if bytes.len() % 2 != 0 {
46        return Err(HexDecodeError::OddLength {
47            length: bytes.len(),
48        });
49    }
50    let mut decoded = Vec::with_capacity(bytes.len() / 2);
51    for pair in bytes.chunks_exact(2) {
52        decoded.push((nibble(pair[0])? << 4) | nibble(pair[1])?);
53    }
54    Ok(decoded)
55}