#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
#[error(transparent)]
pub struct DecodeError(#[from] faster_hex_private::Error);
pub fn encode(data: impl AsRef<[u8]>) -> String {
faster_hex_private::hex_string(data.as_ref())
}
pub fn encode_prefixed(data: impl AsRef<[u8]>) -> String {
let data = data.as_ref();
let mut buf = vec![0; 2 + data.len() * 2];
let (prefix, digits) = buf.split_at_mut(2);
prefix.copy_from_slice(b"0x");
faster_hex_private::hex_encode(data, digits).expect("output buffer is sized to fit");
debug_assert!(buf.is_ascii());
unsafe { String::from_utf8_unchecked(buf) }
}
pub fn decode(input: impl AsRef<[u8]>) -> Result<Vec<u8>, DecodeError> {
let input = input.as_ref();
let mut out = vec![0; input.len() / 2];
faster_hex_private::hex_decode(input, &mut out)?;
Ok(out)
}
pub mod serde {
use serde::{Deserialize as _, Deserializer, Serializer, de};
pub fn serialize<S: Serializer>(
data: impl AsRef<[u8]>,
serializer: S,
) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&super::encode(data))
}
pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: TryFrom<Vec<u8>>,
{
let s = String::deserialize(deserializer)?;
let bytes = super::decode(&s).map_err(de::Error::custom)?;
let len = bytes.len();
T::try_from(bytes).map_err(|_| de::Error::custom(format!("invalid length {len}")))
}
}
#[cfg(test)]
mod tests {
use super::*;
use quickcheck_macros::quickcheck;
#[quickcheck]
fn encode_matches_hex_crate(data: Vec<u8>) -> bool {
encode(&data) == ::hex::encode(&data)
}
#[quickcheck]
fn encode_prefixed_is_ascii_with_prefix(data: Vec<u8>) -> bool {
let s = encode_prefixed(&data);
s.is_ascii() && s.strip_prefix("0x") == Some(encode(&data).as_str())
}
#[quickcheck]
fn decode_matches_hex_crate(data: Vec<u8>, junk: String) -> bool {
[::hex::encode(&data), junk]
.iter()
.all(|s| match (decode(s), ::hex::decode(s)) {
(Ok(ours), Ok(theirs)) => ours == theirs,
(Err(_), Err(_)) => true,
_ => false,
})
}
#[quickcheck]
fn decode_roundtrip_any_case(data: Vec<u8>, flips: Vec<bool>) -> bool {
let s: String = encode(&data)
.chars()
.zip(flips.into_iter().chain(std::iter::repeat(false)))
.map(|(c, up)| if up { c.to_ascii_uppercase() } else { c })
.collect();
decode(&s).unwrap() == data
}
#[quickcheck]
fn decode_no_panic(input: Vec<u8>) {
let _ = decode(&input);
}
#[quickcheck]
fn serde_matches_hex_crate(data: Vec<u8>) -> bool {
#[derive(::serde::Serialize, ::serde::Deserialize)]
struct Ours(#[serde(with = "crate::utils::encoding::hex::serde")] Vec<u8>);
#[derive(::serde::Serialize)]
struct Theirs(#[serde(with = "::hex::serde")] Vec<u8>);
let ours = serde_json::to_string(&Ours(data.clone())).unwrap();
ours == serde_json::to_string(&Theirs(data.clone())).unwrap()
&& serde_json::from_str::<Ours>(&ours).unwrap().0 == data
}
#[test]
fn encode_matches_expectations() {
assert_eq!(encode([]), "");
assert_eq!(encode([0x00, 0xab, 0xff]), "00abff");
assert_eq!(encode_prefixed([]), "0x");
assert_eq!(encode_prefixed([0x00, 0xab, 0xff]), "0x00abff");
}
#[test]
fn decode_accepts_mixed_case() {
assert_eq!(decode("").unwrap(), Vec::<u8>::new());
assert_eq!(decode("00abff").unwrap(), [0x00, 0xab, 0xff]);
assert_eq!(decode("00AbFF").unwrap(), [0x00, 0xab, 0xff]);
}
#[test]
fn decode_rejects_invalid_input() {
for invalid in ["abc", "00gg", "0x00"] {
assert!(decode(invalid).is_err(), "{invalid:?} should be rejected");
}
}
}