#![cfg_attr(not(feature = "std"), no_std)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(
feature = "nightly",
feature(core_intrinsics, inline_const),
allow(internal_features, stable_features)
)]
#![cfg_attr(feature = "portable-simd", feature(portable_simd))]
#![warn(
missing_copy_implementations,
missing_debug_implementations,
missing_docs,
unreachable_pub,
unsafe_op_in_unsafe_fn,
clippy::missing_const_for_fn,
clippy::missing_inline_in_public_items,
clippy::all,
rustdoc::all
)]
#![cfg_attr(not(any(test, feature = "__fuzzing")), warn(unused_crate_dependencies))]
#![deny(unused_must_use, rust_2018_idioms)]
#![allow(
clippy::cast_lossless,
clippy::inline_always,
clippy::let_unit_value,
clippy::must_use_candidate,
clippy::wildcard_imports,
unsafe_op_in_unsafe_fn,
unused_unsafe
)]
#[cfg(feature = "alloc")]
#[allow(unused_imports)]
#[macro_use]
extern crate alloc;
use cfg_if::cfg_if;
#[cfg(feature = "alloc")]
#[allow(unused_imports)]
use alloc::{string::String, vec::Vec};
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use cpufeatures as _;
mod arch;
use arch::{generic, imp};
mod impl_core;
pub mod traits;
#[cfg(feature = "alloc")]
pub use traits::ToHexExt;
mod display;
pub use display::display;
mod error;
pub use error::FromHexError;
#[allow(deprecated)]
pub use traits::{FromHex, ToHex};
cfg_if! {
if #[cfg(feature = "nightly")] {
#[allow(unused_imports)]
use core::intrinsics::{likely, unlikely};
macro_rules! maybe_const_assert {
($($tt:tt)*) => {
const { assert!($($tt)*) }
};
}
} else {
#[allow(unused_imports)]
use core::convert::{identity as likely, identity as unlikely};
macro_rules! maybe_const_assert {
($($tt:tt)*) => {
assert!($($tt)*)
};
}
}
}
cfg_if! {
if #[cfg(feature = "serde")] {
pub mod serde;
#[doc(no_inline)]
pub use self::serde::deserialize;
#[cfg(feature = "alloc")]
#[doc(no_inline)]
pub use self::serde::{serialize, serialize_upper};
}
}
mod buffer;
pub use buffer::Buffer;
mod output;
use output::Output;
pub const HEX_CHARS_LOWER: &[u8; 16] = b"0123456789abcdef";
pub const HEX_CHARS_UPPER: &[u8; 16] = b"0123456789ABCDEF";
pub const HEX_DECODE_LUT: &[u8; 256] = &make_decode_lut();
pub const NIL: u8 = u8::MAX;
#[inline]
pub const fn const_encode<const N: usize, const PREFIX: bool>(
input: &[u8; N],
) -> Buffer<N, PREFIX> {
Buffer::new().const_format(input)
}
#[inline]
pub fn encode_to_slice<T: AsRef<[u8]>>(input: T, output: &mut [u8]) -> Result<(), FromHexError> {
encode_to_slice_inner::<false>(input.as_ref(), output)
}
#[inline]
pub fn encode_to_slice_upper<T: AsRef<[u8]>>(
input: T,
output: &mut [u8],
) -> Result<(), FromHexError> {
encode_to_slice_inner::<true>(input.as_ref(), output)
}
#[inline]
pub fn encode_to_str<T: AsRef<[u8]>>(
input: T,
output: &mut [u8],
) -> Result<&mut str, FromHexError> {
encode_to_str_inner::<false>(input.as_ref(), output)
}
#[inline]
pub fn encode_to_str_upper<T: AsRef<[u8]>>(
input: T,
output: &mut [u8],
) -> Result<&mut str, FromHexError> {
encode_to_str_inner::<true>(input.as_ref(), output)
}
#[cfg(feature = "alloc")]
#[inline]
pub fn encode<T: AsRef<[u8]>>(data: T) -> String {
encode_inner::<false, false>(data.as_ref())
}
#[cfg(feature = "alloc")]
#[inline]
pub fn encode_upper<T: AsRef<[u8]>>(data: T) -> String {
encode_inner::<true, false>(data.as_ref())
}
#[cfg(feature = "alloc")]
#[inline]
pub fn encode_prefixed<T: AsRef<[u8]>>(data: T) -> String {
encode_inner::<false, true>(data.as_ref())
}
#[cfg(feature = "alloc")]
#[inline]
pub fn encode_upper_prefixed<T: AsRef<[u8]>>(data: T) -> String {
encode_inner::<true, true>(data.as_ref())
}
#[inline]
pub const fn const_check(input: &[u8]) -> Result<(), FromHexError> {
if input.len() % 2 != 0 {
return Err(FromHexError::OddLength);
}
let input = strip_prefix(input);
if const_check_raw(input) {
Ok(())
} else {
Err(unsafe { invalid_hex_error(input) })
}
}
#[inline]
pub const fn const_check_raw(input: &[u8]) -> bool {
generic::check(input)
}
#[inline]
pub fn check<T: AsRef<[u8]>>(input: T) -> Result<(), FromHexError> {
#[allow(clippy::missing_const_for_fn)]
fn check_inner(input: &[u8]) -> Result<(), FromHexError> {
if input.len() % 2 != 0 {
return Err(FromHexError::OddLength);
}
let stripped = strip_prefix(input);
if imp::check(stripped) {
Ok(())
} else {
let mut e = unsafe { invalid_hex_error(stripped) };
if let FromHexError::InvalidHexCharacter { ref mut index, .. } = e {
*index += input.len() - stripped.len();
}
Err(e)
}
}
check_inner(input.as_ref())
}
#[inline]
pub fn check_raw<T: AsRef<[u8]>>(input: T) -> bool {
imp::check(input.as_ref())
}
#[inline]
pub const fn const_decode_to_array<const N: usize>(input: &[u8]) -> Result<[u8; N], FromHexError> {
if input.len() % 2 != 0 {
return Err(FromHexError::OddLength);
}
let input = strip_prefix(input);
if input.len() != N * 2 {
return Err(FromHexError::InvalidStringLength);
}
match const_decode_to_array_impl(input) {
Some(output) => Ok(output),
None => Err(unsafe { invalid_hex_error(input) }),
}
}
const fn const_decode_to_array_impl<const N: usize>(input: &[u8]) -> Option<[u8; N]> {
macro_rules! next {
($var:ident, $i:expr) => {
let hex = unsafe { *input.as_ptr().add($i) };
let $var = HEX_DECODE_LUT[hex as usize];
if $var == NIL {
return None;
}
};
}
let mut output = [0; N];
debug_assert!(input.len() == N * 2);
let mut i = 0;
while i < output.len() {
next!(high, i * 2);
next!(low, i * 2 + 1);
output[i] = high << 4 | low;
i += 1;
}
Some(output)
}
#[cfg(feature = "alloc")]
#[inline]
pub fn decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, FromHexError> {
fn decode_inner(input: &[u8]) -> Result<Vec<u8>, FromHexError> {
if unlikely(input.len() % 2 != 0) {
return Err(FromHexError::OddLength);
}
let input = strip_prefix(input);
let len = input.len() / 2;
let mut output = Vec::with_capacity(len);
#[allow(clippy::uninit_vec)]
unsafe {
output.set_len(len);
}
unsafe { decode_checked(input, &mut output) }.map(|()| output)
}
decode_inner(input.as_ref())
}
#[inline]
pub fn decode_to_slice<T: AsRef<[u8]>>(input: T, output: &mut [u8]) -> Result<(), FromHexError> {
decode_to_slice_inner(input.as_ref(), output)
}
#[inline]
pub fn decode_to_array<T: AsRef<[u8]>, const N: usize>(input: T) -> Result<[u8; N], FromHexError> {
fn decode_to_array_inner<const N: usize>(input: &[u8]) -> Result<[u8; N], FromHexError> {
let mut output = impl_core::uninit_array();
let output_slice = unsafe { impl_core::slice_assume_init_mut(&mut output) };
decode_to_slice_inner(input, output_slice)
.map(|()| unsafe { impl_core::array_assume_init(output) })
}
decode_to_array_inner(input.as_ref())
}
#[cfg(feature = "alloc")]
fn encode_inner<const UPPER: bool, const PREFIX: bool>(data: &[u8]) -> String {
let capacity = PREFIX as usize * 2 + data.len() * 2;
let mut buf = Vec::<u8>::with_capacity(capacity);
#[allow(clippy::uninit_vec)]
unsafe {
buf.set_len(capacity)
};
let mut output = buf.as_mut_slice();
if PREFIX {
unsafe {
*output.get_unchecked_mut(0) = b'0';
*output.get_unchecked_mut(1) = b'x';
output = output.get_unchecked_mut(2..);
}
}
unsafe { imp::encode::<UPPER>(data, output) };
unsafe { String::from_utf8_unchecked(buf) }
}
fn encode_to_slice_inner<const UPPER: bool>(
input: &[u8],
output: &mut [u8],
) -> Result<(), FromHexError> {
if unlikely(output.len() != 2 * input.len()) {
return Err(FromHexError::InvalidStringLength);
}
unsafe { imp::encode::<UPPER>(input, output) };
Ok(())
}
fn encode_to_str_inner<'o, const UPPER: bool>(
input: &[u8],
output: &'o mut [u8],
) -> Result<&'o mut str, FromHexError> {
encode_to_slice_inner::<UPPER>(input, output)?;
let s = unsafe { core::str::from_utf8_unchecked_mut(output) };
Ok(s)
}
fn decode_to_slice_inner(input: &[u8], output: &mut [u8]) -> Result<(), FromHexError> {
if unlikely(input.len() % 2 != 0) {
return Err(FromHexError::OddLength);
}
let input = strip_prefix(input);
if unlikely(output.len() != input.len() / 2) {
return Err(FromHexError::InvalidStringLength);
}
unsafe { decode_checked(input, output) }
}
#[inline]
unsafe fn decode_checked(input: &[u8], output: &mut [u8]) -> Result<(), FromHexError> {
debug_assert_eq!(output.len(), input.len() / 2);
if imp::USE_CHECK_FN {
if imp::check(input) {
unsafe { imp::decode_unchecked(input, output) };
return Ok(());
}
} else {
if unsafe { imp::decode_checked(input, output) } {
return Ok(());
}
}
Err(unsafe { invalid_hex_error(input) })
}
#[inline]
const fn byte2hex<const UPPER: bool>(byte: u8) -> (u8, u8) {
let table = get_chars_table::<UPPER>();
let high = table[(byte >> 4) as usize];
let low = table[(byte & 0x0f) as usize];
(high, low)
}
#[inline]
const fn strip_prefix(bytes: &[u8]) -> &[u8] {
match bytes {
[b'0', b'x', rest @ ..] => rest,
_ => bytes,
}
}
#[cold]
#[cfg_attr(debug_assertions, track_caller)]
const unsafe fn invalid_hex_error(input: &[u8]) -> FromHexError {
let mut index = None;
let mut iter = input;
while let [byte, rest @ ..] = iter {
if HEX_DECODE_LUT[*byte as usize] == NIL {
index = Some(input.len() - rest.len() - 1);
break;
}
iter = rest;
}
let index = match index {
Some(index) => index,
None => {
if cfg!(debug_assertions) {
panic!("input was valid but `check` failed")
} else {
unsafe { core::hint::unreachable_unchecked() }
}
}
};
FromHexError::InvalidHexCharacter {
c: input[index] as char,
index,
}
}
#[inline(always)]
const fn get_chars_table<const UPPER: bool>() -> &'static [u8; 16] {
if UPPER {
HEX_CHARS_UPPER
} else {
HEX_CHARS_LOWER
}
}
const fn make_decode_lut() -> [u8; 256] {
let mut lut = [0; 256];
let mut i = 0u8;
loop {
lut[i as usize] = match i {
b'0'..=b'9' => i - b'0',
b'A'..=b'F' => i - b'A' + 10,
b'a'..=b'f' => i - b'a' + 10,
_ => NIL,
};
if i == NIL {
break;
}
i += 1;
}
lut
}
#[allow(
missing_docs,
unused,
clippy::all,
clippy::missing_inline_in_public_items
)]
#[cfg(all(feature = "__fuzzing", not(miri)))]
#[doc(hidden)]
pub mod fuzzing {
use super::*;
use proptest::test_runner::TestCaseResult;
use proptest::{prop_assert, prop_assert_eq};
use std::fmt::Write;
pub fn fuzz(data: &[u8]) -> TestCaseResult {
self::encode(&data)?;
self::decode(&data)?;
Ok(())
}
pub fn encode(input: &[u8]) -> TestCaseResult {
test_buffer::<8, 16>(input)?;
test_buffer::<20, 40>(input)?;
test_buffer::<32, 64>(input)?;
test_buffer::<64, 128>(input)?;
test_buffer::<128, 256>(input)?;
let encoded = crate::encode(input);
let expected = mk_expected(input);
prop_assert_eq!(&encoded, &expected);
let decoded = crate::decode(&encoded).unwrap();
prop_assert_eq!(decoded, input);
Ok(())
}
pub fn decode(input: &[u8]) -> TestCaseResult {
if let Ok(decoded) = crate::decode(input) {
let input_len = strip_prefix(input).len() / 2;
prop_assert_eq!(decoded.len(), input_len);
}
Ok(())
}
fn mk_expected(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for i in bytes {
write!(s, "{i:02x}").unwrap();
}
s
}
fn test_buffer<const N: usize, const LEN: usize>(bytes: &[u8]) -> TestCaseResult {
if let Ok(bytes) = <&[u8; N]>::try_from(bytes) {
let mut buffer = Buffer::<N, false>::new();
let string = buffer.format(bytes).to_string();
prop_assert_eq!(string.len(), bytes.len() * 2);
prop_assert_eq!(string.as_bytes(), buffer.as_byte_array::<LEN>());
prop_assert_eq!(string.as_str(), buffer.as_str());
prop_assert_eq!(string.as_str(), mk_expected(bytes));
let mut buffer = Buffer::<N, true>::new();
let prefixed = buffer.format(bytes).to_string();
prop_assert_eq!(prefixed.len(), 2 + bytes.len() * 2);
prop_assert_eq!(prefixed.as_str(), buffer.as_str());
prop_assert_eq!(prefixed.as_str(), format!("0x{string}"));
prop_assert_eq!(decode_to_array(&string), Ok(*bytes));
prop_assert_eq!(decode_to_array(&prefixed), Ok(*bytes));
prop_assert_eq!(const_decode_to_array(string.as_bytes()), Ok(*bytes));
prop_assert_eq!(const_decode_to_array(prefixed.as_bytes()), Ok(*bytes));
}
Ok(())
}
proptest::proptest! {
#![proptest_config(proptest::prelude::ProptestConfig {
cases: 1024,
..Default::default()
})]
#[test]
fn fuzz_encode(s in ".+") {
encode(s.as_bytes())?;
}
#[test]
fn fuzz_check_true(s in "[0-9a-fA-F]+") {
let s = s.as_bytes();
prop_assert!(crate::check_raw(s));
prop_assert!(crate::const_check_raw(s));
if s.len() % 2 == 0 {
prop_assert!(crate::check(s).is_ok());
prop_assert!(crate::const_check(s).is_ok());
}
}
#[test]
fn fuzz_check_false(s in ".{16}[0-9a-fA-F]+") {
let s = s.as_bytes();
prop_assert!(crate::check(s).is_err());
prop_assert!(crate::const_check(s).is_err());
prop_assert!(!crate::check_raw(s));
prop_assert!(!crate::const_check_raw(s));
}
}
}