Skip to main content

bluetape_rs_codec/
hex.rs

1//! Strict hexadecimal encoding and decoding.
2
3use std::error::Error;
4use std::fmt;
5
6const LOWER_HEX: &[u8; 16] = b"0123456789abcdef";
7const UPPER_HEX: &[u8; 16] = b"0123456789ABCDEF";
8
9/// Error returned when strict hexadecimal decoding rejects caller-owned input.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[non_exhaustive]
12pub enum HexDecodeError {
13    /// The encoded text has an odd byte length.
14    OddLength {
15        /// Number of bytes in the encoded text.
16        len: usize,
17    },
18    /// The encoded text contains a non-hexadecimal byte.
19    InvalidCharacter {
20        /// Zero-based byte position of the invalid character.
21        index: usize,
22        /// Invalid byte value.
23        byte: u8,
24    },
25}
26
27impl fmt::Display for HexDecodeError {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self {
30            Self::OddLength { len } => {
31                write!(f, "hex input must have even byte length, got {len}")
32            }
33            Self::InvalidCharacter { index, byte } => {
34                write!(
35                    f,
36                    "hex input contains invalid byte 0x{byte:02x} at position {index}"
37                )
38            }
39        }
40    }
41}
42
43impl Error for HexDecodeError {}
44
45/// Encodes bytes as lowercase hexadecimal text.
46///
47/// # Examples
48///
49/// ```
50/// use bluetape_rs_codec::encode_hex_lower;
51///
52/// assert_eq!(encode_hex_lower([0x00, 0xab, 0xff]), "00abff");
53/// ```
54#[must_use]
55pub fn encode_hex_lower(bytes: impl AsRef<[u8]>) -> String {
56    encode_hex_with_alphabet(bytes.as_ref(), LOWER_HEX)
57}
58
59/// Encodes bytes as uppercase hexadecimal text.
60///
61/// # Examples
62///
63/// ```
64/// use bluetape_rs_codec::encode_hex_upper;
65///
66/// assert_eq!(encode_hex_upper([0x00, 0xab, 0xff]), "00ABFF");
67/// ```
68#[must_use]
69pub fn encode_hex_upper(bytes: impl AsRef<[u8]>) -> String {
70    encode_hex_with_alphabet(bytes.as_ref(), UPPER_HEX)
71}
72
73/// Decodes strict hexadecimal text into bytes.
74///
75/// The decoder accepts uppercase and lowercase ASCII hexadecimal digits. It
76/// rejects odd-length input, prefixes such as `0x`, whitespace, separators, and
77/// any non-ASCII digit.
78///
79/// # Examples
80///
81/// ```
82/// use bluetape_rs_codec::decode_hex;
83///
84/// assert_eq!(decode_hex("00abFF")?, vec![0x00, 0xab, 0xff]);
85/// # Ok::<(), bluetape_rs_codec::HexDecodeError>(())
86/// ```
87///
88/// # Errors
89///
90/// Returns [`HexDecodeError::OddLength`] when the input has an odd byte length,
91/// or [`HexDecodeError::InvalidCharacter`] with the invalid byte position when
92/// any character is not an ASCII hexadecimal digit.
93pub fn decode_hex(encoded: impl AsRef<str>) -> Result<Vec<u8>, HexDecodeError> {
94    let encoded = encoded.as_ref();
95    let bytes = encoded.as_bytes();
96    if bytes.len() % 2 != 0 {
97        return Err(HexDecodeError::OddLength { len: bytes.len() });
98    }
99
100    let mut decoded = Vec::with_capacity(bytes.len() / 2);
101    for (pair_index, chunk) in bytes.chunks_exact(2).enumerate() {
102        let high_index = pair_index * 2;
103        let high = decode_nibble(chunk[0], high_index)?;
104        let low = decode_nibble(chunk[1], high_index + 1)?;
105        decoded.push((high << 4) | low);
106    }
107
108    Ok(decoded)
109}
110
111fn encode_hex_with_alphabet(bytes: &[u8], alphabet: &[u8; 16]) -> String {
112    let mut encoded = String::with_capacity(bytes.len() * 2);
113    for byte in bytes {
114        encoded.push(alphabet[(byte >> 4) as usize] as char);
115        encoded.push(alphabet[(byte & 0x0f) as usize] as char);
116    }
117    encoded
118}
119
120fn decode_nibble(byte: u8, index: usize) -> Result<u8, HexDecodeError> {
121    match byte {
122        b'0'..=b'9' => Ok(byte - b'0'),
123        b'a'..=b'f' => Ok(byte - b'a' + 10),
124        b'A'..=b'F' => Ok(byte - b'A' + 10),
125        _ => Err(HexDecodeError::InvalidCharacter { index, byte }),
126    }
127}