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
74mod display;
75pub use display::display;
76
77cfg_if! {
80 if #[cfg(feature = "hex")] {
81 pub use hex;
82 #[doc(inline)]
83 pub use hex::{FromHex, FromHexError, ToHex};
84 } else {
85 mod error;
86 pub use error::FromHexError;
87
88 #[allow(deprecated)]
89 pub use traits::{FromHex, ToHex};
90 }
91}
92
93cfg_if! {
95 if #[cfg(feature = "nightly")] {
96 #[allow(unused_imports)]
98 use core::intrinsics::{likely, unlikely};
99
100 macro_rules! maybe_const_assert {
102 ($($tt:tt)*) => {
103 const { assert!($($tt)*) }
104 };
105 }
106 } else {
107 #[allow(unused_imports)]
108 use core::convert::{identity as likely, identity as unlikely};
109
110 macro_rules! maybe_const_assert {
111 ($($tt:tt)*) => {
112 assert!($($tt)*)
113 };
114 }
115 }
116}
117
118cfg_if! {
120 if #[cfg(feature = "serde")] {
121 pub mod serde;
122
123 #[doc(no_inline)]
124 pub use self::serde::deserialize;
125 #[cfg(feature = "alloc")]
126 #[doc(no_inline)]
127 pub use self::serde::{serialize, serialize_upper};
128 }
129}
130
131mod buffer;
132pub use buffer::Buffer;
133
134mod output;
135use output::Output;
136
137pub const HEX_CHARS_LOWER: &[u8; 16] = b"0123456789abcdef";
139
140pub const HEX_CHARS_UPPER: &[u8; 16] = b"0123456789ABCDEF";
142
143pub const HEX_DECODE_LUT: &[u8; 256] = &make_decode_lut();
147
148pub const NIL: u8 = u8::MAX;
150
151#[inline]
160pub const fn const_encode<const N: usize, const PREFIX: bool>(
161 input: &[u8; N],
162) -> Buffer<N, PREFIX> {
163 Buffer::new().const_format(input)
164}
165
166#[inline]
182pub fn encode_to_slice<T: AsRef<[u8]>>(input: T, output: &mut [u8]) -> Result<(), FromHexError> {
183 encode_to_slice_inner::<false>(input.as_ref(), output)
184}
185
186#[inline]
202pub fn encode_to_slice_upper<T: AsRef<[u8]>>(
203 input: T,
204 output: &mut [u8],
205) -> Result<(), FromHexError> {
206 encode_to_slice_inner::<true>(input.as_ref(), output)
207}
208
209#[cfg(feature = "alloc")]
223#[inline]
224pub fn encode<T: AsRef<[u8]>>(data: T) -> String {
225 encode_inner::<false, false>(data.as_ref())
226}
227
228#[cfg(feature = "alloc")]
239#[inline]
240pub fn encode_upper<T: AsRef<[u8]>>(data: T) -> String {
241 encode_inner::<true, false>(data.as_ref())
242}
243
244#[cfg(feature = "alloc")]
255#[inline]
256pub fn encode_prefixed<T: AsRef<[u8]>>(data: T) -> String {
257 encode_inner::<false, true>(data.as_ref())
258}
259
260#[cfg(feature = "alloc")]
271#[inline]
272pub fn encode_upper_prefixed<T: AsRef<[u8]>>(data: T) -> String {
273 encode_inner::<true, true>(data.as_ref())
274}
275
276#[inline]
292pub const fn const_check(input: &[u8]) -> Result<(), FromHexError> {
293 if input.len() % 2 != 0 {
294 return Err(FromHexError::OddLength);
295 }
296 let input = strip_prefix(input);
297 if const_check_raw(input) {
298 Ok(())
299 } else {
300 Err(unsafe { invalid_hex_error(input) })
301 }
302}
303
304#[inline]
326pub const fn const_check_raw(input: &[u8]) -> bool {
327 generic::check(input)
328}
329
330#[inline]
342pub fn check<T: AsRef<[u8]>>(input: T) -> Result<(), FromHexError> {
343 #[allow(clippy::missing_const_for_fn)]
344 fn check_inner(input: &[u8]) -> Result<(), FromHexError> {
345 if input.len() % 2 != 0 {
346 return Err(FromHexError::OddLength);
347 }
348 let stripped = strip_prefix(input);
349 if imp::check(stripped) {
350 Ok(())
351 } else {
352 let mut e = unsafe { invalid_hex_error(stripped) };
353 if let FromHexError::InvalidHexCharacter { ref mut index, .. } = e {
354 *index += input.len() - stripped.len();
355 }
356 Err(e)
357 }
358 }
359
360 check_inner(input.as_ref())
361}
362
363#[inline]
381pub fn check_raw<T: AsRef<[u8]>>(input: T) -> bool {
382 imp::check(input.as_ref())
383}
384
385#[inline]
412pub const fn const_decode_to_array<const N: usize>(input: &[u8]) -> Result<[u8; N], FromHexError> {
413 if input.len() % 2 != 0 {
414 return Err(FromHexError::OddLength);
415 }
416 let input = strip_prefix(input);
417 if input.len() != N * 2 {
418 return Err(FromHexError::InvalidStringLength);
419 }
420 match const_decode_to_array_impl(input) {
421 Some(output) => Ok(output),
422 None => Err(unsafe { invalid_hex_error(input) }),
423 }
424}
425
426const fn const_decode_to_array_impl<const N: usize>(input: &[u8]) -> Option<[u8; N]> {
427 macro_rules! next {
428 ($var:ident, $i:expr) => {
429 let hex = unsafe { *input.as_ptr().add($i) };
430 let $var = HEX_DECODE_LUT[hex as usize];
431 if $var == NIL {
432 return None;
433 }
434 };
435 }
436
437 let mut output = [0; N];
438 debug_assert!(input.len() == N * 2);
439 let mut i = 0;
440 while i < output.len() {
441 next!(high, i * 2);
442 next!(low, i * 2 + 1);
443 output[i] = high << 4 | low;
444 i += 1;
445 }
446 Some(output)
447}
448
449#[cfg(feature = "alloc")]
477#[inline]
478pub fn decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, FromHexError> {
479 fn decode_inner(input: &[u8]) -> Result<Vec<u8>, FromHexError> {
480 if unlikely(input.len() % 2 != 0) {
481 return Err(FromHexError::OddLength);
482 }
483 let input = strip_prefix(input);
484
485 let len = input.len() / 2;
487 let mut output = Vec::with_capacity(len);
488 #[allow(clippy::uninit_vec)]
490 unsafe {
491 output.set_len(len);
492 }
493
494 unsafe { decode_checked(input, &mut output) }.map(|()| output)
496 }
497
498 decode_inner(input.as_ref())
499}
500
501#[inline]
525pub fn decode_to_slice<T: AsRef<[u8]>>(input: T, output: &mut [u8]) -> Result<(), FromHexError> {
526 decode_to_slice_inner(input.as_ref(), output)
527}
528
529#[inline]
552pub fn decode_to_array<T: AsRef<[u8]>, const N: usize>(input: T) -> Result<[u8; N], FromHexError> {
553 fn decode_to_array_inner<const N: usize>(input: &[u8]) -> Result<[u8; N], FromHexError> {
554 let mut output = impl_core::uninit_array();
555 let output_slice = unsafe { impl_core::slice_assume_init_mut(&mut output) };
557 decode_to_slice_inner(input, output_slice)
559 .map(|()| unsafe { impl_core::array_assume_init(output) })
560 }
561
562 decode_to_array_inner(input.as_ref())
563}
564
565#[cfg(feature = "alloc")]
566fn encode_inner<const UPPER: bool, const PREFIX: bool>(data: &[u8]) -> String {
567 let capacity = PREFIX as usize * 2 + data.len() * 2;
568 let mut buf = Vec::<u8>::with_capacity(capacity);
569 #[allow(clippy::uninit_vec)]
571 unsafe {
572 buf.set_len(capacity)
573 };
574 let mut output = buf.as_mut_slice();
575 if PREFIX {
576 unsafe {
578 *output.get_unchecked_mut(0) = b'0';
579 *output.get_unchecked_mut(1) = b'x';
580 output = output.get_unchecked_mut(2..);
581 }
582 }
583 unsafe { imp::encode::<UPPER>(data, output) };
585 unsafe { String::from_utf8_unchecked(buf) }
587}
588
589fn encode_to_slice_inner<const UPPER: bool>(
590 input: &[u8],
591 output: &mut [u8],
592) -> Result<(), FromHexError> {
593 if unlikely(output.len() != 2 * input.len()) {
594 return Err(FromHexError::InvalidStringLength);
595 }
596 unsafe { imp::encode::<UPPER>(input, output) };
598 Ok(())
599}
600
601fn decode_to_slice_inner(input: &[u8], output: &mut [u8]) -> Result<(), FromHexError> {
602 if unlikely(input.len() % 2 != 0) {
603 return Err(FromHexError::OddLength);
604 }
605 let input = strip_prefix(input);
606 if unlikely(output.len() != input.len() / 2) {
607 return Err(FromHexError::InvalidStringLength);
608 }
609 unsafe { decode_checked(input, output) }
611}
612
613#[inline]
617unsafe fn decode_checked(input: &[u8], output: &mut [u8]) -> Result<(), FromHexError> {
618 debug_assert_eq!(output.len(), input.len() / 2);
619
620 if imp::USE_CHECK_FN {
621 if imp::check(input) {
623 unsafe { imp::decode_unchecked(input, output) };
624 return Ok(());
625 }
626 } else {
627 if unsafe { imp::decode_checked(input, output) } {
629 return Ok(());
630 }
631 }
632
633 Err(unsafe { invalid_hex_error(input) })
634}
635
636#[inline]
637const fn byte2hex<const UPPER: bool>(byte: u8) -> (u8, u8) {
638 let table = get_chars_table::<UPPER>();
639 let high = table[(byte >> 4) as usize];
640 let low = table[(byte & 0x0f) as usize];
641 (high, low)
642}
643
644#[inline]
645const fn strip_prefix(bytes: &[u8]) -> &[u8] {
646 match bytes {
647 [b'0', b'x', rest @ ..] => rest,
648 _ => bytes,
649 }
650}
651
652#[cold]
658#[cfg_attr(debug_assertions, track_caller)]
659const unsafe fn invalid_hex_error(input: &[u8]) -> FromHexError {
660 let mut index = None;
662 let mut iter = input;
663 while let [byte, rest @ ..] = iter {
664 if HEX_DECODE_LUT[*byte as usize] == NIL {
665 index = Some(input.len() - rest.len() - 1);
666 break;
667 }
668 iter = rest;
669 }
670
671 let index = match index {
672 Some(index) => index,
673 None => {
674 if cfg!(debug_assertions) {
675 panic!("input was valid but `check` failed")
676 } else {
677 unsafe { core::hint::unreachable_unchecked() }
678 }
679 }
680 };
681
682 FromHexError::InvalidHexCharacter {
683 c: input[index] as char,
684 index,
685 }
686}
687
688#[inline(always)]
689const fn get_chars_table<const UPPER: bool>() -> &'static [u8; 16] {
690 if UPPER {
691 HEX_CHARS_UPPER
692 } else {
693 HEX_CHARS_LOWER
694 }
695}
696
697const fn make_decode_lut() -> [u8; 256] {
698 let mut lut = [0; 256];
699 let mut i = 0u8;
700 loop {
701 lut[i as usize] = match i {
702 b'0'..=b'9' => i - b'0',
703 b'A'..=b'F' => i - b'A' + 10,
704 b'a'..=b'f' => i - b'a' + 10,
705 _ => NIL,
707 };
708 if i == NIL {
709 break;
710 }
711 i += 1;
712 }
713 lut
714}
715
716#[allow(
717 missing_docs,
718 unused,
719 clippy::all,
720 clippy::missing_inline_in_public_items
721)]
722#[cfg(all(feature = "__fuzzing", not(miri)))]
723#[doc(hidden)]
724pub mod fuzzing {
725 use super::*;
726 use proptest::test_runner::TestCaseResult;
727 use proptest::{prop_assert, prop_assert_eq};
728 use std::fmt::Write;
729
730 pub fn fuzz(data: &[u8]) -> TestCaseResult {
731 self::encode(&data)?;
732 self::decode(&data)?;
733 Ok(())
734 }
735
736 pub fn encode(input: &[u8]) -> TestCaseResult {
737 test_buffer::<8, 16>(input)?;
738 test_buffer::<20, 40>(input)?;
739 test_buffer::<32, 64>(input)?;
740 test_buffer::<64, 128>(input)?;
741 test_buffer::<128, 256>(input)?;
742
743 let encoded = crate::encode(input);
744 let expected = mk_expected(input);
745 prop_assert_eq!(&encoded, &expected);
746
747 let decoded = crate::decode(&encoded).unwrap();
748 prop_assert_eq!(decoded, input);
749
750 Ok(())
751 }
752
753 pub fn decode(input: &[u8]) -> TestCaseResult {
754 if let Ok(decoded) = crate::decode(input) {
755 let input_len = strip_prefix(input).len() / 2;
756 prop_assert_eq!(decoded.len(), input_len);
757 }
758
759 Ok(())
760 }
761
762 fn mk_expected(bytes: &[u8]) -> String {
763 let mut s = String::with_capacity(bytes.len() * 2);
764 for i in bytes {
765 write!(s, "{i:02x}").unwrap();
766 }
767 s
768 }
769
770 fn test_buffer<const N: usize, const LEN: usize>(bytes: &[u8]) -> TestCaseResult {
771 if let Ok(bytes) = <&[u8; N]>::try_from(bytes) {
772 let mut buffer = Buffer::<N, false>::new();
773 let string = buffer.format(bytes).to_string();
774 prop_assert_eq!(string.len(), bytes.len() * 2);
775 prop_assert_eq!(string.as_bytes(), buffer.as_byte_array::<LEN>());
776 prop_assert_eq!(string.as_str(), buffer.as_str());
777 prop_assert_eq!(string.as_str(), mk_expected(bytes));
778
779 let mut buffer = Buffer::<N, true>::new();
780 let prefixed = buffer.format(bytes).to_string();
781 prop_assert_eq!(prefixed.len(), 2 + bytes.len() * 2);
782 prop_assert_eq!(prefixed.as_str(), buffer.as_str());
783 prop_assert_eq!(prefixed.as_str(), format!("0x{string}"));
784
785 prop_assert_eq!(decode_to_array(&string), Ok(*bytes));
786 prop_assert_eq!(decode_to_array(&prefixed), Ok(*bytes));
787 prop_assert_eq!(const_decode_to_array(string.as_bytes()), Ok(*bytes));
788 prop_assert_eq!(const_decode_to_array(prefixed.as_bytes()), Ok(*bytes));
789 }
790
791 Ok(())
792 }
793
794 proptest::proptest! {
795 #![proptest_config(proptest::prelude::ProptestConfig {
796 cases: 1024,
797 ..Default::default()
798 })]
799
800 #[test]
801 fn fuzz_encode(s in ".+") {
802 encode(s.as_bytes())?;
803 }
804
805 #[test]
806 fn fuzz_check_true(s in "[0-9a-fA-F]+") {
807 let s = s.as_bytes();
808 prop_assert!(crate::check_raw(s));
809 prop_assert!(crate::const_check_raw(s));
810 if s.len() % 2 == 0 {
811 prop_assert!(crate::check(s).is_ok());
812 prop_assert!(crate::const_check(s).is_ok());
813 }
814 }
815
816 #[test]
817 fn fuzz_check_false(s in ".{16}[0-9a-fA-F]+") {
818 let s = s.as_bytes();
819 prop_assert!(crate::check(s).is_err());
820 prop_assert!(crate::const_check(s).is_err());
821 prop_assert!(!crate::check_raw(s));
822 prop_assert!(!crate::const_check_raw(s));
823 }
824 }
825}