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
77mod error;
78pub use error::FromHexError;
79
80#[allow(deprecated)]
81pub use traits::{FromHex, ToHex};
82
83cfg_if! {
85 if #[cfg(feature = "nightly")] {
86 #[allow(unused_imports)]
88 use core::intrinsics::{likely, unlikely};
89
90 macro_rules! maybe_const_assert {
92 ($($tt:tt)*) => {
93 const { assert!($($tt)*) }
94 };
95 }
96 } else {
97 #[allow(unused_imports)]
98 use core::convert::{identity as likely, identity as unlikely};
99
100 macro_rules! maybe_const_assert {
101 ($($tt:tt)*) => {
102 assert!($($tt)*)
103 };
104 }
105 }
106}
107
108cfg_if! {
110 if #[cfg(feature = "serde")] {
111 pub mod serde;
112
113 #[doc(no_inline)]
114 pub use self::serde::deserialize;
115 #[cfg(feature = "alloc")]
116 #[doc(no_inline)]
117 pub use self::serde::{serialize, serialize_upper};
118 }
119}
120
121mod buffer;
122pub use buffer::Buffer;
123
124mod output;
125use output::Output;
126
127pub const HEX_CHARS_LOWER: &[u8; 16] = b"0123456789abcdef";
129
130pub const HEX_CHARS_UPPER: &[u8; 16] = b"0123456789ABCDEF";
132
133pub const HEX_DECODE_LUT: &[u8; 256] = &make_decode_lut();
137
138pub const NIL: u8 = u8::MAX;
140
141#[inline]
150pub const fn const_encode<const N: usize, const PREFIX: bool>(
151 input: &[u8; N],
152) -> Buffer<N, PREFIX> {
153 Buffer::new().const_format(input)
154}
155
156#[inline]
172pub fn encode_to_slice<T: AsRef<[u8]>>(input: T, output: &mut [u8]) -> Result<(), FromHexError> {
173 encode_to_slice_inner::<false>(input.as_ref(), output)
174}
175
176#[inline]
192pub fn encode_to_slice_upper<T: AsRef<[u8]>>(
193 input: T,
194 output: &mut [u8],
195) -> Result<(), FromHexError> {
196 encode_to_slice_inner::<true>(input.as_ref(), output)
197}
198
199#[cfg(feature = "alloc")]
213#[inline]
214pub fn encode<T: AsRef<[u8]>>(data: T) -> String {
215 encode_inner::<false, false>(data.as_ref())
216}
217
218#[cfg(feature = "alloc")]
229#[inline]
230pub fn encode_upper<T: AsRef<[u8]>>(data: T) -> String {
231 encode_inner::<true, false>(data.as_ref())
232}
233
234#[cfg(feature = "alloc")]
245#[inline]
246pub fn encode_prefixed<T: AsRef<[u8]>>(data: T) -> String {
247 encode_inner::<false, true>(data.as_ref())
248}
249
250#[cfg(feature = "alloc")]
261#[inline]
262pub fn encode_upper_prefixed<T: AsRef<[u8]>>(data: T) -> String {
263 encode_inner::<true, true>(data.as_ref())
264}
265
266#[inline]
282pub const fn const_check(input: &[u8]) -> Result<(), FromHexError> {
283 if input.len() % 2 != 0 {
284 return Err(FromHexError::OddLength);
285 }
286 let input = strip_prefix(input);
287 if const_check_raw(input) {
288 Ok(())
289 } else {
290 Err(unsafe { invalid_hex_error(input) })
291 }
292}
293
294#[inline]
316pub const fn const_check_raw(input: &[u8]) -> bool {
317 generic::check(input)
318}
319
320#[inline]
332pub fn check<T: AsRef<[u8]>>(input: T) -> Result<(), FromHexError> {
333 #[allow(clippy::missing_const_for_fn)]
334 fn check_inner(input: &[u8]) -> Result<(), FromHexError> {
335 if input.len() % 2 != 0 {
336 return Err(FromHexError::OddLength);
337 }
338 let stripped = strip_prefix(input);
339 if imp::check(stripped) {
340 Ok(())
341 } else {
342 let mut e = unsafe { invalid_hex_error(stripped) };
343 if let FromHexError::InvalidHexCharacter { ref mut index, .. } = e {
344 *index += input.len() - stripped.len();
345 }
346 Err(e)
347 }
348 }
349
350 check_inner(input.as_ref())
351}
352
353#[inline]
371pub fn check_raw<T: AsRef<[u8]>>(input: T) -> bool {
372 imp::check(input.as_ref())
373}
374
375#[inline]
402pub const fn const_decode_to_array<const N: usize>(input: &[u8]) -> Result<[u8; N], FromHexError> {
403 if input.len() % 2 != 0 {
404 return Err(FromHexError::OddLength);
405 }
406 let input = strip_prefix(input);
407 if input.len() != N * 2 {
408 return Err(FromHexError::InvalidStringLength);
409 }
410 match const_decode_to_array_impl(input) {
411 Some(output) => Ok(output),
412 None => Err(unsafe { invalid_hex_error(input) }),
413 }
414}
415
416const fn const_decode_to_array_impl<const N: usize>(input: &[u8]) -> Option<[u8; N]> {
417 macro_rules! next {
418 ($var:ident, $i:expr) => {
419 let hex = unsafe { *input.as_ptr().add($i) };
420 let $var = HEX_DECODE_LUT[hex as usize];
421 if $var == NIL {
422 return None;
423 }
424 };
425 }
426
427 let mut output = [0; N];
428 debug_assert!(input.len() == N * 2);
429 let mut i = 0;
430 while i < output.len() {
431 next!(high, i * 2);
432 next!(low, i * 2 + 1);
433 output[i] = high << 4 | low;
434 i += 1;
435 }
436 Some(output)
437}
438
439#[cfg(feature = "alloc")]
467#[inline]
468pub fn decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, FromHexError> {
469 fn decode_inner(input: &[u8]) -> Result<Vec<u8>, FromHexError> {
470 if unlikely(input.len() % 2 != 0) {
471 return Err(FromHexError::OddLength);
472 }
473 let input = strip_prefix(input);
474
475 let len = input.len() / 2;
477 let mut output = Vec::with_capacity(len);
478 #[allow(clippy::uninit_vec)]
480 unsafe {
481 output.set_len(len);
482 }
483
484 unsafe { decode_checked(input, &mut output) }.map(|()| output)
486 }
487
488 decode_inner(input.as_ref())
489}
490
491#[inline]
515pub fn decode_to_slice<T: AsRef<[u8]>>(input: T, output: &mut [u8]) -> Result<(), FromHexError> {
516 decode_to_slice_inner(input.as_ref(), output)
517}
518
519#[inline]
542pub fn decode_to_array<T: AsRef<[u8]>, const N: usize>(input: T) -> Result<[u8; N], FromHexError> {
543 fn decode_to_array_inner<const N: usize>(input: &[u8]) -> Result<[u8; N], FromHexError> {
544 let mut output = impl_core::uninit_array();
545 let output_slice = unsafe { impl_core::slice_assume_init_mut(&mut output) };
547 decode_to_slice_inner(input, output_slice)
549 .map(|()| unsafe { impl_core::array_assume_init(output) })
550 }
551
552 decode_to_array_inner(input.as_ref())
553}
554
555#[cfg(feature = "alloc")]
556fn encode_inner<const UPPER: bool, const PREFIX: bool>(data: &[u8]) -> String {
557 let capacity = PREFIX as usize * 2 + data.len() * 2;
558 let mut buf = Vec::<u8>::with_capacity(capacity);
559 #[allow(clippy::uninit_vec)]
561 unsafe {
562 buf.set_len(capacity)
563 };
564 let mut output = buf.as_mut_slice();
565 if PREFIX {
566 unsafe {
568 *output.get_unchecked_mut(0) = b'0';
569 *output.get_unchecked_mut(1) = b'x';
570 output = output.get_unchecked_mut(2..);
571 }
572 }
573 unsafe { imp::encode::<UPPER>(data, output) };
575 unsafe { String::from_utf8_unchecked(buf) }
577}
578
579fn encode_to_slice_inner<const UPPER: bool>(
580 input: &[u8],
581 output: &mut [u8],
582) -> Result<(), FromHexError> {
583 if unlikely(output.len() != 2 * input.len()) {
584 return Err(FromHexError::InvalidStringLength);
585 }
586 unsafe { imp::encode::<UPPER>(input, output) };
588 Ok(())
589}
590
591fn decode_to_slice_inner(input: &[u8], output: &mut [u8]) -> Result<(), FromHexError> {
592 if unlikely(input.len() % 2 != 0) {
593 return Err(FromHexError::OddLength);
594 }
595 let input = strip_prefix(input);
596 if unlikely(output.len() != input.len() / 2) {
597 return Err(FromHexError::InvalidStringLength);
598 }
599 unsafe { decode_checked(input, output) }
601}
602
603#[inline]
607unsafe fn decode_checked(input: &[u8], output: &mut [u8]) -> Result<(), FromHexError> {
608 debug_assert_eq!(output.len(), input.len() / 2);
609
610 if imp::USE_CHECK_FN {
611 if imp::check(input) {
613 unsafe { imp::decode_unchecked(input, output) };
614 return Ok(());
615 }
616 } else {
617 if unsafe { imp::decode_checked(input, output) } {
619 return Ok(());
620 }
621 }
622
623 Err(unsafe { invalid_hex_error(input) })
624}
625
626#[inline]
627const fn byte2hex<const UPPER: bool>(byte: u8) -> (u8, u8) {
628 let table = get_chars_table::<UPPER>();
629 let high = table[(byte >> 4) as usize];
630 let low = table[(byte & 0x0f) as usize];
631 (high, low)
632}
633
634#[inline]
635const fn strip_prefix(bytes: &[u8]) -> &[u8] {
636 match bytes {
637 [b'0', b'x', rest @ ..] => rest,
638 _ => bytes,
639 }
640}
641
642#[cold]
648#[cfg_attr(debug_assertions, track_caller)]
649const unsafe fn invalid_hex_error(input: &[u8]) -> FromHexError {
650 let mut index = None;
652 let mut iter = input;
653 while let [byte, rest @ ..] = iter {
654 if HEX_DECODE_LUT[*byte as usize] == NIL {
655 index = Some(input.len() - rest.len() - 1);
656 break;
657 }
658 iter = rest;
659 }
660
661 let index = match index {
662 Some(index) => index,
663 None => {
664 if cfg!(debug_assertions) {
665 panic!("input was valid but `check` failed")
666 } else {
667 unsafe { core::hint::unreachable_unchecked() }
668 }
669 }
670 };
671
672 FromHexError::InvalidHexCharacter {
673 c: input[index] as char,
674 index,
675 }
676}
677
678#[inline(always)]
679const fn get_chars_table<const UPPER: bool>() -> &'static [u8; 16] {
680 if UPPER {
681 HEX_CHARS_UPPER
682 } else {
683 HEX_CHARS_LOWER
684 }
685}
686
687const fn make_decode_lut() -> [u8; 256] {
688 let mut lut = [0; 256];
689 let mut i = 0u8;
690 loop {
691 lut[i as usize] = match i {
692 b'0'..=b'9' => i - b'0',
693 b'A'..=b'F' => i - b'A' + 10,
694 b'a'..=b'f' => i - b'a' + 10,
695 _ => NIL,
697 };
698 if i == NIL {
699 break;
700 }
701 i += 1;
702 }
703 lut
704}
705
706#[allow(
707 missing_docs,
708 unused,
709 clippy::all,
710 clippy::missing_inline_in_public_items
711)]
712#[cfg(all(feature = "__fuzzing", not(miri)))]
713#[doc(hidden)]
714pub mod fuzzing {
715 use super::*;
716 use proptest::test_runner::TestCaseResult;
717 use proptest::{prop_assert, prop_assert_eq};
718 use std::fmt::Write;
719
720 pub fn fuzz(data: &[u8]) -> TestCaseResult {
721 self::encode(&data)?;
722 self::decode(&data)?;
723 Ok(())
724 }
725
726 pub fn encode(input: &[u8]) -> TestCaseResult {
727 test_buffer::<8, 16>(input)?;
728 test_buffer::<20, 40>(input)?;
729 test_buffer::<32, 64>(input)?;
730 test_buffer::<64, 128>(input)?;
731 test_buffer::<128, 256>(input)?;
732
733 let encoded = crate::encode(input);
734 let expected = mk_expected(input);
735 prop_assert_eq!(&encoded, &expected);
736
737 let decoded = crate::decode(&encoded).unwrap();
738 prop_assert_eq!(decoded, input);
739
740 Ok(())
741 }
742
743 pub fn decode(input: &[u8]) -> TestCaseResult {
744 if let Ok(decoded) = crate::decode(input) {
745 let input_len = strip_prefix(input).len() / 2;
746 prop_assert_eq!(decoded.len(), input_len);
747 }
748
749 Ok(())
750 }
751
752 fn mk_expected(bytes: &[u8]) -> String {
753 let mut s = String::with_capacity(bytes.len() * 2);
754 for i in bytes {
755 write!(s, "{i:02x}").unwrap();
756 }
757 s
758 }
759
760 fn test_buffer<const N: usize, const LEN: usize>(bytes: &[u8]) -> TestCaseResult {
761 if let Ok(bytes) = <&[u8; N]>::try_from(bytes) {
762 let mut buffer = Buffer::<N, false>::new();
763 let string = buffer.format(bytes).to_string();
764 prop_assert_eq!(string.len(), bytes.len() * 2);
765 prop_assert_eq!(string.as_bytes(), buffer.as_byte_array::<LEN>());
766 prop_assert_eq!(string.as_str(), buffer.as_str());
767 prop_assert_eq!(string.as_str(), mk_expected(bytes));
768
769 let mut buffer = Buffer::<N, true>::new();
770 let prefixed = buffer.format(bytes).to_string();
771 prop_assert_eq!(prefixed.len(), 2 + bytes.len() * 2);
772 prop_assert_eq!(prefixed.as_str(), buffer.as_str());
773 prop_assert_eq!(prefixed.as_str(), format!("0x{string}"));
774
775 prop_assert_eq!(decode_to_array(&string), Ok(*bytes));
776 prop_assert_eq!(decode_to_array(&prefixed), Ok(*bytes));
777 prop_assert_eq!(const_decode_to_array(string.as_bytes()), Ok(*bytes));
778 prop_assert_eq!(const_decode_to_array(prefixed.as_bytes()), Ok(*bytes));
779 }
780
781 Ok(())
782 }
783
784 proptest::proptest! {
785 #![proptest_config(proptest::prelude::ProptestConfig {
786 cases: 1024,
787 ..Default::default()
788 })]
789
790 #[test]
791 fn fuzz_encode(s in ".+") {
792 encode(s.as_bytes())?;
793 }
794
795 #[test]
796 fn fuzz_check_true(s in "[0-9a-fA-F]+") {
797 let s = s.as_bytes();
798 prop_assert!(crate::check_raw(s));
799 prop_assert!(crate::const_check_raw(s));
800 if s.len() % 2 == 0 {
801 prop_assert!(crate::check(s).is_ok());
802 prop_assert!(crate::const_check(s).is_ok());
803 }
804 }
805
806 #[test]
807 fn fuzz_check_false(s in ".{16}[0-9a-fA-F]+") {
808 let s = s.as_bytes();
809 prop_assert!(crate::check(s).is_err());
810 prop_assert!(crate::const_check(s).is_err());
811 prop_assert!(!crate::check_raw(s));
812 prop_assert!(!crate::const_check_raw(s));
813 }
814 }
815}