Skip to main content

faster_hex/
lib.rs

1//! Hexadecimal encoding, decoding and formatting for byte sequences.
2//!
3//! The core operations write into caller-provided buffers without allocating.
4//! SIMD implementations are selected internally where available; a portable scalar
5//! implementation provides the same behavior on other targets.
6//!
7//! # Getting started
8//!
9//! ```
10//! use faster_hex::{hex_decode, hex_encode};
11//!
12//! let mut encoded = [0; 10];
13//! let text = hex_encode(b"hello", &mut encoded)?;
14//! assert_eq!(text, "68656c6c6f");
15//!
16//! let mut decoded = [0; 5];
17//! assert_eq!(hex_decode(text.as_bytes(), &mut decoded)?, b"hello");
18//! # Ok::<(), faster_hex::Error>(())
19//! ```
20//!
21//! # Choosing an operation
22//!
23//! | Destination or task | API |
24//! | --- | --- |
25//! | Encode into a byte buffer | [`hex_encode`], [`hex_encode_upper`] |
26//! | Decode into a byte buffer | [`hex_decode`], [`hex_decode_with_case`] |
27//! | Decode an exact-length array | [`hex_decode_array`], [`hex_decode_array_with_case`] |
28//! | Format borrowed bytes into text | [`Hex`] with [`Display`](core::fmt::Display), [`LowerHex`](core::fmt::LowerHex) or [`UpperHex`](core::fmt::UpperHex) |
29//! | Check characters without decoding | [`hex_check`], [`hex_check_with_case`] |
30//!
31//! With `alloc`, `hex_string` and `hex_string_upper` create owned strings;
32//! `hex_append` and `hex_append_upper` reuse a string's capacity;
33//! `hex_decode_vec` and `hex_decode_vec_with_case` create owned byte vectors.
34//! The `heapless-08` feature provides fixed-capacity strings in `heapless_08`.
35//!
36//! # Conversion contracts
37//!
38//! Encoding writes two ASCII digits per input byte. Decoding consumes the complete
39//! input and requires an even number of ASCII hex digits. Both preserve leading
40//! zeroes and byte order; neither treats the input as an integer. Slice and owned
41//! decoders reject `0x` prefixes, whitespace, separators and non-ASCII characters.
42//! Serde adapters have their own explicit prefix policies.
43//!
44//! Successful slice conversions return exactly the written prefix, borrowing only
45//! the destination. Extra destination capacity remains unchanged. Empty inputs
46//! succeed. Every slice conversion error preserves the entire destination, even
47//! when invalid input occurs after a long valid prefix.
48//!
49//! [`Error`] exposes byte positions and required or exact lengths. Slice decoding
50//! checks odd input, destination capacity, then characters. Array decoding checks
51//! odd input, exact decoded length, then characters. Each function documents its
52//! full error contract. [`hex_check`] checks characters only and can accept odd
53//! lengths; checked decoders already perform validation, so a preceding check is
54//! unnecessary.
55//!
56//! # Crate features
57//!
58//! Features are additive. The defaults are `std` and `serde`. With defaults disabled
59//! and no optional features, the crate has no dependencies and needs neither an
60//! allocator nor the standard library.
61//!
62//! | Feature | Provides |
63//! | --- | --- |
64//! | None | Slice and fixed-array conversion, borrowed formatting, and `core::error::Error` |
65//! | `alloc` | Owned strings and byte vectors; appending to strings |
66//! | `std` | `alloc` and standard-library support in enabled dependencies |
67//! | `serde` | Serde adapters and `alloc`; also works without `std` |
68//! | `heapless-08` | Fixed-capacity strings using `heapless` 0.8, without requiring `alloc` |
69//! | `defmt-03` | `defmt` formatting for errors and case policies |
70//!
71//! For example, enable Serde without the standard library:
72//!
73//! ```toml
74//! [dependencies]
75//! faster-hex = { version = "1", default-features = false, features = ["serde"] }
76//! ```
77//!
78//! # Platforms
79//!
80//! On x86 and x86-64, implementations use SSE4.1, AVX2 and, for checking and
81//! decoding, AVX-512BW. Runtime detection checks CPU and operating-system support
82//! for features not enabled at compile time. AArch64 targets that guarantee NEON
83//! use it directly. Other configurations use the portable fallback. Backend
84//! selection, SIMD thresholds and instruction sequences are implementation details;
85//! no public backend selection or architecture-specific call is required.
86//!
87//! The minimum supported Rust version is 1.95.0 throughout the 1.0.x line.
88#![cfg_attr(not(any(test, feature = "std")), no_std)]
89#![warn(missing_docs)]
90#![cfg_attr(docsrs, feature(doc_cfg))]
91#![cfg_attr(
92    feature = "alloc",
93    doc = r#"
94# Owned output and capacity reuse
95
96[`hex_decode_vec`] returns owned decoded bytes; [`hex_string`] returns owned
97text. [`hex_append`] preserves existing text and returns only its new suffix.
98These functions follow the allocator's normal error handling. Allocation failures
99are not codec errors.
100
101```
102use faster_hex::{hex_append, hex_decode_vec};
103
104let bytes = hex_decode_vec(b"00aB")?;
105let mut text = String::with_capacity(64);
106text.push_str("id: ");
107assert_eq!(hex_append(&bytes, &mut text), "00ab");
108assert_eq!(text, "id: 00ab");
109# Ok::<(), faster_hex::Error>(())
110```
111"#
112)]
113#![cfg_attr(
114    feature = "serde",
115    doc = r##"
116# Serde adapters
117
118The default `#[serde(with = "faster_hex")]` adapter writes lowercase hex with a
119`0x` prefix and accepts either letter case when reading. A required prefix is
120exactly `0x`, never `0X`. Named modules select the wire policy:
121
122| Module | Prefix | Serialization | Accepted letters |
123| --- | --- | --- | --- |
124| [`withpfx_ignorecase`] (default) | `0x` | Lowercase | Either case |
125| [`nopfx_ignorecase`] | None | Lowercase | Either case |
126| [`withpfx_lowercase`] | `0x` | Lowercase | Lowercase |
127| [`nopfx_lowercase`] | None | Lowercase | Lowercase |
128| [`withpfx_uppercase`] | `0x` | Uppercase | Uppercase |
129| [`nopfx_uppercase`] | None | Uppercase | Uppercase |
130
131Each policy also has an `option_` counterpart, an `array` submodule, and a
132`deserialize_bounded` function. Present byte values use strings, including in
133binary formats. Option adapters preserve the format's `Some`/`None` tags; an empty
134present value stays distinct from `None`. For missing struct fields, add
135`#[serde(default)]` alongside the `with` attribute.
136
137Use [`array`](mod@crate::array) for exact-length arrays. Generic adapters instead
138collect into [`FromIterator<u8>`](core::iter::FromIterator) containers; bounded
139collectors can panic when full. [`deserialize_bounded`] limits decoded bytes before output allocation,
140but does not bound the format's input storage or a custom collector's allocations.
141
142```
143#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
144struct Record {
145    #[serde(with = "faster_hex::array")]
146    id: [u8; 2],
147    #[serde(default, with = "faster_hex::option_nopfx_lowercase::array")]
148    extra: Option<[u8; 2]>,
149}
150
151let record = Record { id: [0xab, 1], extra: Some([0xcd, 2]) };
152let json = serde_json::to_string(&record)?;
153assert_eq!(json, r#"{"id":"0xab01","extra":"cd02"}"#);
154assert_eq!(serde_json::from_str::<Record>(&json)?, record);
155assert_eq!(serde_json::from_str::<Record>(r#"{"id":"0xab01"}"#)?.extra, None);
156# Ok::<(), serde_json::Error>(())
157```
158"##
159)]
160
161#[cfg(feature = "alloc")]
162extern crate alloc;
163
164mod decode;
165mod encode;
166mod error;
167mod format;
168
169// Both cargo-fuzz and cargo-afl set this cfg. It is deliberately not a Cargo
170// feature: regular builds (including --all-features) have no backend API.
171// Unit tests share the same safe adapters instead of duplicating unsafe calls.
172#[cfg(any(test, fuzzing))]
173#[doc(hidden)]
174#[allow(missing_docs)]
175pub mod fuzzing;
176
177#[cfg(feature = "heapless-08")]
178#[cfg_attr(docsrs, doc(cfg(feature = "heapless-08")))]
179pub mod heapless_08;
180
181#[cfg(feature = "serde")]
182mod serde;
183
184pub use crate::decode::{
185    hex_check, hex_check_with_case, hex_decode, hex_decode_array, hex_decode_array_with_case,
186    hex_decode_with_case, CheckCase,
187};
188pub use crate::encode::{hex_encode, hex_encode_upper};
189
190#[cfg(feature = "alloc")]
191pub use crate::encode::{hex_append, hex_append_upper, hex_string, hex_string_upper};
192
193#[cfg(feature = "alloc")]
194pub use crate::decode::{hex_decode_vec, hex_decode_vec_with_case};
195
196pub use crate::error::Error;
197pub use crate::format::Hex;
198
199#[cfg(feature = "serde")]
200pub use crate::serde::withpfx_ignorecase::array;
201
202#[cfg(feature = "serde")]
203pub use crate::serde::{
204    deserialize, deserialize_bounded, nopfx_ignorecase, nopfx_lowercase, nopfx_uppercase,
205    option_nopfx_ignorecase, option_nopfx_lowercase, option_nopfx_uppercase,
206    option_withpfx_ignorecase, option_withpfx_lowercase, option_withpfx_uppercase, serialize,
207    withpfx_ignorecase, withpfx_lowercase, withpfx_uppercase,
208};
209
210#[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64"))]
211#[derive(Copy, Clone, PartialEq, Eq, Debug)]
212#[cfg_attr(feature = "defmt-03", derive(defmt::Format))]
213#[allow(dead_code, reason = "Static ISA baselines bypass runtime detection")]
214pub(crate) enum Vectorization {
215    None = 0,
216    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
217    SSE41 = 1,
218    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
219    AVX2 = 2,
220    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
221    // Keep dispatch sparse so adding this backend does not create a jump table.
222    AVX512 = 128,
223    #[cfg(target_arch = "aarch64")]
224    Neon = 3,
225}
226
227#[inline(always)]
228#[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64"))]
229pub(crate) fn vectorization_support() -> Vectorization {
230    #[cfg(all(
231        any(target_arch = "x86", target_arch = "x86_64"),
232        target_feature = "avx512bw",
233        not(miri)
234    ))]
235    {
236        return Vectorization::AVX512;
237    }
238
239    #[cfg(all(
240        any(target_arch = "x86", target_arch = "x86_64"),
241        target_feature = "avx2",
242        not(target_feature = "avx512bw"),
243        not(miri)
244    ))]
245    {
246        return Vectorization::AVX2;
247    }
248
249    #[cfg(all(
250        any(target_arch = "x86", target_arch = "x86_64"),
251        target_feature = "sse",
252        not(target_feature = "avx2"),
253        not(miri)
254    ))]
255    {
256        use core::sync::atomic::{AtomicU8, Ordering};
257        static FLAGS: AtomicU8 = AtomicU8::new(u8::MAX);
258
259        // Relaxed is enough: racing initializers detect the same CPU features.
260        return match FLAGS.load(Ordering::Relaxed) {
261            0 => Vectorization::None,
262            1 => Vectorization::SSE41,
263            2 => Vectorization::AVX2,
264            128 => Vectorization::AVX512,
265            _ => {
266                let backend = vectorization_support_no_cache_x86();
267                FLAGS.store(backend as u8, Ordering::Relaxed);
268                backend
269            }
270        };
271    }
272
273    #[cfg(all(target_arch = "aarch64", target_feature = "neon", not(miri)))]
274    {
275        return Vectorization::Neon;
276    }
277
278    #[allow(unreachable_code)]
279    Vectorization::None
280}
281
282#[cfg(all(
283    any(target_arch = "x86", target_arch = "x86_64"),
284    target_feature = "sse",
285    not(target_feature = "avx2"),
286    not(miri)
287))]
288#[cold]
289fn vectorization_support_no_cache_x86() -> Vectorization {
290    #[cfg(target_arch = "x86")]
291    use core::arch::x86::__cpuid_count;
292    #[cfg(target_arch = "x86_64")]
293    use core::arch::x86_64::__cpuid_count;
294
295    // SGX doesn't support CPUID,
296    // If there's no SSE there might not be CPUID and there's no SSE4.1/AVX2
297    if cfg!(target_env = "sgx") || !cfg!(target_feature = "sse") {
298        return Vectorization::None;
299    }
300
301    // Query only supported basic leaves: out-of-range CPUID results may describe
302    // a different leaf, whose bits must not be interpreted as AVX2 support.
303    let max_leaf = __cpuid_count(0, 0).eax;
304    if max_leaf < 1 {
305        return Vectorization::None;
306    }
307    let proc_info_ecx = __cpuid_count(1, 0).ecx;
308    let have_sse4 = (proc_info_ecx >> 19) & 1 == 1;
309    // If there's no SSE4 there can't be AVX2.
310    if !have_sse4 {
311        return Vectorization::None;
312    }
313
314    let have_xsave = (proc_info_ecx >> 26) & 1 == 1;
315    let have_osxsave = (proc_info_ecx >> 27) & 1 == 1;
316    let have_avx = (proc_info_ecx >> 28) & 1 == 1;
317    if max_leaf >= 7 && have_xsave && have_osxsave && have_avx {
318        // SAFETY: XSAVE is available and enabled by the OS; leaf 7 exists.
319        return unsafe { avx_support_no_cache_x86() };
320    }
321    Vectorization::SSE41
322}
323
324// We enable xsave so it can inline the _xgetbv call.
325// # Safety: Requires XSAVE, OSXSAVE and AVX, and CPUID basic leaf 7 must exist.
326#[target_feature(enable = "xsave")]
327#[cfg(all(
328    any(target_arch = "x86", target_arch = "x86_64"),
329    target_feature = "sse",
330    not(target_feature = "avx2"),
331    not(miri)
332))]
333#[cold]
334unsafe fn avx_support_no_cache_x86() -> Vectorization {
335    #[cfg(target_arch = "x86")]
336    use core::arch::x86::{__cpuid_count, _xgetbv};
337    #[cfg(target_arch = "x86_64")]
338    use core::arch::x86_64::{__cpuid_count, _xgetbv};
339
340    let xcr0 = _xgetbv(0);
341    let os_avx_support = xcr0 & 6 == 6;
342    if os_avx_support {
343        let extended_features_ebx = __cpuid_count(7, 0).ebx;
344        let have_avx2 = (extended_features_ebx >> 5) & 1 == 1;
345        if have_avx2 {
346            // AVX-512 needs opmask and both ZMM state components in addition
347            // to SSE/AVX state. CPUID alone is insufficient for safe dispatch.
348            let avx512 = (1 << 16) | (1 << 30); // AVX-512F and AVX-512BW.
349            if xcr0 & 0xe6 == 0xe6 && extended_features_ebx & avx512 == avx512 {
350                return Vectorization::AVX512;
351            }
352            return Vectorization::AVX2;
353        }
354    }
355    Vectorization::SSE41
356}
357
358#[cfg(test)]
359mod tests;