1#![cfg_attr(not(feature = "std"), no_std)]
20#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
21#![cfg_attr(
22 feature = "nightly",
23 feature(core_intrinsics, inline_const),
24 allow(internal_features, stable_features)
25)]
26#![cfg_attr(feature = "portable-simd", feature(portable_simd))]
27#![warn(
28 missing_copy_implementations,
29 missing_debug_implementations,
30 missing_docs,
31 unreachable_pub,
32 unsafe_op_in_unsafe_fn,
33 clippy::missing_const_for_fn,
34 clippy::missing_inline_in_public_items,
35 clippy::all,
36 rustdoc::all
37)]
38#![cfg_attr(not(any(test, feature = "__fuzzing")), warn(unused_crate_dependencies))]
39#![deny(unused_must_use, rust_2018_idioms)]
40#![allow(
41 clippy::cast_lossless,
42 clippy::inline_always,
43 clippy::let_unit_value,
44 clippy::must_use_candidate,
45 clippy::wildcard_imports,
46 unsafe_op_in_unsafe_fn,
47 unused_unsafe
48)]
49
50#[cfg(feature = "alloc")]
51#[allow(unused_imports)]
52#[macro_use]
53extern crate alloc;
54
55use cfg_if::cfg_if;
56
57#[cfg(feature = "alloc")]
58#[allow(unused_imports)]
59use alloc::{string::String, vec::Vec};
60
61#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
63use cpufeatures as _;
64
65mod arch;
66use arch::{generic, imp};
67
68mod impl_core;
69
70pub mod traits;
71#[cfg(feature = "alloc")]
72pub use traits::ToHexExt;
73
74cfg_if! {
77 if #[cfg(feature = "hex")] {
78 pub use hex;
79 #[doc(inline)]
80 pub use hex::{FromHex, FromHexError, ToHex};
81 } else {
82 mod error;
83 pub use error::FromHexError;
84
85 #[allow(deprecated)]
86 pub use traits::{FromHex, ToHex};
87 }
88}
89
90cfg_if! {
92 if #[cfg(feature = "nightly")] {
93 #[allow(unused_imports)]
95 use core::intrinsics::{likely, unlikely};
96
97 macro_rules! maybe_const_assert {
99 ($($tt:tt)*) => {
100 const { assert!($($tt)*) }
101 };
102 }
103 } else {
104 #[allow(unused_imports)]
105 use core::convert::{identity as likely, identity as unlikely};
106
107 macro_rules! maybe_const_assert {
108 ($($tt:tt)*) => {
109 assert!($($tt)*)
110 };
111 }
112 }
113}
114
115cfg_if! {
117 if #[cfg(feature = "serde")] {
118 pub mod serde;
119
120 #[doc(no_inline)]
121 pub use self::serde::deserialize;
122 #[cfg(feature = "alloc")]
123 #[doc(no_inline)]
124 pub use self::serde::{serialize, serialize_upper};
125 }
126}
127
128mod buffer;
129pub use buffer::Buffer;
130
131pub const HEX_CHARS_LOWER: &[u8; 16] = b"0123456789abcdef";
133
134pub const HEX_CHARS_UPPER: &[u8; 16] = b"0123456789ABCDEF";
136
137pub const HEX_DECODE_LUT: &[u8; 256] = &make_decode_lut();
141
142pub const NIL: u8 = u8::MAX;
144
145#[inline]
154pub const fn const_encode<const N: usize, const PREFIX: bool>(
155 input: &[u8; N],
156) -> Buffer<N, PREFIX> {
157 Buffer::new().const_format(input)
158}
159
160#[inline]
176pub fn encode_to_slice<T: AsRef<[u8]>>(input: T, output: &mut [u8]) -> Result<(), FromHexError> {
177 encode_to_slice_inner::<false>(input.as_ref(), output)
178}
179
180#[inline]
196pub fn encode_to_slice_upper<T: AsRef<[u8]>>(
197 input: T,
198 output: &mut [u8],
199) -> Result<(), FromHexError> {
200 encode_to_slice_inner::<true>(input.as_ref(), output)
201}
202
203#[cfg(feature = "alloc")]
217#[inline]
218pub fn encode<T: AsRef<[u8]>>(data: T) -> String {
219 encode_inner::<false, false>(data.as_ref())
220}
221
222#[cfg(feature = "alloc")]
233#[inline]
234pub fn encode_upper<T: AsRef<[u8]>>(data: T) -> String {
235 encode_inner::<true, false>(data.as_ref())
236}
237
238#[cfg(feature = "alloc")]
249#[inline]
250pub fn encode_prefixed<T: AsRef<[u8]>>(data: T) -> String {
251 encode_inner::<false, true>(data.as_ref())
252}
253
254#[cfg(feature = "alloc")]
265#[inline]
266pub fn encode_upper_prefixed<T: AsRef<[u8]>>(data: T) -> String {
267 encode_inner::<true, true>(data.as_ref())
268}
269
270#[inline]
286pub const fn const_check(input: &[u8]) -> Result<(), FromHexError> {
287 if input.len() % 2 != 0 {
288 return Err(FromHexError::OddLength);
289 }
290 let input = strip_prefix(input);
291 if const_check_raw(input) {
292 Ok(())
293 } else {
294 Err(unsafe { invalid_hex_error(input) })
295 }
296}
297
298#[inline]
320pub const fn const_check_raw(input: &[u8]) -> bool {
321 generic::check(input)
322}
323
324#[inline]
336pub fn check<T: AsRef<[u8]>>(input: T) -> Result<(), FromHexError> {
337 #[allow(clippy::missing_const_for_fn)]
338 fn check_inner(input: &[u8]) -> Result<(), FromHexError> {
339 if input.len() % 2 != 0 {
340 return Err(FromHexError::OddLength);
341 }
342 let stripped = strip_prefix(input);
343 if imp::check(stripped) {
344 Ok(())
345 } else {
346 let mut e = unsafe { invalid_hex_error(stripped) };
347 if let FromHexError::InvalidHexCharacter { ref mut index, .. } = e {
348 *index += input.len() - stripped.len();
349 }
350 Err(e)
351 }
352 }
353
354 check_inner(input.as_ref())
355}
356
357#[inline]
375pub fn check_raw<T: AsRef<[u8]>>(input: T) -> bool {
376 imp::check(input.as_ref())
377}
378
379#[inline]
406pub const fn const_decode_to_array<const N: usize>(input: &[u8]) -> Result<[u8; N], FromHexError> {
407 if input.len() % 2 != 0 {
408 return Err(FromHexError::OddLength);
409 }
410 let input = strip_prefix(input);
411 if input.len() != N * 2 {
412 return Err(FromHexError::InvalidStringLength);
413 }
414 match const_decode_to_array_impl(input) {
415 Some(output) => Ok(output),
416 None => Err(unsafe { invalid_hex_error(input) }),
417 }
418}
419
420const fn const_decode_to_array_impl<const N: usize>(input: &[u8]) -> Option<[u8; N]> {
421 macro_rules! next {
422 ($var:ident, $i:expr) => {
423 let hex = unsafe { *input.as_ptr().add($i) };
424 let $var = HEX_DECODE_LUT[hex as usize];
425 if $var == NIL {
426 return None;
427 }
428 };
429 }
430
431 let mut output = [0; N];
432 debug_assert!(input.len() == N * 2);
433 let mut i = 0;
434 while i < output.len() {
435 next!(high, i * 2);
436 next!(low, i * 2 + 1);
437 output[i] = high << 4 | low;
438 i += 1;
439 }
440 Some(output)
441}
442
443#[cfg(feature = "alloc")]
471#[inline]
472pub fn decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, FromHexError> {
473 fn decode_inner(input: &[u8]) -> Result<Vec<u8>, FromHexError> {
474 if unlikely(input.len() % 2 != 0) {
475 return Err(FromHexError::OddLength);
476 }
477 let input = strip_prefix(input);
478
479 let len = input.len() / 2;
481 let mut output = Vec::with_capacity(len);
482 #[allow(clippy::uninit_vec)]
484 unsafe {
485 output.set_len(len);
486 }
487
488 unsafe { decode_checked(input, &mut output) }.map(|()| output)
490 }
491
492 decode_inner(input.as_ref())
493}
494
495#[inline]
519pub fn decode_to_slice<T: AsRef<[u8]>>(input: T, output: &mut [u8]) -> Result<(), FromHexError> {
520 decode_to_slice_inner(input.as_ref(), output)
521}
522
523#[inline]
546pub fn decode_to_array<T: AsRef<[u8]>, const N: usize>(input: T) -> Result<[u8; N], FromHexError> {
547 fn decode_to_array_inner<const N: usize>(input: &[u8]) -> Result<[u8; N], FromHexError> {
548 let mut output = impl_core::uninit_array();
549 let output_slice = unsafe { impl_core::slice_assume_init_mut(&mut output) };
551 decode_to_slice_inner(input, output_slice)
553 .map(|()| unsafe { impl_core::array_assume_init(output) })
554 }
555
556 decode_to_array_inner(input.as_ref())
557}
558
559#[cfg(feature = "alloc")]
560fn encode_inner<const UPPER: bool, const PREFIX: bool>(data: &[u8]) -> String {
561 let capacity = PREFIX as usize * 2 + data.len() * 2;
562 let mut buf = Vec::<u8>::with_capacity(capacity);
563 #[allow(clippy::uninit_vec)]
565 unsafe {
566 buf.set_len(capacity)
567 };
568 let mut output = buf.as_mut_ptr();
569 if PREFIX {
570 unsafe {
572 output.add(0).write(b'0');
573 output.add(1).write(b'x');
574 output = output.add(2);
575 }
576 }
577 unsafe { imp::encode::<UPPER>(data, output) };
579 unsafe { String::from_utf8_unchecked(buf) }
581}
582
583fn encode_to_slice_inner<const UPPER: bool>(
584 input: &[u8],
585 output: &mut [u8],
586) -> Result<(), FromHexError> {
587 if unlikely(output.len() != 2 * input.len()) {
588 return Err(FromHexError::InvalidStringLength);
589 }
590 unsafe { imp::encode::<UPPER>(input, output.as_mut_ptr()) };
592 Ok(())
593}
594
595fn decode_to_slice_inner(input: &[u8], output: &mut [u8]) -> Result<(), FromHexError> {
596 if unlikely(input.len() % 2 != 0) {
597 return Err(FromHexError::OddLength);
598 }
599 let input = strip_prefix(input);
600 if unlikely(output.len() != input.len() / 2) {
601 return Err(FromHexError::InvalidStringLength);
602 }
603 unsafe { decode_checked(input, output) }
605}
606
607#[inline]
611unsafe fn decode_checked(input: &[u8], output: &mut [u8]) -> Result<(), FromHexError> {
612 debug_assert_eq!(output.len(), input.len() / 2);
613
614 if imp::USE_CHECK_FN {
615 if imp::check(input) {
617 unsafe { imp::decode_unchecked(input, output) };
618 return Ok(());
619 }
620 } else {
621 if unsafe { imp::decode_checked(input, output) } {
623 return Ok(());
624 }
625 }
626
627 Err(unsafe { invalid_hex_error(input) })
628}
629
630#[inline]
631const fn byte2hex<const UPPER: bool>(byte: u8) -> (u8, u8) {
632 let table = get_chars_table::<UPPER>();
633 let high = table[(byte >> 4) as usize];
634 let low = table[(byte & 0x0f) as usize];
635 (high, low)
636}
637
638#[inline]
639const fn strip_prefix(bytes: &[u8]) -> &[u8] {
640 match bytes {
641 [b'0', b'x', rest @ ..] => rest,
642 _ => bytes,
643 }
644}
645
646#[cold]
652#[cfg_attr(debug_assertions, track_caller)]
653const unsafe fn invalid_hex_error(input: &[u8]) -> FromHexError {
654 let mut index = None;
656 let mut iter = input;
657 while let [byte, rest @ ..] = iter {
658 if HEX_DECODE_LUT[*byte as usize] == NIL {
659 index = Some(input.len() - rest.len() - 1);
660 break;
661 }
662 iter = rest;
663 }
664
665 let index = match index {
666 Some(index) => index,
667 None => {
668 if cfg!(debug_assertions) {
669 panic!("input was valid but `check` failed")
670 } else {
671 unsafe { core::hint::unreachable_unchecked() }
672 }
673 }
674 };
675
676 FromHexError::InvalidHexCharacter {
677 c: input[index] as char,
678 index,
679 }
680}
681
682#[inline(always)]
683const fn get_chars_table<const UPPER: bool>() -> &'static [u8; 16] {
684 if UPPER {
685 HEX_CHARS_UPPER
686 } else {
687 HEX_CHARS_LOWER
688 }
689}
690
691const fn make_decode_lut() -> [u8; 256] {
692 let mut lut = [0; 256];
693 let mut i = 0u8;
694 loop {
695 lut[i as usize] = match i {
696 b'0'..=b'9' => i - b'0',
697 b'A'..=b'F' => i - b'A' + 10,
698 b'a'..=b'f' => i - b'a' + 10,
699 _ => NIL,
701 };
702 if i == NIL {
703 break;
704 }
705 i += 1;
706 }
707 lut
708}
709
710#[allow(
711 missing_docs,
712 unused,
713 clippy::all,
714 clippy::missing_inline_in_public_items
715)]
716#[cfg(all(feature = "__fuzzing", not(miri)))]
717#[doc(hidden)]
718pub mod fuzzing {
719 use super::*;
720 use proptest::test_runner::TestCaseResult;
721 use proptest::{prop_assert, prop_assert_eq};
722 use std::fmt::Write;
723
724 pub fn fuzz(data: &[u8]) -> TestCaseResult {
725 self::encode(&data)?;
726 self::decode(&data)?;
727 Ok(())
728 }
729
730 pub fn encode(input: &[u8]) -> TestCaseResult {
731 test_buffer::<8, 16>(input)?;
732 test_buffer::<20, 40>(input)?;
733 test_buffer::<32, 64>(input)?;
734 test_buffer::<64, 128>(input)?;
735 test_buffer::<128, 256>(input)?;
736
737 let encoded = crate::encode(input);
738 let expected = mk_expected(input);
739 prop_assert_eq!(&encoded, &expected);
740
741 let decoded = crate::decode(&encoded).unwrap();
742 prop_assert_eq!(decoded, input);
743
744 Ok(())
745 }
746
747 pub fn decode(input: &[u8]) -> TestCaseResult {
748 if let Ok(decoded) = crate::decode(input) {
749 let input_len = strip_prefix(input).len() / 2;
750 prop_assert_eq!(decoded.len(), input_len);
751 }
752
753 Ok(())
754 }
755
756 fn mk_expected(bytes: &[u8]) -> String {
757 let mut s = String::with_capacity(bytes.len() * 2);
758 for i in bytes {
759 write!(s, "{i:02x}").unwrap();
760 }
761 s
762 }
763
764 fn test_buffer<const N: usize, const LEN: usize>(bytes: &[u8]) -> TestCaseResult {
765 if let Ok(bytes) = <&[u8; N]>::try_from(bytes) {
766 let mut buffer = Buffer::<N, false>::new();
767 let string = buffer.format(bytes).to_string();
768 prop_assert_eq!(string.len(), bytes.len() * 2);
769 prop_assert_eq!(string.as_bytes(), buffer.as_byte_array::<LEN>());
770 prop_assert_eq!(string.as_str(), buffer.as_str());
771 prop_assert_eq!(string.as_str(), mk_expected(bytes));
772
773 let mut buffer = Buffer::<N, true>::new();
774 let prefixed = buffer.format(bytes).to_string();
775 prop_assert_eq!(prefixed.len(), 2 + bytes.len() * 2);
776 prop_assert_eq!(prefixed.as_str(), buffer.as_str());
777 prop_assert_eq!(prefixed.as_str(), format!("0x{string}"));
778
779 prop_assert_eq!(decode_to_array(&string), Ok(*bytes));
780 prop_assert_eq!(decode_to_array(&prefixed), Ok(*bytes));
781 prop_assert_eq!(const_decode_to_array(string.as_bytes()), Ok(*bytes));
782 prop_assert_eq!(const_decode_to_array(prefixed.as_bytes()), Ok(*bytes));
783 }
784
785 Ok(())
786 }
787
788 proptest::proptest! {
789 #![proptest_config(proptest::prelude::ProptestConfig {
790 cases: 1024,
791 ..Default::default()
792 })]
793
794 #[test]
795 fn fuzz_encode(s in ".+") {
796 encode(s.as_bytes())?;
797 }
798
799 #[test]
800 fn fuzz_check_true(s in "[0-9a-fA-F]+") {
801 let s = s.as_bytes();
802 prop_assert!(crate::check_raw(s));
803 prop_assert!(crate::const_check_raw(s));
804 if s.len() % 2 == 0 {
805 prop_assert!(crate::check(s).is_ok());
806 prop_assert!(crate::const_check(s).is_ok());
807 }
808 }
809
810 #[test]
811 fn fuzz_check_false(s in ".{16}[0-9a-fA-F]+") {
812 let s = s.as_bytes();
813 prop_assert!(crate::check(s).is_err());
814 prop_assert!(crate::const_check(s).is_err());
815 prop_assert!(!crate::check_raw(s));
816 prop_assert!(!crate::const_check_raw(s));
817 }
818 }
819}