Skip to main content

faster_hex/
decode.rs

1use crate::error::Error;
2
3#[cfg(target_arch = "aarch64")]
4pub(crate) mod aarch64;
5#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
6pub(crate) mod x86;
7
8const NIL: u8 = u8::MAX;
9
10const fn init_unhex_array(check_case: CheckCase) -> [u8; 256] {
11    let mut arr = [0; 256];
12    let mut i = 0;
13    while i < 256 {
14        arr[i] = match i as u8 {
15            b'0'..=b'9' => i as u8 - b'0',
16            b'a'..=b'f' => match check_case {
17                CheckCase::Lower | CheckCase::None => i as u8 - b'a' + 10,
18                _ => NIL,
19            },
20            b'A'..=b'F' => match check_case {
21                CheckCase::Upper | CheckCase::None => i as u8 - b'A' + 10,
22                _ => NIL,
23            },
24            _ => NIL,
25        };
26        i += 1;
27    }
28    arr
29}
30
31const fn init_unhex4_array(check_case: CheckCase) -> [u8; 256] {
32    let unhex_arr = init_unhex_array(check_case);
33
34    let mut unhex4_arr = [NIL; 256];
35    let mut i = 0;
36    while i < 256 {
37        if unhex_arr[i] != NIL {
38            unhex4_arr[i] = unhex_arr[i] << 4;
39        }
40        i += 1;
41    }
42    unhex4_arr
43}
44
45// ASCII -> hex
46static UNHEX: [u8; 256] = init_unhex_array(CheckCase::None);
47
48// ASCII -> hex, lower case
49static UNHEX_LOWER: [u8; 256] = init_unhex_array(CheckCase::Lower);
50
51// ASCII -> hex, upper case
52static UNHEX_UPPER: [u8; 256] = init_unhex_array(CheckCase::Upper);
53
54// ASCII -> hex << 4
55static UNHEX4: [u8; 256] = init_unhex4_array(CheckCase::None);
56
57/// Returns whether every byte is an ASCII hex digit, accepting either letter case.
58///
59/// Accepts `0` through `9`, `a` through `f`, and `A` through `F`, including mixed
60/// case. Prefixes, whitespace and non-ASCII bytes are rejected. This is a
61/// character-only check: empty and odd-length inputs can pass. It does not
62/// allocate or modify the input.
63///
64/// Use [`hex_decode`] to also require complete byte pairs. There is no need to
65/// call this function before a checked decoder: decoding already validates input.
66///
67/// # Examples
68///
69/// ```
70/// use faster_hex::hex_check;
71///
72/// assert!(hex_check(b"00aBcD"));
73/// assert!(hex_check(b"a")); // Valid characters, but not a complete byte pair.
74/// assert!(hex_check(b""));
75/// assert!(!hex_check(b"0x01"));
76/// assert!(!hex_check(b"00 01"));
77/// ```
78#[inline]
79pub fn hex_check(src: &[u8]) -> bool {
80    hex_check_with_case(src, CheckCase::None)
81}
82
83/// Checks ASCII hex digits against an explicit letter-case policy.
84///
85/// Digits are accepted under every [`CheckCase`] policy. Empty input succeeds;
86/// odd lengths are allowed. Like [`hex_check`], this checks characters only and
87/// neither allocates nor modifies the input.
88///
89/// # Examples
90///
91/// ```
92/// use faster_hex::{hex_check_with_case, CheckCase};
93///
94/// assert!(hex_check_with_case(b"0123", CheckCase::Upper));
95/// assert!(hex_check_with_case(b"ab01", CheckCase::Lower));
96/// assert!(!hex_check_with_case(b"AB01", CheckCase::Lower));
97/// assert!(hex_check_with_case(b"aB01", CheckCase::None));
98/// ```
99#[inline]
100pub fn hex_check_with_case(src: &[u8], check_case: CheckCase) -> bool {
101    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
102    {
103        match crate::vectorization_support() {
104            crate::Vectorization::AVX512 => {
105                // SAFETY: Dispatch checks AVX-512BW and its OS state.
106                unsafe { x86::hex_check_avx512_with_case(src, check_case) }
107            }
108            crate::Vectorization::AVX2 => {
109                // SAFETY: Dispatch guarantees AVX2; the checker bounds every load.
110                unsafe { x86::hex_check_avx2_with_case(src, check_case) }
111            }
112            crate::Vectorization::SSE41 => {
113                // SAFETY: Dispatch checks SSE4.1; the checker bounds every load.
114                unsafe { x86::hex_check_sse_with_case(src, check_case) }
115            }
116            crate::Vectorization::None => hex_check_fallback_with_case(src, check_case),
117        }
118    }
119
120    #[cfg(target_arch = "aarch64")]
121    {
122        match crate::vectorization_support() {
123            crate::Vectorization::Neon => {
124                // SAFETY: Dispatch guarantees NEON; the checker bounds every load.
125                unsafe { aarch64::hex_check_neon_with_case(src, check_case) }
126            }
127            crate::Vectorization::None => hex_check_fallback_with_case(src, check_case),
128        }
129    }
130
131    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")))]
132    hex_check_fallback_with_case(src, check_case)
133}
134
135/// Check if the input is valid hex bytes slice with case check
136pub(crate) fn hex_check_fallback_with_case(src: &[u8], check_case: CheckCase) -> bool {
137    match check_case {
138        CheckCase::None => src.iter().all(|&x| UNHEX[x as usize] != NIL),
139        CheckCase::Lower => src.iter().all(|&x| UNHEX_LOWER[x as usize] != NIL),
140        CheckCase::Upper => src.iter().all(|&x| UNHEX_UPPER[x as usize] != NIL),
141    }
142}
143
144/// Which ASCII letter cases are accepted when checking or decoding hex.
145///
146/// [`None`](Self::None) is the default and accepts mixed case; it does not
147/// disable character validation. All policies accept ASCII digits `0` through
148/// `9` and reject prefixes, whitespace and non-ASCII characters. They do not
149/// change the length rules of the operation using them.
150///
151/// Encoding selects lowercase or uppercase through separate functions, so an
152/// encoding operation never needs this policy.
153///
154/// # Examples
155///
156/// ```
157/// use faster_hex::{hex_decode_with_case, CheckCase};
158///
159/// let mut bytes = [0; 2];
160/// assert_eq!(hex_decode_with_case(b"AB01", &mut bytes, CheckCase::Upper)?,
161///            &[0xab, 1]);
162/// assert!(hex_decode_with_case(b"ab01", &mut bytes, CheckCase::Upper).is_err());
163/// # Ok::<(), faster_hex::Error>(())
164/// ```
165#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
166#[cfg_attr(feature = "defmt-03", derive(defmt::Format))]
167pub enum CheckCase {
168    /// Accept uppercase and lowercase, including a mixture of both.
169    #[default]
170    None,
171    /// Accept digits and lowercase `a` through `f` only.
172    Lower,
173    /// Accept digits and uppercase `A` through `F` only.
174    Upper,
175}
176
177/// Decodes all of `src` into `dst` without allocation, accepting either letter case.
178///
179/// `src` must contain an even number of ASCII hex digits, without a prefix,
180/// whitespace or separators. Uppercase and lowercase digits may be mixed. Leading
181/// zeroes are preserved as bytes; this converts a byte sequence, not an integer.
182///
183/// `dst` must have at least `src.len() / 2` bytes. The returned mutable slice
184/// covers exactly the written prefix and borrows only `dst`. Spare destination
185/// bytes remain unchanged. Empty input returns an empty slice and changes nothing.
186///
187/// Use [`hex_decode_with_case`] to restrict letter case, or [`hex_decode_array`]
188/// when the decoded size must match a fixed array exactly.
189///
190/// # Errors
191///
192/// Errors are checked in this order:
193///
194/// 1. [`Error::OddLength`] if the input has an odd number of bytes.
195/// 2. [`Error::OutputTooSmall`] if the destination is too small. `required` counts
196///    the full decoded output size in bytes.
197/// 3. [`Error::InvalidChar`] for the first invalid input byte. `index` is a
198///    zero-based byte offset in `src`, not a Unicode character position.
199///
200/// Every error leaves the **entire destination unchanged**, including when an
201/// invalid byte occurs after a long valid prefix. A short destination never
202/// causes silent prefix decoding; slice `src` explicitly if that is intended.
203///
204/// # Examples
205///
206/// ```
207/// use faster_hex::hex_decode;
208///
209/// let mut destination = [0xa5; 5];
210/// let bytes = {
211///     let source = *b"00aBcD";
212///     hex_decode(&source, &mut destination)?
213/// }; // The source is no longer needed.
214/// assert_eq!(bytes, &[0, 0xab, 0xcd]);
215/// bytes[0] = 0xff;
216/// assert_eq!(destination, [0xff, 0xab, 0xcd, 0xa5, 0xa5]);
217/// # Ok::<(), faster_hex::Error>(())
218/// ```
219///
220/// Invalid input does not commit a partial result:
221///
222/// ```
223/// use faster_hex::{hex_decode, Error};
224///
225/// let mut destination = [0xa5; 3];
226/// assert!(matches!(hex_decode(b"00ff0g", &mut destination),
227///     Err(Error::InvalidChar { index: 5, byte: b'g', .. })));
228/// assert_eq!(destination, [0xa5; 3]);
229/// ```
230#[inline]
231pub fn hex_decode<'a>(src: &[u8], dst: &'a mut [u8]) -> Result<&'a mut [u8], Error> {
232    hex_decode_with_case(src, dst, CheckCase::None)
233}
234
235/// Decodes all of `src` into `dst` using an explicit letter-case policy.
236///
237/// The size, borrowing and destination-preservation guarantees are identical to
238/// [`hex_decode`]. Digits are accepted under every policy; [`CheckCase::None`]
239/// also accepts mixed-case letters. No allocation is performed.
240///
241/// # Errors
242///
243/// Returns the same errors in the same order as [`hex_decode`]. A letter rejected
244/// by `check_case` is an [`Error::InvalidChar`], with its byte and zero-based
245/// position in `src`. The first invalid byte wins, including when it is a case
246/// violation before another non-hex byte. The entire destination remains unchanged.
247///
248/// # Examples
249///
250/// ```
251/// use faster_hex::{hex_decode_with_case, CheckCase, Error};
252///
253/// let mut bytes = [0; 2];
254/// hex_decode_with_case(b"ab01", &mut bytes, CheckCase::Lower)?;
255/// assert_eq!(bytes, [0xab, 1]);
256/// assert!(matches!(hex_decode_with_case(b"Ab01", &mut bytes, CheckCase::Lower),
257///     Err(Error::InvalidChar { index: 0, byte: b'A', .. })));
258/// assert_eq!(bytes, [0xab, 1]);
259/// # Ok::<(), Error>(())
260/// ```
261#[inline(always)]
262pub fn hex_decode_with_case<'a>(
263    src: &[u8],
264    dst: &'a mut [u8],
265    check_case: CheckCase,
266) -> Result<&'a mut [u8], Error> {
267    if !src.len().is_multiple_of(2) {
268        return Err(Error::OddLength);
269    }
270    let len = src.len() / 2;
271    let dst = dst
272        .get_mut(..len)
273        .ok_or(Error::OutputTooSmall { required: len })?;
274    if decode_checked(src, dst, check_case).is_err() {
275        decode_diagnosed(src, dst, check_case)?;
276    }
277    Ok(dst)
278}
279
280/// Decodes exactly `N` bytes into an array without allocation, accepting either case.
281///
282/// The input grammar is the same as [`hex_decode`], but the decoded length must
283/// equal `N`: both shorter and longer inputs are rejected. The result owns its
284/// bytes independently of the input. Only `N == 0` accepts empty input.
285///
286/// Use [`hex_decode_array_with_case`] for a strict letter-case policy, or
287/// [`hex_decode`] to write into a slice with spare capacity.
288///
289/// # Errors
290///
291/// Checks [`Error::OddLength`] first, [`Error::LengthMismatch`] second, and
292/// [`Error::InvalidChar`] last. The mismatch's `expected` and `actual` fields
293/// both count decoded bytes; character positions count source bytes. Prefixes,
294/// whitespace and separators are rejected as invalid characters.
295///
296/// # Examples
297///
298/// ```
299/// use faster_hex::hex_decode_array;
300/// let bytes = hex_decode_array::<4>(b"0001aBff")?;
301/// assert_eq!(bytes, [0, 1, 0xab, 0xff]);
302/// assert!(hex_decode_array::<4>(b"0001").is_err());
303/// assert_eq!(hex_decode_array::<0>(b"")?, []);
304/// # Ok::<(), faster_hex::Error>(())
305/// ```
306#[inline]
307pub fn hex_decode_array<const N: usize>(src: &[u8]) -> Result<[u8; N], Error> {
308    hex_decode_array_with_case(src, CheckCase::None)
309}
310
311/// Decodes exactly `N` bytes into an array using an explicit letter-case policy.
312///
313/// This has the ownership, exact-length and allocation-free guarantees of
314/// [`hex_decode_array`]. Only `N == 0` accepts empty input.
315///
316/// # Errors
317///
318/// Returns [`Error::OddLength`], [`Error::LengthMismatch`], then
319/// [`Error::InvalidChar`] in that order. A disallowed letter case is an invalid
320/// character. Mismatched lengths count decoded bytes; invalid-character indexes
321/// count source bytes.
322///
323/// # Examples
324///
325/// ```
326/// use faster_hex::{hex_decode_array_with_case, CheckCase};
327///
328/// let id = hex_decode_array_with_case::<2>(b"AB01", CheckCase::Upper)?;
329/// assert_eq!(id, [0xab, 1]);
330/// assert!(hex_decode_array_with_case::<2>(b"ab01", CheckCase::Upper).is_err());
331/// # Ok::<(), faster_hex::Error>(())
332/// ```
333#[inline]
334pub fn hex_decode_array_with_case<const N: usize>(
335    src: &[u8],
336    check_case: CheckCase,
337) -> Result<[u8; N], Error> {
338    if !src.len().is_multiple_of(2) {
339        return Err(Error::OddLength);
340    }
341    let actual = src.len() / 2;
342    if actual != N {
343        return Err(Error::LengthMismatch {
344            expected: N,
345            actual,
346        });
347    }
348    let mut bytes = [0; N];
349    #[cfg(target_arch = "aarch64")]
350    if N == 4 && crate::vectorization_support() == crate::Vectorization::Neon {
351        // A fixed short array can inline the existing NEON block by padding
352        // with valid digits, without reading beyond the source slice.
353        let mut input = [b'0'; 16];
354        input[..src.len()].copy_from_slice(src);
355        let mut output = [0; 8];
356        hex_decode_with_case(&input, &mut output, check_case)?;
357        bytes.copy_from_slice(&output[..N]);
358        return Ok(bytes);
359    }
360    if N > OWNED_DECODE_THRESHOLD {
361        decode_owned_large(src, &mut bytes, check_case)?;
362    } else {
363        hex_decode_with_case(src, &mut bytes, check_case)?;
364    }
365    Ok(bytes)
366}
367
368/// Decodes the complete input into a new vector, accepting either letter case.
369///
370/// Available with `alloc`. The result owns exactly `src.len() / 2` decoded bytes,
371/// independently of the input. Empty input produces an empty vector. The same
372/// strict ASCII grammar as [`hex_decode`] applies: no prefixes, whitespace or
373/// separators. Use [`hex_decode_vec_with_case`] to restrict letter case.
374///
375/// # Errors
376///
377/// Returns [`Error::OddLength`] for odd input, before allocating output. Even
378/// input is checked for [`Error::InvalidChar`] after allocation, so malformed
379/// even-length input can allocate before its first invalid byte is reported.
380///
381/// There is no input-size limit or recoverable allocation-error result. The
382/// vector uses the allocator's normal error handling. Apply an application limit
383/// before calling when necessary, or use [`hex_decode`] with reusable storage.
384///
385/// # Examples
386///
387/// ```
388/// assert_eq!(faster_hex::hex_decode_vec(b"00aBff")?, [0, 0xab, 0xff]);
389/// assert!(faster_hex::hex_decode_vec(b"0x01").is_err());
390/// # Ok::<(), faster_hex::Error>(())
391/// ```
392#[cfg(feature = "alloc")]
393#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
394#[inline]
395pub fn hex_decode_vec(src: &[u8]) -> Result<alloc::vec::Vec<u8>, Error> {
396    hex_decode_vec_with_case(src, CheckCase::None)
397}
398
399/// Decodes the complete input into a new vector using a letter-case policy.
400///
401/// Available with `alloc`. Ownership, grammar and allocation behavior are the
402/// same as [`hex_decode_vec`]. Digits are accepted under every policy.
403///
404/// # Errors
405///
406/// Returns [`Error::OddLength`] before allocation, then [`Error::InvalidChar`]
407/// for the first invalid byte or disallowed letter. As with [`hex_decode_vec`],
408/// allocation failure is not returned as a codec error and no size limit is imposed.
409///
410/// # Examples
411///
412/// ```
413/// use faster_hex::{hex_decode_vec_with_case, CheckCase};
414///
415/// assert_eq!(hex_decode_vec_with_case(b"AB01", CheckCase::Upper)?, [0xab, 1]);
416/// assert!(hex_decode_vec_with_case(b"ab01", CheckCase::Upper).is_err());
417/// # Ok::<(), faster_hex::Error>(())
418/// ```
419#[cfg(feature = "alloc")]
420#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
421#[inline]
422pub fn hex_decode_vec_with_case(
423    src: &[u8],
424    check_case: CheckCase,
425) -> Result<alloc::vec::Vec<u8>, Error> {
426    if !src.len().is_multiple_of(2) {
427        return Err(Error::OddLength);
428    }
429    let mut bytes = alloc::vec![0; src.len() / 2];
430    if bytes.len() > OWNED_DECODE_THRESHOLD {
431        decode_owned_large(src, &mut bytes, check_case)?;
432    } else {
433        hex_decode_with_case(src, &mut bytes, check_case)?;
434    }
435    Ok(bytes)
436}
437
438// Small outputs are sensitive to streaming setup and call-site inlining.
439const OWNED_DECODE_THRESHOLD: usize = 1024;
440
441// Owned callers discard this initialized, exact-size buffer on error. Large
442// inputs can commit one checked block at a time without re-reading valid input.
443// Inline dispatch to preserve array return/copy code generation. The SIMD block
444// loops remain in their target-feature functions.
445#[inline(always)]
446fn decode_owned_large(src: &[u8], dst: &mut [u8], case: CheckCase) -> Result<(), Error> {
447    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
448    let decoded = match crate::vectorization_support() {
449        crate::Vectorization::AVX512 => {
450            // SAFETY: Dispatch establishes AVX-512BW and OS support; the slices
451            // have the exact 2:1 ratio, and output is private until success.
452            unsafe { x86::hex_decode_avx512_owned(src, dst, case) }
453        }
454        crate::Vectorization::AVX2 => {
455            // SAFETY: Dispatch establishes AVX2 and the same slice invariant.
456            unsafe { x86::hex_decode_avx2_owned(src, dst, case) }
457        }
458        _ => decode_checked(src, dst, case),
459    };
460    #[cfg(target_arch = "aarch64")]
461    let decoded = if crate::vectorization_support() == crate::Vectorization::Neon {
462        // SAFETY: NEON is available and the slices have the exact 2:1 ratio.
463        unsafe { aarch64::hex_decode_neon_owned(src, dst, case) }
464    } else {
465        decode_checked(src, dst, case)
466    };
467    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")))]
468    let decoded = decode_checked(src, dst, case);
469    if decoded.is_err() {
470        decode_diagnosed(src, dst, case)?;
471    }
472    Ok(())
473}
474
475// Call after establishing even input and the exact 2:1 source/output ratio.
476// Every path validates the complete input before writing; diagnostics stay at
477// the public boundary. Tests use the same operation after asserting its lengths.
478// Inline the boundary and dispatch together so short decodes can keep their
479// result and SIMD constants in the caller instead of a separate stack frame.
480#[inline(always)]
481pub(crate) fn decode_checked(src: &[u8], dst: &mut [u8], check_case: CheckCase) -> Result<(), ()> {
482    if dst.len() < 8 {
483        return hex_decode_short_scalar(src, dst, check_case);
484    }
485    #[cfg(target_arch = "aarch64")]
486    let len = dst.len();
487    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
488    {
489        match crate::vectorization_support() {
490            crate::Vectorization::AVX512 => {
491                // SAFETY: Dispatch checked AVX2 and AVX-512BW with its OS state;
492                // dst has exactly src.len() / 2 bytes.
493                unsafe { x86::hex_decode_avx512_checked(src, dst, check_case) }
494            }
495            crate::Vectorization::AVX2 => {
496                // SAFETY: AVX2 is available and the slices have the exact 2:1 ratio.
497                unsafe { x86::hex_decode_avx2_checked(src, dst, check_case) }
498            }
499            crate::Vectorization::SSE41 => {
500                // SAFETY: SSE4.1 is available and the slice lengths have the exact ratio.
501                unsafe { x86::hex_decode_sse41_checked(src, dst, check_case) }
502            }
503            crate::Vectorization::None => {
504                if !hex_check_fallback_with_case(src, check_case) {
505                    return Err(());
506                }
507                hex_decode_fallback(src, dst);
508                Ok(())
509            }
510        }
511    }
512
513    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
514    {
515        #[cfg(target_arch = "aarch64")]
516        if (17..=32).contains(&len) && crate::vectorization_support() == crate::Vectorization::Neon
517        {
518            // SAFETY: NEON is available; src has 34..=64 bytes and dst is its exact half.
519            return unsafe { aarch64::hex_decode_bounded_neon(src, dst, check_case) };
520        }
521
522        #[cfg(target_arch = "aarch64")]
523        if (8..=16).contains(&len) && crate::vectorization_support() == crate::Vectorization::Neon {
524            // SAFETY: Each complete 16-byte load and 8-byte store stays in its slice.
525            return unsafe { aarch64::hex_decode_short_neon(src, dst, check_case) };
526        }
527        if !hex_check_with_case(src, check_case) {
528            return Err(());
529        }
530        hex_decode_unchecked(src, dst);
531        Ok(())
532    }
533}
534
535// Fewer than eight output bytes fit in one word. Decode each pair once
536// and commit only after the complete input has passed validation.
537#[inline]
538pub(crate) fn hex_decode_short_scalar(
539    src: &[u8],
540    dst: &mut [u8],
541    case: CheckCase,
542) -> Result<(), ()> {
543    let table = match case {
544        CheckCase::None => &UNHEX,
545        CheckCase::Lower => &UNHEX_LOWER,
546        CheckCase::Upper => &UNHEX_UPPER,
547    };
548    let mut decoded = 0u64;
549    for pair in src.as_chunks::<2>().0 {
550        let high = table[usize::from(pair[0])];
551        let low = table[usize::from(pair[1])];
552        if high | low == NIL {
553            return Err(());
554        }
555        decoded = decoded << 8 | u64::from(high << 4 | low);
556    }
557    // The word is built in input order, so its low byte belongs in the last slot.
558    for slot in dst.iter_mut().rev() {
559        *slot = decoded as u8;
560        decoded >>= 8;
561    }
562    Ok(())
563}
564
565// Backends report validity; diagnostics belong to the public boundary. The
566// successful SIMD path does not track byte positions or construct an error.
567#[cold]
568fn decode_diagnosed(src: &[u8], dst: &mut [u8], case: CheckCase) -> Result<(), Error> {
569    let table = match case {
570        CheckCase::None => &UNHEX,
571        CheckCase::Lower => &UNHEX_LOWER,
572        CheckCase::Upper => &UNHEX_UPPER,
573    };
574    // Skip valid blocks so locating a late error retains SIMD throughput.
575    // Short inputs go directly to the byte scan.
576    let skipped = if src.len() > 64 {
577        src.chunks_exact(64)
578            .take_while(|chunk| hex_check_with_case(chunk, case))
579            .count()
580            * 64
581    } else {
582        0
583    };
584    for (offset, &byte) in src[skipped..].iter().enumerate() {
585        let index = skipped + offset;
586        if table[usize::from(byte)] == NIL {
587            return Err(Error::InvalidChar { index, byte });
588        }
589    }
590    // Every byte was validated. A scalar conversion also handles a backend
591    // rejecting valid input; never return an unwritten output prefix.
592    hex_decode_fallback(src, dst);
593    Ok(())
594}
595
596// Internal conversion only. Even if miscalled with short input, never pass an
597// undersized source to a SIMD kernel. Public callers must use checked decode.
598#[cfg(any(test, not(any(target_arch = "x86", target_arch = "x86_64"))))]
599pub(crate) fn hex_decode_unchecked(src: &[u8], dst: &mut [u8]) {
600    let len = core::cmp::min(src.len() / 2, dst.len());
601    let src = &src[..len * 2];
602    let dst = &mut dst[..len];
603    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
604    {
605        match crate::vectorization_support() {
606            crate::Vectorization::AVX512 => {
607                // SAFETY: Dispatch checks AVX-512BW; the slices have a 2:1 length ratio.
608                unsafe { x86::hex_decode_avx512(src, dst) }
609            }
610            crate::Vectorization::AVX2 => {
611                // SAFETY: Dispatch guarantees AVX2 and the slices have a 2:1 length ratio.
612                unsafe { x86::hex_decode_avx2(src, dst) }
613            }
614            crate::Vectorization::SSE41 => {
615                // SAFETY: Dispatch guarantees SSE4.1 and the slices have a 2:1 length ratio.
616                unsafe { x86::hex_decode_sse41(src, dst) }
617            }
618            crate::Vectorization::None => hex_decode_fallback(src, dst),
619        }
620    }
621    #[cfg(target_arch = "aarch64")]
622    match crate::vectorization_support() {
623        crate::Vectorization::Neon => {
624            // SAFETY: Dispatch guarantees NEON; both slices have the exact 2:1 ratio.
625            unsafe { aarch64::hex_decode_neon(src, dst) }
626        }
627        crate::Vectorization::None => hex_decode_fallback(src, dst),
628    }
629    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")))]
630    hex_decode_fallback(src, dst);
631}
632
633#[inline]
634pub(crate) fn hex_decode_fallback(src: &[u8], dst: &mut [u8]) {
635    for (slot, bytes) in dst.iter_mut().zip(src.chunks_exact(2)) {
636        let a = UNHEX4[bytes[0] as usize];
637        let b = UNHEX[bytes[1] as usize];
638        *slot = a | b;
639    }
640}