1#![cfg_attr(not(feature = "std"), no_std)]
22#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
23#![cfg_attr(
24 feature = "nightly",
25 feature(core_intrinsics, inline_const),
26 allow(internal_features, stable_features)
27)]
28#![cfg_attr(feature = "portable-simd", feature(portable_simd))]
29#![warn(
30 missing_copy_implementations,
31 missing_debug_implementations,
32 missing_docs,
33 unreachable_pub,
34 unsafe_op_in_unsafe_fn,
35 clippy::missing_const_for_fn,
36 clippy::missing_inline_in_public_items,
37 clippy::all,
38 rustdoc::all
39)]
40#![cfg_attr(not(any(test, feature = "__fuzzing")), warn(unused_crate_dependencies))]
41#![deny(unused_must_use, rust_2018_idioms)]
42#![allow(
43 clippy::cast_lossless,
44 clippy::inline_always,
45 clippy::let_unit_value,
46 clippy::must_use_candidate,
47 clippy::wildcard_imports,
48 unsafe_op_in_unsafe_fn,
49 unused_unsafe
50)]
51
52#[cfg(feature = "alloc")]
53#[allow(unused_imports)]
54#[macro_use]
55extern crate alloc;
56
57use cfg_if::cfg_if;
58
59#[cfg(feature = "alloc")]
60#[allow(unused_imports)]
61use alloc::{string::String, vec::Vec};
62
63#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
65use cpufeatures as _;
66
67mod arch;
68use arch::imp;
69
70mod impl_core;
71
72pub mod traits;
73#[cfg(feature = "alloc")]
74pub use traits::ToHexExt;
75
76cfg_if! {
79 if #[cfg(feature = "hex")] {
80 pub use hex;
81 #[doc(inline)]
82 pub use hex::{FromHex, FromHexError, ToHex};
83 } else {
84 mod error;
85 pub use error::FromHexError;
86
87 #[allow(deprecated)]
88 pub use traits::{FromHex, ToHex};
89 }
90}
91
92cfg_if! {
94 if #[cfg(feature = "nightly")] {
95 #[allow(unused_imports)]
97 use core::intrinsics::{likely, unlikely};
98
99 macro_rules! maybe_const_assert {
101 ($($tt:tt)*) => {
102 const { assert!($($tt)*) }
103 };
104 }
105 } else {
106 #[allow(unused_imports)]
107 use core::convert::{identity as likely, identity as unlikely};
108
109 macro_rules! maybe_const_assert {
110 ($($tt:tt)*) => {
111 assert!($($tt)*)
112 };
113 }
114 }
115}
116
117cfg_if! {
119 if #[cfg(feature = "serde")] {
120 pub mod serde;
121
122 #[doc(no_inline)]
123 pub use self::serde::deserialize;
124 #[cfg(feature = "alloc")]
125 #[doc(no_inline)]
126 pub use self::serde::serialize;
127 }
128}
129
130mod buffer;
131pub use buffer::Buffer;
132
133pub const HEX_CHARS_LOWER: &[u8; 16] = b"0123456789abcdef";
135
136pub const HEX_DECODE_LUT: &[u8; 256] = &make_decode_lut();
140
141pub const NIL: u8 = u8::MAX;
143
144#[inline]
153pub const fn const_encode<const N: usize, const PREFIX: bool>(
154 input: &[u8; N],
155) -> Buffer<N, PREFIX> {
156 Buffer::new().const_format(input)
157}
158
159#[inline]
175pub fn encode_to_slice<T: AsRef<[u8]>>(input: T, output: &mut [u8]) -> Result<(), FromHexError> {
176 encode_to_slice_inner(input.as_ref(), output)
177}
178
179#[cfg(feature = "alloc")]
193#[inline]
194pub fn encode<T: AsRef<[u8]>>(data: T) -> String {
195 encode_inner::<false>(data.as_ref())
196}
197
198#[cfg(feature = "alloc")]
209#[inline]
210pub fn encode_prefixed<T: AsRef<[u8]>>(data: T) -> String {
211 encode_inner::<true>(data.as_ref())
212}
213
214#[inline]
235pub const fn const_decode_to_array<const N: usize>(input: &[u8]) -> Result<[u8; N], FromHexError> {
236 if input.len() % 2 != 0 {
237 return Err(FromHexError::OddLength);
238 }
239 if input.len() != N * 2 {
240 return Err(FromHexError::InvalidStringLength);
241 }
242 match const_decode_to_array_impl(input) {
243 Some(output) => Ok(output),
244 None => Err(unsafe { invalid_hex_error(input) }),
245 }
246}
247
248const fn const_decode_to_array_impl<const N: usize>(input: &[u8]) -> Option<[u8; N]> {
249 macro_rules! next {
250 ($var:ident, $i:expr) => {
251 let hex = unsafe { *input.as_ptr().add($i) };
252 let $var = HEX_DECODE_LUT[hex as usize];
253 if $var == NIL {
254 return None;
255 }
256 };
257 }
258
259 let mut output = [0; N];
260 debug_assert!(input.len() == N * 2);
261 let mut i = 0;
262 while i < output.len() {
263 next!(high, i * 2);
264 next!(low, i * 2 + 1);
265 output[i] = high << 4 | low;
266 i += 1;
267 }
268 Some(output)
269}
270
271#[cfg(feature = "alloc")]
292#[inline]
293pub fn decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, FromHexError> {
294 fn decode_inner(input: &[u8]) -> Result<Vec<u8>, FromHexError> {
295 if unlikely(input.len() % 2 != 0) {
296 return Err(FromHexError::OddLength);
297 }
298
299 let len = input.len() / 2;
301 let mut output = Vec::with_capacity(len);
302 #[allow(clippy::uninit_vec)]
304 unsafe {
305 output.set_len(len);
306 }
307
308 unsafe { decode_checked(input, &mut output) }.map(|()| output)
310 }
311
312 decode_inner(input.as_ref())
313}
314
315#[inline]
336pub fn decode_to_slice<T: AsRef<[u8]>>(input: T, output: &mut [u8]) -> Result<(), FromHexError> {
337 decode_to_slice_inner(input.as_ref(), output)
338}
339
340#[inline]
357pub fn decode_to_array<T: AsRef<[u8]>, const N: usize>(input: T) -> Result<[u8; N], FromHexError> {
358 fn decode_to_array_inner<const N: usize>(input: &[u8]) -> Result<[u8; N], FromHexError> {
359 let mut output = impl_core::uninit_array();
360 let output_slice = unsafe { impl_core::slice_assume_init_mut(&mut output) };
362 decode_to_slice_inner(input, output_slice)
364 .map(|()| unsafe { impl_core::array_assume_init(output) })
365 }
366
367 decode_to_array_inner(input.as_ref())
368}
369
370#[cfg(feature = "alloc")]
371fn encode_inner<const PREFIX: bool>(data: &[u8]) -> String {
372 let capacity = PREFIX as usize * 2 + data.len() * 2;
373 let mut buf = Vec::<u8>::with_capacity(capacity);
374 #[allow(clippy::uninit_vec)]
376 unsafe {
377 buf.set_len(capacity)
378 };
379 let mut output = buf.as_mut_ptr();
380 if PREFIX {
381 unsafe {
383 output.add(0).write(b'0');
384 output.add(1).write(b'x');
385 output = output.add(2);
386 }
387 }
388 unsafe { imp::encode(data, output) };
390 unsafe { String::from_utf8_unchecked(buf) }
392}
393
394fn encode_to_slice_inner(input: &[u8], output: &mut [u8]) -> Result<(), FromHexError> {
395 if unlikely(output.len() != 2 * input.len()) {
396 return Err(FromHexError::InvalidStringLength);
397 }
398 unsafe { imp::encode(input, output.as_mut_ptr()) };
400 Ok(())
401}
402
403fn decode_to_slice_inner(input: &[u8], output: &mut [u8]) -> Result<(), FromHexError> {
404 if unlikely(input.len() % 2 != 0) {
405 return Err(FromHexError::OddLength);
406 }
407 if unlikely(output.len() != input.len() / 2) {
408 return Err(FromHexError::InvalidStringLength);
409 }
410 unsafe { decode_checked(input, output) }
412}
413
414#[inline]
418unsafe fn decode_checked(input: &[u8], output: &mut [u8]) -> Result<(), FromHexError> {
419 debug_assert_eq!(output.len(), input.len() / 2);
420
421 if imp::USE_CHECK_FN {
422 if imp::check(input) {
424 unsafe { imp::decode_unchecked(input, output) };
425 return Ok(());
426 }
427 } else {
428 if unsafe { imp::decode_checked(input, output) } {
430 return Ok(());
431 }
432 }
433
434 Err(unsafe { invalid_hex_error(input) })
435}
436
437#[inline]
438const fn byte2hex(byte: u8) -> (u8, u8) {
439 let table = HEX_CHARS_LOWER;
440 let high = table[(byte >> 4) as usize];
441 let low = table[(byte & 0x0f) as usize];
442 (high, low)
443}
444
445#[cold]
451#[cfg_attr(debug_assertions, track_caller)]
452const unsafe fn invalid_hex_error(input: &[u8]) -> FromHexError {
453 let mut index = None;
455 let mut iter = input;
456 while let [byte, rest @ ..] = iter {
457 if HEX_DECODE_LUT[*byte as usize] == NIL {
458 index = Some(input.len() - rest.len() - 1);
459 break;
460 }
461 iter = rest;
462 }
463
464 let index = match index {
465 Some(index) => index,
466 None => {
467 if cfg!(debug_assertions) {
468 panic!("input was valid but `check` failed")
469 } else {
470 unsafe { core::hint::unreachable_unchecked() }
471 }
472 }
473 };
474
475 FromHexError::InvalidHexCharacter {
476 c: input[index] as char,
477 index,
478 }
479}
480
481const fn make_decode_lut() -> [u8; 256] {
482 let mut lut = [0; 256];
483 let mut i = 0u8;
484 loop {
485 lut[i as usize] = match i {
486 b'0'..=b'9' => i - b'0',
487 b'a'..=b'f' => i - b'a' + 10,
488 _ => NIL,
490 };
491 if i == NIL {
492 break;
493 }
494 i += 1;
495 }
496 lut
497}
498
499#[allow(
500 missing_docs,
501 unused,
502 clippy::all,
503 clippy::missing_inline_in_public_items
504)]
505#[cfg(all(feature = "__fuzzing", not(miri)))]
506#[doc(hidden)]
507pub mod fuzzing {
508 use super::*;
509 use proptest::test_runner::TestCaseResult;
510 use proptest::{prop_assert, prop_assert_eq};
511 use std::fmt::Write;
512
513 pub fn fuzz(data: &[u8]) -> TestCaseResult {
514 self::encode(&data)?;
515 self::decode(&data)?;
516 Ok(())
517 }
518
519 pub fn encode(input: &[u8]) -> TestCaseResult {
520 test_buffer::<8, 16>(input)?;
521 test_buffer::<20, 40>(input)?;
522 test_buffer::<32, 64>(input)?;
523 test_buffer::<64, 128>(input)?;
524 test_buffer::<128, 256>(input)?;
525
526 let encoded = crate::encode(input);
527 let expected = mk_expected(input);
528 prop_assert_eq!(&encoded, &expected);
529
530 let decoded = crate::decode(&encoded).unwrap();
531 prop_assert_eq!(decoded, input);
532
533 Ok(())
534 }
535
536 pub fn decode(input: &[u8]) -> TestCaseResult {
537 if let Ok(decoded) = crate::decode(input) {
538 let input_len = input.len() / 2;
539 prop_assert_eq!(decoded.len(), input_len);
540 }
541
542 Ok(())
543 }
544
545 fn mk_expected(bytes: &[u8]) -> String {
546 let mut s = String::with_capacity(bytes.len() * 2);
547 for i in bytes {
548 write!(s, "{i:02x}").unwrap();
549 }
550 s
551 }
552
553 fn test_buffer<const N: usize, const LEN: usize>(bytes: &[u8]) -> TestCaseResult {
554 if let Ok(bytes) = <&[u8; N]>::try_from(bytes) {
555 let mut buffer = Buffer::<N, false>::new();
556 let string = buffer.format(bytes).to_string();
557 prop_assert_eq!(string.len(), bytes.len() * 2);
558 prop_assert_eq!(string.as_bytes(), buffer.as_byte_array::<LEN>());
559 prop_assert_eq!(string.as_str(), buffer.as_str());
560 prop_assert_eq!(string.as_str(), mk_expected(bytes));
561
562 let mut buffer = Buffer::<N, true>::new();
563 let prefixed = buffer.format(bytes).to_string();
564 prop_assert_eq!(prefixed.len(), 2 + bytes.len() * 2);
565 prop_assert_eq!(prefixed.as_str(), buffer.as_str());
566 prop_assert_eq!(prefixed.as_str(), format!("0x{string}"));
567
568 prop_assert_eq!(decode_to_array(&string), Ok(*bytes));
569 prop_assert_eq!(decode_to_array(&prefixed), Ok(*bytes));
570 prop_assert_eq!(const_decode_to_array(string.as_bytes()), Ok(*bytes));
571 prop_assert_eq!(const_decode_to_array(prefixed.as_bytes()), Ok(*bytes));
572 }
573
574 Ok(())
575 }
576
577 proptest::proptest! {
578 #![proptest_config(proptest::prelude::ProptestConfig {
579 cases: 1024,
580 ..Default::default()
581 })]
582
583 #[test]
584 fn fuzz_encode(s in ".+") {
585 encode(s.as_bytes())?;
586 }
587
588 #[test]
589 fn fuzz_check_true(s in "[0-9a-f]+") {
590 let s = s.as_bytes();
591 prop_assert!(crate::check_raw(s));
592 prop_assert!(crate::const_check_raw(s));
593 if s.len() % 2 == 0 {
594 prop_assert!(crate::check(s).is_ok());
595 prop_assert!(crate::const_check(s).is_ok());
596 }
597 }
598
599 #[test]
600 fn fuzz_check_false(s in ".{16}[0-9a-f]+") {
601 let s = s.as_bytes();
602 prop_assert!(crate::check(s).is_err());
603 prop_assert!(crate::const_check(s).is_err());
604 prop_assert!(!crate::check_raw(s));
605 prop_assert!(!crate::const_check_raw(s));
606 }
607 }
608}
609
610#[inline]
625pub const fn const_check(input: &[u8]) -> Result<(), FromHexError> {
626 if input.len() % 2 != 0 {
627 return Err(FromHexError::OddLength);
628 }
629 if const_check_raw(input) {
630 Ok(())
631 } else {
632 Err(unsafe { invalid_hex_error(input) })
633 }
634}
635
636#[inline]
658pub const fn const_check_raw(input: &[u8]) -> bool {
659 let mut i = 0;
660 while i < input.len() {
661 let byte = input[i];
662 if HEX_DECODE_LUT[byte as usize] == NIL {
663 return false;
664 }
665 i += 1;
666 }
667 true
668}
669
670#[inline]
681pub fn check<T: AsRef<[u8]>>(input: T) -> Result<(), FromHexError> {
682 #[allow(clippy::missing_const_for_fn)]
683 fn check_inner(input: &[u8]) -> Result<(), FromHexError> {
684 if input.len() % 2 != 0 {
685 return Err(FromHexError::OddLength);
686 }
687 if imp::check(input) {
688 Ok(())
689 } else {
690 Err(unsafe { invalid_hex_error(input) })
691 }
692 }
693
694 check_inner(input.as_ref())
695}
696
697#[inline]
715pub fn check_raw<T: AsRef<[u8]>>(input: T) -> bool {
716 imp::check(input.as_ref())
717}