use std::ffi::CString;
use std::sync::Arc;
use rs_abieos::{AbiHandle, AbieosError};
use serde_json::Value;
const SPRING_REQUEST_TYPES: [&str; 5] = [
"get_status_request_v0",
"get_blocks_request_v0",
"get_blocks_ack_request_v0",
"get_blocks_request_v1",
"get_status_request_v1",
];
const SPRING_RESULT_TYPES: [&str; 4] = [
"get_status_result_v0",
"get_blocks_result_v0",
"get_blocks_result_v1",
"get_status_result_v1",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AbiEntry {
pub text: Arc<str>,
pub fp: u64,
pub compatible: bool,
}
impl AbiEntry {
pub fn new(text: &str) -> AbiEntry {
let parsed = serde_json::from_str::<Value>(text).ok();
let (fp, compatible) = match &parsed {
Some(value) => (fingerprint_value(value), compatible_value(value)),
None => (fnv1a64(text.as_bytes()), false),
};
AbiEntry {
text: Arc::from(text),
fp,
compatible,
}
}
pub fn fp_hex(&self) -> String {
format!("{:016x}", self.fp)
}
}
pub(crate) fn fnv1a64(bytes: &[u8]) -> u64 {
const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
bytes.iter().fold(OFFSET, |hash, &byte| {
(hash ^ u64::from(byte)).wrapping_mul(PRIME)
})
}
pub fn abi_fingerprint(text: &str) -> u64 {
match serde_json::from_str::<Value>(text) {
Ok(value) => fingerprint_value(&value),
Err(_) => fnv1a64(text.as_bytes()),
}
}
#[cfg_attr(not(test), allow(dead_code))]
pub fn abi_compatible(text: &str) -> bool {
serde_json::from_str::<Value>(text).is_ok_and(|value| compatible_value(&value))
}
fn fingerprint_value(value: &Value) -> u64 {
let mut canonical = Vec::new();
write_canonical(value, &mut canonical);
fnv1a64(&canonical)
}
fn write_canonical(value: &Value, out: &mut Vec<u8>) {
match value {
Value::Object(map) => {
let mut entries: Vec<(&String, &Value)> = map.iter().collect();
entries.sort_by(|a, b| a.0.cmp(b.0));
out.push(b'{');
for (i, (key, item)) in entries.into_iter().enumerate() {
if i > 0 {
out.push(b',');
}
serde_json::to_writer(&mut *out, key).expect("string key");
out.push(b':');
write_canonical(item, out);
}
out.push(b'}');
}
Value::Array(items) => {
out.push(b'[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(b',');
}
write_canonical(item, out);
}
out.push(b']');
}
scalar => serde_json::to_writer(&mut *out, scalar).expect("scalar"),
}
}
fn compatible_value(value: &Value) -> bool {
variant_is_prefix(value, "request", &SPRING_REQUEST_TYPES)
&& variant_is_prefix(value, "result", &SPRING_RESULT_TYPES)
}
fn variant_is_prefix(abi: &Value, name: &str, spring: &[&str]) -> bool {
let Some(variants) = abi.get("variants").and_then(Value::as_array) else {
return false;
};
let Some(types) = variants
.iter()
.find(|v| v.get("name").and_then(Value::as_str) == Some(name))
.and_then(|v| v.get("types"))
.and_then(Value::as_array)
else {
return false;
};
!types.is_empty()
&& types.len() <= spring.len()
&& types
.iter()
.zip(spring)
.all(|(have, want)| have.as_str() == Some(*want))
}
pub(crate) fn parse_upstream_abi(abi_json: &str) -> Result<AbiHandle, AbieosError> {
AbiHandle::from_json(abi_json)
}
pub(crate) fn accept_upstream_abi(abi_json: &str) -> Result<(), AbieosError> {
CString::new(abi_json).map_err(|_| AbieosError::SetAbi("interior NUL byte in input".into()))?;
parse_upstream_abi(abi_json).map(|_| ())
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Barrier};
use std::thread;
use super::{
abi_compatible, abi_fingerprint, accept_upstream_abi, fnv1a64, parse_upstream_abi, AbiEntry,
};
const SHIP_ABI_JSON: &str = include_str!("../mock-ship/src/ship_abi.json");
fn eosio_name(s: &str) -> u64 {
fn symbol(c: u8) -> u64 {
match c {
b'a'..=b'z' => (c - b'a') as u64 + 6,
b'1'..=b'5' => (c - b'1') as u64 + 1,
_ => 0,
}
}
let bytes = s.as_bytes();
let mut name = 0u64;
for (i, &c) in bytes.iter().take(12).enumerate() {
name |= (symbol(c) & 0x1f) << (64 - 5 * (i + 1));
}
if bytes.len() >= 13 {
name |= symbol(bytes[12]) & 0x0f;
}
name
}
fn abi_with_action(action: &str) -> String {
format!(
r#"{{"version":"eosio::abi/1.1","actions":[{{"name":"{action}","type":"uint32","ricardian_contract":""}}]}}"#
)
}
#[test]
fn ship_abi_parses_and_garbage_does_not() {
accept_upstream_abi(SHIP_ABI_JSON).expect("Spring SHiP ABI");
assert!(accept_upstream_abi("{").is_err());
assert!(accept_upstream_abi(r#"{"version":"nope"}"#).is_err());
assert!(accept_upstream_abi("not json").is_err());
}
#[test]
fn interior_nul_is_rejected_like_set_abi() {
let err = accept_upstream_abi("{\"version\":\"eosio::abi/1.1\"\0}").unwrap_err();
let message = err.to_string();
assert!(
message.contains("interior NUL"),
"unexpected error: {message}"
);
}
#[test]
fn concurrent_parses_each_own_their_abi() {
const N: usize = 8;
let barrier = Arc::new(Barrier::new(N));
let mut threads = Vec::with_capacity(N);
for i in 0..N {
let barrier = Arc::clone(&barrier);
threads.push(thread::spawn(move || {
let action = if i % 2 == 0 { "acta" } else { "actb" };
let json = abi_with_action(action);
barrier.wait();
let handle = parse_upstream_abi(&json).expect("abi");
(action, handle)
}));
}
let owned: Vec<_> = threads
.into_iter()
.map(|t| t.join().expect("thread"))
.collect();
for (action, handle) in &owned {
let other = if *action == "acta" { "actb" } else { "acta" };
assert_eq!(
handle.type_for_action(eosio_name(action)),
Some("uint32"),
"{action} must still resolve on its own handle"
);
assert_eq!(handle.type_for_action(eosio_name(other)), None);
}
}
#[test]
fn accept_does_not_keep_a_handle_and_matches_parse() {
let json = abi_with_action("acta");
accept_upstream_abi(&json).expect("valid");
let handle = parse_upstream_abi(&json).expect("valid");
assert_eq!(handle.type_for_action(eosio_name("acta")), Some("uint32"));
}
#[test]
fn fnv1a64_matches_reference_vectors() {
assert_eq!(fnv1a64(b""), 0xcbf2_9ce4_8422_2325);
assert_eq!(fnv1a64(b"a"), 0xaf63_dc4c_8601_ec8c);
assert_eq!(fnv1a64(b"foobar"), 0x8594_4171_f739_67e8);
}
#[test]
fn fingerprint_ignores_key_order_and_whitespace() {
let compact = r#"{"version":"eosio::abi/1.1","structs":[{"name":"a","fields":[]}]}"#;
let reordered = "{\n \"structs\" : [ { \"fields\": [], \"name\": \"a\" } ],\n \"version\": \"eosio::abi/1.1\"\n}";
assert_eq!(abi_fingerprint(compact), abi_fingerprint(reordered));
assert_eq!(
abi_fingerprint(compact),
fnv1a64(br#"{"structs":[{"fields":[],"name":"a"}],"version":"eosio::abi/1.1"}"#)
);
let changed = r#"{"version":"eosio::abi/1.1","structs":[{"name":"b","fields":[]}]}"#;
assert_ne!(abi_fingerprint(compact), abi_fingerprint(changed));
let two = r#"{"types":["x","y"]}"#;
let swapped = r#"{"types":["y","x"]}"#;
assert_ne!(abi_fingerprint(two), abi_fingerprint(swapped));
}
#[test]
fn fingerprint_of_non_json_hashes_the_raw_text() {
assert_eq!(abi_fingerprint("not json"), fnv1a64(b"not json"));
assert_eq!(abi_fingerprint("{"), fnv1a64(b"{"));
}
#[test]
fn spring_and_older_ship_abis_are_compatible() {
assert!(abi_compatible(SHIP_ABI_JSON));
let legacy = r#"{"version":"eosio::abi/1.1","variants":[
{"name":"request","types":["get_status_request_v0","get_blocks_request_v0","get_blocks_ack_request_v0"]},
{"name":"result","types":["get_status_result_v0","get_blocks_result_v0"]}]}"#;
assert!(abi_compatible(legacy));
}
#[test]
fn reordered_missing_or_extended_variants_are_not_compatible() {
let variants = |request: &str, result: &str| {
format!(
r#"{{"version":"eosio::abi/1.1","variants":[{{"name":"request","types":[{request}]}},{{"name":"result","types":[{result}]}}]}}"#
)
};
let request = r#""get_status_request_v0","get_blocks_request_v0""#;
let result = r#""get_status_result_v0","get_blocks_result_v0""#;
assert!(abi_compatible(&variants(request, result)));
let swapped = r#""get_blocks_request_v0","get_status_request_v0""#;
assert!(!abi_compatible(&variants(swapped, result)));
assert!(!abi_compatible(&variants(request, "")));
let unknown = r#""get_status_result_v0","get_blocks_result_v0","get_blocks_result_v1","get_status_result_v1","get_status_result_v2""#;
assert!(!abi_compatible(&variants(request, unknown)));
assert!(!abi_compatible(r#"{"version":"eosio::abi/1.1"}"#));
assert!(!abi_compatible(
r#"{"version":"eosio::abi/1.1","variants":[{"name":"request","types":["get_status_request_v0"]}]}"#
));
assert!(!abi_compatible("not json"));
}
#[test]
fn abi_entry_keeps_the_text_and_matches_the_free_functions() {
let entry = AbiEntry::new(SHIP_ABI_JSON);
assert_eq!(&*entry.text, SHIP_ABI_JSON);
assert_eq!(entry.fp, abi_fingerprint(SHIP_ABI_JSON));
assert!(entry.compatible);
assert_eq!(entry.fp_hex().len(), 16);
assert_eq!(entry.fp_hex(), format!("{:016x}", entry.fp));
let garbage = AbiEntry::new("{");
assert_eq!(garbage.fp, fnv1a64(b"{"));
assert!(!garbage.compatible);
}
}