Skip to main content

forest/utils/encoding/
hex.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4//! Hex encoding/decoding built on the SIMD-accelerated `faster-hex` crate, a drop-in
5//! replacement for the `hex` crate: import this module and `hex::encode`/`hex::decode`
6//! call sites keep working. See benchmark results in
7//! <https://github.com/ChainSafe/forest/pull/7395>.
8
9#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
10#[error(transparent)]
11pub struct DecodeError(#[from] faster_hex_private::Error);
12
13/// Lower-case hex encoding, without prefix.
14pub fn encode(data: impl AsRef<[u8]>) -> String {
15    faster_hex_private::hex_string(data.as_ref())
16}
17
18/// Lower-case hex encoding with a `0x` prefix.
19pub fn encode_prefixed(data: impl AsRef<[u8]>) -> String {
20    let data = data.as_ref();
21    let mut buf = vec![0; 2 + data.len() * 2];
22    let (prefix, digits) = buf.split_at_mut(2);
23    prefix.copy_from_slice(b"0x");
24    faster_hex_private::hex_encode(data, digits).expect("output buffer is sized to fit");
25    debug_assert!(buf.is_ascii());
26    // SAFETY: the prefix and the `hex_encode` output are ASCII.
27    unsafe { String::from_utf8_unchecked(buf) }
28}
29
30/// Decodes hex digits (upper, lower or mixed case, no `0x` prefix) into bytes.
31pub fn decode(input: impl AsRef<[u8]>) -> Result<Vec<u8>, DecodeError> {
32    let input = input.as_ref();
33    let mut out = vec![0; input.len() / 2];
34    faster_hex_private::hex_decode(input, &mut out)?;
35    Ok(out)
36}
37
38/// Usage: `#[serde(with = "crate::utils::encoding::hex::serde")]`, a drop-in
39/// replacement for `hex::serde`: lower-case, no prefix.
40pub mod serde {
41    use serde::{Deserialize as _, Deserializer, Serializer, de};
42
43    pub fn serialize<S: Serializer>(
44        data: impl AsRef<[u8]>,
45        serializer: S,
46    ) -> Result<S::Ok, S::Error> {
47        serializer.serialize_str(&super::encode(data))
48    }
49
50    pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
51    where
52        D: Deserializer<'de>,
53        T: TryFrom<Vec<u8>>,
54    {
55        let s = String::deserialize(deserializer)?;
56        let bytes = super::decode(&s).map_err(de::Error::custom)?;
57        let len = bytes.len();
58        T::try_from(bytes).map_err(|_| de::Error::custom(format!("invalid length {len}")))
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use quickcheck_macros::quickcheck;
66
67    #[quickcheck]
68    fn encode_matches_hex_crate(data: Vec<u8>) -> bool {
69        encode(&data) == ::hex::encode(&data)
70    }
71
72    #[quickcheck]
73    fn encode_prefixed_is_ascii_with_prefix(data: Vec<u8>) -> bool {
74        let s = encode_prefixed(&data);
75        s.is_ascii() && s.strip_prefix("0x") == Some(encode(&data).as_str())
76    }
77
78    /// Accept/reject boundary and decoded bytes agree with the `hex` crate this
79    /// module replaced, on both guaranteed-valid and arbitrary input.
80    #[quickcheck]
81    fn decode_matches_hex_crate(data: Vec<u8>, junk: String) -> bool {
82        [::hex::encode(&data), junk]
83            .iter()
84            .all(|s| match (decode(s), ::hex::decode(s)) {
85                (Ok(ours), Ok(theirs)) => ours == theirs,
86                (Err(_), Err(_)) => true,
87                _ => false,
88            })
89    }
90
91    #[quickcheck]
92    fn decode_roundtrip_any_case(data: Vec<u8>, flips: Vec<bool>) -> bool {
93        let s: String = encode(&data)
94            .chars()
95            .zip(flips.into_iter().chain(std::iter::repeat(false)))
96            .map(|(c, up)| if up { c.to_ascii_uppercase() } else { c })
97            .collect();
98        decode(&s).unwrap() == data
99    }
100
101    #[quickcheck]
102    fn decode_no_panic(input: Vec<u8>) {
103        let _ = decode(&input);
104    }
105
106    #[quickcheck]
107    fn serde_matches_hex_crate(data: Vec<u8>) -> bool {
108        #[derive(::serde::Serialize, ::serde::Deserialize)]
109        struct Ours(#[serde(with = "crate::utils::encoding::hex::serde")] Vec<u8>);
110        #[derive(::serde::Serialize)]
111        struct Theirs(#[serde(with = "::hex::serde")] Vec<u8>);
112
113        let ours = serde_json::to_string(&Ours(data.clone())).unwrap();
114        ours == serde_json::to_string(&Theirs(data.clone())).unwrap()
115            && serde_json::from_str::<Ours>(&ours).unwrap().0 == data
116    }
117
118    #[test]
119    fn encode_matches_expectations() {
120        assert_eq!(encode([]), "");
121        assert_eq!(encode([0x00, 0xab, 0xff]), "00abff");
122        assert_eq!(encode_prefixed([]), "0x");
123        assert_eq!(encode_prefixed([0x00, 0xab, 0xff]), "0x00abff");
124    }
125
126    #[test]
127    fn decode_accepts_mixed_case() {
128        assert_eq!(decode("").unwrap(), Vec::<u8>::new());
129        assert_eq!(decode("00abff").unwrap(), [0x00, 0xab, 0xff]);
130        assert_eq!(decode("00AbFF").unwrap(), [0x00, 0xab, 0xff]);
131    }
132
133    #[test]
134    fn decode_rejects_invalid_input() {
135        for invalid in ["abc", "00gg", "0x00"] {
136            assert!(decode(invalid).is_err(), "{invalid:?} should be rejected");
137        }
138    }
139}