zero_trust_rps/common/
hex.rs

1use std::{
2    fmt::{Debug, Display},
3    str,
4};
5
6use serde::{de::Visitor, Deserialize, Serialize};
7
8pub const HEX_DIGITS: &str = "0123456789abcdef";
9
10#[expect(clippy::char_lit_as_u8)]
11pub const fn parse_ascii_hex_digit(ch: char) -> Option<u8> {
12    match ch {
13        i if i.is_ascii_digit() => Some(i as u8 - ('0' as u8)),
14        i if i.is_ascii_hexdigit() => Some(10 + i.to_ascii_lowercase() as u8 - ('a' as u8)),
15        _ => None,
16    }
17}
18mod test_hex_digit_parsing {
19    use super::parse_ascii_hex_digit;
20    const _: () = assert!(parse_ascii_hex_digit('g').is_none());
21    const _: () = assert!(parse_ascii_hex_digit('ß').is_none());
22    const _: () = assert!(matches!(parse_ascii_hex_digit('0'), Some(0)));
23    const _: () = assert!(matches!(parse_ascii_hex_digit('1'), Some(1)));
24    const _: () = assert!(matches!(parse_ascii_hex_digit('2'), Some(2)));
25    const _: () = assert!(matches!(parse_ascii_hex_digit('3'), Some(3)));
26    const _: () = assert!(matches!(parse_ascii_hex_digit('4'), Some(4)));
27    const _: () = assert!(matches!(parse_ascii_hex_digit('5'), Some(5)));
28    const _: () = assert!(matches!(parse_ascii_hex_digit('6'), Some(6)));
29    const _: () = assert!(matches!(parse_ascii_hex_digit('7'), Some(7)));
30    const _: () = assert!(matches!(parse_ascii_hex_digit('8'), Some(8)));
31    const _: () = assert!(matches!(parse_ascii_hex_digit('9'), Some(9)));
32    const _: () = assert!(matches!(parse_ascii_hex_digit('A'), Some(10)));
33    const _: () = assert!(matches!(parse_ascii_hex_digit('B'), Some(11)));
34    const _: () = assert!(matches!(parse_ascii_hex_digit('C'), Some(12)));
35    const _: () = assert!(matches!(parse_ascii_hex_digit('D'), Some(13)));
36    const _: () = assert!(matches!(parse_ascii_hex_digit('E'), Some(14)));
37    const _: () = assert!(matches!(parse_ascii_hex_digit('F'), Some(15)));
38    const _: () = assert!(matches!(parse_ascii_hex_digit('a'), Some(10)));
39    const _: () = assert!(matches!(parse_ascii_hex_digit('b'), Some(11)));
40    const _: () = assert!(matches!(parse_ascii_hex_digit('c'), Some(12)));
41    const _: () = assert!(matches!(parse_ascii_hex_digit('d'), Some(13)));
42    const _: () = assert!(matches!(parse_ascii_hex_digit('e'), Some(14)));
43    const _: () = assert!(matches!(parse_ascii_hex_digit('f'), Some(15)));
44    use super::HEX_DIGITS;
45    #[allow(dead_code)]
46    const fn verify_hex_digits() {
47        let mut i = 0;
48        loop {
49            assert!(HEX_DIGITS.len() == 16);
50            let ch = HEX_DIGITS.as_bytes()[i] as char;
51            if let Some(val) = parse_ascii_hex_digit(ch) {
52                assert!(val as usize == i);
53            } else {
54                panic!("could not parse")
55            }
56            assert!(ch.is_ascii_hexdigit());
57            assert!(ch.is_ascii_digit() || ch.is_ascii_lowercase());
58
59            i += 1;
60            if i == HEX_DIGITS.len() {
61                break;
62            }
63        }
64    }
65    const _: () = verify_hex_digits();
66}
67
68#[derive(Clone, Copy, PartialEq, Eq)]
69pub struct OwnedHexStr<const LEN: usize, const HLEN: usize> {
70    hex_str: [u8; HLEN],
71}
72
73impl<const LEN: usize, const HLEN: usize> OwnedHexStr<LEN, HLEN> {
74    const __76560: () = assert!(2 * LEN == HLEN);
75
76    #[expect(clippy::char_lit_as_u8)]
77    pub const fn from_bytes(value: [u8; LEN]) -> Self {
78        let mut hex_str = [0; HLEN];
79
80        let mut i = 0;
81
82        loop {
83            hex_str[i * 2] = match value[i] / 16 {
84                i if i < 10 => '0' as u8 + i,
85                i => 'a' as u8 - 10 + i,
86            };
87            hex_str[i * 2 + 1] = match value[i] % 16 {
88                i if i < 10 => '0' as u8 + i,
89                i => 'a' as u8 - 10 + i,
90            };
91            i += 1;
92            if i == LEN {
93                break;
94            }
95        }
96
97        OwnedHexStr { hex_str }
98    }
99
100    #[expect(clippy::char_lit_as_u8)]
101    #[allow(clippy::wrong_self_convention)]
102    pub const fn into_original_bytes(&self) -> [u8; LEN] {
103        let mut bytes = [0; LEN];
104
105        let mut i = 0;
106
107        loop {
108            let upper: u8 = match self.hex_str[i * 2] {
109                i if i.is_ascii_digit() => i - ('0' as u8),
110                i => 10 + i - ('a' as u8),
111            };
112            let lower: u8 = match self.hex_str[i * 2 + 1] {
113                i if i.is_ascii_digit() => i - ('0' as u8),
114                i => 10 + i - ('a' as u8),
115            };
116            bytes[i] = (upper << 4) + lower;
117            i += 1;
118            if i == LEN {
119                break;
120            }
121        }
122
123        bytes
124    }
125
126    #[allow(dead_code)]
127    pub(crate) const fn from_hex_str(string: &str) -> Self {
128        let string = string.as_bytes();
129        assert!(HLEN == string.len());
130
131        let mut hex_str = [0; HLEN];
132
133        let mut i = 0;
134
135        loop {
136            assert!(string[i].is_ascii_hexdigit());
137            hex_str[i] = string[i];
138            i += 1;
139            if i == HLEN {
140                break;
141            }
142        }
143
144        OwnedHexStr { hex_str }
145    }
146
147    pub const fn as_str(&self) -> &str {
148        #[cfg(debug_assertions)]
149        {
150            assert!(self.hex_str.is_ascii());
151            let mut i = 0;
152            loop {
153                assert!(self.hex_str[i].is_ascii_hexdigit());
154                i += 1;
155                if i == HLEN {
156                    break;
157                }
158            }
159        }
160        unsafe { str::from_utf8_unchecked(&self.hex_str) }
161    }
162
163    #[expect(clippy::needless_lifetimes)]
164    #[allow(unused)]
165    pub fn chars<'a>(&'a self) -> impl DoubleEndedIterator<Item = char> + 'a {
166        self.as_str().chars()
167    }
168}
169
170impl<I: Into<[u8; LEN]>, const LEN: usize, const HLEN: usize> From<I> for OwnedHexStr<LEN, HLEN> {
171    fn from(value: I) -> Self {
172        Self::from_bytes(value.into())
173    }
174}
175
176mod test {
177    use super::OwnedHexStr;
178    use const_format::assertcp_eq;
179
180    macro_rules! owned_hex_str {
181        ($arr:expr) => {{
182            assertcp_eq!(
183                OwnedHexStr::<{ $arr.len() }, { $arr.len() * 2 }>::from_bytes($arr)
184                    .into_original_bytes()[0],
185                $arr[0]
186            );
187            OwnedHexStr::<{ $arr.len() }, { $arr.len() * 2 }>::from_bytes($arr)
188        }};
189    }
190
191    assertcp_eq!(owned_hex_str!([175, 254]).as_str(), "affe");
192    assertcp_eq!(owned_hex_str!([222, 173]).as_str(), "dead");
193    assertcp_eq!(owned_hex_str!([42, 233]).as_str(), "2ae9");
194    assertcp_eq!(owned_hex_str!([0, 255]).as_str(), "00ff");
195    assertcp_eq!(owned_hex_str!([10, 9]).as_str(), "0a09");
196    assertcp_eq!(owned_hex_str!([16, 15]).as_str(), "100f");
197}
198
199impl<const LEN: usize, const HLEN: usize> AsRef<str> for OwnedHexStr<LEN, HLEN> {
200    fn as_ref(&self) -> &str {
201        self.as_str()
202    }
203}
204
205impl<const LEN: usize, const HLEN: usize> Display for OwnedHexStr<LEN, HLEN> {
206    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207        Display::fmt(&self.as_str(), fmt)
208    }
209}
210
211impl<const LEN: usize, const HLEN: usize> Debug for OwnedHexStr<LEN, HLEN> {
212    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        Debug::fmt(self.as_str(), fmt)
214    }
215}
216
217impl<const LEN: usize, const HLEN: usize> Serialize for OwnedHexStr<LEN, HLEN> {
218    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
219    where
220        S: serde::Serializer,
221    {
222        serializer.serialize_str(self.as_str())
223    }
224}
225
226impl<'de, const LEN: usize, const HLEN: usize> Deserialize<'de> for OwnedHexStr<LEN, HLEN> {
227    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
228    where
229        D: serde::Deserializer<'de>,
230    {
231        struct VisitorImpl<const LEN: usize, const HLEN: usize>;
232        #[expect(clippy::needless_lifetimes)]
233        impl<'de, const LEN: usize, const HLEN: usize> Visitor<'de> for VisitorImpl<LEN, HLEN> {
234            type Value = OwnedHexStr<LEN, HLEN>;
235
236            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
237                write!(formatter, "a string containing {} hex digits", HLEN)
238            }
239
240            fn visit_str<E>(self, val: &str) -> Result<Self::Value, E>
241            where
242                E: serde::de::Error,
243            {
244                if val.len() != HLEN {
245                    Err(E::custom(format!(
246                        "Expected length {HLEN} got length {}",
247                        val.len()
248                    )))
249                } else if !val.is_ascii() {
250                    Err(E::custom("Expected ASCII string"))
251                } else if let Some(ch) = val.chars().find(|ch| !ch.is_ascii_hexdigit()) {
252                    Err(E::custom(format!("Expected hex digit, got {ch:?}")))
253                } else {
254                    let mut hex_str = [0; HLEN];
255
256                    for (i, b) in val.bytes().enumerate() {
257                        hex_str[i] = b;
258                    }
259
260                    Ok(OwnedHexStr { hex_str })
261                }
262            }
263        }
264        deserializer.deserialize_string(VisitorImpl::<LEN, HLEN> {})
265    }
266}