1use thiserror::Error;
19
20pub(crate) const ENCODE_PAIR: [[u8; 2]; 256] = {
21 const NIBBLE: [u8; 16] = *b"0123456789abcdef";
22 let mut table = [[0u8; 2]; 256];
23 let mut i = 0u16;
24 while i < 256 {
25 table[i as usize] = [NIBBLE[(i >> 4) as usize], NIBBLE[(i & 0x0f) as usize]];
26 i += 1;
27 }
28 table
29};
30
31pub(crate) const DECODE_NIBBLE: [u8; 256] = {
33 let mut table = [0xFFu8; 256];
34 let mut i = 0u8;
35 while i < 10 {
36 table[(b'0' + i) as usize] = i;
37 i += 1;
38 }
39 i = 0;
40 while i < 6 {
41 table[(b'a' + i) as usize] = 10 + i;
42 table[(b'A' + i) as usize] = 10 + i;
43 i += 1;
44 }
45 table
46};
47
48#[must_use]
55pub fn encode(data: impl AsRef<[u8]>) -> String {
56 let bytes = data.as_ref();
57 let buf = bytes
58 .iter()
59 .flat_map(|&byte| ENCODE_PAIR[byte as usize])
60 .collect();
61 String::from_utf8(buf).expect("hex pairs are ASCII")
62}
63
64#[must_use]
71pub fn encode_prefixed(data: impl AsRef<[u8]>) -> String {
72 let bytes = data.as_ref();
73 let buf = b"0x"
74 .iter()
75 .copied()
76 .chain(bytes.iter().flat_map(|&byte| ENCODE_PAIR[byte as usize]))
77 .collect();
78 String::from_utf8(buf).expect("hex pairs are ASCII")
79}
80
81pub fn decode(data: impl AsRef<[u8]>) -> Result<Vec<u8>, DecodeError> {
87 let hex = data.as_ref();
88 if hex.len() % 2 != 0 {
89 return Err(DecodeError::OddLength);
90 }
91
92 if hex.is_empty() {
93 return Ok(Vec::new());
94 }
95
96 if hex.len() < 32 {
97 let mut out = Vec::with_capacity(hex.len() / 2);
98 decode_pairs_into(hex, &mut out)?;
99 return Ok(out);
100 }
101 decode_packed(hex)
102}
103
104fn decode_packed(hex: &[u8]) -> Result<Vec<u8>, DecodeError> {
105 let first = decode_4_bytes(
107 hex[..8]
108 .try_into()
109 .expect("packed hex has at least eight characters"),
110 )?;
111 let mut out = vec![0; hex.len() / 2];
112 out[..4].copy_from_slice(&first);
113 decode_into(&hex[8..], &mut out[4..])?;
114 Ok(out)
115}
116
117fn decode_pairs_into(hex: &[u8], out: &mut Vec<u8>) -> Result<(), DecodeError> {
118 for pair in hex.as_chunks::<2>().0 {
119 let hi = DECODE_NIBBLE[pair[0] as usize];
120 let lo = DECODE_NIBBLE[pair[1] as usize];
121 if (hi | lo) & 0xF0 != 0 {
122 return Err(if hi == 0xFF {
123 DecodeError::InvalidChar(pair[0])
124 } else {
125 DecodeError::InvalidChar(pair[1])
126 });
127 }
128 out.push((hi << 4) | lo);
129 }
130 Ok(())
131}
132
133pub fn decode_array<const N: usize>(data: impl AsRef<[u8]>) -> Result<[u8; N], DecodeError> {
139 let hex = data.as_ref();
140 if hex.len() != N * 2 {
141 return Err(DecodeError::LengthMismatch {
142 expected: N * 2,
143 actual: hex.len(),
144 });
145 }
146 let mut out = [0u8; N];
147 decode_into(hex, &mut out)?;
148 Ok(out)
149}
150
151#[inline]
153fn decode_into(hex: &[u8], out: &mut [u8]) -> Result<(), DecodeError> {
154 debug_assert_eq!(
155 hex.len(),
156 out.len() * 2,
157 "Invariant: hex has two characters per output byte"
158 );
159 let (chunks, tail) = hex.as_chunks::<8>();
160 let (out_chunks, out_tail) = out.as_chunks_mut::<4>();
161 for (decoded, chunk) in out_chunks.iter_mut().zip(chunks) {
162 *decoded = decode_4_bytes(*chunk)?;
163 }
164
165 for (decoded, pair) in out_tail.iter_mut().zip(tail.as_chunks::<2>().0) {
166 let hi = DECODE_NIBBLE[pair[0] as usize];
167 let lo = DECODE_NIBBLE[pair[1] as usize];
168 if (hi | lo) & 0xF0 != 0 {
169 return Err(if hi == 0xFF {
170 DecodeError::InvalidChar(pair[0])
171 } else {
172 DecodeError::InvalidChar(pair[1])
173 });
174 }
175 *decoded = (hi << 4) | lo;
176 }
177 Ok(())
178}
179
180#[inline(always)]
181fn decode_4_bytes(input: [u8; 8]) -> Result<[u8; 4], DecodeError> {
182 const HIGH_BITS: u64 = 0x8080_8080_8080_8080;
183 let word = u64::from_le_bytes(input);
184 let ascii = word & !HIGH_BITS;
185 let lowercase = ascii | 0x2020_2020_2020_2020;
186 let digits_low = (ascii | HIGH_BITS) - 0x3030_3030_3030_3030;
188 let digits_high = 0xb9b9_b9b9_b9b9_b9b9 - ascii;
189 let letters_low = (lowercase | HIGH_BITS) - 0x6161_6161_6161_6161;
190 let letters_high = 0xe6e6_e6e6_e6e6_e6e6 - lowercase;
191 let valid = ((digits_low & digits_high) | (letters_low & letters_high)) & HIGH_BITS;
192 let invalid = (!valid | word) & HIGH_BITS;
193 if invalid != 0 {
194 let index = (invalid.trailing_zeros() / 8) as usize;
196 return Err(DecodeError::InvalidChar(input[index]));
197 }
198
199 let nibbles = (word & 0x0f0f_0f0f_0f0f_0f0f) + ((word >> 6) & 0x0303_0303_0303_0303) * 9;
201 let pairs = ((nibbles & 0x000f_000f_000f_000f) << 4) | ((nibbles >> 8) & 0x000f_000f_000f_000f);
202 let bytes = pairs.to_le_bytes();
203 Ok([bytes[0], bytes[2], bytes[4], bytes[6]])
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Error)]
208pub enum DecodeError {
209 #[error("odd number of hex characters")]
211 OddLength,
212 #[error("invalid hex character: {0:#04x}")]
214 InvalidChar(u8),
215 #[error("expected {expected} hex characters, was {actual}")]
217 LengthMismatch {
218 expected: usize,
220 actual: usize,
222 },
223}
224
225#[cfg(test)]
226mod tests {
227 use std::fmt::Write;
228
229 use proptest::prelude::*;
230 use rstest::rstest;
231
232 use super::*;
233 use crate::hex;
234
235 #[rstest]
236 #[case(b"", "")]
237 #[case(b"\x00", "00")]
238 #[case(b"\xff", "ff")]
239 #[case(b"\xde\xad\xbe\xef", "deadbeef")]
240 #[case(b"hello", "68656c6c6f")]
241 fn test_encode(#[case] input: &[u8], #[case] expected: &str) {
242 assert_eq!(encode(input), expected);
243 }
244
245 #[rstest]
246 #[case("", b"")]
247 #[case("00", b"\x00")]
248 #[case("ff", b"\xff")]
249 #[case("FF", b"\xff")]
250 #[case("deadBEEF", b"\xde\xad\xbe\xef")]
251 #[case("68656c6c6f", b"hello")]
252 fn test_decode(#[case] input: &str, #[case] expected: &[u8]) {
253 assert_eq!(decode(input).unwrap(), expected);
254 }
255
256 #[rstest]
257 fn test_decode_odd_length() {
258 assert_eq!(decode("abc"), Err(DecodeError::OddLength));
259 }
260
261 #[rstest]
262 #[case("zz", DecodeError::InvalidChar(b'z'))]
263 #[case("z0", DecodeError::InvalidChar(b'z'))]
264 #[case("0z", DecodeError::InvalidChar(b'z'))]
265 fn test_decode_invalid_char(#[case] input: &str, #[case] expected: DecodeError) {
266 assert_eq!(decode(input), Err(expected));
267 }
268
269 #[rstest]
270 #[case(b"", "0x")]
271 #[case(b"\xde\xad", "0xdead")]
272 #[case(b"hello", "0x68656c6c6f")]
273 fn test_encode_prefixed(#[case] input: &[u8], #[case] expected: &str) {
274 assert_eq!(encode_prefixed(input), expected);
275 }
276
277 #[rstest]
278 fn test_decode_array() {
279 let result: [u8; 4] = decode_array("deadbeef").unwrap();
280 assert_eq!(result, [0xde, 0xad, 0xbe, 0xef]);
281 }
282
283 #[rstest]
284 fn test_decode_array_invalid_char() {
285 assert_eq!(
286 decode_array::<2>("xxff"),
287 Err(DecodeError::InvalidChar(b'x'))
288 );
289 }
290
291 #[rstest]
292 fn test_decode_array_length_mismatch() {
293 let result = decode_array::<4>("aabb");
294 assert_eq!(
295 result,
296 Err(DecodeError::LengthMismatch {
297 expected: 8,
298 actual: 4
299 })
300 );
301 }
302
303 #[rstest]
304 #[case(DecodeError::OddLength, "odd number of hex characters")]
305 #[case(DecodeError::InvalidChar(b'z'), "invalid hex character: 0x7a")]
306 #[case(
307 DecodeError::LengthMismatch { expected: 8, actual: 4 },
308 "expected 8 hex characters, was 4"
309 )]
310 fn test_decode_error_display(#[case] error: DecodeError, #[case] expected: &str) {
311 assert_eq!(error.to_string(), expected);
312 }
313
314 #[rstest]
315 fn test_roundtrip() {
316 let data = b"The quick brown fox jumps over the lazy dog";
317 assert_eq!(decode(encode(data)).unwrap(), data);
318 }
319
320 fn decode_oracle(input: &[u8]) -> Result<Vec<u8>, DecodeError> {
321 if !input.len().is_multiple_of(2) {
322 return Err(DecodeError::OddLength);
323 }
324 input
325 .as_chunks::<2>()
326 .0
327 .iter()
328 .map(|pair| {
329 let hi = char::from(pair[0])
330 .to_digit(16)
331 .ok_or(DecodeError::InvalidChar(pair[0]))?;
332 let lo = char::from(pair[1])
333 .to_digit(16)
334 .ok_or(DecodeError::InvalidChar(pair[1]))?;
335 Ok(u8::try_from((hi << 4) | lo).unwrap())
336 })
337 .collect()
338 }
339
340 #[rstest]
341 fn test_decode_checks_every_byte_at_each_packed_position() {
342 for position in 0..8 {
343 for byte in 0..=u8::MAX {
344 let mut input = *b"01aBcDeF";
345 input[position] = byte;
346 let expected = decode_oracle(&input);
347 assert_eq!(hex::decode(input), expected);
348 assert_eq!(hex::decode_array::<4>(input).map(Vec::from), expected);
349 }
350 }
351 }
352
353 #[rstest]
354 fn test_decode_checks_every_byte_across_wide_chunks() {
355 for position in 0..48 {
356 for byte in 0..=u8::MAX {
357 let mut input = *b"01aBcDeF01aBcDeF01aBcDeF01aBcDeF01aBcDeF01aBcDeF";
358 input[position] = byte;
359 let expected = decode_oracle(&input);
360 assert_eq!(hex::decode(input), expected);
361 assert_eq!(hex::decode_array::<24>(input).map(Vec::from), expected);
362 }
363 }
364 }
365
366 #[rstest]
367 fn test_decode_reports_first_invalid_byte_across_wide_chunks_and_tail() {
368 for position in 0..50 {
369 let mut input = *b"01aBcDeF01aBcDeF01aBcDeF01aBcDeF01aBcDeF01aBcDeF01";
370 input[position] = 0xff;
371 if position + 1 < input.len() {
372 input[position + 1] = b'z';
373 }
374 assert_eq!(hex::decode(input), Err(DecodeError::InvalidChar(0xff)));
375 assert_eq!(
376 hex::decode_array::<25>(input),
377 Err(DecodeError::InvalidChar(0xff))
378 );
379 }
380 }
381
382 #[rstest]
383 fn test_decode_reports_first_invalid_byte_after_complete_chunks() {
384 for position in 0..24 {
385 let mut input = *b"01aBcDeF01aBcDeF01aBcDeF";
386 input[position] = 0xff;
387 if position + 1 < input.len() {
388 input[position + 1] = b'z';
389 }
390 assert_eq!(hex::decode(input), Err(DecodeError::InvalidChar(0xff)));
391 assert_eq!(
392 hex::decode_array::<12>(input),
393 Err(DecodeError::InvalidChar(0xff))
394 );
395 }
396 }
397
398 #[rstest]
399 fn test_decode_length_errors_precede_invalid_characters() {
400 assert_eq!(hex::decode([0xff; 9]), Err(DecodeError::OddLength));
401 assert_eq!(
402 hex::decode_array::<4>([0xff; 9]),
403 Err(DecodeError::LengthMismatch {
404 expected: 8,
405 actual: 9
406 })
407 );
408 }
409
410 fn assert_array_roundtrip<const N: usize>() {
411 let bytes = std::array::from_fn::<_, N, _>(|i| u8::try_from(i * 7).unwrap());
412 let text: String = format_hex_reference(&bytes).to_ascii_uppercase();
413 assert_eq!(hex::decode_array::<N>(&text), Ok(bytes));
414 assert_eq!(hex::decode(&text), Ok(bytes.to_vec()));
415 }
416
417 #[rstest]
418 fn test_decode_array_handles_empty_short_and_partial_chunks() {
419 assert_array_roundtrip::<0>();
420 assert_array_roundtrip::<1>();
421 assert_array_roundtrip::<2>();
422 assert_array_roundtrip::<3>();
423 assert_array_roundtrip::<4>();
424 assert_array_roundtrip::<5>();
425 assert_array_roundtrip::<6>();
426 assert_array_roundtrip::<7>();
427 assert_array_roundtrip::<8>();
428 assert_array_roundtrip::<9>();
429 assert_array_roundtrip::<32>();
430 }
431
432 proptest! {
433 #[rstest]
434 fn prop_decode_preserves_values_and_error_priority(input in prop::collection::vec(any::<u8>(), 0..256)) {
435 prop_assert_eq!(hex::decode(&input), decode_oracle(&input));
436 }
437
438 #[rstest]
439 fn prop_decode_matches_numeric_formatting(bytes in prop::collection::vec(any::<u8>(), 0..1024)) {
440 let text: String = format_hex_reference(&bytes).to_ascii_uppercase();
441 prop_assert_eq!(hex::decode(&text), Ok(bytes));
442 }
443 }
444
445 #[rstest]
446 fn test_hex_covers_every_byte() {
447 let bytes: Vec<u8> = (0..=u8::MAX).collect();
448 let expected: String = format_hex_reference(&bytes);
449 assert_eq!(hex::encode(&bytes), expected);
450 assert_eq!(hex::encode_prefixed(&bytes), format!("0x{expected}"));
451 assert_eq!(hex::decode(expected.to_uppercase()).unwrap(), bytes);
452 }
453
454 proptest! {
455 #[rstest]
456 fn prop_hex_matches_numeric_formatting(bytes in prop::collection::vec(any::<u8>(), 0..1024)) {
457 let expected: String = format_hex_reference(&bytes);
458 prop_assert_eq!(hex::encode(&bytes), expected);
459 }
460 }
461
462 fn format_hex_reference(bytes: &[u8]) -> String {
463 let mut text = String::with_capacity(bytes.len() * 2);
464
465 for byte in bytes {
466 write!(text, "{byte:02x}").unwrap();
467 }
468 text
469 }
470}