eth-valkyoth-codec 0.18.0

Bounded no_std Ethereum wire codec policy.
Documentation
//! External Ethereum RLP fixture conformance tests.

use std::{env, error::Error, fs, path::Path, path::PathBuf, str, string::String, vec, vec::Vec};

use eth_valkyoth_codec::{
    DecodeError, DecodeLimits, decode_rlp_list, decode_rlp_scalar, encode_decoded_list,
    encode_decoded_scalar,
};
use serde_json::Value;

const FIXTURE_DIR_ENV: &str = "ETH_EXECUTION_TESTS_RLP_DIR";
const REQUIRE_FIXTURES_ENV: &str = "ETH_REQUIRE_EXECUTION_FIXTURES";
const MAX_FIXTURE_DIRECTORY_DEPTH: u32 = 32;

#[test]
fn pinned_ethereum_rlp_fixtures_match_codec() -> Result<(), Box<dyn Error>> {
    let Some(dir) = env::var_os(FIXTURE_DIR_ENV) else {
        if env::var_os(REQUIRE_FIXTURES_ENV).is_some() {
            return Err(
                format!("{FIXTURE_DIR_ENV} must be set when {REQUIRE_FIXTURES_ENV}=1").into(),
            );
        }
        return Ok(());
    };
    let paths = json_files(PathBuf::from(dir))?;
    if paths.is_empty() {
        return Err("no RLP fixture JSON files found".into());
    }

    let mut cases_seen = 0usize;
    let mut failures = Vec::new();
    for path in paths {
        run_fixture_file(&path, &mut cases_seen, &mut failures)?;
    }

    if cases_seen == 0 {
        return Err("no RLP fixture cases found".into());
    }
    if failures.is_empty() {
        Ok(())
    } else {
        Err(failures.join("\n").into())
    }
}

fn json_files(root: PathBuf) -> Result<Vec<PathBuf>, Box<dyn Error>> {
    let mut paths = Vec::new();
    collect_json_files(&root, &mut paths, 0)?;
    paths.sort();
    Ok(paths)
}

fn collect_json_files(
    path: &Path,
    paths: &mut Vec<PathBuf>,
    depth: u32,
) -> Result<(), Box<dyn Error>> {
    if depth > MAX_FIXTURE_DIRECTORY_DEPTH {
        return Err("fixture directory nesting exceeds safety limit".into());
    }
    for entry in fs::read_dir(path)? {
        let entry = entry?;
        let path = entry.path();
        let file_type = entry.file_type()?;
        if file_type.is_symlink() {
            continue;
        }
        if file_type.is_dir() {
            let child_depth = depth
                .checked_add(1)
                .ok_or("fixture directory nesting depth overflow")?;
            collect_json_files(&path, paths, child_depth)?;
        } else if path
            .extension()
            .is_some_and(|extension| extension == "json")
        {
            paths.push(path);
        }
    }
    Ok(())
}

fn run_fixture_file(
    path: &Path,
    cases_seen: &mut usize,
    failures: &mut Vec<String>,
) -> Result<(), Box<dyn Error>> {
    let text = fs::read_to_string(path)?;
    let json: Value = serde_json::from_str(&text)?;
    let Some(cases) = json.as_object() else {
        return Err(format!("{} is not a JSON object", path.display()).into());
    };

    for (name, case) in cases {
        *cases_seen = cases_seen
            .checked_add(1)
            .ok_or("RLP fixture case counter overflow")?;
        run_case(path, name, case, failures)?;
    }
    Ok(())
}

fn run_case(
    path: &Path,
    name: &str,
    case: &Value,
    failures: &mut Vec<String>,
) -> Result<(), Box<dyn Error>> {
    let Some(output) = case.get("out").and_then(Value::as_str) else {
        return Err(format!("{}:{name} is missing string out", path.display()).into());
    };
    let encoded = decode_hex(output)?;
    let valid = !matches!(case.get("in"), Some(Value::String(marker)) if marker == "INVALID");

    match (valid, decode_and_reencode(&encoded)) {
        (true, Ok(round_trip)) if round_trip == encoded => Ok(()),
        (true, Ok(_)) => {
            failures.push(format!(
                "{}:{name} re-encoded to different bytes",
                path.display()
            ));
            Ok(())
        }
        (true, Err(error)) => {
            failures.push(format!(
                "{}:{name} rejected valid RLP: {error:?}",
                path.display()
            ));
            Ok(())
        }
        (false, Ok(_)) => {
            failures.push(format!("{}:{name} accepted invalid RLP", path.display()));
            Ok(())
        }
        (false, Err(_)) => Ok(()),
    }
}

fn decode_and_reencode(input: &[u8]) -> Result<Vec<u8>, DecodeError> {
    let prefix = *input.first().ok_or(DecodeError::Malformed)?;
    let mut output = vec![0_u8; input.len()];
    let written = if prefix <= 0xbf {
        let scalar = decode_rlp_scalar(input, DecodeLimits::TEST_FIXTURE)?;
        encode_decoded_scalar(scalar, &mut output)?
    } else {
        let list = decode_rlp_list(input, DecodeLimits::TEST_FIXTURE)?;
        encode_decoded_list(list, &mut output)?
    };
    output.truncate(written);
    Ok(output)
}

fn decode_hex(input: &str) -> Result<Vec<u8>, Box<dyn Error>> {
    let hex = match input.strip_prefix("0x") {
        Some(stripped) => stripped,
        None => input,
    };
    let mut chunks = hex.as_bytes().chunks_exact(2);
    if !chunks.remainder().is_empty() {
        return Err(format!("hex string has odd length: {input}").into());
    }
    let mut output = Vec::new();
    for chunk in &mut chunks {
        let pair = str::from_utf8(chunk)?;
        output.push(u8::from_str_radix(pair, 16)?);
    }
    Ok(output)
}