Skip to main content

faster_hex/
encode.rs

1// SIMD encoding includes work derived from stdsimd under the MIT license.
2// See LICENSE-THIRD-PARTY/Rust Project Developers.
3
4#[cfg(target_arch = "x86")]
5use core::arch::x86::*;
6#[cfg(target_arch = "x86_64")]
7use core::arch::x86_64::*;
8
9#[cfg(target_arch = "aarch64")]
10use core::arch::aarch64::*;
11
12#[cfg(feature = "alloc")]
13use alloc::{string::String, vec::Vec};
14
15use core::mem::MaybeUninit;
16
17use crate::error::Error;
18
19// SIMD callers establish CPU/OS support and exactly twice the source length
20// in destination space. Kernels initialize every output byte as ASCII; only
21// encode() exposes the initialized prefix as a string.
22const TABLE_LOWER: &[u8; 16] = b"0123456789abcdef";
23const TABLE_UPPER: &[u8; 16] = b"0123456789ABCDEF";
24
25#[cfg(feature = "alloc")]
26#[inline]
27fn hex_string_custom_case(src: &[u8], upper_case: bool) -> String {
28    let len = src.len().checked_mul(2).expect("encoded length overflow");
29    let mut buffer = Vec::with_capacity(len);
30    encode(src, buffer.spare_capacity_mut(), upper_case).expect("capacity reserved for encoding");
31    // SAFETY: encode initialized the first len spare bytes with ASCII. The length
32    // is committed only after encoding returns, so unwinding never exposes it.
33    unsafe {
34        buffer.set_len(len);
35        String::from_utf8_unchecked(buffer)
36    }
37}
38
39/// Encodes `src` as an owned lowercase hexadecimal string.
40///
41/// Each byte produces two ASCII digits, including leading zeroes. The result has
42/// length `src.len() * 2`, contains no prefix or separators, and owns its storage
43/// independently of `src`. Empty input produces an empty string.
44///
45/// Available with the `alloc` feature. To reuse a string's capacity, use
46/// [`hex_append`]. For caller-provided storage, use [`hex_encode`].
47///
48/// # Panics
49///
50/// Panics if the encoded length cannot be represented or exceeds `isize::MAX`
51/// bytes. Allocation failure follows the allocator's error handling; it is not
52/// returned as an [`Error`].
53///
54/// # Examples
55///
56/// ```
57/// use faster_hex::hex_string;
58///
59/// assert_eq!(hex_string(&[0, 0xab, 0xff]), "00abff");
60/// assert_eq!(hex_string(&[]), "");
61/// ```
62#[cfg(feature = "alloc")]
63#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
64#[inline]
65pub fn hex_string(src: &[u8]) -> String {
66    hex_string_custom_case(src, false)
67}
68
69/// Encodes `src` as an owned uppercase hexadecimal string.
70///
71/// This is the uppercase counterpart of [`hex_string`]: each byte produces two
72/// ASCII digits without a prefix or separators. Available with `alloc`.
73///
74/// # Panics
75///
76/// Panics on the same capacity overflows as [`hex_string`]. Allocation failure
77/// follows the allocator's error handling.
78///
79/// # Examples
80///
81/// ```
82/// assert_eq!(faster_hex::hex_string_upper(&[0, 0xab, 0xff]), "00ABFF");
83/// ```
84#[cfg(feature = "alloc")]
85#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
86#[inline]
87pub fn hex_string_upper(src: &[u8]) -> String {
88    hex_string_custom_case(src, true)
89}
90
91#[inline]
92pub(crate) fn hex_encode_custom<'a>(
93    src: &[u8],
94    dst: &'a mut [u8],
95    upper_case: bool,
96) -> Result<&'a mut str, Error> {
97    // SAFETY: MaybeUninit has the same layout as u8. encode only writes initialized
98    // ASCII bytes; it never makes an existing byte uninitialized, including on error.
99    let output = unsafe { core::slice::from_raw_parts_mut(dst.as_mut_ptr().cast(), dst.len()) };
100    encode(src, output, upper_case)
101}
102
103// One boundary for caller-owned bytes, allocation spare capacity and formatting:
104// validate the size, initialize the exact ASCII prefix, then return its string view.
105#[inline]
106pub(crate) fn encode<'a>(
107    src: &[u8],
108    dst: &'a mut [MaybeUninit<u8>],
109    upper_case: bool,
110) -> Result<&'a mut str, Error> {
111    let len = src.len().checked_mul(2).ok_or(Error::Overflow)?;
112    if dst.len() < len {
113        return Err(Error::OutputTooSmall { required: len });
114    }
115    let dst = &mut dst[..len];
116    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
117    {
118        match crate::vectorization_support() {
119            crate::Vectorization::AVX2 | crate::Vectorization::AVX512 => {
120                // SAFETY: Dispatch checked AVX2 and the OS register state.
121                unsafe { hex_encode_avx2(src, dst, upper_case) }
122            }
123            crate::Vectorization::SSE41 => {
124                // SAFETY: Dispatch checks SSE4.1; dst has twice src.len() bytes.
125                unsafe { hex_encode_sse41(src, dst, upper_case) }
126            }
127            crate::Vectorization::None => hex_encode_custom_case_fallback(src, dst, upper_case),
128        }
129    }
130    #[cfg(target_arch = "aarch64")]
131    {
132        if src.len() < 8 {
133            hex_encode_pairs(src, dst, upper_case);
134        } else {
135            match crate::vectorization_support() {
136                crate::Vectorization::Neon => {
137                    // SAFETY: Dispatch checks NEON; dst has twice src.len() bytes.
138                    unsafe { hex_encode_neon(src, dst, upper_case) }
139                }
140                crate::Vectorization::None => hex_encode_custom_case_fallback(src, dst, upper_case),
141            }
142        }
143    }
144    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")))]
145    {
146        hex_encode_custom_case_fallback(src, dst, upper_case);
147    }
148    // SAFETY: Every backend initialized all len elements with ASCII, using the
149    // exact 1:2 length ratio established above. MaybeUninit<u8> has u8's layout,
150    // and ASCII is valid UTF-8. Spare capacity is excluded from this string.
151    Ok(unsafe {
152        core::str::from_utf8_unchecked_mut(core::slice::from_raw_parts_mut(
153            dst.as_mut_ptr().cast(),
154            len,
155        ))
156    })
157}
158
159/// Encodes all of `src` as lowercase hex into `dst` without allocation.
160///
161/// Every input byte produces two ASCII digits. `dst` must have room for at least
162/// `src.len() * 2` bytes; extra capacity is allowed and remains unchanged. The
163/// returned string covers only the written prefix and borrows only `dst`, so
164/// `src` can be dropped or reused while the result is still in use.
165///
166/// No prefix or separators are written. Empty input returns an empty string and
167/// leaves `dst` unchanged. For uppercase digits, use [`hex_encode_upper`].
168///
169/// # Errors
170///
171/// Returns [`Error::Overflow`] if the encoded length cannot be represented as a
172/// `usize`, or [`Error::OutputTooSmall`] if `dst` is too short. Its `required`
173/// field is the full output size in bytes. The entire destination is unchanged
174/// on either error; a short buffer is never partially filled.
175///
176/// # Examples
177///
178/// ```
179/// use faster_hex::hex_encode;
180///
181/// let mut destination = [0xff; 8];
182/// let text = hex_encode(&[0, 0xab, 0xcd], &mut destination)?;
183/// assert_eq!(text, "00abcd");
184/// // Only the returned prefix is modified.
185/// text.make_ascii_uppercase();
186/// assert_eq!(&destination[..6], b"00ABCD");
187/// assert_eq!(&destination[6..], &[0xff; 2]);
188/// # Ok::<(), faster_hex::Error>(())
189/// ```
190///
191/// An insufficient destination is reported without modifying it:
192///
193/// ```
194/// use faster_hex::{hex_encode, Error};
195///
196/// let mut destination = [0xa5; 3];
197/// assert!(matches!(hex_encode(&[0xab, 0xcd], &mut destination),
198///     Err(Error::OutputTooSmall { required: 4, .. })));
199/// assert_eq!(destination, [0xa5; 3]);
200/// ```
201#[inline]
202pub fn hex_encode<'a>(src: &[u8], dst: &'a mut [u8]) -> Result<&'a mut str, Error> {
203    hex_encode_custom(src, dst, false)
204}
205
206/// Encodes all of `src` as uppercase hex into `dst` without allocation.
207///
208/// This has the same length, borrowing and destination-preservation guarantees
209/// as [`hex_encode`], using `A` through `F` instead of `a` through `f`.
210///
211/// # Errors
212///
213/// Returns [`Error::Overflow`] or [`Error::OutputTooSmall`] under the same
214/// conditions as [`hex_encode`]. An error leaves the entire destination unchanged.
215///
216/// # Examples
217///
218/// ```
219/// let mut destination = [0; 6];
220/// assert_eq!(faster_hex::hex_encode_upper(&[0, 0xab, 0xff], &mut destination)?,
221///            "00ABFF");
222/// # Ok::<(), faster_hex::Error>(())
223/// ```
224#[inline]
225pub fn hex_encode_upper<'a>(src: &[u8], dst: &'a mut [u8]) -> Result<&'a mut str, Error> {
226    hex_encode_custom(src, dst, true)
227}
228
229/// Appends lowercase hexadecimal digits to `dst` and returns the appended suffix.
230///
231/// Existing text is preserved, including non-ASCII text. The returned mutable
232/// string borrows only `dst` and contains exactly `src.len() * 2` new ASCII
233/// digits. Empty input returns an empty suffix without changing the string.
234///
235/// Available with `alloc`. Existing capacity is reused without allocation when
236/// sufficient; otherwise the string grows. Call [`String::clear`] first to
237/// replace its contents while retaining the allocation. For a new string, use
238/// [`hex_string`].
239///
240/// # Panics
241///
242/// Panics if the resulting length cannot be represented or exceeds `isize::MAX`
243/// bytes. Allocation failure follows the allocator's error handling.
244///
245/// # Examples
246///
247/// ```
248/// use faster_hex::hex_append;
249///
250/// let mut text = String::with_capacity(64);
251/// text.push_str("hash: ");
252/// assert_eq!(hex_append(&[0xab, 0xcd], &mut text), "abcd");
253/// assert_eq!(text, "hash: abcd");
254///
255/// text.clear();
256/// hex_append(&[0, 1], &mut text);
257/// assert_eq!(text, "0001");
258/// ```
259#[cfg(feature = "alloc")]
260#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
261pub fn hex_append<'a>(src: &[u8], dst: &'a mut String) -> &'a mut str {
262    hex_append_custom(src, dst, false)
263}
264
265/// Appends uppercase hexadecimal digits to `dst` and returns the appended suffix.
266///
267/// Available with `alloc`. Existing content and capacity are handled as in
268/// [`hex_append`]; only the new hex digits use uppercase letters.
269///
270/// # Panics
271///
272/// Panics on the same capacity overflows as [`hex_append`]. Allocation failure
273/// follows the allocator's error handling.
274///
275/// # Examples
276///
277/// ```
278/// let mut text = String::from("hash: ");
279/// assert_eq!(faster_hex::hex_append_upper(&[0xab, 0xcd], &mut text), "ABCD");
280/// assert_eq!(text, "hash: ABCD");
281/// ```
282#[cfg(feature = "alloc")]
283#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
284pub fn hex_append_upper<'a>(src: &[u8], dst: &'a mut String) -> &'a mut str {
285    hex_append_custom(src, dst, true)
286}
287
288#[cfg(feature = "alloc")]
289fn hex_append_custom<'a>(src: &[u8], dst: &'a mut String, upper: bool) -> &'a mut str {
290    let base = dst.len();
291    let additional = src.len().checked_mul(2).expect("encoded length overflow");
292    let len = base
293        .checked_add(additional)
294        .expect("encoded length overflow");
295    dst.reserve(additional);
296    // SAFETY: Only spare capacity beyond the original string is written.
297    // encode fully initializes that suffix as ASCII before set_len commits it;
298    // a panic before the commit leaves the original string valid and unchanged.
299    unsafe {
300        let bytes = dst.as_mut_vec();
301        encode(src, bytes.spare_capacity_mut(), upper).expect("capacity reserved for encoding");
302        bytes.set_len(len);
303        core::str::from_utf8_unchecked_mut(&mut bytes[base..])
304    }
305}
306
307// Each backend receives the exact output prefix sized by encode.
308#[target_feature(enable = "avx2")]
309#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
310#[inline]
311pub(crate) unsafe fn hex_encode_avx2(src: &[u8], dst: &mut [MaybeUninit<u8>], upper_case: bool) {
312    if src.len() < 32 {
313        return hex_encode_sse41(src, dst, upper_case);
314    }
315    let table = if upper_case { TABLE_UPPER } else { TABLE_LOWER };
316    let table = _mm256_broadcastsi128_si256(_mm_loadu_si128(table.as_ptr().cast()));
317    let (blocks, tail) = src.as_chunks::<32>();
318    for (input, output) in blocks.iter().zip(dst.as_chunks_mut::<64>().0) {
319        encode_avx2_32(input, output, table);
320    }
321    if !tail.is_empty() {
322        if let (Some(input), Some(output)) = (src.last_chunk::<32>(), dst.last_chunk_mut::<64>()) {
323            encode_avx2_32(input, output, table);
324        }
325    }
326}
327
328#[inline]
329#[target_feature(enable = "avx2")]
330#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
331unsafe fn encode_avx2_32(src: &[u8; 32], dst: &mut [MaybeUninit<u8>; 64], table: __m256i) {
332    let bytes = _mm256_loadu_si256(src.as_ptr().cast());
333    let mask = _mm256_set1_epi8(15);
334    let high = _mm256_and_si256(_mm256_srli_epi16::<4>(bytes), mask);
335    let low = _mm256_and_si256(bytes, mask);
336    let a = _mm256_unpacklo_epi8(high, low);
337    let b = _mm256_unpackhi_epi8(high, low);
338    _mm256_storeu_si256(
339        dst.as_mut_ptr().cast(),
340        _mm256_shuffle_epi8(table, _mm256_permute2x128_si256::<0x20>(a, b)),
341    );
342    _mm256_storeu_si256(
343        dst.as_mut_ptr().add(32).cast(),
344        _mm256_shuffle_epi8(table, _mm256_permute2x128_si256::<0x31>(a, b)),
345    );
346}
347
348#[target_feature(enable = "sse4.1")]
349#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
350pub(crate) unsafe fn hex_encode_sse41(src: &[u8], dst: &mut [MaybeUninit<u8>], upper_case: bool) {
351    if src.len() < 16 {
352        return hex_encode_custom_case_fallback(src, dst, upper_case);
353    }
354    let table = if upper_case { TABLE_UPPER } else { TABLE_LOWER };
355    let table = _mm_loadu_si128(table.as_ptr().cast());
356    let (blocks, tail) = src.as_chunks::<16>();
357    for (input, output) in blocks.iter().zip(dst.as_chunks_mut::<32>().0) {
358        encode_sse41_16(input, output, table);
359    }
360    if !tail.is_empty() {
361        if let (Some(input), Some(output)) = (src.last_chunk::<16>(), dst.last_chunk_mut::<32>()) {
362            encode_sse41_16(input, output, table);
363        }
364    }
365}
366
367#[inline]
368#[target_feature(enable = "sse4.1")]
369#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
370unsafe fn encode_sse41_16(src: &[u8; 16], dst: &mut [MaybeUninit<u8>; 32], table: __m128i) {
371    let bytes = _mm_loadu_si128(src.as_ptr().cast());
372    let mask = _mm_set1_epi8(15);
373    let high = _mm_shuffle_epi8(table, _mm_and_si128(_mm_srli_epi16::<4>(bytes), mask));
374    let low = _mm_shuffle_epi8(table, _mm_and_si128(bytes, mask));
375    _mm_storeu_si128(dst.as_mut_ptr().cast(), _mm_unpacklo_epi8(high, low));
376    _mm_storeu_si128(
377        dst.as_mut_ptr().add(16).cast(),
378        _mm_unpackhi_epi8(high, low),
379    );
380}
381
382#[inline]
383#[target_feature(enable = "neon")]
384#[cfg(target_arch = "aarch64")]
385pub(crate) unsafe fn hex_encode_neon(src: &[u8], dst: &mut [MaybeUninit<u8>], upper_case: bool) {
386    if src.len() < 8 {
387        return hex_encode_custom_case_fallback(src, dst, upper_case);
388    }
389    let table = if upper_case { TABLE_UPPER } else { TABLE_LOWER };
390    let table = vld1q_u8(table.as_ptr());
391    if src.len() < 16 {
392        if let (Some(input), Some(output)) = (src.first_chunk::<8>(), dst.first_chunk_mut::<16>()) {
393            encode_neon_8(input, output, table);
394        }
395        if src.len() > 8 {
396            if let (Some(input), Some(output)) = (src.last_chunk::<8>(), dst.last_chunk_mut::<16>())
397            {
398                encode_neon_8(input, output, table);
399            }
400        }
401        return;
402    }
403    let (batches, rest) = src.as_chunks::<64>();
404    let (outputs, remaining) = dst.as_chunks_mut::<128>();
405    for (input, output) in batches.iter().zip(outputs) {
406        for (input, output) in input
407            .as_chunks::<16>()
408            .0
409            .iter()
410            .zip(output.as_chunks_mut::<32>().0)
411        {
412            encode_neon_16(input, output, table);
413        }
414    }
415    let (blocks, tail) = rest.as_chunks::<16>();
416    for (input, output) in blocks.iter().zip(remaining.as_chunks_mut::<32>().0) {
417        encode_neon_16(input, output, table);
418    }
419    if !tail.is_empty() {
420        // Re-encode the last complete block to cover a short tail. The overlap
421        // writes the same bytes and never accesses outside either slice.
422        match (src.last_chunk::<16>(), dst.last_chunk_mut::<32>()) {
423            (Some(input), Some(output)) => encode_neon_16(input, output, table),
424            _ => hex_encode_custom_case_fallback(src, dst, upper_case),
425        }
426    }
427}
428
429#[inline]
430#[target_feature(enable = "neon")]
431#[cfg(target_arch = "aarch64")]
432unsafe fn encode_neon_16(src: &[u8; 16], dst: &mut [MaybeUninit<u8>; 32], table: uint8x16_t) {
433    let bytes = vld1q_u8(src.as_ptr());
434    let high = vqtbl1q_u8(table, vshrq_n_u8::<4>(bytes));
435    let low = vqtbl1q_u8(table, vandq_u8(bytes, vdupq_n_u8(15)));
436    vst2q_u8(dst.as_mut_ptr().cast(), uint8x16x2_t(high, low));
437}
438
439const fn encode_pairs(alphabet: &[u8; 16]) -> [[u8; 2]; 256] {
440    let mut pairs = [[0; 2]; 256];
441    let mut byte = 0;
442    while byte < pairs.len() {
443        pairs[byte] = [alphabet[byte >> 4], alphabet[byte & 15]];
444        byte += 1;
445    }
446    pairs
447}
448
449static PAIRS_LOWER: [[u8; 2]; 256] = encode_pairs(TABLE_LOWER);
450static PAIRS_UPPER: [[u8; 2]; 256] = encode_pairs(TABLE_UPPER);
451
452pub(crate) fn hex_encode_custom_case_fallback(
453    src: &[u8],
454    dst: &mut [MaybeUninit<u8>],
455    upper_case: bool,
456) {
457    // Arithmetic lets LLVM vectorize longer inputs on baseline SIMD targets.
458    // Pair lookup avoids per-nibble branches for short or non-SIMD inputs.
459    if cfg!(any(
460        target_feature = "neon",
461        target_feature = "sse2",
462        target_feature = "simd128"
463    )) && src.len() >= 32
464    {
465        let letter = if upper_case { b'A' - 10 } else { b'a' - 10 };
466        let ascii = |nibble| nibble + if nibble < 10 { b'0' } else { letter };
467        for (&byte, pair) in src.iter().zip(dst.chunks_exact_mut(2)) {
468            pair[0].write(ascii(byte >> 4));
469            pair[1].write(ascii(byte & 15));
470        }
471    } else {
472        hex_encode_pairs(src, dst, upper_case);
473    }
474}
475
476#[inline]
477fn hex_encode_pairs(src: &[u8], dst: &mut [MaybeUninit<u8>], upper_case: bool) {
478    let table = if upper_case {
479        &PAIRS_UPPER
480    } else {
481        &PAIRS_LOWER
482    };
483    for (&byte, pair) in src.iter().zip(dst.as_chunks_mut::<2>().0) {
484        *pair = table[byte as usize].map(MaybeUninit::new);
485    }
486}
487
488#[cfg(test)]
489pub(crate) fn hex_encode_fallback(src: &[u8], dst: &mut [u8], upper: bool) {
490    // SAFETY: The scalar encoder only writes initialized ASCII to complete pairs;
491    // untouched bytes retain their previous initialized values, even for short dst.
492    let output = unsafe { core::slice::from_raw_parts_mut(dst.as_mut_ptr().cast(), dst.len()) };
493    hex_encode_custom_case_fallback(src, output, upper);
494}
495
496#[inline]
497#[target_feature(enable = "neon")]
498#[cfg(target_arch = "aarch64")]
499unsafe fn encode_neon_8(src: &[u8; 8], dst: &mut [MaybeUninit<u8>; 16], table: uint8x16_t) {
500    let bytes = vcombine_u8(vld1_u8(src.as_ptr()), vdup_n_u8(0));
501    let high = vqtbl1q_u8(table, vshrq_n_u8::<4>(bytes));
502    let low = vqtbl1q_u8(table, vandq_u8(bytes, vdupq_n_u8(15)));
503    vst1q_u8(dst.as_mut_ptr().cast(), vzip1q_u8(high, low));
504}