use super::error::Error;
pub mod cache;
use cache::MetaCache;
use alloy::primitives::{hex, keccak256};
use futures::future;
use graphql_client::GraphQLQuery;
use rain_metadata_bindings::IDescribedByMetaV1;
use reqwest::Client;
use serde::de::{Deserialize, Deserializer, Visitor};
use serde::ser::{Serialize, SerializeMap, Serializer};
use std::{
collections::{BTreeSet, HashMap},
convert::TryFrom,
fmt::Debug,
sync::Arc,
};
use strum::{EnumIter, EnumString};
use alloy::sol_types::private::Address;
use alloy::providers::Provider;
use alloy::contract::Error as ContractError;
use rain_erc::erc165::{Erc165Error, IERC165, XorSelectors, supports_erc165};
pub mod magic;
pub(crate) mod normalize;
pub(crate) mod query;
pub mod types;
pub use magic::*;
pub use query::*;
#[derive(Copy, Clone, EnumString, EnumIter, strum::Display, Debug, PartialEq)]
#[strum(serialize_all = "kebab-case")]
pub enum KnownMeta {
OpV1,
DotrainV1,
RainlangV1,
SolidityAbiV2,
AuthoringMetaV1,
AuthoringMetaV2,
InterpreterCallerMetaV1,
ExpressionDeployerV2BytecodeV1,
RainlangSourceV1,
AddressList,
DotrainSourceV1,
OrderBuilderStateV1,
RaindexSignedContextOracleV1,
}
impl TryFrom<KnownMagic> for KnownMeta {
type Error = Error;
fn try_from(value: KnownMagic) -> Result<Self, Self::Error> {
match value {
KnownMagic::DotrainV1 => Ok(KnownMeta::DotrainV1),
KnownMagic::RainlangV1 => Ok(KnownMeta::RainlangV1),
KnownMagic::SolidityAbiV2 => Ok(KnownMeta::SolidityAbiV2),
KnownMagic::OpMetaV1 => Ok(KnownMeta::OpV1),
KnownMagic::AuthoringMetaV1 => Ok(KnownMeta::AuthoringMetaV1),
KnownMagic::AuthoringMetaV2 => Ok(KnownMeta::AuthoringMetaV2),
KnownMagic::AddressList => Ok(KnownMeta::AddressList),
KnownMagic::InterpreterCallerMetaV1 => Ok(KnownMeta::InterpreterCallerMetaV1),
KnownMagic::DotrainSourceV1 => Ok(KnownMeta::DotrainSourceV1),
KnownMagic::OrderBuilderStateV1 => Ok(KnownMeta::OrderBuilderStateV1),
KnownMagic::ExpressionDeployerV2BytecodeV1 => {
Ok(KnownMeta::ExpressionDeployerV2BytecodeV1)
}
KnownMagic::RainlangSourceV1 => Ok(KnownMeta::RainlangSourceV1),
KnownMagic::RaindexSignedContextOracleV1 => Ok(KnownMeta::RaindexSignedContextOracleV1),
_ => Err(Error::UnsupportedMeta),
}
}
}
#[derive(
Copy,
Clone,
Debug,
EnumIter,
PartialEq,
EnumString,
strum::Display,
serde::Serialize,
serde::Deserialize,
)]
#[strum(serialize_all = "kebab-case")]
pub enum ContentType {
None,
#[serde(rename = "application/json")]
Json,
#[serde(rename = "application/cbor")]
Cbor,
#[serde(rename = "application/octet-stream")]
OctetStream,
}
#[derive(
Copy,
Clone,
Debug,
EnumIter,
PartialEq,
EnumString,
strum::Display,
serde::Serialize,
serde::Deserialize,
)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
pub enum ContentEncoding {
None,
Identity,
Deflate,
}
impl ContentEncoding {
pub fn encode(&self, data: &[u8]) -> Vec<u8> {
match self {
ContentEncoding::None | ContentEncoding::Identity => data.to_vec(),
ContentEncoding::Deflate => deflate::deflate_bytes_zlib(data),
}
}
pub fn decode(&self, data: &[u8]) -> Result<Vec<u8>, Error> {
Ok(match self {
ContentEncoding::None | ContentEncoding::Identity => data.to_vec(),
ContentEncoding::Deflate => match inflate::inflate_bytes_zlib(data) {
Ok(v) => v,
Err(error) => match inflate::inflate_bytes(data) {
Ok(v) => v,
Err(_) => Err(Error::InflateError(error))?,
},
},
})
}
}
#[derive(
Copy,
Clone,
Debug,
EnumIter,
PartialEq,
EnumString,
strum::Display,
serde::Serialize,
serde::Deserialize,
)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
pub enum ContentLanguage {
None,
En,
}
#[derive(PartialEq, Debug, Clone)]
pub struct RainMetaDocumentV1Item {
pub payload: serde_bytes::ByteBuf,
pub magic: KnownMagic,
pub content_type: ContentType,
pub content_encoding: ContentEncoding,
pub content_language: ContentLanguage,
pub schema: Option<String>,
}
impl TryFrom<RainMetaDocumentV1Item> for String {
type Error = Error;
fn try_from(value: RainMetaDocumentV1Item) -> Result<Self, Self::Error> {
Ok(String::from_utf8(value.unpack()?)?)
}
}
impl TryFrom<RainMetaDocumentV1Item> for Vec<u8> {
type Error = Error;
fn try_from(value: RainMetaDocumentV1Item) -> Result<Self, Self::Error> {
value.unpack()
}
}
impl RainMetaDocumentV1Item {
fn len(&self) -> usize {
let mut l = 2;
if !matches!(self.content_type, ContentType::None) {
l += 1;
}
if !matches!(self.content_encoding, ContentEncoding::None) {
l += 1;
}
if !matches!(self.content_language, ContentLanguage::None) {
l += 1;
}
if self.schema.is_some() {
l += 1;
}
l
}
pub fn hash(&self, as_rain_meta_document: bool) -> Result<[u8; 32], Error> {
if as_rain_meta_document {
Ok(keccak256(Self::cbor_encode_seq(
&vec![self.clone()],
KnownMagic::RainMetaDocumentV1,
)?)
.0)
} else {
Ok(keccak256(self.cbor_encode()?).0)
}
}
pub fn cbor_encode(&self) -> Result<Vec<u8>, Error> {
let mut bytes: Vec<u8> = vec![];
Ok(serde_cbor::to_writer(&mut bytes, &self).map(|_| bytes)?)
}
pub fn cbor_encode_seq(
seq: &Vec<RainMetaDocumentV1Item>,
magic: KnownMagic,
) -> Result<Vec<u8>, Error> {
let mut bytes: Vec<u8> = magic.to_prefix_bytes().to_vec();
for item in seq {
serde_cbor::to_writer(&mut bytes, &item)?;
}
Ok(bytes)
}
pub fn cbor_decode(data: &[u8]) -> Result<Vec<RainMetaDocumentV1Item>, Error> {
let mut metas: Vec<RainMetaDocumentV1Item> = vec![];
let mut consumed: usize = 0;
let mut is_rain_document_meta = false;
let mut len = data.len();
if data.starts_with(&KnownMagic::RainMetaDocumentV1.to_prefix_bytes()) {
is_rain_document_meta = true;
len -= 8;
}
let mut deserializer = match is_rain_document_meta {
true => serde_cbor::Deserializer::from_slice(&data[8..]),
false => serde_cbor::Deserializer::from_slice(data),
};
while match ItemOrDropped::deserialize(&mut deserializer) {
Ok(decoded) => {
consumed = deserializer.byte_offset();
if let ItemOrDropped::Item(meta) = decoded {
metas.push(meta);
}
true
}
Err(error) => {
if error.is_eof() {
false
} else {
Err(Error::SerdeCborError(error))?
}
}
} {}
if metas.is_empty() || len != consumed {
Err(Error::CorruptMeta)?
}
Ok(metas)
}
pub fn unpack(&self) -> Result<Vec<u8>, Error> {
ContentEncoding::decode(&self.content_encoding, self.payload.as_ref())
}
pub fn unpack_into<T: TryFrom<Self, Error = Error>>(self) -> Result<T, Error> {
match self.magic {
KnownMagic::OpMetaV1
| KnownMagic::DotrainV1
| KnownMagic::RainlangV1
| KnownMagic::SolidityAbiV2
| KnownMagic::AuthoringMetaV1
| KnownMagic::AuthoringMetaV2
| KnownMagic::AddressList
| KnownMagic::InterpreterCallerMetaV1
| KnownMagic::ExpressionDeployerV2BytecodeV1
| KnownMagic::DotrainSourceV1
| KnownMagic::OrderBuilderStateV1
| KnownMagic::RainlangSourceV1
| KnownMagic::RaindexSignedContextOracleV1 => T::try_from(self),
_ => Err(Error::UnsupportedMeta)?,
}
}
}
impl Serialize for RainMetaDocumentV1Item {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(Some(self.len()))?;
map.serialize_entry(&0, &self.payload)?;
map.serialize_entry(&1, &(self.magic as u64))?;
match self.content_type {
ContentType::None => {}
content_type => map.serialize_entry(&2, &content_type)?,
}
match self.content_encoding {
ContentEncoding::None => {}
content_encoding => map.serialize_entry(&3, &content_encoding)?,
}
match self.content_language {
ContentLanguage::None => {}
content_language => map.serialize_entry(&4, &content_language)?,
}
if let Some(schema) = &self.schema {
map.serialize_entry(&(KnownMagic::OaSchema as u64), schema)?;
}
map.end()
}
}
enum ItemOrDropped {
Item(RainMetaDocumentV1Item),
Dropped,
}
impl<'de> Deserialize<'de> for ItemOrDropped {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
fn set_once<V, E: serde::de::Error>(
slot: &mut Option<V>,
value: V,
field: &'static str,
) -> Result<(), E> {
if slot.is_some() {
return Err(serde::de::Error::duplicate_field(field));
}
*slot = Some(value);
Ok(())
}
struct EncodedMap;
impl<'de> Visitor<'de> for EncodedMap {
type Value = ItemOrDropped;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("rain meta cbor encoded bytes")
}
fn visit_map<T: serde::de::MapAccess<'de>>(
self,
mut map: T,
) -> Result<Self::Value, T::Error> {
const OA_SCHEMA_KEY: u64 = KnownMagic::OaSchema as u64;
let mut payload = None;
let mut magic: Option<u64> = None;
let mut content_type = None;
let mut content_encoding = None;
let mut content_language = None;
let mut schema = None;
let mut unknown_keys: BTreeSet<u64> = BTreeSet::new();
while match map.next_key::<u64>() {
Ok(Some(key)) => {
match key {
0 => set_once(&mut payload, map.next_value()?, "payload")?,
1 => set_once(&mut magic, map.next_value()?, "magic number")?,
2 => set_once(&mut content_type, map.next_value()?, "content type")?,
3 => set_once(
&mut content_encoding,
map.next_value()?,
"content encoding",
)?,
4 => set_once(
&mut content_language,
map.next_value()?,
"content language",
)?,
OA_SCHEMA_KEY => set_once(&mut schema, map.next_value()?, "schema")?,
_ => {
if !unknown_keys.insert(key) {
return Err(serde::de::Error::custom(format!(
"duplicate map key: {key}"
)));
}
map.next_value::<serde::de::IgnoredAny>()?;
}
};
true
}
Ok(None) => false,
Err(error) => Err(error)?,
} {}
let (Some(payload), Some(magic_number)) = (payload, magic) else {
return Err(serde::de::Error::custom(
"cbor item omits a mandatory key (0 payload, 1 magic)",
));
};
if magic_number == KnownMagic::RainMetaDocumentV1 as u64 {
return Err(serde::de::Error::custom(
"rain meta document magic number as an item magic number",
));
}
let Ok(magic) = KnownMagic::try_from(magic_number) else {
return Ok(ItemOrDropped::Dropped);
};
let content_type = content_type.unwrap_or(ContentType::None);
let content_encoding = content_encoding.unwrap_or(ContentEncoding::None);
let content_language = content_language.unwrap_or(ContentLanguage::None);
Ok(ItemOrDropped::Item(RainMetaDocumentV1Item {
payload,
magic,
content_type,
content_encoding,
content_language,
schema,
}))
}
}
deserializer.deserialize_map(EncodedMap)
}
}
impl<'de> Deserialize<'de> for RainMetaDocumentV1Item {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
match ItemOrDropped::deserialize(deserializer)? {
ItemOrDropped::Item(item) => Ok(item),
ItemOrDropped::Dropped => Err(serde::de::Error::custom(
"not a rain meta item this version reads: a mandatory key is missing or the magic number is unknown",
)),
}
}
}
pub async fn search(hash: &str, subgraphs: &Vec<String>) -> Result<query::MetaResponse, Error> {
if subgraphs.is_empty() {
return Err(Error::NoRecordFound);
}
let request_body = query::MetaQuery::build_query(query::meta_query::Variables {
hash: Some(hash.to_ascii_lowercase()),
});
let mut promises = vec![];
let client = Arc::new(Client::builder().build().map_err(Error::ReqwestError)?);
for url in subgraphs {
promises.push(Box::pin(query::process_meta_query(
client.clone(),
&request_body,
url,
)));
}
let response_value = future::select_ok(promises.drain(..)).await?.0;
Ok(response_value)
}
pub async fn implements_i_described_by_meta_v1<P: Provider>(
provider: &P,
contract_address: Address,
) -> Result<bool, Erc165Error> {
if !supports_erc165(provider, contract_address).await? {
return Ok(false);
}
let interface_id_res = IDescribedByMetaV1::IDescribedByMetaV1Calls::xor_selectors();
if interface_id_res.is_err() {
return Ok(false);
}
match IERC165::new(contract_address, provider)
.supportsInterface(interface_id_res.unwrap().into())
.call()
.await
{
Ok(supported) => Ok(supported),
Err(error)
if error.as_revert_data().is_some()
|| matches!(error, ContractError::ZeroData(_, _)) =>
{
Ok(false)
}
Err(error) => Err(error.into()),
}
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Store {
subgraphs: Vec<String>,
cache: MetaCache,
dotrain_cache: HashMap<String, Vec<u8>>,
}
impl Default for Store {
fn default() -> Self {
Store::new()
}
}
impl Store {
pub fn new() -> Store {
Store {
subgraphs: vec![],
cache: MetaCache::default(),
dotrain_cache: HashMap::new(),
}
}
pub fn create(
subgraphs: &Vec<String>,
cache: &MetaCache,
dotrain_cache: &HashMap<String, Vec<u8>>,
) -> Store {
let mut store = Store::new();
store.add_subgraphs(subgraphs);
for (hash, bytes) in cache.iter() {
let _ = store.update_with(hash, bytes);
}
for (uri, hash) in dotrain_cache {
if !store.dotrain_cache.contains_key(uri) && store.cache.contains_key(hash) {
store.dotrain_cache.insert(uri.clone(), hash.clone());
}
}
store
}
pub fn subgraphs(&self) -> &Vec<String> {
&self.subgraphs
}
pub fn add_subgraphs(&mut self, subgraphs: &Vec<String>) {
for sg in subgraphs {
if !self.subgraphs.contains(sg) {
self.subgraphs.push(sg.to_string());
}
}
}
pub fn cache(&self) -> &MetaCache {
&self.cache
}
pub fn get_meta(&self, hash: &[u8]) -> Option<&Vec<u8>> {
self.cache.get(hash)
}
pub fn dotrain_cache(&self) -> &HashMap<String, Vec<u8>> {
&self.dotrain_cache
}
pub fn get_dotrain_hash(&self, uri: &str) -> Option<&Vec<u8>> {
self.dotrain_cache.get(uri)
}
pub fn get_dotrain_uri(&self, hash: &[u8]) -> Option<&String> {
for (uri, h) in &self.dotrain_cache {
if h == hash {
return Some(uri);
}
}
None
}
pub fn get_dotrain_meta(&self, uri: &str) -> Option<&Vec<u8>> {
self.get_meta(self.dotrain_cache.get(uri)?)
}
pub fn delete_dotrain(&mut self, uri: &str, keep_meta: bool) {
if let Some(kv) = self.dotrain_cache.remove_entry(uri) {
if !keep_meta {
self.cache.remove(&kv.1);
}
};
}
pub fn merge(&mut self, other: &Store) {
self.add_subgraphs(&other.subgraphs);
for (hash, bytes) in other.cache.iter() {
if !self.cache.contains_key(hash) {
let _ = self.cache.insert_verified(hash, bytes.clone());
}
}
for (uri, hash) in &other.dotrain_cache {
if !self.dotrain_cache.contains_key(uri) {
self.dotrain_cache.insert(uri.clone(), hash.clone());
}
}
}
fn insert_verified(&mut self, hash: &[u8], bytes: Vec<u8>) -> Result<&Vec<u8>, Error> {
self.cache.insert_verified(hash, bytes.clone())?;
self.store_content(&bytes);
self.get_meta(hash).ok_or(Error::NoRecordFound)
}
pub async fn update(&mut self, hash: &[u8]) -> Result<&Vec<u8>, Error> {
let meta = search(&hex::encode_prefixed(hash), &self.subgraphs).await?;
self.insert_verified(hash, meta.bytes)
}
pub async fn update_check(&mut self, hash: &[u8]) -> Result<&Vec<u8>, Error> {
if self.cache.contains_key(hash) {
return self.get_meta(hash).ok_or(Error::NoRecordFound);
}
self.update(hash).await
}
pub fn update_with(&mut self, hash: &[u8], bytes: &[u8]) -> Result<&Vec<u8>, Error> {
if self.cache.contains_key(hash) {
return self.get_meta(hash).ok_or(Error::NoRecordFound);
}
self.insert_verified(hash, bytes.to_vec())
}
pub fn set_dotrain(
&mut self,
text: &str,
uri: &str,
keep_old: bool,
) -> Result<(Vec<u8>, Vec<u8>), Error> {
let bytes = RainMetaDocumentV1Item {
payload: serde_bytes::ByteBuf::from(text.as_bytes()),
magic: KnownMagic::DotrainV1,
content_type: ContentType::OctetStream,
content_encoding: ContentEncoding::None,
content_language: ContentLanguage::None,
schema: None,
}
.cbor_encode()?;
let new_hash = keccak256(&bytes).0.to_vec();
if let Some(h) = self.dotrain_cache.get(uri) {
let old_hash = h.clone();
if new_hash == old_hash {
self.cache.insert_verified(&new_hash, bytes)?;
Ok((new_hash, vec![]))
} else {
self.cache.insert_verified(&new_hash, bytes)?;
self.dotrain_cache.insert(uri.to_string(), new_hash.clone());
if !keep_old {
self.cache.remove(&old_hash);
}
Ok((new_hash, old_hash))
}
} else {
self.dotrain_cache.insert(uri.to_string(), new_hash.clone());
self.cache.insert_verified(&new_hash, bytes)?;
Ok((new_hash, vec![]))
}
}
fn store_content(&mut self, bytes: &[u8]) {
if let Ok(meta_maps) = RainMetaDocumentV1Item::cbor_decode(bytes) {
if bytes.starts_with(&KnownMagic::RainMetaDocumentV1.to_prefix_bytes()) {
for meta_map in &meta_maps {
if let Ok(encoded_bytes) = meta_map.cbor_encode() {
let _ = self
.cache
.insert_verified(&keccak256(&encoded_bytes).0, encoded_bytes);
}
}
}
}
}
}
pub fn str_to_bytes32(text: &str) -> Result<[u8; 32], Error> {
let bytes: &[u8] = text.as_bytes();
if bytes.len() > 32 {
return Err(Error::BiggerThan32Bytes);
}
if bytes.contains(&0u8) {
return Err(Error::NulByteInInput);
}
let mut b32 = [0u8; 32];
b32[..bytes.len()].copy_from_slice(bytes);
Ok(b32)
}
pub fn bytes32_to_str(bytes: &[u8; 32]) -> Result<&str, Error> {
let mut len = 32;
if let Some((pos, _)) = itertools::Itertools::find_position(&mut bytes.iter(), |b| **b == 0u8) {
len = pos;
};
Ok(std::str::from_utf8(&bytes[..len])?)
}
#[cfg(all(test, not(target_family = "wasm")))]
mod tests {
use super::{
*, bytes32_to_str, magic::KnownMagic, str_to_bytes32, types::authoring::v1::AuthoringMeta,
ContentEncoding, ContentLanguage, ContentType, Error, RainMetaDocumentV1Item,
};
use alloy::providers::ProviderBuilder;
use alloy::{providers::mock::Asserter, rpc::json_rpc::ErrorPayload, sol_types::SolType};
use serde_json::json;
#[test]
fn authoring_meta_roundtrip() -> Result<(), Error> {
let authoring_meta_content = r#"[
{
"word": "stack",
"description": "Copies an existing value from the stack.",
"operandParserOffset": 16
},
{
"word": "constant",
"description": "Copies a constant value onto the stack.",
"operandParserOffset": 16
}
]"#;
let authoring_meta: AuthoringMeta = serde_json::from_str(authoring_meta_content)?;
let authoring_meta_abi_encoded = authoring_meta.abi_encode_validate()?;
let expected_abi_encoded = <alloy::sol!((bytes32, uint8, string)[])>::abi_encode(&vec![
(
str_to_bytes32("stack")?,
16u8,
"Copies an existing value from the stack.".to_string(),
),
(
str_to_bytes32("constant")?,
16u8,
"Copies a constant value onto the stack.".to_string(),
),
]);
assert_eq!(authoring_meta_abi_encoded, expected_abi_encoded);
let meta_map = RainMetaDocumentV1Item {
payload: serde_bytes::ByteBuf::from(authoring_meta_abi_encoded.clone()),
magic: KnownMagic::AuthoringMetaV1,
content_type: ContentType::Cbor,
content_encoding: ContentEncoding::None,
content_language: ContentLanguage::None,
schema: None,
};
let cbor_encoded = meta_map.cbor_encode()?;
assert_eq!(cbor_encoded[0], 0xa3);
assert_eq!(cbor_encoded[1], 0x00);
assert_eq!(cbor_encoded[2], 0b010_11001);
assert_eq!(cbor_encoded[3], 0b000_00010);
assert_eq!(cbor_encoded[4], 0b000_00000);
assert_eq!(cbor_encoded[5..517], authoring_meta_abi_encoded);
assert_eq!(cbor_encoded[517], 0x01);
assert_eq!(cbor_encoded[518], 0b000_11011);
assert_eq!(
&cbor_encoded[519..527],
KnownMagic::AuthoringMetaV1.to_prefix_bytes()
);
assert_eq!(cbor_encoded[527], 0x02);
assert_eq!(cbor_encoded[528], 0b011_10000);
assert_eq!(&cbor_encoded[529..], "application/cbor".as_bytes());
let cbor_decoded = RainMetaDocumentV1Item::cbor_decode(&cbor_encoded)?;
assert_eq!(cbor_decoded.len(), 1);
assert_eq!(cbor_decoded[0], meta_map);
Ok(())
}
#[test]
fn dotrain_meta_roundtrip() -> Result<(), Error> {
let dotrain_content = "#main _ _: int-add(1 2) int-add(2 3)";
let dotrain_content_bytes = dotrain_content.as_bytes().to_vec();
let content_encoding = ContentEncoding::Deflate;
let deflated_payload = content_encoding.encode(&dotrain_content_bytes);
let meta_map = RainMetaDocumentV1Item {
payload: serde_bytes::ByteBuf::from(deflated_payload.clone()),
magic: KnownMagic::DotrainV1,
content_type: ContentType::OctetStream,
content_encoding,
content_language: ContentLanguage::En,
schema: None,
};
let cbor_encoded = meta_map.cbor_encode()?;
assert_eq!(cbor_encoded[0], 0xa5);
assert_eq!(cbor_encoded[1], 0x00);
assert_eq!(cbor_encoded[2], 0b010_11000);
assert_eq!(cbor_encoded[3], 0b001_00100);
assert_eq!(cbor_encoded[4..40], deflated_payload);
assert_eq!(cbor_encoded[40], 0x01);
assert_eq!(cbor_encoded[41], 0b000_11011);
assert_eq!(
&cbor_encoded[42..50],
KnownMagic::DotrainV1.to_prefix_bytes()
);
assert_eq!(cbor_encoded[50], 0x02);
assert_eq!(cbor_encoded[51], 0b011_11000);
assert_eq!(cbor_encoded[52], 0b000_11000);
assert_eq!(&cbor_encoded[53..77], "application/octet-stream".as_bytes());
assert_eq!(cbor_encoded[77], 0x03);
assert_eq!(cbor_encoded[78], 0b011_00111);
assert_eq!(&cbor_encoded[79..86], "deflate".as_bytes());
assert_eq!(cbor_encoded[86], 0x04);
assert_eq!(cbor_encoded[87], 0b011_00010);
assert_eq!(&cbor_encoded[88..], "en".as_bytes());
let cbor_decoded = RainMetaDocumentV1Item::cbor_decode(&cbor_encoded)?;
assert_eq!(cbor_decoded.len(), 1);
assert_eq!(cbor_decoded[0], meta_map);
Ok(())
}
#[test]
fn meta_seq_roundtrip() -> Result<(), Error> {
let authoring_meta_content = r#"[
{
"word": "stack",
"description": "Copies an existing value from the stack.",
"operandParserOffset": 16
},
{
"word": "constant",
"description": "Copies a constant value onto the stack.",
"operandParserOffset": 16
}
]"#;
let authoring_meta: AuthoringMeta = serde_json::from_str(authoring_meta_content)?;
let authoring_meta_abi_encoded = authoring_meta.abi_encode_validate()?;
let meta_map_1 = RainMetaDocumentV1Item {
payload: serde_bytes::ByteBuf::from(authoring_meta_abi_encoded.clone()),
magic: KnownMagic::AuthoringMetaV1,
content_type: ContentType::Cbor,
content_encoding: ContentEncoding::None,
content_language: ContentLanguage::None,
schema: None,
};
let dotrain_content = "#main _ _: int-add(1 2) int-add(2 3)";
let dotrain_content_bytes = dotrain_content.as_bytes().to_vec();
let content_encoding = ContentEncoding::Deflate;
let deflated_payload = content_encoding.encode(&dotrain_content_bytes);
let meta_map_2 = RainMetaDocumentV1Item {
payload: serde_bytes::ByteBuf::from(deflated_payload.clone()),
magic: KnownMagic::DotrainV1,
content_type: ContentType::OctetStream,
content_encoding,
content_language: ContentLanguage::En,
schema: None,
};
let cbor_encoded = RainMetaDocumentV1Item::cbor_encode_seq(
&vec![meta_map_1.clone(), meta_map_2.clone()],
KnownMagic::RainMetaDocumentV1,
)?;
assert_eq!(
&cbor_encoded[0..8],
KnownMagic::RainMetaDocumentV1.to_prefix_bytes()
);
assert_eq!(cbor_encoded[8], 0xa3);
assert_eq!(cbor_encoded[9], 0x00);
assert_eq!(cbor_encoded[10], 0b010_11001);
assert_eq!(cbor_encoded[11], 0b000_00010);
assert_eq!(cbor_encoded[12], 0b000_00000);
assert_eq!(cbor_encoded[13..525], authoring_meta_abi_encoded);
assert_eq!(cbor_encoded[525], 0x01);
assert_eq!(cbor_encoded[526], 0b000_11011);
assert_eq!(
&cbor_encoded[527..535],
KnownMagic::AuthoringMetaV1.to_prefix_bytes()
);
assert_eq!(cbor_encoded[535], 0x02);
assert_eq!(cbor_encoded[536], 0b011_10000);
assert_eq!(&cbor_encoded[537..553], "application/cbor".as_bytes());
assert_eq!(cbor_encoded[553], 0xa5);
assert_eq!(cbor_encoded[554], 0x00);
assert_eq!(cbor_encoded[555], 0b010_11000);
assert_eq!(cbor_encoded[556], 0b001_00100);
assert_eq!(cbor_encoded[557..593], deflated_payload);
assert_eq!(cbor_encoded[593], 0x01);
assert_eq!(cbor_encoded[594], 0b000_11011);
assert_eq!(
&cbor_encoded[595..603],
KnownMagic::DotrainV1.to_prefix_bytes()
);
assert_eq!(cbor_encoded[603], 0x02);
assert_eq!(cbor_encoded[604], 0b011_11000);
assert_eq!(cbor_encoded[605], 0b000_11000);
assert_eq!(
&cbor_encoded[606..630],
"application/octet-stream".as_bytes()
);
assert_eq!(cbor_encoded[630], 0x03);
assert_eq!(cbor_encoded[631], 0b011_00111);
assert_eq!(&cbor_encoded[632..639], "deflate".as_bytes());
assert_eq!(cbor_encoded[639], 0x04);
assert_eq!(cbor_encoded[640], 0b011_00010);
assert_eq!(&cbor_encoded[641..], "en".as_bytes());
let cbor_decoded = RainMetaDocumentV1Item::cbor_decode(&cbor_encoded)?;
assert_eq!(cbor_decoded.len(), 2);
assert_eq!(cbor_decoded[0], meta_map_1);
assert_eq!(cbor_decoded[1], meta_map_2);
Ok(())
}
#[test]
fn test_bytes32_to_str() {
let text_bytes_list = vec![
(
"",
hex!("0000000000000000000000000000000000000000000000000000000000000000"),
),
(
"A",
hex!("4100000000000000000000000000000000000000000000000000000000000000"),
),
(
"ABCDEFGHIJKLMNOPQRSTUVWXYZ012345",
hex!("4142434445464748494a4b4c4d4e4f505152535455565758595a303132333435"),
),
(
"!@#$%^&*(),./;'[]",
hex!("21402324255e262a28292c2e2f3b275b5d000000000000000000000000000000"),
),
];
for (text, bytes) in text_bytes_list {
assert_eq!(text, bytes32_to_str(&bytes).unwrap());
}
}
#[test]
fn test_str_to_bytes32() {
let text_bytes_list = vec![
(
"",
hex!("0000000000000000000000000000000000000000000000000000000000000000"),
),
(
"A",
hex!("4100000000000000000000000000000000000000000000000000000000000000"),
),
(
"ABCDEFGHIJKLMNOPQRSTUVWXYZ012345",
hex!("4142434445464748494a4b4c4d4e4f505152535455565758595a303132333435"),
),
(
"!@#$%^&*(),./;'[]",
hex!("21402324255e262a28292c2e2f3b275b5d000000000000000000000000000000"),
),
];
for (text, bytes) in text_bytes_list {
assert_eq!(bytes, str_to_bytes32(text).unwrap());
}
}
#[test]
fn test_str_to_bytes32_long() {
assert!(matches!(
str_to_bytes32("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456").unwrap_err(),
Error::BiggerThan32Bytes
));
}
#[test]
fn test_str_to_bytes32_rejects_nul() {
for text in [
"\0",
"\0a",
"a\0",
"a\0b",
"abcdefghijklmnopqrstuvwxyz01234\0",
] {
assert!(
matches!(str_to_bytes32(text), Err(Error::NulByteInInput)),
"nul bearing input {:?} accepted",
text
);
}
}
#[test]
fn test_str_to_bytes32_round_trip() -> Result<(), Error> {
let mut seen: Vec<[u8; 32]> = vec![];
for text in [
"",
"a",
"stack",
"!@#$%^&*(),./;'[]",
"ABCDEFGHIJKLMNOPQRSTUVWXYZ012345",
] {
let bytes = str_to_bytes32(text)?;
assert_eq!(bytes32_to_str(&bytes)?, text);
assert!(!seen.contains(&bytes), "input {:?} collided", text);
seen.push(bytes);
}
Ok(())
}
#[tokio::test]
async fn test_implements_i_describe_by_meta_v1() {
async fn new_server_client() -> (Asserter, impl Provider) {
let asserter = Asserter::new();
let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
asserter.push_success(
&"0x0000000000000000000000000000000000000000000000000000000000000001",
);
asserter.push_success(
&"0x0000000000000000000000000000000000000000000000000000000000000000",
);
(asserter, provider)
}
let address = Address::random();
let (asserter, provider) = new_server_client().await;
asserter
.push_success(&"0x0000000000000000000000000000000000000000000000000000000000000001");
let result = implements_i_described_by_meta_v1(&provider, address)
.await
.unwrap();
assert!(result);
let (asserter, provider) = new_server_client().await;
asserter
.push_success(&"0x0000000000000000000000000000000000000000000000000000000000000000");
let result = implements_i_described_by_meta_v1(&provider, address)
.await
.unwrap();
assert!(!result);
let (asserter, provider) = new_server_client().await;
asserter.push_failure(ErrorPayload {
code: -32003,
message: "execution reverted".into(),
data: Some(serde_json::value::to_raw_value(&json!("0x00")).unwrap()),
});
let result = implements_i_described_by_meta_v1(&provider, address)
.await
.unwrap();
assert!(!result);
}
#[test]
fn oa_schema_map_key_roundtrip() -> Result<(), Error> {
let payload = vec![0x01, 0x02, 0x03];
let schema = "QmSchemaHash1234567890abcdefghijklmnopqrstuvwx".to_string();
assert_eq!(schema.len(), 46);
let meta_map = RainMetaDocumentV1Item {
payload: serde_bytes::ByteBuf::from(payload.clone()),
magic: KnownMagic::OaStructure,
content_type: ContentType::Json,
content_encoding: ContentEncoding::Deflate,
content_language: ContentLanguage::None,
schema: Some(schema.clone()),
};
let cbor_encoded = meta_map.cbor_encode()?;
assert_eq!(cbor_encoded[0], 0xa5);
assert_eq!(cbor_encoded[1], 0x00);
assert_eq!(cbor_encoded[2], 0b010_00011);
assert_eq!(cbor_encoded[3..6], payload);
assert_eq!(cbor_encoded[6], 0x01);
assert_eq!(cbor_encoded[7], 0b000_11011);
assert_eq!(
&cbor_encoded[8..16],
KnownMagic::OaStructure.to_prefix_bytes()
);
assert_eq!(cbor_encoded[16], 0x02);
assert_eq!(cbor_encoded[17], 0b011_10000);
assert_eq!(&cbor_encoded[18..34], "application/json".as_bytes());
assert_eq!(cbor_encoded[34], 0x03);
assert_eq!(cbor_encoded[35], 0b011_00111);
assert_eq!(&cbor_encoded[36..43], "deflate".as_bytes());
assert_eq!(cbor_encoded[43], 0b000_11011);
assert_eq!(
&cbor_encoded[44..52],
KnownMagic::OaSchema.to_prefix_bytes()
);
assert_eq!(cbor_encoded[52], 0b011_11000);
assert_eq!(cbor_encoded[53], 46);
assert_eq!(&cbor_encoded[54..], schema.as_bytes());
let cbor_decoded = RainMetaDocumentV1Item::cbor_decode(&cbor_encoded)?;
assert_eq!(cbor_decoded.len(), 1);
assert_eq!(cbor_decoded[0], meta_map);
Ok(())
}
#[test]
fn no_schema_key_encodes_as_before_roundtrip() -> Result<(), Error> {
let payload = vec![0x0a, 0x0b];
let meta_map = RainMetaDocumentV1Item {
payload: serde_bytes::ByteBuf::from(payload.clone()),
magic: KnownMagic::OaStructure,
content_type: ContentType::None,
content_encoding: ContentEncoding::None,
content_language: ContentLanguage::None,
schema: None,
};
let cbor_encoded = meta_map.cbor_encode()?;
assert_eq!(cbor_encoded[0], 0xa2);
assert_eq!(cbor_encoded[1], 0x00);
assert_eq!(cbor_encoded[2], 0b010_00010);
assert_eq!(cbor_encoded[3..5], payload);
assert_eq!(cbor_encoded[5], 0x01);
assert_eq!(cbor_encoded[6], 0b000_11011);
assert_eq!(
&cbor_encoded[7..],
KnownMagic::OaStructure.to_prefix_bytes()
);
let cbor_decoded = RainMetaDocumentV1Item::cbor_decode(&cbor_encoded)?;
assert_eq!(cbor_decoded.len(), 1);
assert_eq!(cbor_decoded[0], meta_map);
Ok(())
}
#[test]
fn unknown_map_key_index_is_ignored() -> Result<(), Error> {
let mut bytes: Vec<u8> = vec![
0xa3, 0x00, 0x40, 0x01, 0x1b,
];
bytes.extend_from_slice(&KnownMagic::DotrainSourceV1.to_prefix_bytes());
bytes.extend_from_slice(&[0x05, 0x07]);
let decoded = RainMetaDocumentV1Item::cbor_decode(&bytes)?;
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0], plain_item(KnownMagic::DotrainSourceV1, vec![]));
Ok(())
}
#[test]
fn non_oa_schema_extra_map_key_is_ignored() -> Result<(), Error> {
let mut bytes: Vec<u8> = vec![
0xa3, 0x00, 0x41, 0xff, 0x01, 0x1b,
];
bytes.extend_from_slice(&KnownMagic::OaStructure.to_prefix_bytes());
bytes.push(0x1b);
bytes.extend_from_slice(&KnownMagic::OaHashList.to_prefix_bytes());
bytes.extend_from_slice(&[0x62, 0x68, 0x69]);
let decoded = RainMetaDocumentV1Item::cbor_decode(&bytes)?;
assert_eq!(decoded.len(), 1);
let expected = plain_item(KnownMagic::OaStructure, vec![0xff]);
assert_eq!(decoded[0], expected);
assert_eq!(decoded[0].schema, None);
Ok(())
}
#[test]
fn unknown_map_key_consumes_its_whole_value() -> Result<(), Error> {
let mut bytes: Vec<u8> = vec![0xa3, 0x00, 0x41, 0x01, 0x01, 0x1b];
bytes.extend_from_slice(&KnownMagic::DotrainV1.to_prefix_bytes());
bytes.extend_from_slice(&[0x05, 0xa1, 0x18, 0x2a, 0x82, 0x01, 0x02]);
bytes.extend_from_slice(&handwritten_map());
let decoded = RainMetaDocumentV1Item::cbor_decode(&bytes)?;
assert_eq!(decoded.len(), 2);
let expected = plain_item(KnownMagic::DotrainV1, vec![0x01]);
assert_eq!(decoded[0], expected);
assert_eq!(decoded[1], expected);
Ok(())
}
#[test]
fn ignored_map_key_is_absent_from_the_reencoding() -> Result<(), Error> {
let mut bytes: Vec<u8> = vec![0xa3, 0x00, 0x41, 0x01, 0x01, 0x1b];
bytes.extend_from_slice(&KnownMagic::DotrainV1.to_prefix_bytes());
bytes.extend_from_slice(&[0x05, 0x07]);
let decoded = RainMetaDocumentV1Item::cbor_decode(&bytes)?;
assert_eq!(decoded[0].cbor_encode()?, handwritten_map());
assert_eq!(decoded[0].hash(false)?, keccak256(handwritten_map()).0);
assert_ne!(decoded[0].hash(false)?, keccak256(&bytes).0);
Ok(())
}
#[test]
fn non_integer_map_key_errors() {
let mut text_key: Vec<u8> = vec![0xa3, 0x00, 0x41, 0x01, 0x01, 0x1b];
text_key.extend_from_slice(&KnownMagic::DotrainV1.to_prefix_bytes());
text_key.extend_from_slice(&[0x61, 0x35, 0x07]);
assert!(matches!(
RainMetaDocumentV1Item::cbor_decode(&text_key),
Err(Error::SerdeCborError(_))
));
let mut negative_key: Vec<u8> = vec![0xa3, 0x00, 0x41, 0x01, 0x01, 0x1b];
negative_key.extend_from_slice(&KnownMagic::DotrainV1.to_prefix_bytes());
negative_key.extend_from_slice(&[0x20, 0x07]);
assert!(matches!(
RainMetaDocumentV1Item::cbor_decode(&negative_key),
Err(Error::SerdeCborError(_))
));
}
#[test]
fn unknown_map_key_does_not_stand_in_for_a_mandatory_key() {
let mut bytes: Vec<u8> = vec![0xa2, 0x05, 0x07, 0x01, 0x1b];
bytes.extend_from_slice(&KnownMagic::DotrainV1.to_prefix_bytes());
assert!(RainMetaDocumentV1Item::cbor_decode(&bytes).is_err());
}
fn plain_item(magic: KnownMagic, payload: Vec<u8>) -> RainMetaDocumentV1Item {
RainMetaDocumentV1Item {
payload: serde_bytes::ByteBuf::from(payload),
magic,
content_type: ContentType::None,
content_encoding: ContentEncoding::None,
content_language: ContentLanguage::None,
schema: None,
}
}
fn sample_authoring_doc() -> (AuthoringMeta, Vec<u8>) {
let authoring_meta: AuthoringMeta = serde_json::from_str(
r#"[{"word":"stack","description":"Copies an existing value from the stack.","operandParserOffset":16}]"#,
)
.unwrap();
let abi = authoring_meta.abi_encode_validate().unwrap();
let item = RainMetaDocumentV1Item {
payload: serde_bytes::ByteBuf::from(abi),
magic: KnownMagic::AuthoringMetaV1,
content_type: ContentType::Cbor,
content_encoding: ContentEncoding::None,
content_language: ContentLanguage::None,
schema: None,
};
let doc =
RainMetaDocumentV1Item::cbor_encode_seq(&vec![item], KnownMagic::RainMetaDocumentV1)
.unwrap();
(authoring_meta, doc)
}
fn sample_dotrain_item() -> RainMetaDocumentV1Item {
RainMetaDocumentV1Item {
payload: serde_bytes::ByteBuf::from("some dotrain body".as_bytes()),
magic: KnownMagic::DotrainV1,
content_type: ContentType::OctetStream,
content_encoding: ContentEncoding::None,
content_language: ContentLanguage::None,
schema: None,
}
}
fn handwritten_map() -> Vec<u8> {
vec![
0xa2, 0x00, 0x41, 0x01, 0x01, 0x1b, 0xff, 0xda, 0xc2, 0xf2, 0xf3, 0x7b, 0xe8, 0x94, ]
}
#[test]
fn test_hash_bare_vs_document() -> Result<(), Error> {
let map_bytes = handwritten_map();
let mut doc_bytes: Vec<u8> = vec![0xff, 0x0a, 0x89, 0xc6, 0x74, 0xee, 0x78, 0x74];
doc_bytes.extend_from_slice(&map_bytes);
let item = plain_item(KnownMagic::DotrainV1, vec![0x01]);
assert_eq!(item.hash(false)?, keccak256(&map_bytes).0);
assert_eq!(item.hash(true)?, keccak256(&doc_bytes).0);
assert_ne!(item.hash(false)?, item.hash(true)?);
Ok(())
}
#[test]
fn test_cbor_decode_empty_is_corrupt() {
assert!(matches!(
RainMetaDocumentV1Item::cbor_decode(&[]),
Err(Error::CorruptMeta)
));
let prefix = KnownMagic::RainMetaDocumentV1.to_prefix_bytes();
assert!(matches!(
RainMetaDocumentV1Item::cbor_decode(&prefix),
Err(Error::CorruptMeta)
));
}
#[test]
fn test_cbor_decode_trailing_truncated_is_corrupt() {
let mut bytes = handwritten_map();
bytes.push(0x1b); assert!(matches!(
RainMetaDocumentV1Item::cbor_decode(&bytes),
Err(Error::CorruptMeta)
));
}
#[test]
fn test_cbor_decode_truncated_item_is_corrupt() {
let mut sole = handwritten_map();
sole.pop();
assert!(matches!(
RainMetaDocumentV1Item::cbor_decode(&sole),
Err(Error::CorruptMeta)
));
assert!(matches!(
RainMetaDocumentV1Item::cbor_decode(&[0xa2, 0x00, 0x41, 0x01]),
Err(Error::CorruptMeta)
));
let mut after_complete = handwritten_map();
after_complete.extend_from_slice(&[0xa2, 0x00]);
assert!(matches!(
RainMetaDocumentV1Item::cbor_decode(&after_complete),
Err(Error::CorruptMeta)
));
}
#[test]
fn test_cbor_decode_trailing_garbage_errors() {
let mut bytes = handwritten_map();
bytes.push(0xff); assert!(matches!(
RainMetaDocumentV1Item::cbor_decode(&bytes),
Err(Error::SerdeCborError(_))
));
}
#[test]
fn test_cbor_decode_missing_payload_is_corrupt() {
let mut bytes: Vec<u8> = vec![0xa1, 0x01, 0x1b]; bytes.extend_from_slice(&KnownMagic::DotrainV1.to_prefix_bytes());
assert!(RainMetaDocumentV1Item::cbor_decode(&bytes).is_err());
let good = plain_item(KnownMagic::DotrainV1, vec![0x42])
.cbor_encode()
.unwrap();
let mut document = KnownMagic::RainMetaDocumentV1.to_prefix_bytes().to_vec();
document.extend_from_slice(&bytes);
document.extend_from_slice(&good);
assert!(RainMetaDocumentV1Item::cbor_decode(&document).is_err());
}
#[test]
fn test_cbor_decode_missing_magic_is_corrupt() {
let bytes: Vec<u8> = vec![0xa1, 0x00, 0x41, 0x01]; assert!(RainMetaDocumentV1Item::cbor_decode(&bytes).is_err());
let good = plain_item(KnownMagic::DotrainV1, vec![0x42])
.cbor_encode()
.unwrap();
let mut document = KnownMagic::RainMetaDocumentV1.to_prefix_bytes().to_vec();
document.extend_from_slice(&bytes);
document.extend_from_slice(&good);
assert!(RainMetaDocumentV1Item::cbor_decode(&document).is_err());
}
#[test]
fn test_cbor_decode_unknown_magic_is_dropped() {
let mut bytes: Vec<u8> = vec![0xa2, 0x00, 0x41, 0x01, 0x01, 0x1b];
bytes.extend_from_slice(&0xdeadbeefdeadbeefu64.to_be_bytes());
assert!(matches!(
RainMetaDocumentV1Item::cbor_decode(&bytes),
Err(Error::CorruptMeta)
));
}
#[test]
fn test_cbor_decode_drops_only_the_unknown_magic_item() {
let good = plain_item(KnownMagic::DotrainV1, vec![0x42])
.cbor_encode()
.unwrap();
let mut unknown_magic: Vec<u8> = vec![0xa2, 0x00, 0x41, 0x01, 0x01, 0x1b];
unknown_magic.extend_from_slice(&0xdeadbeefdeadbeefu64.to_be_bytes());
let mut document = KnownMagic::RainMetaDocumentV1.to_prefix_bytes().to_vec();
document.extend_from_slice(&unknown_magic);
document.extend_from_slice(&good);
let items = RainMetaDocumentV1Item::cbor_decode(&document).unwrap();
assert_eq!(items.len(), 1, "expected only the good item");
assert_eq!(items[0].magic, KnownMagic::DotrainV1);
assert_eq!(items[0].payload.as_ref(), &[0x42]);
}
#[test]
fn test_cbor_decode_nested_document_magic_is_not_dropped() {
let good = plain_item(KnownMagic::DotrainV1, vec![0x42])
.cbor_encode()
.unwrap();
let mut nested: Vec<u8> = vec![0xa2, 0x00, 0x41, 0x01, 0x01, 0x1b];
nested.extend_from_slice(&KnownMagic::RainMetaDocumentV1.to_prefix_bytes());
let mut document = KnownMagic::RainMetaDocumentV1.to_prefix_bytes().to_vec();
document.extend_from_slice(&nested);
document.extend_from_slice(&good);
assert!(matches!(
RainMetaDocumentV1Item::cbor_decode(&document),
Err(Error::SerdeCborError(_))
));
}
fn handwritten_entries(entries: &[(Vec<u8>, Vec<u8>)]) -> Vec<u8> {
assert!(entries.len() < 24);
let mut bytes = vec![0xa0 | entries.len() as u8];
for (key, value) in entries {
bytes.extend_from_slice(key);
bytes.extend_from_slice(value);
}
bytes
}
fn handwritten_magic(magic: KnownMagic) -> Vec<u8> {
let mut bytes = vec![0x1b];
bytes.extend_from_slice(&magic.to_prefix_bytes());
bytes
}
fn handwritten_text(text: &str) -> Vec<u8> {
assert!(text.len() < 24);
let mut bytes = vec![0x60 | text.len() as u8];
bytes.extend_from_slice(text.as_bytes());
bytes
}
#[test]
fn test_cbor_decode_duplicate_payload_key_errors() {
let mut bytes: Vec<u8> = vec![0xa3, 0x00, 0x41, 0x01, 0x00, 0x41, 0x02, 0x01, 0x1b];
bytes.extend_from_slice(&KnownMagic::DotrainV1.to_prefix_bytes());
let error = RainMetaDocumentV1Item::cbor_decode(&bytes).unwrap_err();
assert!(matches!(error, Error::SerdeCborError(_)));
assert!(
error.to_string().contains("duplicate field `payload`"),
"{error}"
);
}
#[test]
fn test_cbor_decode_duplicate_any_key_errors() -> Result<(), Error> {
let cases: [(Vec<u8>, Vec<u8>, Vec<u8>); 6] = [
(vec![0x00], vec![0x41, 0x01], vec![0x41, 0x02]),
(
vec![0x01],
handwritten_magic(KnownMagic::DotrainV1),
handwritten_magic(KnownMagic::RainlangV1),
),
(
vec![0x02],
handwritten_text("application/cbor"),
handwritten_text("application/json"),
),
(
vec![0x03],
handwritten_text("identity"),
handwritten_text("deflate"),
),
(vec![0x04], handwritten_text("en"), handwritten_text("none")),
(
handwritten_magic(KnownMagic::OaSchema),
handwritten_text("hi"),
handwritten_text("bye"),
),
];
let base: Vec<(Vec<u8>, Vec<u8>)> = cases
.iter()
.map(|(key, value, _)| (key.clone(), value.clone()))
.collect();
let decoded = RainMetaDocumentV1Item::cbor_decode(&handwritten_entries(&base))?;
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].payload.as_ref(), &[0x01]);
assert_eq!(decoded[0].magic, KnownMagic::DotrainV1);
assert_eq!(decoded[0].content_type, ContentType::Cbor);
assert_eq!(decoded[0].content_encoding, ContentEncoding::Identity);
assert_eq!(decoded[0].content_language, ContentLanguage::En);
assert_eq!(decoded[0].schema.as_deref(), Some("hi"));
for (key, value, other_value) in &cases {
for repeated in [value, other_value] {
let mut entries = base.clone();
entries.push((key.clone(), repeated.clone()));
let error = RainMetaDocumentV1Item::cbor_decode(&handwritten_entries(&entries))
.unwrap_err();
assert!(
matches!(error, Error::SerdeCborError(_)),
"{key:?} {error:?}"
);
assert!(
error.to_string().contains("duplicate field"),
"{key:?} {error}"
);
}
}
Ok(())
}
#[test]
fn test_cbor_decode_duplicate_unknown_key_errors() -> Result<(), Error> {
let base: Vec<(Vec<u8>, Vec<u8>)> = vec![
(vec![0x00], vec![0x41, 0x01]),
(vec![0x01], handwritten_magic(KnownMagic::DotrainV1)),
];
let unknown: [(Vec<u8>, Vec<u8>, Vec<u8>); 2] = [
(vec![0x05], vec![0x07], vec![0x08]),
(
handwritten_magic(KnownMagic::OaHashList),
handwritten_text("hi"),
handwritten_text("bye"),
),
];
for (key, value, other_value) in &unknown {
let mut entries = base.clone();
entries.push((key.clone(), value.clone()));
let decoded = RainMetaDocumentV1Item::cbor_decode(&handwritten_entries(&entries))?;
assert_eq!(decoded, vec![plain_item(KnownMagic::DotrainV1, vec![0x01])]);
for repeated in [value, other_value] {
let mut duplicated = entries.clone();
duplicated.push((key.clone(), repeated.clone()));
let error = RainMetaDocumentV1Item::cbor_decode(&handwritten_entries(&duplicated))
.unwrap_err();
assert!(
matches!(error, Error::SerdeCborError(_)),
"{key:?} {error:?}"
);
assert!(
error.to_string().contains("duplicate map key"),
"{key:?} {error}"
);
}
}
Ok(())
}
#[test]
fn test_cbor_decode_handwritten_document_magic_item() {
let bytes: Vec<u8> = vec![
0xa2, 0x00, 0x41, 0x01, 0x01, 0x1b, 0xff, 0x0a, 0x89, 0xc6, 0x74, 0xee, 0x78, 0x74, ];
assert!(RainMetaDocumentV1Item::cbor_decode(&bytes).is_err());
}
#[test]
fn test_document_magic_item_carries_a_nested_document() -> Result<(), Error> {
let inner = plain_item(KnownMagic::DotrainV1, vec![0x01]);
let inner_doc = RainMetaDocumentV1Item::cbor_encode_seq(
&vec![inner.clone()],
KnownMagic::RainMetaDocumentV1,
)?;
let outer = plain_item(KnownMagic::RainMetaDocumentV1, inner_doc.clone());
let outer_doc = RainMetaDocumentV1Item::cbor_encode_seq(
&vec![outer.clone()],
KnownMagic::RainMetaDocumentV1,
)?;
assert!(RainMetaDocumentV1Item::cbor_decode(&outer_doc).is_err());
assert_eq!(
RainMetaDocumentV1Item::cbor_decode(&inner_doc)?,
vec![inner]
);
Ok(())
}
#[test]
fn test_document_magic_item_is_not_unpackable() {
assert!(matches!(
KnownMeta::try_from(KnownMagic::RainMetaDocumentV1),
Err(Error::UnsupportedMeta)
));
assert!(matches!(
plain_item(KnownMagic::RainMetaDocumentV1, vec![0x01]).unpack_into::<Vec<u8>>(),
Err(Error::UnsupportedMeta)
));
}
#[test]
fn test_unpack_decodes_content_encoding() -> Result<(), Error> {
let content = b"unpack me via deflate".to_vec();
let packed = ContentEncoding::Deflate.encode(&content);
assert_ne!(packed, content);
let mut item = plain_item(KnownMagic::DotrainV1, packed);
item.content_encoding = ContentEncoding::Deflate;
assert_eq!(item.unpack()?, content);
let item = plain_item(KnownMagic::DotrainV1, content.clone());
assert_eq!(item.unpack()?, content);
Ok(())
}
#[test]
fn test_unpack_into_whitelist() {
use strum::IntoEnumIterator;
let supported = [
KnownMagic::OpMetaV1,
KnownMagic::DotrainV1,
KnownMagic::RainlangV1,
KnownMagic::SolidityAbiV2,
KnownMagic::AuthoringMetaV1,
KnownMagic::AuthoringMetaV2,
KnownMagic::AddressList,
KnownMagic::InterpreterCallerMetaV1,
KnownMagic::ExpressionDeployerV2BytecodeV1,
KnownMagic::DotrainSourceV1,
KnownMagic::OrderBuilderStateV1,
KnownMagic::RainlangSourceV1,
KnownMagic::RaindexSignedContextOracleV1,
];
for magic in supported {
let unpacked: Vec<u8> = plain_item(magic, vec![0x61]).unpack_into().unwrap();
assert_eq!(unpacked, vec![0x61], "{:?}", magic);
}
let unsupported = [
KnownMagic::RainMetaDocumentV1,
KnownMagic::WebDataV1,
KnownMagic::OaSchema,
KnownMagic::OaHashList,
KnownMagic::OaStructure,
KnownMagic::OaTokenImage,
KnownMagic::OaTokenCredentialLinks,
];
for magic in unsupported {
let result: Result<Vec<u8>, Error> = plain_item(magic, vec![0x61]).unpack_into();
assert!(matches!(result, Err(Error::UnsupportedMeta)), "{:?}", magic);
}
assert_eq!(
supported.len() + unsupported.len(),
KnownMagic::iter().count()
);
}
#[test]
fn test_try_into_string_invalid_utf8_errors() {
let item = plain_item(KnownMagic::DotrainV1, vec![0xff, 0xfe]);
let result: Result<String, Error> = item.try_into();
assert!(matches!(result, Err(Error::FromUtf8Error(_))));
}
#[test]
fn test_try_into_vec_decodes_encoding() -> Result<(), Error> {
let content = b"raw bytecode bytes \x00\x01\x02".to_vec();
let packed = ContentEncoding::Deflate.encode(&content);
let mut item = plain_item(KnownMagic::ExpressionDeployerV2BytecodeV1, packed.clone());
item.content_encoding = ContentEncoding::Deflate;
let unpacked: Vec<u8> = item.try_into()?;
assert_eq!(unpacked, content);
assert_ne!(unpacked, packed);
Ok(())
}
#[test]
fn test_content_encoding_deflate_roundtrip() -> Result<(), Error> {
let content = b"hello rain deflate fixture hello rain deflate fixture".to_vec();
let encoded = ContentEncoding::Deflate.encode(&content);
assert_ne!(encoded, content);
assert_eq!(encoded[0], 0x78);
assert_eq!(ContentEncoding::Deflate.decode(&encoded)?, content);
Ok(())
}
#[test]
fn test_content_encoding_passthrough() -> Result<(), Error> {
let data = vec![0x00, 0xff, 0x10];
for encoding in [ContentEncoding::None, ContentEncoding::Identity] {
assert_eq!(encoding.encode(&data), data, "{:?}", encoding);
assert_eq!(encoding.decode(&data)?, data, "{:?}", encoding);
}
Ok(())
}
#[test]
fn test_content_encoding_decode_fixtures() -> Result<(), Error> {
let content = b"hello rain deflate fixture".to_vec();
let zlib: Vec<u8> = vec![
120, 156, 203, 72, 205, 201, 201, 87, 40, 74, 204, 204, 83, 72, 73, 77, 203, 73, 44,
73, 85, 72, 203, 172, 40, 41, 45, 74, 5, 0, 132, 64, 9, 251,
];
let raw: Vec<u8> = vec![
203, 72, 205, 201, 201, 87, 40, 74, 204, 204, 83, 72, 73, 77, 203, 73, 44, 73, 85, 72,
203, 172, 40, 41, 45, 74, 5, 0,
];
assert_eq!(ContentEncoding::Deflate.decode(&zlib)?, content);
assert_eq!(ContentEncoding::Deflate.decode(&raw)?, content);
Ok(())
}
#[test]
fn test_content_encoding_decode_garbage_errors() {
let garbage = [0xffu8, 0xff, 0xff, 0xff];
assert!(matches!(
ContentEncoding::Deflate.decode(&garbage),
Err(Error::InflateError(_))
));
}
#[test]
fn test_content_headers_strum_names() {
use std::str::FromStr;
assert_eq!(
ContentEncoding::from_str("deflate").unwrap(),
ContentEncoding::Deflate
);
assert_eq!(
ContentEncoding::from_str("identity").unwrap(),
ContentEncoding::Identity
);
assert_eq!(
ContentEncoding::from_str("none").unwrap(),
ContentEncoding::None
);
assert_eq!(ContentEncoding::Deflate.to_string(), "deflate");
assert_eq!(
ContentType::from_str("octet-stream").unwrap(),
ContentType::OctetStream
);
assert_eq!(ContentType::from_str("json").unwrap(), ContentType::Json);
assert_eq!(ContentType::Json.to_string(), "json");
assert_eq!(
ContentLanguage::from_str("en").unwrap(),
ContentLanguage::En
);
}
#[test]
fn test_known_meta_try_from_magic() {
let cases: [(KnownMagic, KnownMeta); 13] = [
(KnownMagic::OpMetaV1, KnownMeta::OpV1),
(KnownMagic::DotrainV1, KnownMeta::DotrainV1),
(KnownMagic::RainlangV1, KnownMeta::RainlangV1),
(KnownMagic::SolidityAbiV2, KnownMeta::SolidityAbiV2),
(KnownMagic::AuthoringMetaV1, KnownMeta::AuthoringMetaV1),
(KnownMagic::AuthoringMetaV2, KnownMeta::AuthoringMetaV2),
(KnownMagic::AddressList, KnownMeta::AddressList),
(
KnownMagic::InterpreterCallerMetaV1,
KnownMeta::InterpreterCallerMetaV1,
),
(
KnownMagic::ExpressionDeployerV2BytecodeV1,
KnownMeta::ExpressionDeployerV2BytecodeV1,
),
(KnownMagic::RainlangSourceV1, KnownMeta::RainlangSourceV1),
(KnownMagic::DotrainSourceV1, KnownMeta::DotrainSourceV1),
(
KnownMagic::OrderBuilderStateV1,
KnownMeta::OrderBuilderStateV1,
),
(
KnownMagic::RaindexSignedContextOracleV1,
KnownMeta::RaindexSignedContextOracleV1,
),
];
for (magic, meta) in cases {
assert_eq!(KnownMeta::try_from(magic).unwrap(), meta, "{:?}", magic);
}
for magic in [
KnownMagic::RainMetaDocumentV1,
KnownMagic::WebDataV1,
KnownMagic::OaSchema,
KnownMagic::OaHashList,
KnownMagic::OaStructure,
KnownMagic::OaTokenImage,
KnownMagic::OaTokenCredentialLinks,
] {
assert!(
matches!(KnownMeta::try_from(magic), Err(Error::UnsupportedMeta)),
"{:?}",
magic
);
}
}
#[test]
fn test_known_meta_strum_parse_display() {
use std::str::FromStr;
assert_eq!(
KnownMeta::from_str("solidity-abi-v2").unwrap(),
KnownMeta::SolidityAbiV2
);
assert_eq!(
KnownMeta::from_str("interpreter-caller-meta-v1").unwrap(),
KnownMeta::InterpreterCallerMetaV1
);
assert_eq!(KnownMeta::SolidityAbiV2.to_string(), "solidity-abi-v2");
}
#[tokio::test]
async fn test_search_lowercases_hash() {
use httpmock::prelude::*;
let (_, doc) = sample_authoring_doc();
let hash_upper = format!("0x{}", "AB".repeat(32));
let server = MockServer::start();
let mock = server.mock(|when, then| {
when.method(POST)
.body_contains(hash_upper.to_ascii_lowercase());
then.status(200).json_body(json!({
"data": {"meta": {"__typename": "RainMetaV1", "rawBytes": hex::encode_prefixed(&doc)}}
}));
});
let response = search(&hash_upper, &vec![server.url("/sg")]).await.unwrap();
assert_eq!(response.bytes, doc);
mock.assert();
}
#[tokio::test]
async fn test_search_first_success_wins() {
use httpmock::prelude::*;
let (_, doc) = sample_authoring_doc();
let bad = MockServer::start();
let _bad_mock = bad.mock(|when, then| {
when.method(POST);
then.status(500).body("subgraph down");
});
let good = MockServer::start();
let _good_mock = good.mock(|when, then| {
when.method(POST);
then.status(200).json_body(json!({
"data": {"meta": {"__typename": "RainMetaV1", "rawBytes": hex::encode_prefixed(&doc)}}
}));
});
let response = search(
&format!("0x{}", "11".repeat(32)),
&vec![bad.url("/sg"), good.url("/sg")],
)
.await
.unwrap();
assert_eq!(response.bytes, doc);
}
#[tokio::test]
async fn test_search_empty_subgraphs_is_a_miss() {
let hash = format!("0x{}", "33".repeat(32));
assert!(matches!(
search(&hash, &vec![]).await,
Err(Error::NoRecordFound)
));
}
#[tokio::test]
async fn test_implements_erc165_gate_short_circuits() {
let address = Address::random();
let asserter = Asserter::new();
let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
asserter
.push_success(&"0x0000000000000000000000000000000000000000000000000000000000000000");
asserter
.push_success(&"0x0000000000000000000000000000000000000000000000000000000000000001");
assert!(!implements_i_described_by_meta_v1(&provider, address)
.await
.unwrap());
let asserter = Asserter::new();
let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
asserter.push_failure(ErrorPayload {
code: -32000,
message: "connection reset".into(),
data: None,
});
asserter
.push_success(&"0x0000000000000000000000000000000000000000000000000000000000000001");
assert!(implements_i_described_by_meta_v1(&provider, address)
.await
.is_err());
}
#[tokio::test]
async fn test_implements_empty_response_is_false() {
let address = Address::random();
let asserter = Asserter::new();
let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
asserter
.push_success(&"0x0000000000000000000000000000000000000000000000000000000000000001");
asserter
.push_success(&"0x0000000000000000000000000000000000000000000000000000000000000000");
asserter.push_success(&"0x");
assert!(!implements_i_described_by_meta_v1(&provider, address)
.await
.unwrap());
}
#[tokio::test]
async fn test_implements_described_by_call_error_is_unknown() {
let address = Address::random();
let asserter = Asserter::new();
let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
asserter
.push_success(&"0x0000000000000000000000000000000000000000000000000000000000000001");
asserter
.push_success(&"0x0000000000000000000000000000000000000000000000000000000000000000");
asserter.push_failure(ErrorPayload {
code: -32005,
message: "rate limit exceeded".into(),
data: None,
});
let error = implements_i_described_by_meta_v1(&provider, address)
.await
.unwrap_err();
assert!(error.to_string().contains("rate limit exceeded"));
}
#[tokio::test]
async fn test_implements_undecodable_response_is_unknown() {
let address = Address::random();
let asserter = Asserter::new();
let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
asserter
.push_success(&"0x0000000000000000000000000000000000000000000000000000000000000001");
asserter
.push_success(&"0x0000000000000000000000000000000000000000000000000000000000000000");
asserter.push_success(&"0xdeadbeef");
implements_i_described_by_meta_v1(&provider, address)
.await
.unwrap_err();
}
#[tokio::test]
async fn test_store_constructors_inject_no_subgraphs() {
assert!(Store::new().subgraphs().is_empty());
assert!(Store::default().subgraphs().is_empty());
assert!(
Store::create(&vec![], &MetaCache::default(), &HashMap::new())
.subgraphs()
.is_empty()
);
let hash = [0u8; 32];
let mut store = Store::default();
assert!(store.update(&hash).await.is_err());
}
#[test]
fn test_store_create_validates_entries() {
let (_, doc) = sample_authoring_doc();
let good_hash = keccak256(&doc).0.to_vec();
let mut cache = MetaCache::default();
cache.insert_verified(&good_hash, doc.clone()).unwrap();
let mut dotrain_cache = HashMap::new();
dotrain_cache.insert("a.rain".to_string(), good_hash.clone());
dotrain_cache.insert("missing.rain".to_string(), vec![0x44u8; 32]);
let store = Store::create(
&vec!["https://example.com/custom-sg".to_string()],
&cache,
&dotrain_cache,
);
assert_eq!(
store.subgraphs(),
&vec!["https://example.com/custom-sg".to_string()]
);
assert_eq!(store.get_meta(&good_hash), Some(&doc));
assert_eq!(store.get_dotrain_hash("a.rain"), Some(&good_hash));
assert_eq!(store.get_dotrain_hash("missing.rain"), None);
}
#[test]
fn test_store_add_subgraphs_dedupe() {
let mut store = Store::new();
store.add_subgraphs(&vec!["sg-a".to_string()]);
store.add_subgraphs(&vec!["sg-a".to_string(), "sg-b".to_string()]);
assert_eq!(
store.subgraphs(),
&vec!["sg-a".to_string(), "sg-b".to_string()]
);
}
#[test]
fn test_store_dotrain_getters_and_set_fresh() {
let mut store = Store::new();
let text = "some dotrain content";
let (hash, old) = store.set_dotrain(text, "file.rain", false).unwrap();
assert!(old.is_empty());
let expected_item = RainMetaDocumentV1Item {
payload: serde_bytes::ByteBuf::from(text.as_bytes()),
magic: KnownMagic::DotrainV1,
content_type: ContentType::OctetStream,
content_encoding: ContentEncoding::None,
content_language: ContentLanguage::None,
schema: None,
};
let expected_bytes = expected_item.cbor_encode().unwrap();
assert_eq!(hash, keccak256(&expected_bytes).0.to_vec());
assert_eq!(store.get_dotrain_hash("file.rain"), Some(&hash));
assert_eq!(store.get_dotrain_uri(&hash), Some(&"file.rain".to_string()));
assert_eq!(store.get_dotrain_meta("file.rain"), Some(&expected_bytes));
assert_eq!(store.get_dotrain_hash("other.rain"), None);
assert_eq!(store.get_dotrain_uri(&[0u8; 32]), None);
assert_eq!(store.get_dotrain_meta("other.rain"), None);
}
#[test]
fn test_store_set_dotrain_branches() {
let mut store = Store::new();
let (hash_one, _) = store.set_dotrain("text one", "a.rain", false).unwrap();
let (hash_same, old_same) = store.set_dotrain("text one", "a.rain", false).unwrap();
assert_eq!(hash_same, hash_one);
assert!(old_same.is_empty());
assert!(store.get_meta(&hash_one).is_some());
let (hash_two, old_two) = store.set_dotrain("text two", "a.rain", false).unwrap();
assert_ne!(hash_two, hash_one);
assert_eq!(old_two, hash_one);
assert_eq!(store.get_dotrain_hash("a.rain"), Some(&hash_two));
assert!(store.get_meta(&hash_one).is_none());
assert!(store.get_meta(&hash_two).is_some());
let (hash_three, old_three) = store.set_dotrain("text three", "a.rain", true).unwrap();
assert_eq!(old_three, hash_two);
assert_eq!(store.get_dotrain_hash("a.rain"), Some(&hash_three));
assert!(store.get_meta(&hash_two).is_some());
assert!(store.get_meta(&hash_three).is_some());
}
#[test]
fn test_store_delete_dotrain_keep_meta() {
let mut store = Store::new();
let (hash, _) = store.set_dotrain("dotrain body", "d.rain", false).unwrap();
store.delete_dotrain("d.rain", false);
assert_eq!(store.get_dotrain_hash("d.rain"), None);
assert!(store.get_meta(&hash).is_none());
let (hash_again, _) = store.set_dotrain("dotrain body", "d.rain", false).unwrap();
store.delete_dotrain("d.rain", true);
assert_eq!(store.get_dotrain_hash("d.rain"), None);
assert!(store.get_meta(&hash_again).is_some());
}
#[test]
fn test_store_merge_semantics() {
let mut ours = Store::new();
let mut theirs = Store::new();
let mine = b"mine".to_vec();
let yours = b"yours".to_vec();
let mine_hash = keccak256(&mine).0.to_vec();
let yours_hash = keccak256(&yours).0.to_vec();
ours.update_with(&mine_hash, &mine).unwrap();
theirs.update_with(&yours_hash, &yours).unwrap();
let (hash_ours, _) = ours.set_dotrain("content a", "x.rain", false).unwrap();
let (_hash_theirs, _) = theirs.set_dotrain("content b", "x.rain", false).unwrap();
theirs.add_subgraphs(&vec!["sg-their".to_string()]);
ours.merge(&theirs);
assert_eq!(ours.get_meta(&mine_hash), Some(&mine));
assert_eq!(ours.get_meta(&yours_hash), Some(&yours));
assert_eq!(ours.get_dotrain_hash("x.rain"), Some(&hash_ours));
assert!(ours.subgraphs().contains(&"sg-their".to_string()));
}
#[tokio::test]
async fn test_store_update_and_update_check() {
use httpmock::prelude::*;
let authoring_meta: AuthoringMeta = serde_json::from_str(
r#"[{"word":"stack","description":"Copies an existing value from the stack.","operandParserOffset":16}]"#,
)
.unwrap();
let item_one = RainMetaDocumentV1Item {
payload: serde_bytes::ByteBuf::from(authoring_meta.abi_encode_validate().unwrap()),
magic: KnownMagic::AuthoringMetaV1,
content_type: ContentType::Cbor,
content_encoding: ContentEncoding::None,
content_language: ContentLanguage::None,
schema: None,
};
let item_two = sample_dotrain_item();
let doc = RainMetaDocumentV1Item::cbor_encode_seq(
&vec![item_one.clone(), item_two.clone()],
KnownMagic::RainMetaDocumentV1,
)
.unwrap();
let requested = keccak256(&doc).0.to_vec();
let server = MockServer::start();
let _mock = server.mock(|when, then| {
when.method(POST);
then.status(200).json_body(json!({
"data": {"meta": {"__typename": "RainMetaV1", "rawBytes": hex::encode_prefixed(&doc)}}
}));
});
let mut store = Store::new();
store.add_subgraphs(&vec![server.url("/sg")]);
let fetched = store.update(&requested).await.cloned().unwrap();
assert_eq!(fetched, doc);
assert_eq!(store.get_meta(&requested), Some(&doc));
let inner_one = item_one.cbor_encode().unwrap();
let inner_two = item_two.cbor_encode().unwrap();
assert_eq!(
store.get_meta(keccak256(&inner_one).0.as_ref()),
Some(&inner_one)
);
assert_eq!(
store.get_meta(keccak256(&inner_two).0.as_ref()),
Some(&inner_two)
);
let mut cached_store = Store::new();
let bytes = b"standalone meta bytes".to_vec();
let hash = keccak256(&bytes).0.to_vec();
assert!(cached_store.update_with(&hash, &bytes).is_ok());
assert_eq!(cached_store.update_check(&hash).await.unwrap(), &bytes);
}
#[tokio::test]
async fn test_store_update_rejects_hash_mismatch() {
use httpmock::prelude::*;
let (_, doc) = sample_authoring_doc();
let requested = keccak256(b"the real content").0.to_vec();
let server = MockServer::start();
let _mock = server.mock(|when, then| {
when.method(POST);
then.status(200).json_body(json!({
"data": {"meta": {"__typename": "RainMetaV1", "rawBytes": hex::encode_prefixed(&doc)}}
}));
});
let mut store = Store::new();
store.add_subgraphs(&vec![server.url("/sg")]);
assert!(store.update(&requested).await.is_err());
assert!(store.get_meta(&requested).is_none());
assert!(store.cache().is_empty());
assert!(store.update_check(&requested).await.is_err());
}
#[tokio::test]
async fn test_store_no_subgraphs_lookups_return_none() {
let hash = [0u8; 32];
let mut store = Store::new();
assert!(store.update(&hash).await.is_err());
assert!(store.update_check(&hash).await.is_err());
assert!(store.cache().is_empty());
}
#[test]
fn test_store_update_with_validation_and_content() {
let mut store = Store::new();
let bytes = b"payload bytes".to_vec();
let wrong_hash = vec![0x99u8; 32];
match store.update_with(&wrong_hash, &bytes).unwrap_err() {
Error::CorruptRecord(message) => {
assert!(
message.contains(&hex::encode_prefixed(&wrong_hash)),
"{}",
message
)
}
other => panic!("expected CorruptRecord, got {:?}", other),
}
assert!(store.get_meta(&wrong_hash).is_none());
let hash = keccak256(&bytes).0.to_vec();
assert_eq!(store.update_with(&hash, &bytes).unwrap(), &bytes);
let mut seeded = Store::new();
let planted = b"planted value".to_vec();
let planted_hash = keccak256(&planted).0.to_vec();
seeded.update_with(&planted_hash, &planted).unwrap();
assert_eq!(seeded.cache().len(), 1);
assert_eq!(
seeded.update_with(&planted_hash, &planted).unwrap(),
&planted
);
assert_eq!(seeded.cache().len(), 1);
let (_, doc) = sample_authoring_doc();
let doc_hash = keccak256(&doc).0.to_vec();
let mut doc_store = Store::new();
assert!(doc_store.update_with(&doc_hash, &doc).is_ok());
let inner = doc[8..].to_vec();
assert_eq!(store_inner_lookup(&doc_store, &inner), Some(inner.clone()));
let item_a = sample_dotrain_item().cbor_encode().unwrap();
let (_, doc_b) = sample_authoring_doc();
let item_b = doc_b[8..].to_vec();
let seq = [item_a.clone(), item_b].concat();
let seq_hash = keccak256(&seq).0.to_vec();
let mut seq_store = Store::new();
assert!(seq_store.update_with(&seq_hash, &seq).is_ok());
assert_eq!(store_inner_lookup(&seq_store, &item_a), None);
}
fn store_inner_lookup(store: &Store, inner_encoded: &[u8]) -> Option<Vec<u8>> {
store.get_meta(keccak256(inner_encoded).0.as_ref()).cloned()
}
#[test]
fn test_bytes32_to_str_invalid_utf8() {
let mut bytes = [0u8; 32];
bytes[0] = 0xf0;
bytes[1] = 0x28;
bytes[2] = 0x8c;
bytes[3] = 0x28;
assert!(matches!(bytes32_to_str(&bytes), Err(Error::Utf8Error(_))));
let no_nul = [0xffu8; 32];
assert!(matches!(bytes32_to_str(&no_nul), Err(Error::Utf8Error(_))));
}
}