Skip to main content

pamoja_lorawan/
hex.rs

1//! Identifiers and keys written in hexadecimal, the way a network server shows them.
2
3/// Reads bytes written in hexadecimal, most significant first, as a network server shows a
4/// DevEUI, a JoinEUI or a root key.
5///
6/// It runs at compile time, so a program can take its identifiers from the environment it is
7/// built in, with `env!`, rather than keep a key in its source.
8///
9/// # Arguments
10///
11/// * `text` - two hexadecimal digits for each byte, in either case, with nothing between.
12///
13/// # Returns
14///
15/// The bytes, or `None` when the text is not exactly two digits a byte or holds anything but
16/// hexadecimal digits.
17///
18/// # Examples
19///
20/// ```
21/// use pamoja_lorawan::parse_hex;
22///
23/// // In a firmware build: `parse_hex(env!("LORAWAN_DEV_EUI"))`.
24/// const DEV_EUI: [u8; 8] = match parse_hex("70B3D57ED0051234") {
25///     Some(eui) => eui,
26///     None => panic!("a DevEUI is eight bytes of hexadecimal"),
27/// };
28/// assert_eq!(DEV_EUI, [0x70, 0xB3, 0xD5, 0x7E, 0xD0, 0x05, 0x12, 0x34]);
29///
30/// assert_eq!(parse_hex::<8>("70b3d57ed0051234"), Some(DEV_EUI));
31/// assert_eq!(parse_hex::<8>("70B3D57ED00512"), None, "a byte short");
32/// assert_eq!(parse_hex::<2>("0G12"), None, "not a hexadecimal digit");
33/// assert_eq!(parse_hex::<2>("0x12"), None, "no prefix");
34/// ```
35pub const fn parse_hex<const N: usize>(text: &str) -> Option<[u8; N]> {
36    let digits = text.as_bytes();
37    if digits.len() != N.saturating_mul(2) {
38        return None;
39    }
40    let mut bytes = [0u8; N];
41    let mut index = 0;
42    while index < N {
43        let (Some(high), Some(low)) = (nibble(digits[2 * index]), nibble(digits[2 * index + 1]))
44        else {
45            return None;
46        };
47        bytes[index] = (high << 4) | low;
48        index += 1;
49    }
50    Some(bytes)
51}
52
53/// The value of one hexadecimal digit.
54const fn nibble(digit: u8) -> Option<u8> {
55    match digit {
56        b'0'..=b'9' => Some(digit - b'0'),
57        b'a'..=b'f' => Some(digit - b'a' + 10),
58        b'A'..=b'F' => Some(digit - b'A' + 10),
59        _ => None,
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn every_digit_reads_as_its_value() {
69        let all: [u8; 8] = parse_hex("0123456789abcdef").expect("hex");
70        assert_eq!(all, [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]);
71        let upper: [u8; 3] = parse_hex("ABCDEF").expect("hex");
72        assert_eq!(upper, [0xAB, 0xCD, 0xEF]);
73        assert_eq!(parse_hex::<0>(""), Some([]));
74    }
75
76    #[test]
77    fn anything_but_two_digits_a_byte_is_refused() {
78        assert_eq!(parse_hex::<16>("3333333333333333333333333333333"), None);
79        assert_eq!(parse_hex::<16>("333333333333333333333333333333333"), None);
80        assert_eq!(parse_hex::<4>("33 33 33 33"), None);
81        assert_eq!(parse_hex::<2>("12-4"), None);
82        assert_eq!(parse_hex::<1>("é"), None);
83    }
84}