Skip to main content

vector_core/simd/
hex.rs

1//! SIMD-accelerated hex encoding and decoding
2//!
3//! # Performance
4//!
5//! **Encoding (32 bytes → 64 hex chars):**
6//! - `format!()`: ~1630 ns
7//! - Scalar LUT: ~35 ns (47x faster)
8//! - NEON SIMD (ARM64): ~26 ns (62x faster)
9//! - SSE2 SIMD (x86_64): ~30 ns (estimated)
10//! - AVX2 SIMD (x86_64): ~25 ns (estimated)
11//!
12//! **Decoding (64 hex chars → 32 bytes):**
13//! - NEON SIMD (ARM64): ~3 ns (7x faster than LUT)
14//! - SSE2 SIMD (x86_64): ~5 ns (estimated)
15//! - Scalar LUT fallback: ~19 ns
16//!
17//! # Algorithm
18//!
19//! **NEON (ARM64):** Uses TBL instruction for 16-byte lookup table
20//!
21//! **SSE2/AVX2 (x86_64):** Uses arithmetic approach:
22//! 1. Split bytes into nibbles (high = byte >> 4, low = byte & 0x0F)
23//! 2. Compare nibbles > 9 to identify hex letters (a-f)
24//! 3. Add '0' (0x30) to all, then add 0x27 for letters (a-f)
25//! 4. Interleave and store
26
27#[cfg(target_arch = "aarch64")]
28use std::arch::aarch64::*;
29
30#[cfg(target_arch = "x86_64")]
31use std::arch::x86_64::*;
32
33// ============================================================================
34// Lookup Tables
35// ============================================================================
36
37/// Nibble-to-hex lookup table for NEON SIMD (16 bytes fits in one register).
38#[cfg(target_arch = "aarch64")]
39const HEX_NIBBLE: &[u8; 16] = b"0123456789abcdef";
40
41/// Lookup table for scalar hex encoding (non-SIMD platforms).
42/// Each byte maps to its 2-char hex representation.
43#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
44const HEX_ENCODE_LUT: &[u8; 512] = b"000102030405060708090a0b0c0d0e0f\
45101112131415161718191a1b1c1d1e1f\
46202122232425262728292a2b2c2d2e2f\
47303132333435363738393a3b3c3d3e3f\
48404142434445464748494a4b4c4d4e4f\
49505152535455565758595a5b5c5d5e5f\
50606162636465666768696a6b6c6d6e6f\
51707172737475767778797a7b7c7d7e7f\
52808182838485868788898a8b8c8d8e8f\
53909192939495969798999a9b9c9d9e9f\
54a0a1a2a3a4a5a6a7a8a9aaabacadaeaf\
55b0b1b2b3b4b5b6b7b8b9babbbcbdbebf\
56c0c1c2c3c4c5c6c7c8c9cacbcccdcecf\
57d0d1d2d3d4d5d6d7d8d9dadbdcdddedf\
58e0e1e2e3e4e5e6e7e8e9eaebecedeeef\
59f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff";
60
61/// Compile-time lookup table for hex character to nibble conversion.
62/// Maps ASCII byte values to their nibble value (0-15), invalid chars map to 0.
63const HEX_DECODE_LUT: [u8; 256] = {
64    let mut table = [0u8; 256];
65    let mut i = 0;
66    while i < 256 {
67        table[i] = match i as u8 {
68            b'0'..=b'9' => (i as u8) - b'0',
69            b'a'..=b'f' => (i as u8) - b'a' + 10,
70            b'A'..=b'F' => (i as u8) - b'A' + 10,
71            _ => 0,
72        };
73        i += 1;
74    }
75    table
76};
77
78// ============================================================================
79// Hex Encoding - NEON (ARM64)
80// ============================================================================
81
82/// Convert 32-byte array to hex string using NEON SIMD (ARM64).
83///
84/// # Performance
85/// - ~26 ns total (including String allocation)
86/// - Zero-copy: writes directly into String buffer
87/// - 62x faster than format!()
88#[cfg(target_arch = "aarch64")]
89#[inline]
90pub fn bytes_to_hex_32(bytes: &[u8; 32]) -> String {
91    unsafe {
92        // Allocate String directly - no intermediate buffer, no copy
93        let mut s = String::with_capacity(64);
94        let buf = s.as_mut_vec().as_mut_ptr();
95        let hex_lut = vld1q_u8(HEX_NIBBLE.as_ptr());
96
97        for chunk_idx in 0..2 {
98            let offset = chunk_idx * 16;
99            let out_offset = chunk_idx * 32;
100
101            let input = vld1q_u8(bytes.as_ptr().add(offset));
102            let hi_nibbles = vshrq_n_u8(input, 4);
103            let lo_nibbles = vandq_u8(input, vdupq_n_u8(0x0f));
104            let hi_hex = vqtbl1q_u8(hex_lut, hi_nibbles);
105            let lo_hex = vqtbl1q_u8(hex_lut, lo_nibbles);
106            let result_lo = vzip1q_u8(hi_hex, lo_hex);
107            let result_hi = vzip2q_u8(hi_hex, lo_hex);
108
109            vst1q_u8(buf.add(out_offset), result_lo);
110            vst1q_u8(buf.add(out_offset + 16), result_hi);
111        }
112
113        // SAFETY: We wrote exactly 64 ASCII hex chars (0-9, a-f)
114        s.as_mut_vec().set_len(64);
115        s
116    }
117}
118
119/// Convert 16-byte array to hex string using NEON SIMD (ARM64).
120/// Zero-copy: writes directly into String buffer.
121#[cfg(target_arch = "aarch64")]
122#[inline]
123pub fn bytes_to_hex_16(bytes: &[u8; 16]) -> String {
124    unsafe {
125        // Allocate String directly - no intermediate buffer, no copy
126        let mut s = String::with_capacity(32);
127        let buf = s.as_mut_vec().as_mut_ptr();
128        let hex_lut = vld1q_u8(HEX_NIBBLE.as_ptr());
129        let input = vld1q_u8(bytes.as_ptr());
130
131        let hi_nibbles = vshrq_n_u8(input, 4);
132        let lo_nibbles = vandq_u8(input, vdupq_n_u8(0x0f));
133        let hi_hex = vqtbl1q_u8(hex_lut, hi_nibbles);
134        let lo_hex = vqtbl1q_u8(hex_lut, lo_nibbles);
135        let result_lo = vzip1q_u8(hi_hex, lo_hex);
136        let result_hi = vzip2q_u8(hi_hex, lo_hex);
137
138        vst1q_u8(buf, result_lo);
139        vst1q_u8(buf.add(16), result_hi);
140
141        // SAFETY: We wrote exactly 32 ASCII hex chars (0-9, a-f)
142        s.as_mut_vec().set_len(32);
143        s
144    }
145}
146
147// ============================================================================
148// Hex Encoding - x86_64 SIMD (SSE2 + AVX2)
149// ============================================================================
150
151/// Internal: AVX2 implementation for 32-byte hex encoding.
152/// Processes all 32 bytes in a single operation using 256-bit registers.
153///
154/// # Safety
155/// Caller must ensure AVX2 is available (use `is_x86_feature_detected!`).
156///
157/// # Reference
158/// Algorithm based on faster-hex crate (MIT license):
159/// https://github.com/nervosnetwork/faster-hex
160#[cfg(target_arch = "x86_64")]
161#[target_feature(enable = "avx2")]
162#[inline]
163unsafe fn hex_encode_32_avx2(bytes: &[u8; 32], buf: *mut u8) {
164    // Constants for hex conversion
165    let and4bits = _mm256_set1_epi8(0x0f);
166    let nines = _mm256_set1_epi8(9);
167    let ascii_zero = _mm256_set1_epi8(b'0' as i8);
168    // 'a' - 9 - 1 = 87, so nibble + 87 = 'a' for nibble 10
169    let ascii_a_offset = _mm256_set1_epi8((b'a' - 9 - 1) as i8);
170
171    // Load all 32 bytes at once
172    let invec = _mm256_loadu_si256(bytes.as_ptr() as *const __m256i);
173
174    // Extract nibbles: low = byte & 0x0F, high = (byte >> 4) & 0x0F
175    // Note: srli_epi64 shifts 64-bit lanes, but we mask afterward so it's fine
176    let lo_nibbles = _mm256_and_si256(invec, and4bits);
177    let hi_nibbles = _mm256_and_si256(_mm256_srli_epi64(invec, 4), and4bits);
178
179    // Compare > 9 to identify hex letters (a-f)
180    let lo_gt9 = _mm256_cmpgt_epi8(lo_nibbles, nines);
181    let hi_gt9 = _mm256_cmpgt_epi8(hi_nibbles, nines);
182
183    // Convert to ASCII using blendv for conditional offset:
184    // if nibble <= 9: nibble + '0'
185    // if nibble > 9:  nibble + ('a' - 10) = nibble + 87
186    let lo_hex = _mm256_add_epi8(
187        lo_nibbles,
188        _mm256_blendv_epi8(ascii_zero, ascii_a_offset, lo_gt9),
189    );
190    let hi_hex = _mm256_add_epi8(
191        hi_nibbles,
192        _mm256_blendv_epi8(ascii_zero, ascii_a_offset, hi_gt9),
193    );
194
195    // Interleave high and low nibbles: [H0,L0,H1,L1,...]
196    // Note: AVX2 unpack operates within 128-bit lanes, so output is:
197    //   res1: [H0,L0..H7,L7 | H16,L16..H23,L23]  (bytes 0-7, 16-23)
198    //   res2: [H8,L8..H15,L15 | H24,L24..H31,L31] (bytes 8-15, 24-31)
199    let res1 = _mm256_unpacklo_epi8(hi_hex, lo_hex);
200    let res2 = _mm256_unpackhi_epi8(hi_hex, lo_hex);
201
202    // Store with lane correction using storeu2_m128i:
203    // res1 low 128 bits  -> positions 0-15  (bytes 0-7 interleaved)
204    // res2 low 128 bits  -> positions 16-31 (bytes 8-15 interleaved)
205    // res1 high 128 bits -> positions 32-47 (bytes 16-23 interleaved)
206    // res2 high 128 bits -> positions 48-63 (bytes 24-31 interleaved)
207    _mm256_storeu2_m128i(
208        buf.add(32) as *mut __m128i,  // high 128 bits
209        buf as *mut __m128i,          // low 128 bits
210        res1,
211    );
212    _mm256_storeu2_m128i(
213        buf.add(48) as *mut __m128i,  // high 128 bits
214        buf.add(16) as *mut __m128i,  // low 128 bits
215        res2,
216    );
217}
218
219/// Internal: SSE2 implementation for 32-byte hex encoding.
220/// Processes 16 bytes at a time using 128-bit registers.
221#[cfg(target_arch = "x86_64")]
222#[target_feature(enable = "sse2")]
223#[inline]
224unsafe fn hex_encode_32_sse2(bytes: &[u8; 32], buf: *mut u8) {
225    let mask_lo = _mm_set1_epi8(0x0f);
226    let nine = _mm_set1_epi8(9);
227    let ascii_zero = _mm_set1_epi8(b'0' as i8);
228    let letter_offset = _mm_set1_epi8(0x27); // 'a' - '0' - 10 = 0x27
229
230    for chunk_idx in 0..2 {
231        let offset = chunk_idx * 16;
232        let out_offset = chunk_idx * 32;
233
234        let input = _mm_loadu_si128(bytes.as_ptr().add(offset) as *const __m128i);
235
236        // Extract nibbles (use epi16 shift then mask)
237        let hi_nibbles = _mm_and_si128(_mm_srli_epi16(input, 4), mask_lo);
238        let lo_nibbles = _mm_and_si128(input, mask_lo);
239
240        // Convert: nibble + '0' + ((nibble > 9) ? 0x27 : 0)
241        let hi_gt9 = _mm_cmpgt_epi8(hi_nibbles, nine);
242        let lo_gt9 = _mm_cmpgt_epi8(lo_nibbles, nine);
243
244        let hi_hex = _mm_add_epi8(
245            _mm_add_epi8(hi_nibbles, ascii_zero),
246            _mm_and_si128(hi_gt9, letter_offset),
247        );
248        let lo_hex = _mm_add_epi8(
249            _mm_add_epi8(lo_nibbles, ascii_zero),
250            _mm_and_si128(lo_gt9, letter_offset),
251        );
252
253        // Interleave and store
254        let result_lo = _mm_unpacklo_epi8(hi_hex, lo_hex);
255        let result_hi = _mm_unpackhi_epi8(hi_hex, lo_hex);
256
257        _mm_storeu_si128(buf.add(out_offset) as *mut __m128i, result_lo);
258        _mm_storeu_si128(buf.add(out_offset + 16) as *mut __m128i, result_hi);
259    }
260}
261
262/// Convert 32-byte array to hex string using SIMD (x86_64).
263///
264/// Automatically uses AVX2 if available, otherwise falls back to SSE2.
265/// Zero-copy: writes directly into String buffer.
266#[cfg(target_arch = "x86_64")]
267#[inline]
268pub fn bytes_to_hex_32(bytes: &[u8; 32]) -> String {
269    unsafe {
270        let mut s = String::with_capacity(64);
271        let buf = s.as_mut_vec().as_mut_ptr();
272
273        // Runtime feature detection (cached after first call)
274        if is_x86_feature_detected!("avx2") {
275            hex_encode_32_avx2(bytes, buf);
276        } else {
277            hex_encode_32_sse2(bytes, buf);
278        }
279
280        s.as_mut_vec().set_len(64);
281        s
282    }
283}
284
285/// Internal: SSE2 implementation for 16-byte hex encoding.
286#[cfg(target_arch = "x86_64")]
287#[target_feature(enable = "sse2")]
288#[inline]
289unsafe fn hex_encode_16_sse2(bytes: &[u8; 16], buf: *mut u8) {
290    let mask_lo = _mm_set1_epi8(0x0f);
291    let nine = _mm_set1_epi8(9);
292    let ascii_zero = _mm_set1_epi8(b'0' as i8);
293    let letter_offset = _mm_set1_epi8(0x27);
294
295    let input = _mm_loadu_si128(bytes.as_ptr() as *const __m128i);
296
297    let hi_nibbles = _mm_and_si128(_mm_srli_epi16(input, 4), mask_lo);
298    let lo_nibbles = _mm_and_si128(input, mask_lo);
299
300    let hi_gt9 = _mm_cmpgt_epi8(hi_nibbles, nine);
301    let lo_gt9 = _mm_cmpgt_epi8(lo_nibbles, nine);
302
303    let hi_hex = _mm_add_epi8(
304        _mm_add_epi8(hi_nibbles, ascii_zero),
305        _mm_and_si128(hi_gt9, letter_offset),
306    );
307    let lo_hex = _mm_add_epi8(
308        _mm_add_epi8(lo_nibbles, ascii_zero),
309        _mm_and_si128(lo_gt9, letter_offset),
310    );
311
312    let result_lo = _mm_unpacklo_epi8(hi_hex, lo_hex);
313    let result_hi = _mm_unpackhi_epi8(hi_hex, lo_hex);
314
315    _mm_storeu_si128(buf as *mut __m128i, result_lo);
316    _mm_storeu_si128(buf.add(16) as *mut __m128i, result_hi);
317}
318
319/// Convert 16-byte array to hex string using SSE2 SIMD (x86_64).
320#[cfg(target_arch = "x86_64")]
321#[inline]
322pub fn bytes_to_hex_16(bytes: &[u8; 16]) -> String {
323    unsafe {
324        let mut s = String::with_capacity(32);
325        let buf = s.as_mut_vec().as_mut_ptr();
326        hex_encode_16_sse2(bytes, buf);
327        s.as_mut_vec().set_len(32);
328        s
329    }
330}
331
332// ============================================================================
333// Hex Encoding - Scalar Fallback (other architectures)
334// ============================================================================
335
336/// Fallback: Convert 32-byte array to hex using scalar LUT.
337#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
338#[inline]
339pub fn bytes_to_hex_32(bytes: &[u8; 32]) -> String {
340    unsafe {
341        let mut s = String::with_capacity(64);
342        let buf = s.as_mut_vec().as_mut_ptr();
343        for (i, &b) in bytes.iter().enumerate() {
344            let idx = (b as usize) * 2;
345            *buf.add(i * 2) = HEX_ENCODE_LUT[idx];
346            *buf.add(i * 2 + 1) = HEX_ENCODE_LUT[idx + 1];
347        }
348        s.as_mut_vec().set_len(64);
349        s
350    }
351}
352
353/// Fallback: Convert 16-byte array to hex using scalar LUT.
354#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
355#[inline]
356pub fn bytes_to_hex_16(bytes: &[u8; 16]) -> String {
357    unsafe {
358        let mut s = String::with_capacity(32);
359        let buf = s.as_mut_vec().as_mut_ptr();
360        for (i, &b) in bytes.iter().enumerate() {
361            let idx = (b as usize) * 2;
362            *buf.add(i * 2) = HEX_ENCODE_LUT[idx];
363            *buf.add(i * 2 + 1) = HEX_ENCODE_LUT[idx + 1];
364        }
365        s.as_mut_vec().set_len(32);
366        s
367    }
368}
369
370// ============================================================================
371// Hex Encoding - Variable Length
372// ============================================================================
373
374/// Convert a byte slice to a hex string.
375///
376/// For fixed-size arrays, prefer [`bytes_to_hex_32`] or [`bytes_to_hex_16`]
377/// which use SIMD acceleration:
378/// - **ARM64**: NEON with TBL lookup
379/// - **x86_64**: AVX2 (if available) or SSE2 fallback
380///
381/// Zero-copy: writes directly into String buffer.
382pub fn bytes_to_hex_string(bytes: &[u8]) -> String {
383    // Use optimized paths for common fixed sizes
384    if bytes.len() == 32 {
385        return bytes_to_hex_32(bytes.try_into().unwrap());
386    }
387    if bytes.len() == 16 {
388        return bytes_to_hex_16(bytes.try_into().unwrap());
389    }
390
391    let out_len = bytes.len().checked_mul(2).expect("hex string length overflow");
392
393    #[cfg(target_arch = "aarch64")]
394    unsafe {
395        // Allocate once, write directly - no intermediate buffers
396        let mut s = String::with_capacity(out_len);
397        let out_ptr = s.as_mut_vec().as_mut_ptr();
398        let chunks = bytes.len() / 16;
399        let hex_lut = vld1q_u8(HEX_NIBBLE.as_ptr());
400
401        // SIMD: process 16 input bytes -> 32 output bytes per iteration
402        for i in 0..chunks {
403            let input = vld1q_u8(bytes.as_ptr().add(i * 16));
404            let hi = vshrq_n_u8(input, 4);
405            let lo = vandq_u8(input, vdupq_n_u8(0x0f));
406            let hi_hex = vqtbl1q_u8(hex_lut, hi);
407            let lo_hex = vqtbl1q_u8(hex_lut, lo);
408            vst1q_u8(out_ptr.add(i * 32), vzip1q_u8(hi_hex, lo_hex));
409            vst1q_u8(out_ptr.add(i * 32 + 16), vzip2q_u8(hi_hex, lo_hex));
410        }
411
412        // Scalar for remaining bytes (0-15 bytes)
413        let remainder_start = chunks * 16;
414        let mut out_idx = chunks * 32;
415        for &b in &bytes[remainder_start..] {
416            *out_ptr.add(out_idx) = HEX_NIBBLE[(b >> 4) as usize];
417            *out_ptr.add(out_idx + 1) = HEX_NIBBLE[(b & 0xf) as usize];
418            out_idx += 2;
419        }
420
421        s.as_mut_vec().set_len(out_len);
422        s
423    }
424
425    #[cfg(target_arch = "x86_64")]
426    unsafe {
427        // Allocate once, write directly - no intermediate buffers
428        let mut s = String::with_capacity(out_len);
429        let out_ptr = s.as_mut_vec().as_mut_ptr();
430        let chunks = bytes.len() / 16;
431
432        // SSE2 constants
433        let mask_lo = _mm_set1_epi8(0x0f);
434        let nine = _mm_set1_epi8(9);
435        let ascii_zero = _mm_set1_epi8(b'0' as i8);
436        let letter_offset = _mm_set1_epi8(0x27);
437
438        // SIMD: process 16 input bytes -> 32 output bytes per iteration
439        for i in 0..chunks {
440            let input = _mm_loadu_si128(bytes.as_ptr().add(i * 16) as *const __m128i);
441
442            let hi = _mm_and_si128(_mm_srli_epi16(input, 4), mask_lo);
443            let lo = _mm_and_si128(input, mask_lo);
444
445            let hi_gt9 = _mm_cmpgt_epi8(hi, nine);
446            let lo_gt9 = _mm_cmpgt_epi8(lo, nine);
447
448            let hi_hex = _mm_add_epi8(
449                _mm_add_epi8(hi, ascii_zero),
450                _mm_and_si128(hi_gt9, letter_offset),
451            );
452            let lo_hex = _mm_add_epi8(
453                _mm_add_epi8(lo, ascii_zero),
454                _mm_and_si128(lo_gt9, letter_offset),
455            );
456
457            _mm_storeu_si128(out_ptr.add(i * 32) as *mut __m128i, _mm_unpacklo_epi8(hi_hex, lo_hex));
458            _mm_storeu_si128(out_ptr.add(i * 32 + 16) as *mut __m128i, _mm_unpackhi_epi8(hi_hex, lo_hex));
459        }
460
461        // Scalar for remaining bytes (0-15 bytes)
462        const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
463        let remainder_start = chunks * 16;
464        let mut out_idx = chunks * 32;
465        for &b in &bytes[remainder_start..] {
466            *out_ptr.add(out_idx) = HEX_CHARS[(b >> 4) as usize];
467            *out_ptr.add(out_idx + 1) = HEX_CHARS[(b & 0xf) as usize];
468            out_idx += 2;
469        }
470
471        s.as_mut_vec().set_len(out_len);
472        s
473    }
474
475    #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
476    unsafe {
477        // Allocate once, write directly
478        let mut s = String::with_capacity(out_len);
479        let out_ptr = s.as_mut_vec().as_mut_ptr();
480        for (i, &b) in bytes.iter().enumerate() {
481            let idx = (b as usize) * 2;
482            *out_ptr.add(i * 2) = HEX_ENCODE_LUT[idx];
483            *out_ptr.add(i * 2 + 1) = HEX_ENCODE_LUT[idx + 1];
484        }
485        s.as_mut_vec().set_len(out_len);
486        s
487    }
488}
489
490// ============================================================================
491// Hex Decoding - SIMD Accelerated
492// ============================================================================
493
494/// Convert hex string to fixed 32-byte array.
495///
496/// # Performance
497/// - NEON (ARM64): ~2.5 ns / 8 cycles (7.7x faster than LUT)
498/// - SSE2 (x86_64): ~5 ns (estimated)
499/// - Scalar fallback: ~19 ns
500///
501/// # Note
502/// Invalid hex characters are treated as 0x00. Short strings are zero-padded.
503#[cfg(target_arch = "aarch64")]
504#[inline]
505pub fn hex_to_bytes_32(hex: &str) -> [u8; 32] {
506    let h = hex.as_bytes();
507
508    // Fast path: exactly 64 chars, use SIMD
509    if h.len() >= 64 {
510        return unsafe { hex_decode_32_neon(h) };
511    }
512
513    // Slow path for short strings (zero-pad on left)
514    hex_to_bytes_32_scalar_padded(h)
515}
516
517/// NEON implementation: decode 64 hex chars to 32 bytes
518///
519/// Optimized algorithm:
520/// 1. Simplified nibble conversion: (char & 0x0F) + 9*(char has bit 0x40 set)
521///    - For '0'-'9': (0x30-0x39 & 0x0F) = 0-9, bit 0x40 not set, so +0
522///    - For 'A'-'F': (0x41-0x46 & 0x0F) = 1-6, bit 0x40 set, so +9 = 10-15
523///    - For 'a'-'f': (0x61-0x66 & 0x0F) = 1-6, bit 0x40 set, so +9 = 10-15
524/// 2. Uses SLI (Shift Left and Insert) to combine nibbles in one instruction
525/// 3. Fully unrolled for maximum throughput
526#[cfg(target_arch = "aarch64")]
527#[inline]
528unsafe fn hex_decode_32_neon(h: &[u8]) -> [u8; 32] {
529    let mut result = [0u8; 32];
530
531    let mask_0f = vdupq_n_u8(0x0F);
532    let mask_40 = vdupq_n_u8(0x40);
533    let nine = vdupq_n_u8(9);
534
535    // Load all 64 hex chars at once
536    let hex_0 = vld1q_u8(h.as_ptr());
537    let hex_1 = vld1q_u8(h.as_ptr().add(16));
538    let hex_2 = vld1q_u8(h.as_ptr().add(32));
539    let hex_3 = vld1q_u8(h.as_ptr().add(48));
540
541    // Convert ASCII to nibbles using simplified algorithm
542    // (char & 0x0F) + 9 if letter (bit 0x40 set)
543    let lo0 = vandq_u8(hex_0, mask_0f);
544    let lo1 = vandq_u8(hex_1, mask_0f);
545    let lo2 = vandq_u8(hex_2, mask_0f);
546    let lo3 = vandq_u8(hex_3, mask_0f);
547
548    let is_letter0 = vtstq_u8(hex_0, mask_40);
549    let is_letter1 = vtstq_u8(hex_1, mask_40);
550    let is_letter2 = vtstq_u8(hex_2, mask_40);
551    let is_letter3 = vtstq_u8(hex_3, mask_40);
552
553    let n0 = vaddq_u8(lo0, vandq_u8(is_letter0, nine));
554    let n1 = vaddq_u8(lo1, vandq_u8(is_letter1, nine));
555    let n2 = vaddq_u8(lo2, vandq_u8(is_letter2, nine));
556    let n3 = vaddq_u8(lo3, vandq_u8(is_letter3, nine));
557
558    // Pack nibbles to bytes using UZP + SLI
559    // SLI (Shift Left and Insert) combines shift+or into one instruction
560    let evens_a = vuzp1q_u8(n0, n1);
561    let odds_a = vuzp2q_u8(n0, n1);
562    let bytes_a = vsliq_n_u8(odds_a, evens_a, 4);
563
564    let evens_b = vuzp1q_u8(n2, n3);
565    let odds_b = vuzp2q_u8(n2, n3);
566    let bytes_b = vsliq_n_u8(odds_b, evens_b, 4);
567
568    vst1q_u8(result.as_mut_ptr(), bytes_a);
569    vst1q_u8(result.as_mut_ptr().add(16), bytes_b);
570
571    result
572}
573
574/// NEON: per-lane hex validity for a 16-char vector. Returns 0xFF iff EVERY lane is `[0-9A-Fa-f]`,
575/// else 0x00. Three parallel range tests (`[0-9]`, `[A-F]`, `[a-f]`) OR'd per lane; the horizontal
576/// min collapses "all lanes valid" to a single byte.
577#[cfg(target_arch = "aarch64")]
578#[inline]
579unsafe fn neon_all_hex(c: uint8x16_t) -> u8 {
580    let is_digit = vandq_u8(vcgeq_u8(c, vdupq_n_u8(b'0')), vcleq_u8(c, vdupq_n_u8(b'9')));
581    let is_upper = vandq_u8(vcgeq_u8(c, vdupq_n_u8(b'A')), vcleq_u8(c, vdupq_n_u8(b'F')));
582    let is_lower = vandq_u8(vcgeq_u8(c, vdupq_n_u8(b'a')), vcleq_u8(c, vdupq_n_u8(b'f')));
583    vminvq_u8(vorrq_u8(vorrq_u8(is_digit, is_upper), is_lower))
584}
585
586/// NEON: validate + decode 64 hex chars to 32 bytes. Returns None if any char isn't `[0-9A-Fa-f]`.
587/// Validation reuses the same vector loads as the decode, so a clean id costs one extra range-test
588/// pass over the (L1-hot) bytes — same speed class as the unchecked decode.
589#[cfg(target_arch = "aarch64")]
590#[inline]
591unsafe fn hex_decode_32_neon_checked(h: &[u8]) -> Option<[u8; 32]> {
592    let hex_0 = vld1q_u8(h.as_ptr());
593    let hex_1 = vld1q_u8(h.as_ptr().add(16));
594    let hex_2 = vld1q_u8(h.as_ptr().add(32));
595    let hex_3 = vld1q_u8(h.as_ptr().add(48));
596    if (neon_all_hex(hex_0) & neon_all_hex(hex_1) & neon_all_hex(hex_2) & neon_all_hex(hex_3)) != 0xFF {
597        return None;
598    }
599    Some(hex_decode_32_neon(h))
600}
601
602/// x86_64 SIMD implementation
603#[cfg(target_arch = "x86_64")]
604#[inline]
605pub fn hex_to_bytes_32(hex: &str) -> [u8; 32] {
606    let h = hex.as_bytes();
607
608    if h.len() >= 64 {
609        // SAFETY: We verified length >= 64, and hex_decode_32_sse2 only reads first 64 bytes
610        let arr: &[u8; 64] = h[..64].try_into().unwrap();
611        return unsafe { hex_decode_32_sse2(arr) };
612    }
613
614    hex_to_bytes_32_scalar_padded(h)
615}
616
617/// SSE2 implementation: decode 64 hex chars to 32 bytes
618///
619/// Uses the same algorithm as NEON: `(char & 0x0F) + 9*(char has bit 0x40 set)`
620/// This correctly handles '0'-'9', 'A'-'F', and 'a'-'f'.
621///
622/// # Safety
623/// Caller must ensure input contains only valid hex characters.
624/// Invalid input produces garbage output (no validation performed).
625#[cfg(target_arch = "x86_64")]
626#[target_feature(enable = "sse2")]
627#[inline]
628unsafe fn hex_decode_32_sse2(h: &[u8; 64]) -> [u8; 32] {
629    let mut result = [0u8; 32];
630
631    // Same algorithm as NEON: (char & 0x0F) + 9 if letter
632    let mask_0f = _mm_set1_epi8(0x0F);
633    let mask_40 = _mm_set1_epi8(0x40);
634    let nine = _mm_set1_epi8(9);
635    let hi_mask = _mm_set1_epi16(0x00F0u16 as i16);
636    let lo_mask = _mm_set1_epi16(0x000Fu16 as i16);
637    let zero = _mm_setzero_si128();
638
639    // Process 16 hex chars -> 8 bytes at a time (4 iterations)
640    for chunk in 0..4 {
641        let in_offset = chunk * 16;
642        let out_offset = chunk * 8;
643
644        let hex_chars = _mm_loadu_si128(h.as_ptr().add(in_offset) as *const __m128i);
645
646        // Convert ASCII to nibbles using NEON-style algorithm:
647        // nibble = (char & 0x0F) + ((char & 0x40) == 0x40 ? 9 : 0)
648        let lo = _mm_and_si128(hex_chars, mask_0f);
649        let masked = _mm_and_si128(hex_chars, mask_40);
650        let is_letter = _mm_cmpeq_epi8(masked, mask_40);
651        let nine_if_letter = _mm_and_si128(is_letter, nine);
652        let nibbles = _mm_add_epi8(lo, nine_if_letter);
653
654        // Pack pairs of nibbles into bytes
655        let hi_nibbles = _mm_slli_epi16(nibbles, 4);
656        let hi = _mm_and_si128(hi_nibbles, hi_mask);
657        let lo_shifted = _mm_and_si128(_mm_srli_epi16(nibbles, 8), lo_mask);
658        let combined = _mm_or_si128(hi, lo_shifted);
659
660        let packed = _mm_packus_epi16(combined, zero);
661        _mm_storel_epi64(result.as_mut_ptr().add(out_offset) as *mut __m128i, packed);
662    }
663
664    result
665}
666
667/// SSE2: whether all 16 lanes of `c` are hex `[0-9A-Fa-f]`. Every hex char is < 0x80, so signed
668/// `cmpgt` range tests are exact (`c > lo-1 && hi+1 > c`); movemask == 0xFFFF iff every lane valid.
669#[cfg(target_arch = "x86_64")]
670#[target_feature(enable = "sse2")]
671#[inline]
672unsafe fn sse2_chunk_all_hex(c: __m128i) -> bool {
673    let is_digit = _mm_and_si128(_mm_cmpgt_epi8(c, _mm_set1_epi8(0x2F)), _mm_cmpgt_epi8(_mm_set1_epi8(0x3A), c));
674    let is_upper = _mm_and_si128(_mm_cmpgt_epi8(c, _mm_set1_epi8(0x40)), _mm_cmpgt_epi8(_mm_set1_epi8(0x47), c));
675    let is_lower = _mm_and_si128(_mm_cmpgt_epi8(c, _mm_set1_epi8(0x60)), _mm_cmpgt_epi8(_mm_set1_epi8(0x67), c));
676    let valid = _mm_or_si128(_mm_or_si128(is_digit, is_upper), is_lower);
677    _mm_movemask_epi8(valid) == 0xFFFF
678}
679
680/// SSE2: validate + decode 64 hex chars to 32 bytes. None if any char isn't `[0-9A-Fa-f]`.
681#[cfg(target_arch = "x86_64")]
682#[target_feature(enable = "sse2")]
683#[inline]
684unsafe fn hex_decode_32_sse2_checked(h: &[u8; 64]) -> Option<[u8; 32]> {
685    let mut chunk = 0;
686    while chunk < 4 {
687        let c = _mm_loadu_si128(h.as_ptr().add(chunk * 16) as *const __m128i);
688        if !sse2_chunk_all_hex(c) {
689            return None;
690        }
691        chunk += 1;
692    }
693    Some(hex_decode_32_sse2(h))
694}
695
696/// SSE2 implementation: decode 32 hex chars to 16 bytes
697///
698/// Uses the same algorithm as NEON: `(char & 0x0F) + 9*(char has bit 0x40 set)`
699/// This correctly handles '0'-'9', 'A'-'F', and 'a'-'f'.
700///
701/// # Safety
702/// Caller must ensure input contains only valid hex characters.
703/// Invalid input produces garbage output (no validation performed).
704#[cfg(target_arch = "x86_64")]
705#[target_feature(enable = "sse2")]
706#[inline]
707unsafe fn hex_decode_16_sse2(h: &[u8; 32]) -> [u8; 16] {
708    let mut result = [0u8; 16];
709
710    // Same algorithm as NEON: (char & 0x0F) + 9 if letter
711    let mask_0f = _mm_set1_epi8(0x0F);
712    let mask_40 = _mm_set1_epi8(0x40);
713    let nine = _mm_set1_epi8(9);
714    let hi_mask = _mm_set1_epi16(0x00F0u16 as i16);
715    let lo_mask = _mm_set1_epi16(0x000Fu16 as i16);
716    let zero = _mm_setzero_si128();
717
718    // Process 16 hex chars -> 8 bytes at a time (2 iterations for 16 bytes)
719    for chunk in 0..2 {
720        let in_offset = chunk * 16;
721        let out_offset = chunk * 8;
722
723        let hex_chars = _mm_loadu_si128(h.as_ptr().add(in_offset) as *const __m128i);
724
725        // Convert ASCII to nibbles using NEON-style algorithm:
726        // nibble = (char & 0x0F) + ((char & 0x40) == 0x40 ? 9 : 0)
727        let lo = _mm_and_si128(hex_chars, mask_0f);
728        let masked = _mm_and_si128(hex_chars, mask_40);
729        let is_letter = _mm_cmpeq_epi8(masked, mask_40);
730        let nine_if_letter = _mm_and_si128(is_letter, nine);
731        let nibbles = _mm_add_epi8(lo, nine_if_letter);
732
733        // Pack pairs of nibbles into bytes
734        let hi_nibbles = _mm_slli_epi16(nibbles, 4);
735        let hi = _mm_and_si128(hi_nibbles, hi_mask);
736        let lo_shifted = _mm_and_si128(_mm_srli_epi16(nibbles, 8), lo_mask);
737        let combined = _mm_or_si128(hi, lo_shifted);
738
739        let packed = _mm_packus_epi16(combined, zero);
740        _mm_storel_epi64(result.as_mut_ptr().add(out_offset) as *mut __m128i, packed);
741    }
742
743    result
744}
745
746/// Scalar fallback for other platforms
747#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
748#[inline]
749pub fn hex_to_bytes_32(hex: &str) -> [u8; 32] {
750    let h = hex.as_bytes();
751
752    if h.len() >= 64 {
753        let mut bytes = [0u8; 32];
754        for i in 0..32 {
755            bytes[i] = (HEX_DECODE_LUT[h[i * 2] as usize] << 4)
756                     | HEX_DECODE_LUT[h[i * 2 + 1] as usize];
757        }
758        return bytes;
759    }
760
761    hex_to_bytes_32_scalar_padded(h)
762}
763
764/// Scalar helper for short/padded hex strings (all platforms)
765#[inline]
766fn hex_to_bytes_32_scalar_padded(h: &[u8]) -> [u8; 32] {
767    let mut bytes = [0u8; 32];
768    let hex_len = h.len();
769    let start_idx = (64 - hex_len) / 2;
770    let mut out_idx = start_idx / 2;
771
772    let mut i = 0;
773    while i + 1 < hex_len && out_idx < 32 {
774        bytes[out_idx] = (HEX_DECODE_LUT[h[i] as usize] << 4)
775                       | HEX_DECODE_LUT[h[i + 1] as usize];
776        out_idx += 1;
777        i += 2;
778    }
779    bytes
780}
781
782/// Validating decode of EXACTLY 64 hex chars → `[u8; 32]`, for PUBLICLY-obtained ids (inbound
783/// events). A valid signature attests authorship, NOT that a tag is well-formed hex, so a hostile
784/// peer can still ship garbage here — this returns `None` unless `hex` is precisely 64 ASCII hex
785/// digits. Validation runs in-register on the SIMD path (NEON / SSE2), keeping the network boundary
786/// in the same speed class as the unchecked decode. Deterministic/internal ids (DB rows, our own
787/// encrypted self-lists, frontend command params) should use the infallible [`hex_to_bytes_32`].
788#[cfg(target_arch = "aarch64")]
789#[inline]
790pub fn hex_to_bytes_32_checked(hex: &str) -> Option<[u8; 32]> {
791    let h = hex.as_bytes();
792    if h.len() != 64 {
793        return None;
794    }
795    unsafe { hex_decode_32_neon_checked(h) }
796}
797
798/// SSE2 variant — see the aarch64 `hex_to_bytes_32_checked` for semantics.
799#[cfg(target_arch = "x86_64")]
800#[inline]
801pub fn hex_to_bytes_32_checked(hex: &str) -> Option<[u8; 32]> {
802    let h = hex.as_bytes();
803    if h.len() != 64 {
804        return None;
805    }
806    let arr: &[u8; 64] = h[..64].try_into().unwrap();
807    unsafe { hex_decode_32_sse2_checked(arr) }
808}
809
810/// Scalar fallback — see the aarch64 `hex_to_bytes_32_checked` for semantics.
811#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
812#[inline]
813pub fn hex_to_bytes_32_checked(hex: &str) -> Option<[u8; 32]> {
814    let h = hex.as_bytes();
815    if h.len() != 64 || !h.iter().all(u8::is_ascii_hexdigit) {
816        return None;
817    }
818    Some(hex_to_bytes_32(hex))
819}
820
821// ============================================================================
822// Hex Decoding - 16 bytes
823// ============================================================================
824
825/// Convert hex string to fixed 16-byte array.
826///
827/// # Performance
828/// - NEON (ARM64): ~2 ns
829/// - Scalar fallback: ~10 ns
830#[cfg(target_arch = "aarch64")]
831#[inline]
832pub fn hex_to_bytes_16(hex: &str) -> [u8; 16] {
833    let h = hex.as_bytes();
834
835    if h.len() >= 32 {
836        return unsafe { hex_decode_16_neon(h) };
837    }
838
839    hex_to_bytes_16_scalar_padded(h)
840}
841
842/// NEON implementation: decode 32 hex chars to 16 bytes
843///
844/// Uses the same optimized algorithm as hex_decode_32_neon:
845/// - Simplified nibble conversion: (char & 0x0F) + 9*(char has bit 0x40 set)
846/// - SLI instruction to combine nibbles in one operation
847#[cfg(target_arch = "aarch64")]
848#[inline]
849unsafe fn hex_decode_16_neon(h: &[u8]) -> [u8; 16] {
850    let mut result = [0u8; 16];
851
852    let mask_0f = vdupq_n_u8(0x0F);
853    let mask_40 = vdupq_n_u8(0x40);
854    let nine = vdupq_n_u8(9);
855
856    // Load 32 hex characters
857    let hex_0 = vld1q_u8(h.as_ptr());
858    let hex_1 = vld1q_u8(h.as_ptr().add(16));
859
860    // Convert ASCII to nibbles: (char & 0x0F) + 9 if letter
861    let lo0 = vandq_u8(hex_0, mask_0f);
862    let lo1 = vandq_u8(hex_1, mask_0f);
863
864    let is_letter0 = vtstq_u8(hex_0, mask_40);
865    let is_letter1 = vtstq_u8(hex_1, mask_40);
866
867    let n0 = vaddq_u8(lo0, vandq_u8(is_letter0, nine));
868    let n1 = vaddq_u8(lo1, vandq_u8(is_letter1, nine));
869
870    // Pack nibbles using UZP + SLI
871    let evens = vuzp1q_u8(n0, n1);
872    let odds = vuzp2q_u8(n0, n1);
873    let bytes = vsliq_n_u8(odds, evens, 4);
874
875    vst1q_u8(result.as_mut_ptr(), bytes);
876    result
877}
878
879#[cfg(target_arch = "x86_64")]
880#[inline]
881pub fn hex_to_bytes_16(hex: &str) -> [u8; 16] {
882    let h = hex.as_bytes();
883
884    if h.len() >= 32 {
885        // SAFETY: We verified length >= 32, and hex_decode_16_sse2 only reads first 32 bytes
886        let arr: &[u8; 32] = h[..32].try_into().unwrap();
887        return unsafe { hex_decode_16_sse2(arr) };
888    }
889
890    hex_to_bytes_16_scalar_padded(h)
891}
892
893#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
894#[inline]
895pub fn hex_to_bytes_16(hex: &str) -> [u8; 16] {
896    let h = hex.as_bytes();
897
898    if h.len() >= 32 {
899        let mut bytes = [0u8; 16];
900        for i in 0..16 {
901            bytes[i] = (HEX_DECODE_LUT[h[i * 2] as usize] << 4)
902                     | HEX_DECODE_LUT[h[i * 2 + 1] as usize];
903        }
904        return bytes;
905    }
906
907    hex_to_bytes_16_scalar_padded(h)
908}
909
910#[inline]
911fn hex_to_bytes_16_scalar_padded(h: &[u8]) -> [u8; 16] {
912    let mut bytes = [0u8; 16];
913    let hex_len = h.len();
914    let start_idx = (32 - hex_len) / 2;
915    let mut out_idx = start_idx / 2;
916
917    let mut i = 0;
918    while i + 1 < hex_len && out_idx < 16 {
919        bytes[out_idx] = (HEX_DECODE_LUT[h[i] as usize] << 4)
920                       | HEX_DECODE_LUT[h[i + 1] as usize];
921        out_idx += 1;
922        i += 2;
923    }
924    bytes
925}
926
927// ============================================================================
928// Hex Decoding - Variable Length
929// ============================================================================
930
931/// Convert hex string to bytes (arbitrary length).
932///
933/// Uses SIMD for the bulk of the conversion when input is large enough.
934// `set_len` before init is deliberate: every architecture branch writes all `out_len` bytes
935// through `out_ptr` before `result` is read, so pre-zeroing would only waste a memset on a hot path.
936#[allow(clippy::uninit_vec)]
937pub fn hex_string_to_bytes(s: &str) -> Vec<u8> {
938    let h = s.as_bytes();
939    let out_len = h.len() / 2;
940    let mut result = Vec::with_capacity(out_len);
941
942    #[cfg(target_arch = "aarch64")]
943    unsafe {
944        result.set_len(out_len);
945        let out_ptr: *mut u8 = result.as_mut_ptr();
946
947        let mask_0f = vdupq_n_u8(0x0F);
948        let mask_40 = vdupq_n_u8(0x40);
949        let nine = vdupq_n_u8(9);
950
951        let chunks = out_len / 16; // 32 hex chars -> 16 bytes per chunk
952        for chunk in 0..chunks {
953            let in_offset = chunk * 32;
954            let out_offset = chunk * 16;
955
956            // Load 32 hex characters
957            let hex_0 = vld1q_u8(h.as_ptr().add(in_offset));
958            let hex_1 = vld1q_u8(h.as_ptr().add(in_offset + 16));
959
960            // Convert ASCII to nibbles: (char & 0x0F) + 9 if letter
961            let lo0 = vandq_u8(hex_0, mask_0f);
962            let lo1 = vandq_u8(hex_1, mask_0f);
963
964            let is_letter0 = vtstq_u8(hex_0, mask_40);
965            let is_letter1 = vtstq_u8(hex_1, mask_40);
966
967            let n0 = vaddq_u8(lo0, vandq_u8(is_letter0, nine));
968            let n1 = vaddq_u8(lo1, vandq_u8(is_letter1, nine));
969
970            // Pack nibbles using UZP + SLI
971            let evens = vuzp1q_u8(n0, n1);
972            let odds = vuzp2q_u8(n0, n1);
973            let bytes = vsliq_n_u8(odds, evens, 4);
974
975            vst1q_u8(out_ptr.add(out_offset), bytes);
976        }
977
978        // Scalar remainder
979        let remainder_start = chunks * 32;
980        let mut out_idx = chunks * 16;
981        let mut i = remainder_start;
982        while i + 1 < h.len() {
983            *out_ptr.add(out_idx) = (HEX_DECODE_LUT[h[i] as usize] << 4)
984                                  | HEX_DECODE_LUT[h[i + 1] as usize];
985            out_idx += 1;
986            i += 2;
987        }
988    }
989
990    #[cfg(target_arch = "x86_64")]
991    unsafe {
992        result.set_len(out_len);
993        let out_ptr: *mut u8 = result.as_mut_ptr();
994
995        let mask_0f = _mm_set1_epi8(0x0F);
996        let mask_40 = _mm_set1_epi8(0x40);
997        let nine = _mm_set1_epi8(9);
998        let hi_mask = _mm_set1_epi16(0x00F0u16 as i16);
999        let lo_mask = _mm_set1_epi16(0x000Fu16 as i16);
1000        let zero = _mm_setzero_si128();
1001
1002        let chunks = out_len / 8; // 16 hex chars → 8 output bytes per SSE2 iteration
1003        for chunk in 0..chunks {
1004            let in_offset = chunk * 16;
1005            let out_offset = chunk * 8;
1006
1007            let hex_chars = _mm_loadu_si128(h.as_ptr().add(in_offset) as *const __m128i);
1008
1009            // Convert ASCII to nibbles: (char & 0x0F) + 9 if letter (bit 0x40 set)
1010            let lo = _mm_and_si128(hex_chars, mask_0f);
1011            let masked = _mm_and_si128(hex_chars, mask_40);
1012            let is_letter = _mm_cmpeq_epi8(masked, mask_40);
1013            let nine_if_letter = _mm_and_si128(is_letter, nine);
1014            let nibbles = _mm_add_epi8(lo, nine_if_letter);
1015
1016            // Pack pairs of nibbles into bytes (same as hex_decode_32_sse2)
1017            let hi_nibbles = _mm_slli_epi16(nibbles, 4);
1018            let hi = _mm_and_si128(hi_nibbles, hi_mask);
1019            let lo_shifted = _mm_and_si128(_mm_srli_epi16(nibbles, 8), lo_mask);
1020            let combined = _mm_or_si128(hi, lo_shifted);
1021
1022            let packed = _mm_packus_epi16(combined, zero);
1023            _mm_storel_epi64(out_ptr.add(out_offset) as *mut __m128i, packed);
1024        }
1025
1026        // Scalar remainder
1027        let remainder_start = chunks * 16;
1028        let mut out_idx = chunks * 8;
1029        let mut i = remainder_start;
1030        while i + 1 < h.len() {
1031            *out_ptr.add(out_idx) = (HEX_DECODE_LUT[h[i] as usize] << 4)
1032                                  | HEX_DECODE_LUT[h[i + 1] as usize];
1033            out_idx += 1;
1034            i += 2;
1035        }
1036    }
1037
1038    #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
1039    {
1040        for chunk in h.chunks(2) {
1041            if chunk.len() == 2 {
1042                result.push(
1043                    (HEX_DECODE_LUT[chunk[0] as usize] << 4) | HEX_DECODE_LUT[chunk[1] as usize]
1044                );
1045            }
1046        }
1047    }
1048
1049    result
1050}
1051
1052/// Whether every byte of `h` is an ASCII hex digit. Full 16-byte chunks are checked in-register with
1053/// the same lane-validator the fixed-32 path uses; the sub-16 remainder falls to a scalar scan.
1054#[cfg(target_arch = "aarch64")]
1055#[inline]
1056fn all_ascii_hex(h: &[u8]) -> bool {
1057    let chunks = h.len() / 16;
1058    unsafe {
1059        let mut acc = 0xFFu8;
1060        for i in 0..chunks {
1061            acc &= neon_all_hex(vld1q_u8(h.as_ptr().add(i * 16)));
1062        }
1063        if acc != 0xFF {
1064            return false;
1065        }
1066    }
1067    h[chunks * 16..].iter().all(u8::is_ascii_hexdigit)
1068}
1069
1070/// SSE2 twin of the NEON `all_ascii_hex` — 16-byte chunks validated in-register, scalar remainder.
1071#[cfg(target_arch = "x86_64")]
1072#[inline]
1073fn all_ascii_hex(h: &[u8]) -> bool {
1074    let chunks = h.len() / 16;
1075    unsafe {
1076        for i in 0..chunks {
1077            let c = _mm_loadu_si128(h.as_ptr().add(i * 16) as *const __m128i);
1078            if !sse2_chunk_all_hex(c) {
1079                return false;
1080            }
1081        }
1082    }
1083    h[chunks * 16..].iter().all(u8::is_ascii_hexdigit)
1084}
1085
1086/// Scalar fallback validator.
1087#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
1088#[inline]
1089fn all_ascii_hex(h: &[u8]) -> bool {
1090    h.iter().all(u8::is_ascii_hexdigit)
1091}
1092
1093/// Validating arbitrary-length hex decode → `Some(bytes)` only when `s` is even-length and every char
1094/// is `[0-9A-Fa-f]`, else `None`. The infallible [`hex_string_to_bytes`] silently maps non-hex to 0, so
1095/// this is the variant for decoding hex that could be malformed (corrupt storage, untrusted input).
1096/// Validation runs in-register on the SIMD path (full 16-char chunks), so the check is the same speed
1097/// class as the decode it guards.
1098#[inline]
1099pub fn hex_string_to_bytes_checked(s: &str) -> Option<Vec<u8>> {
1100    let h = s.as_bytes();
1101    if h.len() % 2 != 0 || !all_ascii_hex(h) {
1102        return None;
1103    }
1104    Some(hex_string_to_bytes(s))
1105}
1106
1107#[cfg(test)]
1108mod tests {
1109    use super::*;
1110
1111    #[test]
1112    fn checked_varlen_decode_accepts_valid_and_rejects_garbage() {
1113        // Even-length hex of assorted lengths (incl. non-multiples of 16 chars, exercising the
1114        // SIMD chunk + scalar remainder) decodes identically to the infallible path.
1115        for len_bytes in [0usize, 1, 7, 8, 16, 39, 78, 256] {
1116            let bytes: Vec<u8> = (0..len_bytes).map(|i| (i * 7 + 3) as u8).collect();
1117            let hex = bytes_to_hex_string(&bytes);
1118            assert_eq!(hex_string_to_bytes_checked(&hex), Some(hex_string_to_bytes(&hex)), "{len_bytes}B");
1119            assert_eq!(hex_string_to_bytes_checked(&hex).unwrap(), bytes, "{len_bytes}B roundtrip");
1120        }
1121        // Uppercase accepted too.
1122        assert!(hex_string_to_bytes_checked("DEADBEEF").is_some());
1123        // Odd length → None.
1124        assert_eq!(hex_string_to_bytes_checked("abc"), None);
1125        // Non-hex in a FULL 16-char chunk (position 5) → None.
1126        assert_eq!(hex_string_to_bytes_checked("00112g3344556677"), None, "bad char in SIMD chunk");
1127        // Non-hex in the sub-16 REMAINDER (a 20-char string: 16-char chunk OK, bad char at 18) → None.
1128        assert_eq!(hex_string_to_bytes_checked("00112233445566778z99"), None, "bad char in remainder");
1129        // Empty → Some(empty).
1130        assert_eq!(hex_string_to_bytes_checked(""), Some(Vec::new()));
1131    }
1132
1133    #[test]
1134    fn checked_decode_accepts_valid_and_rejects_garbage() {
1135        // Valid lowercase, uppercase, and mixed all decode to the SAME bytes as the unchecked path.
1136        let lower = "00112233445566778899aabbccddeeff0123456789abcdeffedcba9876543210";
1137        let upper = lower.to_uppercase();
1138        assert_eq!(hex_to_bytes_32_checked(lower), Some(hex_to_bytes_32(lower)));
1139        assert_eq!(hex_to_bytes_32_checked(&upper), Some(hex_to_bytes_32(lower)));
1140        assert_eq!(hex_to_bytes_32_checked(lower).unwrap()[0], 0x00);
1141        assert_eq!(hex_to_bytes_32_checked(lower).unwrap()[31], 0x10);
1142
1143        // Wrong length → None (too short, too long, empty).
1144        assert_eq!(hex_to_bytes_32_checked(&lower[..63]), None);
1145        assert_eq!(hex_to_bytes_32_checked(&format!("{lower}0")), None);
1146        assert_eq!(hex_to_bytes_32_checked(""), None);
1147
1148        // A single non-hex char anywhere → None (first, middle, last byte positions).
1149        let mut bad = lower.to_string();
1150        bad.replace_range(0..1, "g");
1151        assert_eq!(hex_to_bytes_32_checked(&bad), None, "non-hex at start");
1152        let mut bad = lower.to_string();
1153        bad.replace_range(32..33, "Z");
1154        assert_eq!(hex_to_bytes_32_checked(&bad), None, "non-hex in middle");
1155        let mut bad = lower.to_string();
1156        bad.replace_range(63..64, " ");
1157        assert_eq!(hex_to_bytes_32_checked(&bad), None, "non-hex at end");
1158
1159        // Range-boundary chars: the byte just outside each valid run must be rejected.
1160        // '/' (0x2F) | ':' (0x3A) | '@' (0x40) | 'G' (0x47) | '`' (0x60) | 'g' (0x67).
1161        for bad_char in ['/', ':', '@', 'G', '`', 'g'] {
1162            let mut s = lower.to_string();
1163            s.replace_range(10..11, &bad_char.to_string());
1164            assert_eq!(hex_to_bytes_32_checked(&s), None, "boundary char {bad_char:?} must reject");
1165        }
1166        // ...and the inclusive endpoints must be accepted.
1167        for ok_char in ['0', '9', 'a', 'f', 'A', 'F'] {
1168            let mut s = lower.to_string();
1169            s.replace_range(10..11, &ok_char.to_string());
1170            assert!(hex_to_bytes_32_checked(&s).is_some(), "valid char {ok_char:?} must accept");
1171        }
1172    }
1173
1174    #[test]
1175    fn test_hex_encode_32() {
1176        let bytes: [u8; 32] = [
1177            0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
1178            0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
1179            0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
1180            0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10,
1181        ];
1182        let hex = bytes_to_hex_32(&bytes);
1183        assert_eq!(hex, "00112233445566778899aabbccddeeff0123456789abcdeffedcba9876543210");
1184    }
1185
1186    #[test]
1187    fn test_hex_decode_32() {
1188        let hex = "00112233445566778899aabbccddeeff0123456789abcdeffedcba9876543210";
1189        let bytes = hex_to_bytes_32(hex);
1190        assert_eq!(bytes[0], 0x00);
1191        assert_eq!(bytes[15], 0xff);
1192        assert_eq!(bytes[31], 0x10);
1193    }
1194
1195    #[test]
1196    fn test_roundtrip() {
1197        let original: [u8; 32] = [42; 32];
1198        let hex = bytes_to_hex_32(&original);
1199        let decoded = hex_to_bytes_32(&hex);
1200        assert_eq!(original, decoded);
1201    }
1202
1203    #[test]
1204    fn test_hex_decode_16() {
1205        let hex = "00112233445566778899aabbccddeeff";
1206        let bytes = hex_to_bytes_16(hex);
1207        assert_eq!(bytes[0], 0x00);
1208        assert_eq!(bytes[7], 0x77);
1209        assert_eq!(bytes[15], 0xff);
1210    }
1211
1212    #[test]
1213    fn test_hex_decode_uppercase() {
1214        // Test that uppercase hex is decoded correctly
1215        let lowercase = "00112233445566778899aabbccddeeff0123456789abcdeffedcba9876543210";
1216        let uppercase = "00112233445566778899AABBCCDDEEFF0123456789ABCDEFFEDCBA9876543210";
1217        assert_eq!(hex_to_bytes_32(lowercase), hex_to_bytes_32(uppercase));
1218    }
1219
1220    #[test]
1221    fn test_hex_string_to_bytes() {
1222        let hex = "deadbeef";
1223        let bytes = hex_string_to_bytes(hex);
1224        assert_eq!(bytes, vec![0xde, 0xad, 0xbe, 0xef]);
1225    }
1226
1227    #[test]
1228    fn test_hex_string_to_bytes_long() {
1229        // Test variable-length decode with longer input (uses SIMD path)
1230        let hex = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff";
1231        let bytes = hex_string_to_bytes(hex);
1232        assert_eq!(bytes.len(), 32);
1233        assert_eq!(bytes[0], 0x00);
1234        assert_eq!(bytes[15], 0xff);
1235        assert_eq!(bytes[31], 0xff);
1236    }
1237
1238    #[test]
1239    fn test_roundtrip_16() {
1240        let original: [u8; 16] = [0xab; 16];
1241        let hex = bytes_to_hex_16(&original);
1242        let decoded = hex_to_bytes_16(&hex);
1243        assert_eq!(original, decoded);
1244    }
1245
1246}