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
9use anyhow::Context as _;
10
11#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
12#[error(transparent)]
13pub struct DecodeError(#[from] faster_hex_private::Error);
14
15/// Lower-case hex encoding, without prefix.
16pub fn encode(data: impl AsRef<[u8]>) -> String {
17    faster_hex_private::hex_string(data.as_ref())
18}
19
20/// Lower-case hex encoding with a `0x` prefix.
21pub fn encode_prefixed(data: impl AsRef<[u8]>) -> String {
22    let data = data.as_ref();
23    let mut buf = vec![0; 2 + data.len() * 2];
24    let (prefix, digits) = buf.split_at_mut(2);
25    prefix.copy_from_slice(b"0x");
26    faster_hex_private::hex_encode(data, digits).expect("output buffer is sized to fit");
27    debug_assert!(buf.is_ascii());
28    // SAFETY: the prefix and the `hex_encode` output are ASCII.
29    unsafe { String::from_utf8_unchecked(buf) }
30}
31
32/// Parses a `0x`-prefixed hex integer, e.g. `0x1a`.
33///
34/// A sign is rejected for every `T`, matching Go's `strconv.ParseUint`.
35pub fn parse_prefixed_int<T>(input: &str) -> anyhow::Result<T>
36where
37    T: num_traits::Num,
38    <T as num_traits::Num>::FromStrRadixErr: std::fmt::Display,
39{
40    let digits = input
41        .strip_prefix("0x")
42        .with_context(|| format!("not a 0x-prefixed hex integer: {input}"))?;
43    anyhow::ensure!(
44        !digits.starts_with(['+', '-']),
45        "signed hex integer: {input}"
46    );
47    T::from_str_radix(digits, 16).map_err(|e| anyhow::anyhow!("invalid hex integer {input}: {e}"))
48}
49
50/// Decodes hex digits (upper, lower or mixed case, no `0x` prefix) into bytes.
51pub fn decode(input: impl AsRef<[u8]>) -> Result<Vec<u8>, DecodeError> {
52    let input = input.as_ref();
53    let mut out = vec![0; input.len() / 2];
54    faster_hex_private::hex_decode(input, &mut out)?;
55    Ok(out)
56}
57
58/// Usage: `#[serde(with = "crate::utils::encoding::hex::serde")]`, a drop-in
59/// replacement for `hex::serde`: lower-case, no prefix.
60pub mod serde {
61    use serde::{Deserialize as _, Deserializer, Serializer, de};
62
63    pub fn serialize<S: Serializer>(
64        data: impl AsRef<[u8]>,
65        serializer: S,
66    ) -> Result<S::Ok, S::Error> {
67        serializer.serialize_str(&super::encode(data))
68    }
69
70    pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
71    where
72        D: Deserializer<'de>,
73        T: TryFrom<Vec<u8>>,
74    {
75        let s = String::deserialize(deserializer)?;
76        let bytes = super::decode(&s).map_err(de::Error::custom)?;
77        let len = bytes.len();
78        T::try_from(bytes).map_err(|_| de::Error::custom(format!("invalid length {len}")))
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use quickcheck_macros::quickcheck;
86    use rstest::rstest;
87
88    #[quickcheck]
89    fn encode_matches_hex_crate(data: Vec<u8>) -> bool {
90        encode(&data) == ::hex::encode(&data)
91    }
92
93    #[quickcheck]
94    fn encode_prefixed_is_ascii_with_prefix(data: Vec<u8>) -> bool {
95        let s = encode_prefixed(&data);
96        s.is_ascii() && s.strip_prefix("0x") == Some(encode(&data).as_str())
97    }
98
99    /// Accept/reject boundary and decoded bytes agree with the `hex` crate this
100    /// module replaced, on both guaranteed-valid and arbitrary input.
101    #[quickcheck]
102    fn decode_matches_hex_crate(data: Vec<u8>, junk: String) -> bool {
103        [::hex::encode(&data), junk]
104            .iter()
105            .all(|s| match (decode(s), ::hex::decode(s)) {
106                (Ok(ours), Ok(theirs)) => ours == theirs,
107                (Err(_), Err(_)) => true,
108                _ => false,
109            })
110    }
111
112    #[quickcheck]
113    fn decode_roundtrip_any_case(data: Vec<u8>, flips: Vec<bool>) -> bool {
114        let s: String = encode(&data)
115            .chars()
116            .zip(flips.into_iter().chain(std::iter::repeat(false)))
117            .map(|(c, up)| if up { c.to_ascii_uppercase() } else { c })
118            .collect();
119        decode(&s).unwrap() == data
120    }
121
122    #[quickcheck]
123    fn decode_no_panic(input: Vec<u8>) {
124        let _ = decode(&input);
125    }
126
127    #[quickcheck]
128    fn serde_matches_hex_crate(data: Vec<u8>) -> bool {
129        #[derive(::serde::Serialize, ::serde::Deserialize)]
130        struct Ours(#[serde(with = "crate::utils::encoding::hex::serde")] Vec<u8>);
131        #[derive(::serde::Serialize)]
132        struct Theirs(#[serde(with = "::hex::serde")] Vec<u8>);
133
134        let ours = serde_json::to_string(&Ours(data.clone())).unwrap();
135        ours == serde_json::to_string(&Theirs(data.clone())).unwrap()
136            && serde_json::from_str::<Ours>(&ours).unwrap().0 == data
137    }
138
139    #[test]
140    fn encode_matches_expectations() {
141        assert_eq!(encode([]), "");
142        assert_eq!(encode([0x00, 0xab, 0xff]), "00abff");
143        assert_eq!(encode_prefixed([]), "0x");
144        assert_eq!(encode_prefixed([0x00, 0xab, 0xff]), "0x00abff");
145    }
146
147    #[test]
148    fn decode_accepts_mixed_case() {
149        assert_eq!(decode("").unwrap(), Vec::<u8>::new());
150        assert_eq!(decode("00abff").unwrap(), [0x00, 0xab, 0xff]);
151        assert_eq!(decode("00AbFF").unwrap(), [0x00, 0xab, 0xff]);
152    }
153
154    #[test]
155    fn decode_rejects_invalid_input() {
156        for invalid in ["abc", "00gg", "0x00"] {
157            assert!(decode(invalid).is_err(), "{invalid:?} should be rejected");
158        }
159    }
160
161    #[rstest]
162    #[case("0x0", 0)]
163    #[case("0x1a", 26)]
164    #[case("0x1A", 26)]
165    fn parse_prefixed_int_accepts(#[case] input: &str, #[case] expected: u64) {
166        assert_eq!(parse_prefixed_int::<u64>(input).unwrap(), expected);
167    }
168
169    #[rstest]
170    #[case("")]
171    #[case("0")]
172    #[case("1a")]
173    #[case("0x")]
174    #[case("0X1a")]
175    #[case("0xg")]
176    #[case(" 0x1")]
177    #[case("0x1 ")]
178    // Multi-byte UTF-8 at the prefix boundary.
179    #[case("0é")]
180    #[case("0x\u{e9}")]
181    #[case("0x-1")]
182    #[case("0x+1")]
183    #[case("0x10000000000000000")]
184    fn parse_prefixed_int_rejects(#[case] input: &str) {
185        assert!(parse_prefixed_int::<u64>(input).is_err());
186        assert!(parse_prefixed_int::<i64>(input).is_err());
187    }
188
189    #[test]
190    fn parse_prefixed_int_is_bounded_by_target_type() {
191        assert_eq!(
192            parse_prefixed_int::<u64>("0x8000000000000000").unwrap(),
193            1 << 63
194        );
195        assert!(parse_prefixed_int::<i64>("0x8000000000000000").is_err());
196    }
197
198    #[quickcheck]
199    fn parse_prefixed_int_no_panic(input: String) {
200        for candidate in [input.clone(), format!("0x{input}")] {
201            let _ = parse_prefixed_int::<u64>(&candidate);
202            let _ = parse_prefixed_int::<i64>(&candidate);
203        }
204    }
205}