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}