forest-filecoin 0.36.0

Rust Filecoin implementation.
Documentation
// Copyright 2019-2026 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

//! Hex encoding/decoding built on the SIMD-accelerated `faster-hex` crate, a drop-in
//! replacement for the `hex` crate: import this module and `hex::encode`/`hex::decode`
//! call sites keep working. See benchmark results in
//! <https://github.com/ChainSafe/forest/pull/7395>.

use anyhow::Context as _;

#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
#[error(transparent)]
pub struct DecodeError(#[from] faster_hex_private::Error);

/// Lower-case hex encoding, without prefix.
pub fn encode(data: impl AsRef<[u8]>) -> String {
    faster_hex_private::hex_string(data.as_ref())
}

/// Lower-case hex encoding with a `0x` prefix.
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());
    // SAFETY: the prefix and the `hex_encode` output are ASCII.
    unsafe { String::from_utf8_unchecked(buf) }
}

/// Parses a `0x`-prefixed hex integer, e.g. `0x1a`.
///
/// A sign is rejected for every `T`, matching Go's `strconv.ParseUint`.
pub fn parse_prefixed_int<T>(input: &str) -> anyhow::Result<T>
where
    T: num_traits::Num,
    <T as num_traits::Num>::FromStrRadixErr: std::fmt::Display,
{
    let digits = input
        .strip_prefix("0x")
        .with_context(|| format!("not a 0x-prefixed hex integer: {input}"))?;
    anyhow::ensure!(
        !digits.starts_with(['+', '-']),
        "signed hex integer: {input}"
    );
    T::from_str_radix(digits, 16).map_err(|e| anyhow::anyhow!("invalid hex integer {input}: {e}"))
}

/// Decodes hex digits (upper, lower or mixed case, no `0x` prefix) into bytes.
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)
}

/// Usage: `#[serde(with = "crate::utils::encoding::hex::serde")]`, a drop-in
/// replacement for `hex::serde`: lower-case, no prefix.
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;
    use rstest::rstest;

    #[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())
    }

    /// Accept/reject boundary and decoded bytes agree with the `hex` crate this
    /// module replaced, on both guaranteed-valid and arbitrary input.
    #[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");
        }
    }

    #[rstest]
    #[case("0x0", 0)]
    #[case("0x1a", 26)]
    #[case("0x1A", 26)]
    fn parse_prefixed_int_accepts(#[case] input: &str, #[case] expected: u64) {
        assert_eq!(parse_prefixed_int::<u64>(input).unwrap(), expected);
    }

    #[rstest]
    #[case("")]
    #[case("0")]
    #[case("1a")]
    #[case("0x")]
    #[case("0X1a")]
    #[case("0xg")]
    #[case(" 0x1")]
    #[case("0x1 ")]
    // Multi-byte UTF-8 at the prefix boundary.
    #[case("")]
    #[case("0x\u{e9}")]
    #[case("0x-1")]
    #[case("0x+1")]
    #[case("0x10000000000000000")]
    fn parse_prefixed_int_rejects(#[case] input: &str) {
        assert!(parse_prefixed_int::<u64>(input).is_err());
        assert!(parse_prefixed_int::<i64>(input).is_err());
    }

    #[test]
    fn parse_prefixed_int_is_bounded_by_target_type() {
        assert_eq!(
            parse_prefixed_int::<u64>("0x8000000000000000").unwrap(),
            1 << 63
        );
        assert!(parse_prefixed_int::<i64>("0x8000000000000000").is_err());
    }

    #[quickcheck]
    fn parse_prefixed_int_no_panic(input: String) {
        for candidate in [input.clone(), format!("0x{input}")] {
            let _ = parse_prefixed_int::<u64>(&candidate);
            let _ = parse_prefixed_int::<i64>(&candidate);
        }
    }
}