1use std::{
10 borrow::Cow,
11 fmt::{self, Write},
12};
13
14#[derive(Copy, Clone, Debug)]
16pub enum DecodeError {
17 BadOutputLength,
18 InvalidCharacter,
19 OddInputLength,
20}
21
22impl std::error::Error for DecodeError {}
23
24impl fmt::Display for DecodeError {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 let s = match self {
27 Self::BadOutputLength =>
28 "output buffer length != half input length",
29 Self::InvalidCharacter => "input contains non-hex character",
30 Self::OddInputLength => "input string length must be even",
31 };
32 write!(f, "hex decode error: {s}")
33 }
34}
35
36pub fn encode(bytes: &[u8]) -> String {
41 let mut out = vec![0u8; bytes.len() * 2];
42
43 for (src, dst) in bytes.iter().zip(out.chunks_exact_mut(2)) {
44 dst[0] = encode_nibble(src >> 4);
45 dst[1] = encode_nibble(src & 0x0f);
46 }
47
48 unsafe { String::from_utf8_unchecked(out) }
50}
51
52pub fn decode(hex: &str) -> Result<Vec<u8>, DecodeError> {
54 let hex_chunks = hex_str_to_chunks(hex)?;
55 let mut out = vec![0u8; hex_chunks.len()];
56 decode_to_slice_inner(hex_chunks, &mut out).map(|()| out)
57}
58
59pub const fn decode_const<const N: usize>(hex: &[u8]) -> [u8; N] {
65 if hex.len() != N * 2 {
66 panic!("hex input is the wrong length");
67 }
68
69 let mut bytes = [0u8; N];
70 let mut idx = 0;
71 let mut err = 0;
72
73 while idx < N {
74 let b_hi = decode_nibble(hex[2 * idx]);
75 let b_lo = decode_nibble(hex[(2 * idx) + 1]);
76 let byte = (b_hi << 4) | b_lo;
77 err |= byte >> 8;
78 bytes[idx] = byte as u8;
79 idx += 1;
80 }
81
82 match err {
83 0 => bytes,
84 _ => panic!("invalid hex char"),
85 }
86}
87
88pub fn decode_to_slice(hex: &str, out: &mut [u8]) -> Result<(), DecodeError> {
91 let hex_chunks = hex_str_to_chunks(hex)?;
92 decode_to_slice_inner(hex_chunks, out)
93}
94
95#[inline]
106pub fn display(bytes: &[u8]) -> HexDisplay<'_> {
107 HexDisplay(bytes)
108}
109
110pub trait FromHex: Sized {
126 fn from_hex(s: &str) -> Result<Self, DecodeError>;
127}
128
129impl FromHex for Vec<u8> {
130 fn from_hex(s: &str) -> Result<Self, DecodeError> {
131 decode(s)
132 }
133}
134
135impl FromHex for Cow<'_, [u8]> {
136 fn from_hex(s: &str) -> Result<Self, DecodeError> {
137 decode(s).map(Cow::Owned)
138 }
139}
140
141impl<const N: usize> FromHex for [u8; N] {
142 fn from_hex(s: &str) -> Result<Self, DecodeError> {
143 let mut out = [0u8; N];
144 decode_to_slice(s, out.as_mut_slice())?;
145 Ok(out)
146 }
147}
148
149pub struct HexDisplay<'a>(&'a [u8]);
154
155impl fmt::Display for HexDisplay<'_> {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 for byte in self.0 {
158 f.write_char(encode_nibble(byte >> 4) as char)?;
159 f.write_char(encode_nibble(byte & 0x0f) as char)?;
160 }
161 Ok(())
162 }
163}
164
165impl fmt::Debug for HexDisplay<'_> {
166 #[inline]
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 write!(f, "\"{self}\"")
169 }
170}
171
172fn hex_str_to_chunks(hex: &str) -> Result<&[[u8; 2]], DecodeError> {
175 let (hex_chunks, extra) = hex.as_bytes().as_chunks::<2>();
176 if extra.is_empty() {
177 Ok(hex_chunks)
178 } else {
179 Err(DecodeError::OddInputLength)
180 }
181}
182
183fn decode_to_slice_inner(
184 hex_chunks: &[[u8; 2]],
185 out: &mut [u8],
186) -> Result<(), DecodeError> {
187 if hex_chunks.len() != out.len() {
188 return Err(DecodeError::BadOutputLength);
189 }
190
191 let mut err = 0;
192 for (&[c_hi, c_lo], out_i) in hex_chunks.iter().zip(out) {
193 let byte = (decode_nibble(c_hi) << 4) | decode_nibble(c_lo);
194 err |= byte >> 8;
195 *out_i = byte as u8;
196 }
197
198 match err {
199 0 => Ok(()),
200 _ => Err(DecodeError::InvalidCharacter),
201 }
202}
203
204#[inline(always)]
208#[allow(non_upper_case_globals)]
209const fn encode_nibble(nib: u8) -> u8 {
210 const b_0: i16 = b'0' as i16;
219 const b_9: i16 = b'9' as i16;
220 const b_a: i16 = b'a' as i16;
221
222 let nib = nib as i16;
223 let base = nib + b_0;
224 let gap_9a = ((b_9 - b_0 - nib) >> 8) & (b_a - b_9 - 1);
230 (base + gap_9a) as u8
231}
232
233#[inline(always)]
235const fn decode_nibble(src: u8) -> u16 {
236 let byte = src as i16;
239 let mut ret: i16 = -1;
240
241 ret += (((0x2fi16 - byte) & (byte - 0x3a)) >> 8) & (byte - 47);
244 ret += (((0x60i16 - byte) & (byte - 0x67)) >> 8) & (byte - 86);
247
248 ret as u16
249}
250
251#[cfg(test)]
252mod test {
253 use proptest::{
254 arbitrary::any, char, collection::vec, prop_assert_eq, proptest,
255 strategy::Strategy,
256 };
257
258 use super::*;
259
260 #[inline]
261 fn is_even(x: usize) -> bool {
262 x & 1 == 0
263 }
264
265 #[test]
266 fn test_encode() {
267 assert_eq!("", encode(&[]));
268 assert_eq!(
269 "01348900abff",
270 encode(&[0x01, 0x34, 0x89, 0x00, 0xab, 0xff])
271 );
272 }
273
274 #[test]
275 fn test_decode_const() {
276 const FOO: [u8; 6] = decode_const(b"01348900abff");
277 assert_eq!(&FOO, &[0x01, 0x34, 0x89, 0x00, 0xab, 0xff]);
278 }
279
280 #[test]
281 fn test_roundtrip_b2s2b() {
282 let bytes = &[0x01, 0x34, 0x89, 0x00, 0xab, 0xff];
283 assert_eq!(bytes.as_slice(), decode(&encode(bytes)).unwrap());
284
285 proptest!(|(bytes in vec(any::<u8>(), 0..10))| {
286 assert_eq!(bytes.as_slice(), decode(&encode(&bytes)).unwrap());
287 })
288 }
289
290 #[test]
291 fn test_roundtrip_s2b2s() {
292 let hex = "01348900abff";
293 assert_eq!(hex, encode(&decode(hex).unwrap()));
294
295 let hex_char = char::ranges(['0'..='9', 'a'..='f'].as_slice().into());
296 let hex_chars = vec(hex_char, 0..10);
297 let hex_strs =
298 hex_chars.prop_filter_map("no odd length hex strings", |chars| {
299 if is_even(chars.len()) {
300 Some(String::from_iter(chars))
301 } else {
302 None
303 }
304 });
305
306 proptest!(|(hex in hex_strs)| {
307 assert_eq!(hex.to_ascii_lowercase(), encode(&decode(&hex).unwrap()));
308 })
309 }
310
311 #[test]
312 fn test_encode_display_equiv() {
313 proptest!(|(bytes: Vec<u8>)| {
314 let out1 = encode(&bytes);
315 let out2 = display(&bytes).to_string();
316 prop_assert_eq!(out1, out2);
317 });
318 }
319}