Skip to main content

rain_metadata/meta/
mod.rs

1use super::error::Error;
2
3pub mod cache;
4use cache::MetaCache;
5use alloy::primitives::{hex, keccak256};
6use futures::future;
7use graphql_client::GraphQLQuery;
8use rain_metadata_bindings::IDescribedByMetaV1;
9use reqwest::Client;
10use serde::de::{Deserialize, Deserializer, Visitor};
11use serde::ser::{Serialize, SerializeMap, Serializer};
12use std::{
13    collections::{BTreeSet, HashMap},
14    convert::TryFrom,
15    fmt::Debug,
16    sync::Arc,
17};
18use strum::{EnumIter, EnumString};
19use alloy::sol_types::private::Address;
20use alloy::providers::Provider;
21use alloy::contract::Error as ContractError;
22use rain_erc::erc165::{Erc165Error, IERC165, XorSelectors, supports_erc165};
23
24pub mod magic;
25pub(crate) mod normalize;
26pub(crate) mod query;
27pub mod types;
28
29pub use magic::*;
30pub use query::*;
31
32/// All known meta identifiers
33#[derive(Copy, Clone, EnumString, EnumIter, strum::Display, Debug, PartialEq)]
34#[strum(serialize_all = "kebab-case")]
35pub enum KnownMeta {
36    /// Ops meta v1. Still a known meta - the magic number is in the
37    /// metadata-v1 table and an item can legitimately carry it - but this
38    /// crate no longer models or validates the payload. The interpreter
39    /// describes its words as AuthoringMetaV2 now (`LibAllStandardOps`
40    /// publishes exactly that), and the only surviving op meta references in
41    /// the org are deprecated IExpressionDeployer interfaces.
42    OpV1,
43    DotrainV1,
44    RainlangV1,
45    SolidityAbiV2,
46    AuthoringMetaV1,
47    AuthoringMetaV2,
48    InterpreterCallerMetaV1,
49    ExpressionDeployerV2BytecodeV1,
50    RainlangSourceV1,
51    AddressList,
52    DotrainSourceV1,
53    OrderBuilderStateV1,
54    RaindexSignedContextOracleV1,
55}
56
57impl TryFrom<KnownMagic> for KnownMeta {
58    type Error = Error;
59    fn try_from(value: KnownMagic) -> Result<Self, Self::Error> {
60        match value {
61            KnownMagic::DotrainV1 => Ok(KnownMeta::DotrainV1),
62            KnownMagic::RainlangV1 => Ok(KnownMeta::RainlangV1),
63            KnownMagic::SolidityAbiV2 => Ok(KnownMeta::SolidityAbiV2),
64            KnownMagic::OpMetaV1 => Ok(KnownMeta::OpV1),
65            KnownMagic::AuthoringMetaV1 => Ok(KnownMeta::AuthoringMetaV1),
66            KnownMagic::AuthoringMetaV2 => Ok(KnownMeta::AuthoringMetaV2),
67            KnownMagic::AddressList => Ok(KnownMeta::AddressList),
68            KnownMagic::InterpreterCallerMetaV1 => Ok(KnownMeta::InterpreterCallerMetaV1),
69            KnownMagic::DotrainSourceV1 => Ok(KnownMeta::DotrainSourceV1),
70            KnownMagic::OrderBuilderStateV1 => Ok(KnownMeta::OrderBuilderStateV1),
71            KnownMagic::ExpressionDeployerV2BytecodeV1 => {
72                Ok(KnownMeta::ExpressionDeployerV2BytecodeV1)
73            }
74            KnownMagic::RainlangSourceV1 => Ok(KnownMeta::RainlangSourceV1),
75            KnownMagic::RaindexSignedContextOracleV1 => Ok(KnownMeta::RaindexSignedContextOracleV1),
76            _ => Err(Error::UnsupportedMeta),
77        }
78    }
79}
80
81/// Content type of a cbor meta map
82#[derive(
83    Copy,
84    Clone,
85    Debug,
86    EnumIter,
87    PartialEq,
88    EnumString,
89    strum::Display,
90    serde::Serialize,
91    serde::Deserialize,
92)]
93#[strum(serialize_all = "kebab-case")]
94pub enum ContentType {
95    None,
96    #[serde(rename = "application/json")]
97    Json,
98    #[serde(rename = "application/cbor")]
99    Cbor,
100    #[serde(rename = "application/octet-stream")]
101    OctetStream,
102}
103
104/// Content encoding of a cbor meta map
105#[derive(
106    Copy,
107    Clone,
108    Debug,
109    EnumIter,
110    PartialEq,
111    EnumString,
112    strum::Display,
113    serde::Serialize,
114    serde::Deserialize,
115)]
116#[serde(rename_all = "kebab-case")]
117#[strum(serialize_all = "kebab-case")]
118pub enum ContentEncoding {
119    None,
120    Identity,
121    Deflate,
122}
123
124impl ContentEncoding {
125    /// encode the data based on the variant
126    pub fn encode(&self, data: &[u8]) -> Vec<u8> {
127        match self {
128            ContentEncoding::None | ContentEncoding::Identity => data.to_vec(),
129            ContentEncoding::Deflate => deflate::deflate_bytes_zlib(data),
130        }
131    }
132
133    /// decode the data based on the variant
134    pub fn decode(&self, data: &[u8]) -> Result<Vec<u8>, Error> {
135        Ok(match self {
136            ContentEncoding::None | ContentEncoding::Identity => data.to_vec(),
137            ContentEncoding::Deflate => match inflate::inflate_bytes_zlib(data) {
138                Ok(v) => v,
139                Err(error) => match inflate::inflate_bytes(data) {
140                    Ok(v) => v,
141                    Err(_) => Err(Error::InflateError(error))?,
142                },
143            },
144        })
145    }
146}
147
148/// Content language of a cbor meta map
149#[derive(
150    Copy,
151    Clone,
152    Debug,
153    EnumIter,
154    PartialEq,
155    EnumString,
156    strum::Display,
157    serde::Serialize,
158    serde::Deserialize,
159)]
160#[serde(rename_all = "kebab-case")]
161#[strum(serialize_all = "kebab-case")]
162pub enum ContentLanguage {
163    None,
164    En,
165}
166
167/// # Rain Meta Document v1 Item (meta map)
168///
169/// represents a rain meta data and configuration that can be cbor encoded or unpacked back to the meta types
170#[derive(PartialEq, Debug, Clone)]
171pub struct RainMetaDocumentV1Item {
172    pub payload: serde_bytes::ByteBuf,
173    pub magic: KnownMagic,
174    pub content_type: ContentType,
175    pub content_encoding: ContentEncoding,
176    pub content_language: ContentLanguage,
177    /// optional reference to the schema of the payload, encoded under the
178    /// [KnownMagic::OaSchema] magic number as an additional cbor map key
179    /// beyond the standard 0-4 keys
180    pub schema: Option<String>,
181}
182
183// this implementation is mainly used by Rainlang and Dotrain metas as they are aliased type for String
184impl TryFrom<RainMetaDocumentV1Item> for String {
185    type Error = Error;
186    fn try_from(value: RainMetaDocumentV1Item) -> Result<Self, Self::Error> {
187        Ok(String::from_utf8(value.unpack()?)?)
188    }
189}
190
191// this implementation is mainly used by ExpressionDeployerV2Bytecode meta as it is aliased type for Vec<u8>
192impl TryFrom<RainMetaDocumentV1Item> for Vec<u8> {
193    type Error = Error;
194    fn try_from(value: RainMetaDocumentV1Item) -> Result<Self, Self::Error> {
195        value.unpack()
196    }
197}
198
199impl RainMetaDocumentV1Item {
200    fn len(&self) -> usize {
201        let mut l = 2;
202        if !matches!(self.content_type, ContentType::None) {
203            l += 1;
204        }
205        if !matches!(self.content_encoding, ContentEncoding::None) {
206            l += 1;
207        }
208        if !matches!(self.content_language, ContentLanguage::None) {
209            l += 1;
210        }
211        if self.schema.is_some() {
212            l += 1;
213        }
214        l
215    }
216
217    /// method to hash(keccak256) the cbor encoded bytes of this instance
218    pub fn hash(&self, as_rain_meta_document: bool) -> Result<[u8; 32], Error> {
219        if as_rain_meta_document {
220            Ok(keccak256(Self::cbor_encode_seq(
221                &vec![self.clone()],
222                KnownMagic::RainMetaDocumentV1,
223            )?)
224            .0)
225        } else {
226            Ok(keccak256(self.cbor_encode()?).0)
227        }
228    }
229
230    /// method to cbor encode
231    pub fn cbor_encode(&self) -> Result<Vec<u8>, Error> {
232        let mut bytes: Vec<u8> = vec![];
233        Ok(serde_cbor::to_writer(&mut bytes, &self).map(|_| bytes)?)
234    }
235
236    /// builds a cbor sequence from given MetaMaps
237    pub fn cbor_encode_seq(
238        seq: &Vec<RainMetaDocumentV1Item>,
239        magic: KnownMagic,
240    ) -> Result<Vec<u8>, Error> {
241        let mut bytes: Vec<u8> = magic.to_prefix_bytes().to_vec();
242        for item in seq {
243            serde_cbor::to_writer(&mut bytes, &item)?;
244        }
245        Ok(bytes)
246    }
247
248    /// method to cbor decode from given bytes
249    pub fn cbor_decode(data: &[u8]) -> Result<Vec<RainMetaDocumentV1Item>, Error> {
250        let mut metas: Vec<RainMetaDocumentV1Item> = vec![];
251        let mut consumed: usize = 0;
252        let mut is_rain_document_meta = false;
253        let mut len = data.len();
254        if data.starts_with(&KnownMagic::RainMetaDocumentV1.to_prefix_bytes()) {
255            is_rain_document_meta = true;
256            len -= 8;
257        }
258        let mut deserializer = match is_rain_document_meta {
259            true => serde_cbor::Deserializer::from_slice(&data[8..]),
260            false => serde_cbor::Deserializer::from_slice(data),
261        };
262        // straight off the stream, not via serde_cbor::Value, whose BTreeMap
263        // silently collapses the duplicate keys the visitor has to reject
264        while match Self::deserialize(&mut deserializer) {
265            Ok(meta) => {
266                consumed = deserializer.byte_offset();
267                metas.push(meta);
268                true
269            }
270            Err(error) => {
271                if error.is_eof() {
272                    false
273                } else {
274                    Err(Error::SerdeCborError(error))?
275                }
276            }
277        } {}
278
279        if metas.is_empty() || len != consumed {
280            Err(Error::CorruptMeta)?
281        }
282        Ok(metas)
283    }
284
285    // unpack the payload based on the configuration
286    pub fn unpack(&self) -> Result<Vec<u8>, Error> {
287        ContentEncoding::decode(&self.content_encoding, self.payload.as_ref())
288    }
289
290    // unpacks the payload to given meta type based on configuration
291    pub fn unpack_into<T: TryFrom<Self, Error = Error>>(self) -> Result<T, Error> {
292        match self.magic {
293            KnownMagic::OpMetaV1
294            | KnownMagic::DotrainV1
295            | KnownMagic::RainlangV1
296            | KnownMagic::SolidityAbiV2
297            | KnownMagic::AuthoringMetaV1
298            | KnownMagic::AuthoringMetaV2
299            | KnownMagic::AddressList
300            | KnownMagic::InterpreterCallerMetaV1
301            | KnownMagic::ExpressionDeployerV2BytecodeV1
302            | KnownMagic::DotrainSourceV1
303            | KnownMagic::OrderBuilderStateV1
304            | KnownMagic::RainlangSourceV1
305            | KnownMagic::RaindexSignedContextOracleV1 => T::try_from(self),
306            _ => Err(Error::UnsupportedMeta)?,
307        }
308    }
309}
310
311impl Serialize for RainMetaDocumentV1Item {
312    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
313        let mut map = serializer.serialize_map(Some(self.len()))?;
314        map.serialize_entry(&0, &self.payload)?;
315        map.serialize_entry(&1, &(self.magic as u64))?;
316        match self.content_type {
317            ContentType::None => {}
318            content_type => map.serialize_entry(&2, &content_type)?,
319        }
320        match self.content_encoding {
321            ContentEncoding::None => {}
322            content_encoding => map.serialize_entry(&3, &content_encoding)?,
323        }
324        match self.content_language {
325            ContentLanguage::None => {}
326            content_language => map.serialize_entry(&4, &content_language)?,
327        }
328        if let Some(schema) = &self.schema {
329            map.serialize_entry(&(KnownMagic::OaSchema as u64), schema)?;
330        }
331        map.end()
332    }
333}
334
335impl<'de> Deserialize<'de> for RainMetaDocumentV1Item {
336    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
337        /// RFC 8949 ยง5.6: a map with duplicate keys is not valid, so a second
338        /// sighting of a key is an error rather than an overwrite.
339        fn set_once<V, E: serde::de::Error>(
340            slot: &mut Option<V>,
341            value: V,
342            field: &'static str,
343        ) -> Result<(), E> {
344            if slot.is_some() {
345                return Err(serde::de::Error::duplicate_field(field));
346            }
347            *slot = Some(value);
348            Ok(())
349        }
350
351        struct EncodedMap;
352        impl<'de> Visitor<'de> for EncodedMap {
353            type Value = RainMetaDocumentV1Item;
354
355            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
356                formatter.write_str("rain meta cbor encoded bytes")
357            }
358
359            fn visit_map<T: serde::de::MapAccess<'de>>(
360                self,
361                mut map: T,
362            ) -> Result<Self::Value, T::Error> {
363                const OA_SCHEMA_KEY: u64 = KnownMagic::OaSchema as u64;
364                let mut payload = None;
365                let mut magic: Option<u64> = None;
366                let mut content_type = None;
367                let mut content_encoding = None;
368                let mut content_language = None;
369                let mut schema = None;
370                // the recognised keys guard themselves through set_once; an
371                // unknown key has no slot to be occupied, so its repeats are
372                // tracked here
373                let mut unknown_keys: BTreeSet<u64> = BTreeSet::new();
374                while match map.next_key::<u64>() {
375                    Ok(Some(key)) => {
376                        match key {
377                            0 => set_once(&mut payload, map.next_value()?, "payload")?,
378                            1 => set_once(&mut magic, map.next_value()?, "magic number")?,
379                            2 => set_once(&mut content_type, map.next_value()?, "content type")?,
380                            3 => set_once(
381                                &mut content_encoding,
382                                map.next_value()?,
383                                "content encoding",
384                            )?,
385                            4 => set_once(
386                                &mut content_language,
387                                map.next_value()?,
388                                "content language",
389                            )?,
390                            OA_SCHEMA_KEY => set_once(&mut schema, map.next_value()?, "schema")?,
391                            // the map structure exists so later conventions can
392                            // add indexes that older tooling adopts "or not" in
393                            // a backwards compatible way, so an index this
394                            // version does not know is skipped, not an error.
395                            // ยง5.6 does not ask whether the decoder understands
396                            // a key, so the repeat of one is still invalid even
397                            // though the value behind it is never read.
398                            _ => {
399                                if !unknown_keys.insert(key) {
400                                    return Err(serde::de::Error::custom(format!(
401                                        "duplicate map key: {key}"
402                                    )));
403                                }
404                                map.next_value::<serde::de::IgnoredAny>()?;
405                            }
406                        };
407                        true
408                    }
409                    Ok(None) => false,
410                    Err(error) => Err(error)?,
411                } {}
412                let payload = payload.ok_or_else(|| serde::de::Error::missing_field("payload"))?;
413                let magic = match magic
414                    .ok_or_else(|| serde::de::Error::missing_field("magic number"))?
415                    .try_into()
416                {
417                    Ok(m) => m,
418                    _ => Err(serde::de::Error::custom("unknown magic number"))?,
419                };
420                let content_type = content_type.unwrap_or(ContentType::None);
421                let content_encoding = content_encoding.unwrap_or(ContentEncoding::None);
422                let content_language = content_language.unwrap_or(ContentLanguage::None);
423
424                Ok(RainMetaDocumentV1Item {
425                    payload,
426                    magic,
427                    content_type,
428                    content_encoding,
429                    content_language,
430                    schema,
431                })
432            }
433        }
434        deserializer.deserialize_map(EncodedMap)
435    }
436}
437
438/// searches for a meta matching the given hash in given subgraphs urls
439pub async fn search(hash: &str, subgraphs: &Vec<String>) -> Result<query::MetaResponse, Error> {
440    // future::select_ok panics on an empty iterator.
441    if subgraphs.is_empty() {
442        return Err(Error::NoRecordFound);
443    }
444    let request_body = query::MetaQuery::build_query(query::meta_query::Variables {
445        hash: Some(hash.to_ascii_lowercase()),
446    });
447    let mut promises = vec![];
448
449    let client = Arc::new(Client::builder().build().map_err(Error::ReqwestError)?);
450    for url in subgraphs {
451        promises.push(Box::pin(query::process_meta_query(
452            client.clone(),
453            &request_body,
454            url,
455        )));
456    }
457    let response_value = future::select_ok(promises.drain(..)).await?.0;
458    Ok(response_value)
459}
460
461/// checks if the given contract implements IDescribeByMetaV1 interface
462///
463/// `Err` is rain-erc's "answer unknown": a transport or decode failure stopped
464/// the probe, never a contract that lacks the interface. Both probes here can
465/// raise it - the erc165 one this delegates to rain-erc, and the interface id
466/// one below - and neither may be flattened into `Ok(false)`, which is the
467/// contract answering "no".
468pub async fn implements_i_described_by_meta_v1<P: Provider>(
469    provider: &P,
470    contract_address: Address,
471) -> Result<bool, Erc165Error> {
472    if !supports_erc165(provider, contract_address).await? {
473        return Ok(false);
474    }
475
476    let interface_id_res = IDescribedByMetaV1::IDescribedByMetaV1Calls::xor_selectors();
477    if interface_id_res.is_err() {
478        return Ok(false);
479    }
480
481    match IERC165::new(contract_address, provider)
482        .supportsInterface(interface_id_res.unwrap().into())
483        .call()
484        .await
485    {
486        Ok(supported) => Ok(supported),
487        Err(error)
488            if error.as_revert_data().is_some()
489                || matches!(error, ContractError::ZeroData(_, _)) =>
490        {
491            Ok(false)
492        }
493        Err(error) => Err(error.into()),
494    }
495}
496
497/// # Meta Storage(CAS)
498///
499/// In-memory CAS (content addressed storage) for Rain metadata which basically stores
500/// k/v pairs of meta hash and meta bytes, as well as providing functionalities to
501/// easliy read/write to the CAS.
502///
503/// Hashes are normal bytes and meta bytes are valid cbor encoded as data bytes.
504///
505/// ## Examples
506///
507/// ```
508/// use rain_metadata::Store;
509/// use rain_metadata::meta::cache::MetaCache;
510/// use std::collections::HashMap;
511///
512/// // to instantiate with an empty subgraph list
513/// let mut store = Store::new();
514///
515/// // or to instantiate with initial values
516/// let mut store = Store::create(
517///     &vec!["sg-url-1".to_string()],
518///     &MetaCache::default(),
519///     &HashMap::new(),
520/// );
521///
522/// // add a new subgraph endpoint url to the subgraph list
523/// store.add_subgraphs(&vec!["sg-url-2".to_string()]);
524///
525/// // merge another Store into this one
526/// store.merge(&Store::new());
527///
528/// // updates the meta store with some bytes and the hash they hash to - a
529/// // pair that does not is refused, so the hash is derived rather than picked
530/// let bytes = vec![0u8, 1u8];
531/// let hash = alloy::primitives::keccak256(&bytes).0.to_vec();
532/// store.update_with(&hash, &bytes).unwrap();
533///
534/// // `Store::update(&hash)` is async; it searches each subgraph for `hash` and
535/// // populates the cache with the result. Call it from an async context with `.await`.
536///
537/// // to get a record from the store
538/// let _meta = store.get_meta(&hash);
539///
540/// // Store is agnostic to dotrain contents โ€” it just maps the hash of the content
541/// // to the given uri and puts it as a new meta into the meta cache.
542/// let dotrain_uri = "path/to/file.rain";
543/// let dotrain_content = "/* some dotrain source */";
544/// let (_new_hash, _old_hash) = store
545///     .set_dotrain(dotrain_content, dotrain_uri, false)
546///     .unwrap();
547///
548/// // to get dotrain meta bytes given a uri
549/// let _dotrain_meta_bytes = store.get_dotrain_meta(dotrain_uri);
550/// ```
551#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
552pub struct Store {
553    subgraphs: Vec<String>,
554    cache: MetaCache,
555    dotrain_cache: HashMap<String, Vec<u8>>,
556}
557
558impl Default for Store {
559    fn default() -> Self {
560        Store::new()
561    }
562}
563
564impl Store {
565    /// lazily creates a new instance with no subgraphs
566    /// it is recommended to use create() instead with initial values
567    pub fn new() -> Store {
568        Store {
569            subgraphs: vec![],
570            cache: MetaCache::default(),
571            dotrain_cache: HashMap::new(),
572        }
573    }
574
575    /// creates new instance of Store with given initial values
576    /// it checks the validity of each item of the provided values and only stores those that are valid
577    pub fn create(
578        subgraphs: &Vec<String>,
579        cache: &MetaCache,
580        dotrain_cache: &HashMap<String, Vec<u8>>,
581    ) -> Store {
582        let mut store = Store::new();
583        store.add_subgraphs(subgraphs);
584        for (hash, bytes) in cache.iter() {
585            let _ = store.update_with(hash, bytes);
586        }
587        for (uri, hash) in dotrain_cache {
588            if !store.dotrain_cache.contains_key(uri) && store.cache.contains_key(hash) {
589                store.dotrain_cache.insert(uri.clone(), hash.clone());
590            }
591        }
592        store
593    }
594
595    /// all subgraph endpoints in this instance
596    pub fn subgraphs(&self) -> &Vec<String> {
597        &self.subgraphs
598    }
599
600    /// add new subgraph endpoints
601    pub fn add_subgraphs(&mut self, subgraphs: &Vec<String>) {
602        for sg in subgraphs {
603            if !self.subgraphs.contains(sg) {
604                self.subgraphs.push(sg.to_string());
605            }
606        }
607    }
608
609    /// getter method for the whole meta cache
610    pub fn cache(&self) -> &MetaCache {
611        &self.cache
612    }
613
614    /// get the corresponding meta bytes of the given hash if it exists
615    pub fn get_meta(&self, hash: &[u8]) -> Option<&Vec<u8>> {
616        self.cache.get(hash)
617    }
618
619    /// getter method for the whole dotrain cache
620    pub fn dotrain_cache(&self) -> &HashMap<String, Vec<u8>> {
621        &self.dotrain_cache
622    }
623
624    /// get the corresponding dotrain hash of the given dotrain uri if it exists
625    pub fn get_dotrain_hash(&self, uri: &str) -> Option<&Vec<u8>> {
626        self.dotrain_cache.get(uri)
627    }
628
629    /// get the corresponding uri of the given dotrain hash if it exists
630    pub fn get_dotrain_uri(&self, hash: &[u8]) -> Option<&String> {
631        for (uri, h) in &self.dotrain_cache {
632            if h == hash {
633                return Some(uri);
634            }
635        }
636        None
637    }
638
639    /// get the corresponding meta bytes of the given dotrain uri if it exists
640    pub fn get_dotrain_meta(&self, uri: &str) -> Option<&Vec<u8>> {
641        self.get_meta(self.dotrain_cache.get(uri)?)
642    }
643
644    /// deletes a dotrain record given a uri
645    pub fn delete_dotrain(&mut self, uri: &str, keep_meta: bool) {
646        if let Some(kv) = self.dotrain_cache.remove_entry(uri) {
647            if !keep_meta {
648                self.cache.remove(&kv.1);
649            }
650        };
651    }
652
653    /// lazilly merges another Store to the current one, avoids duplicates
654    /// every map keeps the entry this Store already has on a key collision
655    pub fn merge(&mut self, other: &Store) {
656        self.add_subgraphs(&other.subgraphs);
657        for (hash, bytes) in other.cache.iter() {
658            if !self.cache.contains_key(hash) {
659                // entries are verified by construction, so copying one cannot
660                // introduce an unverified entry
661                let _ = self.cache.insert_verified(hash, bytes.clone());
662            }
663        }
664        for (uri, hash) in &other.dotrain_cache {
665            if !self.dotrain_cache.contains_key(uri) {
666                self.dotrain_cache.insert(uri.clone(), hash.clone());
667            }
668        }
669    }
670
671    /// Caches `bytes` under `hash` via [MetaCache::insert_verified], then
672    /// unpacks the items they carry into the cache too. The gate itself lives
673    /// on [MetaCache], which has no other way in.
674    fn insert_verified(&mut self, hash: &[u8], bytes: Vec<u8>) -> Result<&Vec<u8>, Error> {
675        self.cache.insert_verified(hash, bytes.clone())?;
676        self.store_content(&bytes);
677        self.get_meta(hash).ok_or(Error::NoRecordFound)
678    }
679
680    /// updates the meta cache by searching through all subgraphs for the given
681    /// hash, and returns the reference to the meta bytes in the cache if it was
682    /// found. Refreshes unconditionally; [Self::update_check] is the variant
683    /// that leaves an already cached hash alone.
684    pub async fn update(&mut self, hash: &[u8]) -> Result<&Vec<u8>, Error> {
685        let meta = search(&hex::encode_prefixed(hash), &self.subgraphs).await?;
686        self.insert_verified(hash, meta.bytes)
687    }
688
689    /// first checks if the meta is stored, if not will perform update()
690    pub async fn update_check(&mut self, hash: &[u8]) -> Result<&Vec<u8>, Error> {
691        // The NoRecordFound arm is unreachable, contains_key having just
692        // proved the key is present. It is spelled this way rather than as
693        // `if let Some(cached) = self.get_meta(hash)` because that holds an
694        // immutable borrow of self across the mutable call below it, which
695        // the borrow checker refuses.
696        if self.cache.contains_key(hash) {
697            return self.get_meta(hash).ok_or(Error::NoRecordFound);
698        }
699        self.update(hash).await
700    }
701
702    /// updates the meta cache with the given hash and meta bytes, and returns
703    /// the reference to the bytes if they were accepted. Leaves an already
704    /// cached hash alone, as [Self::update_check] does for the subgraph path.
705    pub fn update_with(&mut self, hash: &[u8], bytes: &[u8]) -> Result<&Vec<u8>, Error> {
706        // The NoRecordFound arm is unreachable, contains_key having just
707        // proved the key is present. It is spelled this way rather than as
708        // `if let Some(cached) = self.get_meta(hash)` because that holds an
709        // immutable borrow of self across the mutable call below it, which
710        // the borrow checker refuses.
711        if self.cache.contains_key(hash) {
712            return self.get_meta(hash).ok_or(Error::NoRecordFound);
713        }
714        self.insert_verified(hash, bytes.to_vec())
715    }
716
717    /// stores (or updates in case the URI already exists) the given dotrain text as meta into the store cache
718    /// and maps it to the given uri (path), it should be noted that reading the content of the dotrain is not in
719    /// the scope of Store and handling and passing on a correct URI (path) for the given text must be handled
720    /// externally by the implementer
721    pub fn set_dotrain(
722        &mut self,
723        text: &str,
724        uri: &str,
725        keep_old: bool,
726    ) -> Result<(Vec<u8>, Vec<u8>), Error> {
727        let bytes = RainMetaDocumentV1Item {
728            payload: serde_bytes::ByteBuf::from(text.as_bytes()),
729            magic: KnownMagic::DotrainV1,
730            content_type: ContentType::OctetStream,
731            content_encoding: ContentEncoding::None,
732            content_language: ContentLanguage::None,
733            schema: None,
734        }
735        .cbor_encode()?;
736        let new_hash = keccak256(&bytes).0.to_vec();
737        if let Some(h) = self.dotrain_cache.get(uri) {
738            let old_hash = h.clone();
739            if new_hash == old_hash {
740                self.cache.insert_verified(&new_hash, bytes)?;
741                Ok((new_hash, vec![]))
742            } else {
743                self.cache.insert_verified(&new_hash, bytes)?;
744                self.dotrain_cache.insert(uri.to_string(), new_hash.clone());
745                if !keep_old {
746                    self.cache.remove(&old_hash);
747                }
748                Ok((new_hash, old_hash))
749            }
750        } else {
751            self.dotrain_cache.insert(uri.to_string(), new_hash.clone());
752            self.cache.insert_verified(&new_hash, bytes)?;
753            Ok((new_hash, vec![]))
754        }
755    }
756
757    /// decodes each meta and stores the inner meta items into the cache
758    /// if any of the inner items is an authoring meta, stores it in authoring meta cache as well
759    /// returns the reference to the authoring bytes if the meta bytes contained any
760    fn store_content(&mut self, bytes: &[u8]) {
761        if let Ok(meta_maps) = RainMetaDocumentV1Item::cbor_decode(bytes) {
762            if bytes.starts_with(&KnownMagic::RainMetaDocumentV1.to_prefix_bytes()) {
763                for meta_map in &meta_maps {
764                    if let Ok(encoded_bytes) = meta_map.cbor_encode() {
765                        // the key is this item's own digest, so the gate can
766                        // only pass - routing through it anyway means no
767                        // reader has to work that out
768                        let _ = self
769                            .cache
770                            .insert_verified(&keccak256(&encoded_bytes).0, encoded_bytes);
771                    }
772                }
773            }
774        }
775    }
776}
777
778/// converts string to bytes32
779///
780/// Right padding with `0u8` is the encoding, so [`bytes32_to_str`] ends the
781/// string at the first `0u8` and cannot carry one. An input holding a nul is
782/// rejected rather than round tripped into a shorter string.
783pub fn str_to_bytes32(text: &str) -> Result<[u8; 32], Error> {
784    let bytes: &[u8] = text.as_bytes();
785    if bytes.len() > 32 {
786        return Err(Error::BiggerThan32Bytes);
787    }
788    if bytes.contains(&0u8) {
789        return Err(Error::NulByteInInput);
790    }
791    let mut b32 = [0u8; 32];
792    b32[..bytes.len()].copy_from_slice(bytes);
793    Ok(b32)
794}
795
796/// converts bytes32 to string
797pub fn bytes32_to_str(bytes: &[u8; 32]) -> Result<&str, Error> {
798    let mut len = 32;
799    if let Some((pos, _)) = itertools::Itertools::find_position(&mut bytes.iter(), |b| **b == 0u8) {
800        len = pos;
801    };
802    Ok(std::str::from_utf8(&bytes[..len])?)
803}
804
805#[cfg(all(test, not(target_family = "wasm")))]
806mod tests {
807    use super::{
808        *, bytes32_to_str, magic::KnownMagic, str_to_bytes32, types::authoring::v1::AuthoringMeta,
809        ContentEncoding, ContentLanguage, ContentType, Error, RainMetaDocumentV1Item,
810    };
811    use alloy::providers::ProviderBuilder;
812    use alloy::{providers::mock::Asserter, rpc::json_rpc::ErrorPayload, sol_types::SolType};
813    use serde_json::json;
814
815    /// Roundtrip test for an authoring meta
816    /// original content -> pack -> MetaMap -> cbor encode -> cbor decode -> MetaMap -> unpack -> original content,
817    #[test]
818    fn authoring_meta_roundtrip() -> Result<(), Error> {
819        let authoring_meta_content = r#"[
820            {
821                "word": "stack",
822                "description": "Copies an existing value from the stack.",
823                "operandParserOffset": 16
824            },
825            {
826                "word": "constant",
827                "description": "Copies a constant value onto the stack.",
828                "operandParserOffset": 16
829            }
830        ]"#;
831        let authoring_meta: AuthoringMeta = serde_json::from_str(authoring_meta_content)?;
832
833        // abi encode the authoring meta with performing validation
834        let authoring_meta_abi_encoded = authoring_meta.abi_encode_validate()?;
835        let expected_abi_encoded = <alloy::sol!((bytes32, uint8, string)[])>::abi_encode(&vec![
836            (
837                str_to_bytes32("stack")?,
838                16u8,
839                "Copies an existing value from the stack.".to_string(),
840            ),
841            (
842                str_to_bytes32("constant")?,
843                16u8,
844                "Copies a constant value onto the stack.".to_string(),
845            ),
846        ]);
847        // check the encoded bytes agaiinst the expected
848        assert_eq!(authoring_meta_abi_encoded, expected_abi_encoded);
849
850        let meta_map = RainMetaDocumentV1Item {
851            payload: serde_bytes::ByteBuf::from(authoring_meta_abi_encoded.clone()),
852            magic: KnownMagic::AuthoringMetaV1,
853            content_type: ContentType::Cbor,
854            content_encoding: ContentEncoding::None,
855            content_language: ContentLanguage::None,
856            schema: None,
857        };
858        let cbor_encoded = meta_map.cbor_encode()?;
859
860        // cbor map with 3 keys
861        assert_eq!(cbor_encoded[0], 0xa3);
862        // key 0
863        assert_eq!(cbor_encoded[1], 0x00);
864        // major type 2 (bytes) length 512
865        assert_eq!(cbor_encoded[2], 0b010_11001);
866        assert_eq!(cbor_encoded[3], 0b000_00010);
867        assert_eq!(cbor_encoded[4], 0b000_00000);
868        // payload
869        assert_eq!(cbor_encoded[5..517], authoring_meta_abi_encoded);
870        // key 1
871        assert_eq!(cbor_encoded[517], 0x01);
872        // major type 0 (unsigned integer) value 27
873        assert_eq!(cbor_encoded[518], 0b000_11011);
874        // magic number
875        assert_eq!(
876            &cbor_encoded[519..527],
877            KnownMagic::AuthoringMetaV1.to_prefix_bytes()
878        );
879        // key 2
880        assert_eq!(cbor_encoded[527], 0x02);
881        // text string application/cbor length 16
882        assert_eq!(cbor_encoded[528], 0b011_10000);
883        // the string application/cbor, must be the end of data
884        assert_eq!(&cbor_encoded[529..], "application/cbor".as_bytes());
885
886        // decode the data back to MetaMap
887        let cbor_decoded = RainMetaDocumentV1Item::cbor_decode(&cbor_encoded)?;
888        // the length of decoded maps must be 1 as we only had 1 encoded item
889        assert_eq!(cbor_decoded.len(), 1);
890        // decoded item must be equal to the original meta_map
891        assert_eq!(cbor_decoded[0], meta_map);
892
893        Ok(())
894    }
895
896    /// Roundtrip test for a dotrain meta
897    /// original content -> pack -> MetaMap -> cbor encode -> cbor decode -> MetaMap -> unpack -> original content,
898    #[test]
899    fn dotrain_meta_roundtrip() -> Result<(), Error> {
900        let dotrain_content = "#main _ _: int-add(1 2) int-add(2 3)";
901        let dotrain_content_bytes = dotrain_content.as_bytes().to_vec();
902
903        let content_encoding = ContentEncoding::Deflate;
904        let deflated_payload = content_encoding.encode(&dotrain_content_bytes);
905
906        let meta_map = RainMetaDocumentV1Item {
907            payload: serde_bytes::ByteBuf::from(deflated_payload.clone()),
908            magic: KnownMagic::DotrainV1,
909            content_type: ContentType::OctetStream,
910            content_encoding,
911            content_language: ContentLanguage::En,
912            schema: None,
913        };
914        let cbor_encoded = meta_map.cbor_encode()?;
915
916        // cbor map with 5 keys
917        assert_eq!(cbor_encoded[0], 0xa5);
918        // key 0
919        assert_eq!(cbor_encoded[1], 0x00);
920        // major type 2 (bytes) length 36
921        assert_eq!(cbor_encoded[2], 0b010_11000);
922        assert_eq!(cbor_encoded[3], 0b001_00100);
923        // assert_eq!(cbor_encoded[4], 0b000_00000);
924        // payload
925        assert_eq!(cbor_encoded[4..40], deflated_payload);
926        // key 1
927        assert_eq!(cbor_encoded[40], 0x01);
928        // major type 0 (unsigned integer) value 27
929        assert_eq!(cbor_encoded[41], 0b000_11011);
930        // magic number
931        assert_eq!(
932            &cbor_encoded[42..50],
933            KnownMagic::DotrainV1.to_prefix_bytes()
934        );
935        // key 2
936        assert_eq!(cbor_encoded[50], 0x02);
937        // text string application/octet-stream length 24
938        assert_eq!(cbor_encoded[51], 0b011_11000);
939        assert_eq!(cbor_encoded[52], 0b000_11000);
940        // the string application/octet-stream
941        assert_eq!(&cbor_encoded[53..77], "application/octet-stream".as_bytes());
942        // key 3
943        assert_eq!(cbor_encoded[77], 0x03);
944        // text string deflate length 7
945        assert_eq!(cbor_encoded[78], 0b011_00111);
946        // the string deflate
947        assert_eq!(&cbor_encoded[79..86], "deflate".as_bytes());
948        // key 4
949        assert_eq!(cbor_encoded[86], 0x04);
950        // text string en length 2
951        assert_eq!(cbor_encoded[87], 0b011_00010);
952        // the string identity, must be the end of data
953        assert_eq!(&cbor_encoded[88..], "en".as_bytes());
954
955        // decode the data back to MetaMap
956        let cbor_decoded = RainMetaDocumentV1Item::cbor_decode(&cbor_encoded)?;
957        // the length of decoded maps must be 1 as we only had 1 encoded item
958        assert_eq!(cbor_decoded.len(), 1);
959        // decoded item must be equal to the original meta_map
960        assert_eq!(cbor_decoded[0], meta_map);
961
962        Ok(())
963    }
964
965    /// Roundtrip test for a meta sequence
966    /// original content -> pack -> MetaMap -> cbor encode -> cbor decode -> MetaMap -> unpack -> original content,
967    #[test]
968    fn meta_seq_roundtrip() -> Result<(), Error> {
969        let authoring_meta_content = r#"[
970            {
971                "word": "stack",
972                "description": "Copies an existing value from the stack.",
973                "operandParserOffset": 16
974            },
975            {
976                "word": "constant",
977                "description": "Copies a constant value onto the stack.",
978                "operandParserOffset": 16
979            }
980        ]"#;
981        let authoring_meta: AuthoringMeta = serde_json::from_str(authoring_meta_content)?;
982        let authoring_meta_abi_encoded = authoring_meta.abi_encode_validate()?;
983        let meta_map_1 = RainMetaDocumentV1Item {
984            payload: serde_bytes::ByteBuf::from(authoring_meta_abi_encoded.clone()),
985            magic: KnownMagic::AuthoringMetaV1,
986            content_type: ContentType::Cbor,
987            content_encoding: ContentEncoding::None,
988            content_language: ContentLanguage::None,
989            schema: None,
990        };
991
992        let dotrain_content = "#main _ _: int-add(1 2) int-add(2 3)";
993        let dotrain_content_bytes = dotrain_content.as_bytes().to_vec();
994        let content_encoding = ContentEncoding::Deflate;
995        let deflated_payload = content_encoding.encode(&dotrain_content_bytes);
996        let meta_map_2 = RainMetaDocumentV1Item {
997            payload: serde_bytes::ByteBuf::from(deflated_payload.clone()),
998            magic: KnownMagic::DotrainV1,
999            content_type: ContentType::OctetStream,
1000            content_encoding,
1001            content_language: ContentLanguage::En,
1002            schema: None,
1003        };
1004
1005        // cbor encode as RainMetaDocument sequence
1006        let cbor_encoded = RainMetaDocumentV1Item::cbor_encode_seq(
1007            &vec![meta_map_1.clone(), meta_map_2.clone()],
1008            KnownMagic::RainMetaDocumentV1,
1009        )?;
1010
1011        // 8 byte magic number prefix
1012        assert_eq!(
1013            &cbor_encoded[0..8],
1014            KnownMagic::RainMetaDocumentV1.to_prefix_bytes()
1015        );
1016
1017        // first item in the encoded bytes
1018        // cbor map with 3 keys
1019        assert_eq!(cbor_encoded[8], 0xa3);
1020        // key 0
1021        assert_eq!(cbor_encoded[9], 0x00);
1022        // major type 2 (bytes) length 512
1023        assert_eq!(cbor_encoded[10], 0b010_11001);
1024        assert_eq!(cbor_encoded[11], 0b000_00010);
1025        assert_eq!(cbor_encoded[12], 0b000_00000);
1026        // payload
1027        assert_eq!(cbor_encoded[13..525], authoring_meta_abi_encoded);
1028        // key 1
1029        assert_eq!(cbor_encoded[525], 0x01);
1030        // major type 0 (unsigned integer) value 27
1031        assert_eq!(cbor_encoded[526], 0b000_11011);
1032        // magic number
1033        assert_eq!(
1034            &cbor_encoded[527..535],
1035            KnownMagic::AuthoringMetaV1.to_prefix_bytes()
1036        );
1037        // key 2
1038        assert_eq!(cbor_encoded[535], 0x02);
1039        // text string application/cbor length 16
1040        assert_eq!(cbor_encoded[536], 0b011_10000);
1041        // the string application/cbor, must be the end of data
1042        assert_eq!(&cbor_encoded[537..553], "application/cbor".as_bytes());
1043
1044        // second item in the encoded bytes
1045        // cbor map with 5 keys
1046        assert_eq!(cbor_encoded[553], 0xa5);
1047        // key 0
1048        assert_eq!(cbor_encoded[554], 0x00);
1049        // major type 2 (bytes) length 36
1050        assert_eq!(cbor_encoded[555], 0b010_11000);
1051        assert_eq!(cbor_encoded[556], 0b001_00100);
1052        // assert_eq!(cbor_encoded[4], 0b000_00000);
1053        // payload
1054        assert_eq!(cbor_encoded[557..593], deflated_payload);
1055        // key 1
1056        assert_eq!(cbor_encoded[593], 0x01);
1057        // major type 0 (unsigned integer) value 27
1058        assert_eq!(cbor_encoded[594], 0b000_11011);
1059        // magic number
1060        assert_eq!(
1061            &cbor_encoded[595..603],
1062            KnownMagic::DotrainV1.to_prefix_bytes()
1063        );
1064        // key 2
1065        assert_eq!(cbor_encoded[603], 0x02);
1066        // text string application/octet-stream length 24
1067        assert_eq!(cbor_encoded[604], 0b011_11000);
1068        assert_eq!(cbor_encoded[605], 0b000_11000);
1069        // the string application/octet-stream
1070        assert_eq!(
1071            &cbor_encoded[606..630],
1072            "application/octet-stream".as_bytes()
1073        );
1074        // key 3
1075        assert_eq!(cbor_encoded[630], 0x03);
1076        // text string deflate length 7
1077        assert_eq!(cbor_encoded[631], 0b011_00111);
1078        // the string deflate
1079        assert_eq!(&cbor_encoded[632..639], "deflate".as_bytes());
1080        // key 4
1081        assert_eq!(cbor_encoded[639], 0x04);
1082        // text string en length 2
1083        assert_eq!(cbor_encoded[640], 0b011_00010);
1084        // the string identity, must be the end of data
1085        assert_eq!(&cbor_encoded[641..], "en".as_bytes());
1086
1087        // decode the data back to MetaMap
1088        let cbor_decoded = RainMetaDocumentV1Item::cbor_decode(&cbor_encoded)?;
1089        // the length of decoded maps must be 2 as we had 2 encoded item
1090        assert_eq!(cbor_decoded.len(), 2);
1091
1092        // decoded item 1 must be equal to the original meta_map_1
1093        assert_eq!(cbor_decoded[0], meta_map_1);
1094        // decoded item 2 must be equal to the original meta_map_2
1095        assert_eq!(cbor_decoded[1], meta_map_2);
1096
1097        Ok(())
1098    }
1099
1100    #[test]
1101    fn test_bytes32_to_str() {
1102        let text_bytes_list = vec![
1103            (
1104                "",
1105                hex!("0000000000000000000000000000000000000000000000000000000000000000"),
1106            ),
1107            (
1108                "A",
1109                hex!("4100000000000000000000000000000000000000000000000000000000000000"),
1110            ),
1111            (
1112                "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345",
1113                hex!("4142434445464748494a4b4c4d4e4f505152535455565758595a303132333435"),
1114            ),
1115            (
1116                "!@#$%^&*(),./;'[]",
1117                hex!("21402324255e262a28292c2e2f3b275b5d000000000000000000000000000000"),
1118            ),
1119        ];
1120
1121        for (text, bytes) in text_bytes_list {
1122            assert_eq!(text, bytes32_to_str(&bytes).unwrap());
1123        }
1124    }
1125
1126    #[test]
1127    fn test_str_to_bytes32() {
1128        let text_bytes_list = vec![
1129            (
1130                "",
1131                hex!("0000000000000000000000000000000000000000000000000000000000000000"),
1132            ),
1133            (
1134                "A",
1135                hex!("4100000000000000000000000000000000000000000000000000000000000000"),
1136            ),
1137            (
1138                "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345",
1139                hex!("4142434445464748494a4b4c4d4e4f505152535455565758595a303132333435"),
1140            ),
1141            (
1142                "!@#$%^&*(),./;'[]",
1143                hex!("21402324255e262a28292c2e2f3b275b5d000000000000000000000000000000"),
1144            ),
1145        ];
1146
1147        for (text, bytes) in text_bytes_list {
1148            assert_eq!(bytes, str_to_bytes32(text).unwrap());
1149        }
1150    }
1151
1152    #[test]
1153    fn test_str_to_bytes32_long() {
1154        assert!(matches!(
1155            str_to_bytes32("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456").unwrap_err(),
1156            Error::BiggerThan32Bytes
1157        ));
1158    }
1159
1160    /// A nul cannot survive the padding convention bytes32_to_str decodes, so
1161    /// it is rejected on the way in wherever it sits, including the pair the
1162    /// issue collides ("a" and "a\0").
1163    #[test]
1164    fn test_str_to_bytes32_rejects_nul() {
1165        for text in [
1166            "\0",
1167            "\0a",
1168            "a\0",
1169            "a\0b",
1170            "abcdefghijklmnopqrstuvwxyz01234\0",
1171        ] {
1172            assert!(
1173                matches!(str_to_bytes32(text), Err(Error::NulByteInInput)),
1174                "nul bearing input {:?} accepted",
1175                text
1176            );
1177        }
1178    }
1179
1180    /// Everything str_to_bytes32 accepts comes back out of bytes32_to_str
1181    /// unchanged, and no two of them share a bytes32.
1182    #[test]
1183    fn test_str_to_bytes32_round_trip() -> Result<(), Error> {
1184        let mut seen: Vec<[u8; 32]> = vec![];
1185        for text in [
1186            "",
1187            "a",
1188            "stack",
1189            "!@#$%^&*(),./;'[]",
1190            "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345",
1191        ] {
1192            let bytes = str_to_bytes32(text)?;
1193            assert_eq!(bytes32_to_str(&bytes)?, text);
1194            assert!(!seen.contains(&bytes), "input {:?} collided", text);
1195            seen.push(bytes);
1196        }
1197        Ok(())
1198    }
1199
1200    #[tokio::test]
1201    async fn test_implements_i_describe_by_meta_v1() {
1202        // makes new server/client with success response for erc165 check
1203        async fn new_server_client() -> (Asserter, impl Provider) {
1204            let asserter = Asserter::new();
1205            let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
1206
1207            // Mock a responses for successful supports erc165 check
1208            asserter.push_success(
1209                &"0x0000000000000000000000000000000000000000000000000000000000000001",
1210            );
1211            asserter.push_success(
1212                &"0x0000000000000000000000000000000000000000000000000000000000000000",
1213            );
1214
1215            (asserter, provider)
1216        }
1217
1218        let address = Address::random();
1219
1220        // mock a true response for implements IDescribedByMetaV1
1221        let (asserter, provider) = new_server_client().await;
1222        asserter
1223            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000001");
1224        let result = implements_i_described_by_meta_v1(&provider, address)
1225            .await
1226            .unwrap();
1227        assert!(result);
1228
1229        // mock a false response for implements IDescribedByMetaV1
1230        let (asserter, provider) = new_server_client().await;
1231        asserter
1232            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000000");
1233        let result = implements_i_described_by_meta_v1(&provider, address)
1234            .await
1235            .unwrap();
1236        assert!(!result);
1237
1238        // mock a revert response for implements IDescribedByMetaV1
1239        let (asserter, provider) = new_server_client().await;
1240        asserter.push_failure(ErrorPayload {
1241            code: -32003,
1242            message: "execution reverted".into(),
1243            data: Some(serde_json::value::to_raw_value(&json!("0x00")).unwrap()),
1244        });
1245        let result = implements_i_described_by_meta_v1(&provider, address)
1246            .await
1247            .unwrap();
1248        assert!(!result);
1249    }
1250
1251    /// Roundtrip test for a meta map carrying the OaSchema magic number as an
1252    /// additional CBOR map key beyond the standard 0-4 keys.
1253    /// MetaMap (with schema) -> cbor encode -> cbor decode -> MetaMap, assert equality
1254    #[test]
1255    fn oa_schema_map_key_roundtrip() -> Result<(), Error> {
1256        let payload = vec![0x01, 0x02, 0x03];
1257        // an IPFS hash referencing the schema of the payload, as written by
1258        // the SFT frontend under the OaSchema map key
1259        let schema = "QmSchemaHash1234567890abcdefghijklmnopqrstuvwx".to_string();
1260        assert_eq!(schema.len(), 46);
1261
1262        let meta_map = RainMetaDocumentV1Item {
1263            payload: serde_bytes::ByteBuf::from(payload.clone()),
1264            magic: KnownMagic::OaStructure,
1265            content_type: ContentType::Json,
1266            content_encoding: ContentEncoding::Deflate,
1267            content_language: ContentLanguage::None,
1268            schema: Some(schema.clone()),
1269        };
1270        let cbor_encoded = meta_map.cbor_encode()?;
1271
1272        // cbor map with 5 keys (0, 1, 2, 3 and the OaSchema magic)
1273        assert_eq!(cbor_encoded[0], 0xa5);
1274        // key 0
1275        assert_eq!(cbor_encoded[1], 0x00);
1276        // major type 2 (bytes) length 3
1277        assert_eq!(cbor_encoded[2], 0b010_00011);
1278        // payload
1279        assert_eq!(cbor_encoded[3..6], payload);
1280        // key 1
1281        assert_eq!(cbor_encoded[6], 0x01);
1282        // major type 0 (unsigned integer) value 27
1283        assert_eq!(cbor_encoded[7], 0b000_11011);
1284        // magic number
1285        assert_eq!(
1286            &cbor_encoded[8..16],
1287            KnownMagic::OaStructure.to_prefix_bytes()
1288        );
1289        // key 2
1290        assert_eq!(cbor_encoded[16], 0x02);
1291        // text string application/json length 16
1292        assert_eq!(cbor_encoded[17], 0b011_10000);
1293        assert_eq!(&cbor_encoded[18..34], "application/json".as_bytes());
1294        // key 3
1295        assert_eq!(cbor_encoded[34], 0x03);
1296        // text string deflate length 7
1297        assert_eq!(cbor_encoded[35], 0b011_00111);
1298        assert_eq!(&cbor_encoded[36..43], "deflate".as_bytes());
1299        // the OaSchema magic as key, major type 0 (unsigned integer) value 27
1300        assert_eq!(cbor_encoded[43], 0b000_11011);
1301        assert_eq!(
1302            &cbor_encoded[44..52],
1303            KnownMagic::OaSchema.to_prefix_bytes()
1304        );
1305        // schema value, text string length 46
1306        assert_eq!(cbor_encoded[52], 0b011_11000);
1307        assert_eq!(cbor_encoded[53], 46);
1308        // the schema hash string, must be the end of data
1309        assert_eq!(&cbor_encoded[54..], schema.as_bytes());
1310
1311        // decode the data back to MetaMap
1312        let cbor_decoded = RainMetaDocumentV1Item::cbor_decode(&cbor_encoded)?;
1313        // the length of decoded maps must be 1 as we only had 1 encoded item
1314        assert_eq!(cbor_decoded.len(), 1);
1315        // decoded item must be equal to the original meta_map
1316        assert_eq!(cbor_decoded[0], meta_map);
1317
1318        Ok(())
1319    }
1320
1321    /// A meta map without the schema key must keep encoding exactly as before
1322    /// (no schema entry on the wire) and roundtrip with schema None
1323    #[test]
1324    fn no_schema_key_encodes_as_before_roundtrip() -> Result<(), Error> {
1325        let payload = vec![0x0a, 0x0b];
1326        let meta_map = RainMetaDocumentV1Item {
1327            payload: serde_bytes::ByteBuf::from(payload.clone()),
1328            magic: KnownMagic::OaStructure,
1329            content_type: ContentType::None,
1330            content_encoding: ContentEncoding::None,
1331            content_language: ContentLanguage::None,
1332            schema: None,
1333        };
1334        let cbor_encoded = meta_map.cbor_encode()?;
1335
1336        // cbor map with only the 2 mandatory keys
1337        assert_eq!(cbor_encoded[0], 0xa2);
1338        // key 0
1339        assert_eq!(cbor_encoded[1], 0x00);
1340        // major type 2 (bytes) length 2
1341        assert_eq!(cbor_encoded[2], 0b010_00010);
1342        // payload
1343        assert_eq!(cbor_encoded[3..5], payload);
1344        // key 1
1345        assert_eq!(cbor_encoded[5], 0x01);
1346        // major type 0 (unsigned integer) value 27
1347        assert_eq!(cbor_encoded[6], 0b000_11011);
1348        // magic number, must be the end of data
1349        assert_eq!(
1350            &cbor_encoded[7..],
1351            KnownMagic::OaStructure.to_prefix_bytes()
1352        );
1353
1354        let cbor_decoded = RainMetaDocumentV1Item::cbor_decode(&cbor_encoded)?;
1355        assert_eq!(cbor_decoded.len(), 1);
1356        assert_eq!(cbor_decoded[0], meta_map);
1357
1358        Ok(())
1359    }
1360
1361    /// A map key this version has no meaning for is a future index, so it is
1362    /// skipped and the rest of the map decodes
1363    #[test]
1364    fn unknown_map_key_index_is_ignored() -> Result<(), Error> {
1365        let mut bytes: Vec<u8> = vec![
1366            // cbor map with 3 keys
1367            0xa3, // key 0, bytes payload of length 0
1368            0x00, 0x40, // key 1, unsigned integer magic number
1369            0x01, 0x1b,
1370        ];
1371        bytes.extend_from_slice(&KnownMagic::DotrainSourceV1.to_prefix_bytes());
1372        // key 5, a plausible future index, unsigned integer value 7
1373        bytes.extend_from_slice(&[0x05, 0x07]);
1374
1375        let decoded = RainMetaDocumentV1Item::cbor_decode(&bytes)?;
1376        assert_eq!(decoded.len(), 1);
1377        assert_eq!(decoded[0], plain_item(KnownMagic::DotrainSourceV1, vec![]));
1378
1379        Ok(())
1380    }
1381
1382    /// A magic number other than OaSchema used as an extra map key is a future
1383    /// magic keyed entry, skipped the same way, and leaves schema unset
1384    #[test]
1385    fn non_oa_schema_extra_map_key_is_ignored() -> Result<(), Error> {
1386        // build a map identical to a valid 2 key meta map but with an extra
1387        // OaHashList magic key carrying a text string
1388        let mut bytes: Vec<u8> = vec![
1389            // cbor map with 3 keys
1390            0xa3, // key 0, bytes payload of length 1
1391            0x00, 0x41, 0xff, // key 1, unsigned integer magic number
1392            0x01, 0x1b,
1393        ];
1394        bytes.extend_from_slice(&KnownMagic::OaStructure.to_prefix_bytes());
1395        // the OaHashList magic as key
1396        bytes.push(0x1b);
1397        bytes.extend_from_slice(&KnownMagic::OaHashList.to_prefix_bytes());
1398        // text string value of length 2
1399        bytes.extend_from_slice(&[0x62, 0x68, 0x69]);
1400
1401        let decoded = RainMetaDocumentV1Item::cbor_decode(&bytes)?;
1402        assert_eq!(decoded.len(), 1);
1403        let expected = plain_item(KnownMagic::OaStructure, vec![0xff]);
1404        assert_eq!(decoded[0], expected);
1405        assert_eq!(decoded[0].schema, None);
1406
1407        Ok(())
1408    }
1409
1410    /// The whole value of an unknown key is consumed however nested, so the
1411    /// item that follows it in the sequence still decodes
1412    #[test]
1413    fn unknown_map_key_consumes_its_whole_value() -> Result<(), Error> {
1414        let mut bytes: Vec<u8> = vec![0xa3, 0x00, 0x41, 0x01, 0x01, 0x1b];
1415        bytes.extend_from_slice(&KnownMagic::DotrainV1.to_prefix_bytes());
1416        // key 5, value {42: [1, 2]}
1417        bytes.extend_from_slice(&[0x05, 0xa1, 0x18, 0x2a, 0x82, 0x01, 0x02]);
1418        bytes.extend_from_slice(&handwritten_map());
1419
1420        let decoded = RainMetaDocumentV1Item::cbor_decode(&bytes)?;
1421        assert_eq!(decoded.len(), 2);
1422        let expected = plain_item(KnownMagic::DotrainV1, vec![0x01]);
1423        assert_eq!(decoded[0], expected);
1424        assert_eq!(decoded[1], expected);
1425
1426        Ok(())
1427    }
1428
1429    /// An ignored key is not re-encoded, so the item's hash is the hash of what
1430    /// this version can represent and not of the bytes it decoded
1431    #[test]
1432    fn ignored_map_key_is_absent_from_the_reencoding() -> Result<(), Error> {
1433        let mut bytes: Vec<u8> = vec![0xa3, 0x00, 0x41, 0x01, 0x01, 0x1b];
1434        bytes.extend_from_slice(&KnownMagic::DotrainV1.to_prefix_bytes());
1435        bytes.extend_from_slice(&[0x05, 0x07]);
1436
1437        let decoded = RainMetaDocumentV1Item::cbor_decode(&bytes)?;
1438        assert_eq!(decoded[0].cbor_encode()?, handwritten_map());
1439        assert_eq!(decoded[0].hash(false)?, keccak256(handwritten_map()).0);
1440        assert_ne!(decoded[0].hash(false)?, keccak256(&bytes).0);
1441
1442        Ok(())
1443    }
1444
1445    /// Only integer keys are indexes. The spec rules out the HTTP header names
1446    /// as keys, so a key that is not an unsigned integer is not a future index
1447    /// to skip over
1448    #[test]
1449    fn non_integer_map_key_errors() {
1450        let mut text_key: Vec<u8> = vec![0xa3, 0x00, 0x41, 0x01, 0x01, 0x1b];
1451        text_key.extend_from_slice(&KnownMagic::DotrainV1.to_prefix_bytes());
1452        // key "5", unsigned integer value 7
1453        text_key.extend_from_slice(&[0x61, 0x35, 0x07]);
1454        assert!(matches!(
1455            RainMetaDocumentV1Item::cbor_decode(&text_key),
1456            Err(Error::SerdeCborError(_))
1457        ));
1458
1459        let mut negative_key: Vec<u8> = vec![0xa3, 0x00, 0x41, 0x01, 0x01, 0x1b];
1460        negative_key.extend_from_slice(&KnownMagic::DotrainV1.to_prefix_bytes());
1461        // key -1, unsigned integer value 7
1462        negative_key.extend_from_slice(&[0x20, 0x07]);
1463        assert!(matches!(
1464            RainMetaDocumentV1Item::cbor_decode(&negative_key),
1465            Err(Error::SerdeCborError(_))
1466        ));
1467    }
1468
1469    /// An unknown key is skipped, never counted as one of the mandatory keys
1470    #[test]
1471    fn unknown_map_key_does_not_stand_in_for_a_mandatory_key() {
1472        let mut bytes: Vec<u8> = vec![0xa2, 0x05, 0x07, 0x01, 0x1b];
1473        bytes.extend_from_slice(&KnownMagic::DotrainV1.to_prefix_bytes());
1474        assert!(matches!(
1475            RainMetaDocumentV1Item::cbor_decode(&bytes),
1476            Err(Error::SerdeCborError(_))
1477        ));
1478    }
1479
1480    fn plain_item(magic: KnownMagic, payload: Vec<u8>) -> RainMetaDocumentV1Item {
1481        RainMetaDocumentV1Item {
1482            payload: serde_bytes::ByteBuf::from(payload),
1483            magic,
1484            content_type: ContentType::None,
1485            content_encoding: ContentEncoding::None,
1486            content_language: ContentLanguage::None,
1487            schema: None,
1488        }
1489    }
1490
1491    // ---- helpers for the CAS / search tests ----
1492
1493    fn sample_authoring_doc() -> (AuthoringMeta, Vec<u8>) {
1494        let authoring_meta: AuthoringMeta = serde_json::from_str(
1495            r#"[{"word":"stack","description":"Copies an existing value from the stack.","operandParserOffset":16}]"#,
1496        )
1497        .unwrap();
1498        let abi = authoring_meta.abi_encode_validate().unwrap();
1499        let item = RainMetaDocumentV1Item {
1500            payload: serde_bytes::ByteBuf::from(abi),
1501            magic: KnownMagic::AuthoringMetaV1,
1502            content_type: ContentType::Cbor,
1503            content_encoding: ContentEncoding::None,
1504            content_language: ContentLanguage::None,
1505            schema: None,
1506        };
1507        let doc =
1508            RainMetaDocumentV1Item::cbor_encode_seq(&vec![item], KnownMagic::RainMetaDocumentV1)
1509                .unwrap();
1510        (authoring_meta, doc)
1511    }
1512
1513    fn sample_dotrain_item() -> RainMetaDocumentV1Item {
1514        RainMetaDocumentV1Item {
1515            payload: serde_bytes::ByteBuf::from("some dotrain body".as_bytes()),
1516            magic: KnownMagic::DotrainV1,
1517            content_type: ContentType::OctetStream,
1518            content_encoding: ContentEncoding::None,
1519            content_language: ContentLanguage::None,
1520            schema: None,
1521        }
1522    }
1523
1524    /// Handwritten canonical cbor for {0: h'01', 1: DotrainV1 magic}, written
1525    /// out byte by byte from the cbor spec, independent of cbor_encode.
1526    fn handwritten_map() -> Vec<u8> {
1527        vec![
1528            0xa2, // map(2)
1529            0x00, // key 0
1530            0x41, 0x01, // bytes(1) 0x01
1531            0x01, // key 1
1532            0x1b, 0xff, 0xda, 0xc2, 0xf2, 0xf3, 0x7b, 0xe8, 0x94, // u64 DotrainV1
1533        ]
1534    }
1535
1536    /// hash(false) is keccak256 of the bare cbor map and hash(true) is
1537    /// keccak256 of the rain meta document prefix followed by the same map,
1538    /// pinned against independently handwritten bytes.
1539    #[test]
1540    fn test_hash_bare_vs_document() -> Result<(), Error> {
1541        let map_bytes = handwritten_map();
1542        let mut doc_bytes: Vec<u8> = vec![0xff, 0x0a, 0x89, 0xc6, 0x74, 0xee, 0x78, 0x74];
1543        doc_bytes.extend_from_slice(&map_bytes);
1544
1545        let item = plain_item(KnownMagic::DotrainV1, vec![0x01]);
1546        assert_eq!(item.hash(false)?, keccak256(&map_bytes).0);
1547        assert_eq!(item.hash(true)?, keccak256(&doc_bytes).0);
1548        assert_ne!(item.hash(false)?, item.hash(true)?);
1549        Ok(())
1550    }
1551
1552    /// Empty input and a bare document prefix with no items are corrupt metas.
1553    #[test]
1554    fn test_cbor_decode_empty_is_corrupt() {
1555        assert!(matches!(
1556            RainMetaDocumentV1Item::cbor_decode(&[]),
1557            Err(Error::CorruptMeta)
1558        ));
1559        let prefix = KnownMagic::RainMetaDocumentV1.to_prefix_bytes();
1560        assert!(matches!(
1561            RainMetaDocumentV1Item::cbor_decode(&prefix),
1562            Err(Error::CorruptMeta)
1563        ));
1564    }
1565
1566    /// A valid map followed by truncated trailing bytes must not decode: the
1567    /// data does not end exactly at the last complete item.
1568    #[test]
1569    fn test_cbor_decode_trailing_truncated_is_corrupt() {
1570        let mut bytes = handwritten_map();
1571        bytes.push(0x1b); // u64 header with all 8 payload bytes missing
1572        assert!(matches!(
1573            RainMetaDocumentV1Item::cbor_decode(&bytes),
1574            Err(Error::CorruptMeta)
1575        ));
1576    }
1577
1578    /// Every way an item can run out of bytes is corrupt meta, not a serde
1579    /// cbor error: a truncated sole item, a map header promising entries the
1580    /// input does not carry, and a truncated item after a complete one.
1581    #[test]
1582    fn test_cbor_decode_truncated_item_is_corrupt() {
1583        let mut sole = handwritten_map();
1584        sole.pop();
1585        assert!(matches!(
1586            RainMetaDocumentV1Item::cbor_decode(&sole),
1587            Err(Error::CorruptMeta)
1588        ));
1589
1590        assert!(matches!(
1591            RainMetaDocumentV1Item::cbor_decode(&[0xa2, 0x00, 0x41, 0x01]),
1592            Err(Error::CorruptMeta)
1593        ));
1594
1595        let mut after_complete = handwritten_map();
1596        after_complete.extend_from_slice(&[0xa2, 0x00]);
1597        assert!(matches!(
1598            RainMetaDocumentV1Item::cbor_decode(&after_complete),
1599            Err(Error::CorruptMeta)
1600        ));
1601    }
1602
1603    /// A valid map followed by a byte that is not valid cbor surfaces the
1604    /// serde cbor error.
1605    #[test]
1606    fn test_cbor_decode_trailing_garbage_errors() {
1607        let mut bytes = handwritten_map();
1608        bytes.push(0xff); // lone break byte
1609        assert!(matches!(
1610            RainMetaDocumentV1Item::cbor_decode(&bytes),
1611            Err(Error::SerdeCborError(_))
1612        ));
1613    }
1614
1615    /// A map without the mandatory payload key 0 must not decode.
1616    #[test]
1617    fn test_cbor_decode_missing_payload_errors() {
1618        let mut bytes: Vec<u8> = vec![0xa1, 0x01, 0x1b]; // {1: DotrainV1}
1619        bytes.extend_from_slice(&KnownMagic::DotrainV1.to_prefix_bytes());
1620        assert!(matches!(
1621            RainMetaDocumentV1Item::cbor_decode(&bytes),
1622            Err(Error::SerdeCborError(_))
1623        ));
1624    }
1625
1626    /// A map without the mandatory magic key 1 must not decode.
1627    #[test]
1628    fn test_cbor_decode_missing_magic_errors() {
1629        let bytes: Vec<u8> = vec![0xa1, 0x00, 0x41, 0x01]; // {0: h'01'}
1630        assert!(matches!(
1631            RainMetaDocumentV1Item::cbor_decode(&bytes),
1632            Err(Error::SerdeCborError(_))
1633        ));
1634    }
1635
1636    /// A map carrying an unknown magic number value must not decode.
1637    #[test]
1638    fn test_cbor_decode_unknown_magic_errors() {
1639        let mut bytes: Vec<u8> = vec![0xa2, 0x00, 0x41, 0x01, 0x01, 0x1b];
1640        bytes.extend_from_slice(&0xdeadbeefdeadbeefu64.to_be_bytes());
1641        assert!(matches!(
1642            RainMetaDocumentV1Item::cbor_decode(&bytes),
1643            Err(Error::SerdeCborError(_))
1644        ));
1645    }
1646
1647    /// A cbor map header plus the given already encoded key/value pairs,
1648    /// written per the cbor spec rather than through the encoder, so a map can
1649    /// carry a key twice which no encoder emits.
1650    fn handwritten_entries(entries: &[(Vec<u8>, Vec<u8>)]) -> Vec<u8> {
1651        assert!(entries.len() < 24);
1652        let mut bytes = vec![0xa0 | entries.len() as u8];
1653        for (key, value) in entries {
1654            bytes.extend_from_slice(key);
1655            bytes.extend_from_slice(value);
1656        }
1657        bytes
1658    }
1659
1660    /// cbor unsigned integer of the magic number, as a map key or a value.
1661    fn handwritten_magic(magic: KnownMagic) -> Vec<u8> {
1662        let mut bytes = vec![0x1b];
1663        bytes.extend_from_slice(&magic.to_prefix_bytes());
1664        bytes
1665    }
1666
1667    /// cbor text string of a string shorter than 24 bytes.
1668    fn handwritten_text(text: &str) -> Vec<u8> {
1669        assert!(text.len() < 24);
1670        let mut bytes = vec![0x60 | text.len() as u8];
1671        bytes.extend_from_slice(text.as_bytes());
1672        bytes
1673    }
1674
1675    /// The repro from #191: a map repeating key 0 must not decode with the
1676    /// last payload winning.
1677    #[test]
1678    fn test_cbor_decode_duplicate_payload_key_errors() {
1679        let mut bytes: Vec<u8> = vec![0xa3, 0x00, 0x41, 0x01, 0x00, 0x41, 0x02, 0x01, 0x1b];
1680        bytes.extend_from_slice(&KnownMagic::DotrainV1.to_prefix_bytes());
1681        let error = RainMetaDocumentV1Item::cbor_decode(&bytes).unwrap_err();
1682        assert!(matches!(error, Error::SerdeCborError(_)));
1683        assert!(
1684            error.to_string().contains("duplicate field `payload`"),
1685            "{error}"
1686        );
1687    }
1688
1689    /// RFC 8949 ยง5.6: every recognised key is rejected when the map carries it
1690    /// twice, whether the repeated value differs from the first or matches it.
1691    #[test]
1692    fn test_cbor_decode_duplicate_any_key_errors() -> Result<(), Error> {
1693        let cases: [(Vec<u8>, Vec<u8>, Vec<u8>); 6] = [
1694            (vec![0x00], vec![0x41, 0x01], vec![0x41, 0x02]),
1695            (
1696                vec![0x01],
1697                handwritten_magic(KnownMagic::DotrainV1),
1698                handwritten_magic(KnownMagic::RainlangV1),
1699            ),
1700            (
1701                vec![0x02],
1702                handwritten_text("application/cbor"),
1703                handwritten_text("application/json"),
1704            ),
1705            (
1706                vec![0x03],
1707                handwritten_text("identity"),
1708                handwritten_text("deflate"),
1709            ),
1710            (vec![0x04], handwritten_text("en"), handwritten_text("none")),
1711            (
1712                handwritten_magic(KnownMagic::OaSchema),
1713                handwritten_text("hi"),
1714                handwritten_text("bye"),
1715            ),
1716        ];
1717        let base: Vec<(Vec<u8>, Vec<u8>)> = cases
1718            .iter()
1719            .map(|(key, value, _)| (key.clone(), value.clone()))
1720            .collect();
1721
1722        let decoded = RainMetaDocumentV1Item::cbor_decode(&handwritten_entries(&base))?;
1723        assert_eq!(decoded.len(), 1);
1724        assert_eq!(decoded[0].payload.as_ref(), &[0x01]);
1725        assert_eq!(decoded[0].magic, KnownMagic::DotrainV1);
1726        assert_eq!(decoded[0].content_type, ContentType::Cbor);
1727        assert_eq!(decoded[0].content_encoding, ContentEncoding::Identity);
1728        assert_eq!(decoded[0].content_language, ContentLanguage::En);
1729        assert_eq!(decoded[0].schema.as_deref(), Some("hi"));
1730
1731        for (key, value, other_value) in &cases {
1732            for repeated in [value, other_value] {
1733                let mut entries = base.clone();
1734                entries.push((key.clone(), repeated.clone()));
1735                let error = RainMetaDocumentV1Item::cbor_decode(&handwritten_entries(&entries))
1736                    .unwrap_err();
1737                assert!(
1738                    matches!(error, Error::SerdeCborError(_)),
1739                    "{key:?} {error:?}"
1740                );
1741                assert!(
1742                    error.to_string().contains("duplicate field"),
1743                    "{key:?} {error}"
1744                );
1745            }
1746        }
1747        Ok(())
1748    }
1749
1750    /// An index this version has no meaning for is skipped rather than
1751    /// rejected, but RFC 8949 ยง5.6 does not ask whether a key is understood:
1752    /// a map that carries an unknown index twice is invalid too, whether that
1753    /// index is a plain integer or a magic number other than OaSchema.
1754    #[test]
1755    fn test_cbor_decode_duplicate_unknown_key_errors() -> Result<(), Error> {
1756        let base: Vec<(Vec<u8>, Vec<u8>)> = vec![
1757            (vec![0x00], vec![0x41, 0x01]),
1758            (vec![0x01], handwritten_magic(KnownMagic::DotrainV1)),
1759        ];
1760        // key 5 as a plausible future index and the OaHashList magic as a
1761        // future magic keyed entry, each with the value cbor to repeat it with
1762        let unknown: [(Vec<u8>, Vec<u8>, Vec<u8>); 2] = [
1763            (vec![0x05], vec![0x07], vec![0x08]),
1764            (
1765                handwritten_magic(KnownMagic::OaHashList),
1766                handwritten_text("hi"),
1767                handwritten_text("bye"),
1768            ),
1769        ];
1770
1771        for (key, value, other_value) in &unknown {
1772            let mut entries = base.clone();
1773            entries.push((key.clone(), value.clone()));
1774            let decoded = RainMetaDocumentV1Item::cbor_decode(&handwritten_entries(&entries))?;
1775            assert_eq!(decoded, vec![plain_item(KnownMagic::DotrainV1, vec![0x01])]);
1776
1777            for repeated in [value, other_value] {
1778                let mut duplicated = entries.clone();
1779                duplicated.push((key.clone(), repeated.clone()));
1780                let error = RainMetaDocumentV1Item::cbor_decode(&handwritten_entries(&duplicated))
1781                    .unwrap_err();
1782                assert!(
1783                    matches!(error, Error::SerdeCborError(_)),
1784                    "{key:?} {error:?}"
1785                );
1786                assert!(
1787                    error.to_string().contains("duplicate map key"),
1788                    "{key:?} {error}"
1789                );
1790            }
1791        }
1792        Ok(())
1793    }
1794
1795    /// A handwritten item map carrying the rain meta document magic under key
1796    /// 1 decodes, so accepting the document magic as an item magic is the
1797    /// decoder's own behaviour and not an artefact of this crate's encoder.
1798    #[test]
1799    fn test_cbor_decode_handwritten_document_magic_item() {
1800        let bytes: Vec<u8> = vec![
1801            0xa2, // map(2)
1802            0x00, // key 0
1803            0x41, 0x01, // bytes(1) 0x01
1804            0x01, // key 1
1805            0x1b, 0xff, 0x0a, 0x89, 0xc6, 0x74, 0xee, 0x78, 0x74, // u64 RainMetaDocumentV1
1806        ];
1807        // The document magic in an item's magic position is structurally
1808        // invalid, so the meta carrying it does not decode.
1809        // rainlanguage/rain.metadata#204.
1810        assert!(RainMetaDocumentV1Item::cbor_decode(&bytes).is_err());
1811    }
1812
1813    /// The document magic as an item's own magic marks a payload that is
1814    /// itself a complete rain meta document, which
1815    /// `OrderBuilderStateV1::extract_from_meta` recurses into, so the codec
1816    /// must carry such an item in both directions and leave its payload byte
1817    /// for byte intact.
1818    #[test]
1819    fn test_document_magic_item_carries_a_nested_document() -> Result<(), Error> {
1820        let inner = plain_item(KnownMagic::DotrainV1, vec![0x01]);
1821        let inner_doc = RainMetaDocumentV1Item::cbor_encode_seq(
1822            &vec![inner.clone()],
1823            KnownMagic::RainMetaDocumentV1,
1824        )?;
1825        let outer = plain_item(KnownMagic::RainMetaDocumentV1, inner_doc.clone());
1826        let outer_doc = RainMetaDocumentV1Item::cbor_encode_seq(
1827            &vec![outer.clone()],
1828            KnownMagic::RainMetaDocumentV1,
1829        )?;
1830
1831        // Encoding can still write the document magic into an item's magic
1832        // position, and the payload really is a whole document. Decoding
1833        // refuses it anyway: a nested document is not a shape to descend into,
1834        // it is a corrupt meta, and the usable item inside does not rescue it.
1835        // rainlanguage/rain.metadata#204.
1836        assert!(RainMetaDocumentV1Item::cbor_decode(&outer_doc).is_err());
1837        assert_eq!(
1838            RainMetaDocumentV1Item::cbor_decode(&inner_doc)?,
1839            vec![inner]
1840        );
1841        Ok(())
1842    }
1843
1844    /// Nesting is not a leaf meta type: the unpack layer rejects the document
1845    /// magic so that no payload conversion is ever handed a whole document.
1846    #[test]
1847    fn test_document_magic_item_is_not_unpackable() {
1848        assert!(matches!(
1849            KnownMeta::try_from(KnownMagic::RainMetaDocumentV1),
1850            Err(Error::UnsupportedMeta)
1851        ));
1852        assert!(matches!(
1853            plain_item(KnownMagic::RainMetaDocumentV1, vec![0x01]).unpack_into::<Vec<u8>>(),
1854            Err(Error::UnsupportedMeta)
1855        ));
1856    }
1857
1858    /// unpack decodes the payload according to the content encoding.
1859    #[test]
1860    fn test_unpack_decodes_content_encoding() -> Result<(), Error> {
1861        let content = b"unpack me via deflate".to_vec();
1862        let packed = ContentEncoding::Deflate.encode(&content);
1863        assert_ne!(packed, content);
1864        let mut item = plain_item(KnownMagic::DotrainV1, packed);
1865        item.content_encoding = ContentEncoding::Deflate;
1866        assert_eq!(item.unpack()?, content);
1867
1868        let item = plain_item(KnownMagic::DotrainV1, content.clone());
1869        assert_eq!(item.unpack()?, content);
1870        Ok(())
1871    }
1872
1873    /// The 13 meta magics unpack; the document magic, the web data magic and
1874    /// the Oa magics are rejected with UnsupportedMeta.
1875    #[test]
1876    fn test_unpack_into_whitelist() {
1877        use strum::IntoEnumIterator;
1878        let supported = [
1879            KnownMagic::OpMetaV1,
1880            KnownMagic::DotrainV1,
1881            KnownMagic::RainlangV1,
1882            KnownMagic::SolidityAbiV2,
1883            KnownMagic::AuthoringMetaV1,
1884            KnownMagic::AuthoringMetaV2,
1885            KnownMagic::AddressList,
1886            KnownMagic::InterpreterCallerMetaV1,
1887            KnownMagic::ExpressionDeployerV2BytecodeV1,
1888            KnownMagic::DotrainSourceV1,
1889            KnownMagic::OrderBuilderStateV1,
1890            KnownMagic::RainlangSourceV1,
1891            KnownMagic::RaindexSignedContextOracleV1,
1892        ];
1893        for magic in supported {
1894            let unpacked: Vec<u8> = plain_item(magic, vec![0x61]).unpack_into().unwrap();
1895            assert_eq!(unpacked, vec![0x61], "{:?}", magic);
1896        }
1897        let unsupported = [
1898            KnownMagic::RainMetaDocumentV1,
1899            KnownMagic::WebDataV1,
1900            KnownMagic::OaSchema,
1901            KnownMagic::OaHashList,
1902            KnownMagic::OaStructure,
1903            KnownMagic::OaTokenImage,
1904            KnownMagic::OaTokenCredentialLinks,
1905        ];
1906        for magic in unsupported {
1907            let result: Result<Vec<u8>, Error> = plain_item(magic, vec![0x61]).unpack_into();
1908            assert!(matches!(result, Err(Error::UnsupportedMeta)), "{:?}", magic);
1909        }
1910        // together the two lists cover every variant
1911        assert_eq!(
1912            supported.len() + unsupported.len(),
1913            KnownMagic::iter().count()
1914        );
1915    }
1916
1917    /// Invalid utf8 payloads error when unpacking into String rather than
1918    /// being replaced lossily.
1919    #[test]
1920    fn test_try_into_string_invalid_utf8_errors() {
1921        let item = plain_item(KnownMagic::DotrainV1, vec![0xff, 0xfe]);
1922        let result: Result<String, Error> = item.try_into();
1923        assert!(matches!(result, Err(Error::FromUtf8Error(_))));
1924    }
1925
1926    /// Unpacking into Vec<u8> decodes the content encoding first.
1927    #[test]
1928    fn test_try_into_vec_decodes_encoding() -> Result<(), Error> {
1929        let content = b"raw bytecode bytes \x00\x01\x02".to_vec();
1930        let packed = ContentEncoding::Deflate.encode(&content);
1931        let mut item = plain_item(KnownMagic::ExpressionDeployerV2BytecodeV1, packed.clone());
1932        item.content_encoding = ContentEncoding::Deflate;
1933        let unpacked: Vec<u8> = item.try_into()?;
1934        assert_eq!(unpacked, content);
1935        assert_ne!(unpacked, packed);
1936        Ok(())
1937    }
1938
1939    /// Deflate encode produces a zlib stream (RFC1950 CMF byte 0x78) that is
1940    /// actually compressed and roundtrips through decode.
1941    #[test]
1942    fn test_content_encoding_deflate_roundtrip() -> Result<(), Error> {
1943        let content = b"hello rain deflate fixture hello rain deflate fixture".to_vec();
1944        let encoded = ContentEncoding::Deflate.encode(&content);
1945        assert_ne!(encoded, content);
1946        assert_eq!(encoded[0], 0x78);
1947        assert_eq!(ContentEncoding::Deflate.decode(&encoded)?, content);
1948        Ok(())
1949    }
1950
1951    /// None and Identity pass data through unchanged on encode and decode.
1952    #[test]
1953    fn test_content_encoding_passthrough() -> Result<(), Error> {
1954        let data = vec![0x00, 0xff, 0x10];
1955        for encoding in [ContentEncoding::None, ContentEncoding::Identity] {
1956            assert_eq!(encoding.encode(&data), data, "{:?}", encoding);
1957            assert_eq!(encoding.decode(&data)?, data, "{:?}", encoding);
1958        }
1959        Ok(())
1960    }
1961
1962    /// Decode accepts a zlib stream and falls back to a raw deflate stream.
1963    /// Fixtures generated out of band from "hello rain deflate fixture".
1964    #[test]
1965    fn test_content_encoding_decode_fixtures() -> Result<(), Error> {
1966        let content = b"hello rain deflate fixture".to_vec();
1967        let zlib: Vec<u8> = vec![
1968            120, 156, 203, 72, 205, 201, 201, 87, 40, 74, 204, 204, 83, 72, 73, 77, 203, 73, 44,
1969            73, 85, 72, 203, 172, 40, 41, 45, 74, 5, 0, 132, 64, 9, 251,
1970        ];
1971        let raw: Vec<u8> = vec![
1972            203, 72, 205, 201, 201, 87, 40, 74, 204, 204, 83, 72, 73, 77, 203, 73, 44, 73, 85, 72,
1973            203, 172, 40, 41, 45, 74, 5, 0,
1974        ];
1975        assert_eq!(ContentEncoding::Deflate.decode(&zlib)?, content);
1976        assert_eq!(ContentEncoding::Deflate.decode(&raw)?, content);
1977        Ok(())
1978    }
1979
1980    /// Data that is neither a zlib stream nor a raw deflate stream errors
1981    /// with InflateError instead of returning bytes.
1982    #[test]
1983    fn test_content_encoding_decode_garbage_errors() {
1984        let garbage = [0xffu8, 0xff, 0xff, 0xff];
1985        assert!(matches!(
1986            ContentEncoding::Deflate.decode(&garbage),
1987            Err(Error::InflateError(_))
1988        ));
1989    }
1990
1991    /// The CLI-facing strum names for the content headers are kebab-case.
1992    #[test]
1993    fn test_content_headers_strum_names() {
1994        use std::str::FromStr;
1995        assert_eq!(
1996            ContentEncoding::from_str("deflate").unwrap(),
1997            ContentEncoding::Deflate
1998        );
1999        assert_eq!(
2000            ContentEncoding::from_str("identity").unwrap(),
2001            ContentEncoding::Identity
2002        );
2003        assert_eq!(
2004            ContentEncoding::from_str("none").unwrap(),
2005            ContentEncoding::None
2006        );
2007        assert_eq!(ContentEncoding::Deflate.to_string(), "deflate");
2008        assert_eq!(
2009            ContentType::from_str("octet-stream").unwrap(),
2010            ContentType::OctetStream
2011        );
2012        assert_eq!(ContentType::from_str("json").unwrap(), ContentType::Json);
2013        assert_eq!(ContentType::Json.to_string(), "json");
2014        assert_eq!(
2015            ContentLanguage::from_str("en").unwrap(),
2016            ContentLanguage::En
2017        );
2018    }
2019
2020    /// Every documented meta magic maps to its KnownMeta while the document
2021    /// magic and the Oa magics are unsupported.
2022    #[test]
2023    fn test_known_meta_try_from_magic() {
2024        let cases: [(KnownMagic, KnownMeta); 13] = [
2025            (KnownMagic::OpMetaV1, KnownMeta::OpV1),
2026            (KnownMagic::DotrainV1, KnownMeta::DotrainV1),
2027            (KnownMagic::RainlangV1, KnownMeta::RainlangV1),
2028            (KnownMagic::SolidityAbiV2, KnownMeta::SolidityAbiV2),
2029            (KnownMagic::AuthoringMetaV1, KnownMeta::AuthoringMetaV1),
2030            (KnownMagic::AuthoringMetaV2, KnownMeta::AuthoringMetaV2),
2031            (KnownMagic::AddressList, KnownMeta::AddressList),
2032            (
2033                KnownMagic::InterpreterCallerMetaV1,
2034                KnownMeta::InterpreterCallerMetaV1,
2035            ),
2036            (
2037                KnownMagic::ExpressionDeployerV2BytecodeV1,
2038                KnownMeta::ExpressionDeployerV2BytecodeV1,
2039            ),
2040            (KnownMagic::RainlangSourceV1, KnownMeta::RainlangSourceV1),
2041            (KnownMagic::DotrainSourceV1, KnownMeta::DotrainSourceV1),
2042            (
2043                KnownMagic::OrderBuilderStateV1,
2044                KnownMeta::OrderBuilderStateV1,
2045            ),
2046            (
2047                KnownMagic::RaindexSignedContextOracleV1,
2048                KnownMeta::RaindexSignedContextOracleV1,
2049            ),
2050        ];
2051        for (magic, meta) in cases {
2052            assert_eq!(KnownMeta::try_from(magic).unwrap(), meta, "{:?}", magic);
2053        }
2054        for magic in [
2055            KnownMagic::RainMetaDocumentV1,
2056            KnownMagic::WebDataV1,
2057            KnownMagic::OaSchema,
2058            KnownMagic::OaHashList,
2059            KnownMagic::OaStructure,
2060            KnownMagic::OaTokenImage,
2061            KnownMagic::OaTokenCredentialLinks,
2062        ] {
2063            assert!(
2064                matches!(KnownMeta::try_from(magic), Err(Error::UnsupportedMeta)),
2065                "{:?}",
2066                magic
2067            );
2068        }
2069    }
2070
2071    /// KnownMeta parses from and displays as the kebab-case names used by the
2072    /// CLI (validate --meta, build, schema show).
2073    #[test]
2074    fn test_known_meta_strum_parse_display() {
2075        use std::str::FromStr;
2076        assert_eq!(
2077            KnownMeta::from_str("solidity-abi-v2").unwrap(),
2078            KnownMeta::SolidityAbiV2
2079        );
2080        assert_eq!(
2081            KnownMeta::from_str("interpreter-caller-meta-v1").unwrap(),
2082            KnownMeta::InterpreterCallerMetaV1
2083        );
2084        assert_eq!(KnownMeta::SolidityAbiV2.to_string(), "solidity-abi-v2");
2085    }
2086
2087    /// search() lowercases the hash before building the query variables.
2088    #[tokio::test]
2089    async fn test_search_lowercases_hash() {
2090        use httpmock::prelude::*;
2091        let (_, doc) = sample_authoring_doc();
2092        let hash_upper = format!("0x{}", "AB".repeat(32));
2093        let server = MockServer::start();
2094        let mock = server.mock(|when, then| {
2095            when.method(POST)
2096                .body_contains(hash_upper.to_ascii_lowercase());
2097            then.status(200).json_body(json!({
2098                "data": {"meta": {"__typename": "RainMetaV1", "rawBytes": hex::encode_prefixed(&doc)}}
2099            }));
2100        });
2101        let response = search(&hash_upper, &vec![server.url("/sg")]).await.unwrap();
2102        assert_eq!(response.bytes, doc);
2103        mock.assert();
2104    }
2105
2106    /// search() queries every subgraph and the first success wins even when
2107    /// an earlier subgraph fails.
2108    #[tokio::test]
2109    async fn test_search_first_success_wins() {
2110        use httpmock::prelude::*;
2111        let (_, doc) = sample_authoring_doc();
2112        let bad = MockServer::start();
2113        let _bad_mock = bad.mock(|when, then| {
2114            when.method(POST);
2115            then.status(500).body("subgraph down");
2116        });
2117        let good = MockServer::start();
2118        let _good_mock = good.mock(|when, then| {
2119            when.method(POST);
2120            then.status(200).json_body(json!({
2121                "data": {"meta": {"__typename": "RainMetaV1", "rawBytes": hex::encode_prefixed(&doc)}}
2122            }));
2123        });
2124        let response = search(
2125            &format!("0x{}", "11".repeat(32)),
2126            &vec![bad.url("/sg"), good.url("/sg")],
2127        )
2128        .await
2129        .unwrap();
2130        assert_eq!(response.bytes, doc);
2131    }
2132
2133    /// An empty subgraph list has nothing to fan out to, so the search
2134    /// reports a miss rather than reaching futures::select_ok, which panics on
2135    /// an empty iterator.
2136    #[tokio::test]
2137    async fn test_search_empty_subgraphs_is_a_miss() {
2138        let hash = format!("0x{}", "33".repeat(32));
2139        assert!(matches!(
2140            search(&hash, &vec![]).await,
2141            Err(Error::NoRecordFound)
2142        ));
2143    }
2144
2145    /// The erc165 gate short circuits: neither answer reaches the
2146    /// IDescribedByMetaV1 supportsInterface call, so the queued "true" is
2147    /// never consumed and cannot be mistaken for the contract's own answer.
2148    ///
2149    /// The two answers are not the same fact. A contract that says no is
2150    /// `Ok(false)`; a probe that could not finish is an error, because a
2151    /// transport failure is not a contract declining an interface. rain-erc
2152    /// makes that distinction itself - "callers can treat that as answer
2153    /// unknown rather than silently reading no support" - and an
2154    /// `unwrap_or(false)` here threw it away.
2155    #[tokio::test]
2156    async fn test_implements_erc165_gate_short_circuits() {
2157        let address = Address::random();
2158
2159        // erc165 check1 answers false
2160        let asserter = Asserter::new();
2161        let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
2162        asserter
2163            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000000");
2164        asserter
2165            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000001");
2166        assert!(!implements_i_described_by_meta_v1(&provider, address)
2167            .await
2168            .unwrap());
2169
2170        // erc165 probe errors. Getting an error back is itself the proof of
2171        // the short circuit: had the queued "true" been consumed by the
2172        // interface probe, this would be Ok(true).
2173        let asserter = Asserter::new();
2174        let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
2175        asserter.push_failure(ErrorPayload {
2176            code: -32000,
2177            message: "connection reset".into(),
2178            data: None,
2179        });
2180        asserter
2181            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000001");
2182        assert!(implements_i_described_by_meta_v1(&provider, address)
2183            .await
2184            .is_err());
2185    }
2186
2187    /// An empty eth_call response is a contract that did not answer, which
2188    /// ERC-165 reads as "does not implement".
2189    #[tokio::test]
2190    async fn test_implements_empty_response_is_false() {
2191        let address = Address::random();
2192        let asserter = Asserter::new();
2193        let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
2194        asserter
2195            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000001");
2196        asserter
2197            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000000");
2198        asserter.push_success(&"0x");
2199        assert!(!implements_i_described_by_meta_v1(&provider, address)
2200            .await
2201            .unwrap());
2202    }
2203
2204    /// A non-revert failure of the IDescribedByMetaV1 supportsInterface call is
2205    /// "answer unknown": it propagates as Err rather than reading as "does not
2206    /// implement".
2207    #[tokio::test]
2208    async fn test_implements_described_by_call_error_is_unknown() {
2209        let address = Address::random();
2210        let asserter = Asserter::new();
2211        let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
2212        asserter
2213            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000001");
2214        asserter
2215            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000000");
2216        asserter.push_failure(ErrorPayload {
2217            code: -32005,
2218            message: "rate limit exceeded".into(),
2219            data: None,
2220        });
2221        let error = implements_i_described_by_meta_v1(&provider, address)
2222            .await
2223            .unwrap_err();
2224        assert!(error.to_string().contains("rate limit exceeded"));
2225    }
2226
2227    /// A non-empty IDescribedByMetaV1 supportsInterface response that does not
2228    /// decode as bool is a decode failure, so "answer unknown" rather than
2229    /// "does not implement".
2230    #[tokio::test]
2231    async fn test_implements_undecodable_response_is_unknown() {
2232        let address = Address::random();
2233        let asserter = Asserter::new();
2234        let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
2235        asserter
2236            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000001");
2237        asserter
2238            .push_success(&"0x0000000000000000000000000000000000000000000000000000000000000000");
2239        asserter.push_success(&"0xdeadbeef");
2240        implements_i_described_by_meta_v1(&provider, address)
2241            .await
2242            .unwrap_err();
2243    }
2244
2245    /// No constructor injects a subgraph the caller did not ask for, and a
2246    /// store with none resolves every network lookup to None rather than
2247    /// reaching the select_ok panic.
2248    #[tokio::test]
2249    async fn test_store_constructors_inject_no_subgraphs() {
2250        assert!(Store::new().subgraphs().is_empty());
2251        assert!(Store::default().subgraphs().is_empty());
2252        assert!(
2253            Store::create(&vec![], &MetaCache::default(), &HashMap::new())
2254                .subgraphs()
2255                .is_empty()
2256        );
2257
2258        let hash = [0u8; 32];
2259        let mut store = Store::default();
2260        assert!(store.update(&hash).await.is_err());
2261    }
2262
2263    /// create() takes only the given subgraphs, and keeps a dotrain uri only
2264    /// when its hash is present in the cache.
2265    ///
2266    /// This used to assert create() dropped a cache entry whose bytes did not
2267    /// hash to its key. That entry is no longer constructible: create() takes
2268    /// a [MetaCache], which has no way to hold one, so there is nothing left
2269    /// for create() to validate.
2270    #[test]
2271    fn test_store_create_validates_entries() {
2272        let (_, doc) = sample_authoring_doc();
2273        let good_hash = keccak256(&doc).0.to_vec();
2274        let mut cache = MetaCache::default();
2275        cache.insert_verified(&good_hash, doc.clone()).unwrap();
2276        let mut dotrain_cache = HashMap::new();
2277        dotrain_cache.insert("a.rain".to_string(), good_hash.clone());
2278        dotrain_cache.insert("missing.rain".to_string(), vec![0x44u8; 32]);
2279
2280        let store = Store::create(
2281            &vec!["https://example.com/custom-sg".to_string()],
2282            &cache,
2283            &dotrain_cache,
2284        );
2285
2286        assert_eq!(
2287            store.subgraphs(),
2288            &vec!["https://example.com/custom-sg".to_string()]
2289        );
2290        assert_eq!(store.get_meta(&good_hash), Some(&doc));
2291        assert_eq!(store.get_dotrain_hash("a.rain"), Some(&good_hash));
2292        assert_eq!(store.get_dotrain_hash("missing.rain"), None);
2293    }
2294
2295    /// add_subgraphs skips urls already present.
2296    #[test]
2297    fn test_store_add_subgraphs_dedupe() {
2298        let mut store = Store::new();
2299        store.add_subgraphs(&vec!["sg-a".to_string()]);
2300        store.add_subgraphs(&vec!["sg-a".to_string(), "sg-b".to_string()]);
2301        assert_eq!(
2302            store.subgraphs(),
2303            &vec!["sg-a".to_string(), "sg-b".to_string()]
2304        );
2305    }
2306
2307    /// set_dotrain on a fresh uri returns (new_hash, empty), keyed by the
2308    /// keccak of the cbor encoded DotrainV1 meta item, and every dotrain
2309    /// getter resolves it.
2310    #[test]
2311    fn test_store_dotrain_getters_and_set_fresh() {
2312        let mut store = Store::new();
2313        let text = "some dotrain content";
2314        let (hash, old) = store.set_dotrain(text, "file.rain", false).unwrap();
2315        assert!(old.is_empty());
2316        let expected_item = RainMetaDocumentV1Item {
2317            payload: serde_bytes::ByteBuf::from(text.as_bytes()),
2318            magic: KnownMagic::DotrainV1,
2319            content_type: ContentType::OctetStream,
2320            content_encoding: ContentEncoding::None,
2321            content_language: ContentLanguage::None,
2322            schema: None,
2323        };
2324        let expected_bytes = expected_item.cbor_encode().unwrap();
2325        assert_eq!(hash, keccak256(&expected_bytes).0.to_vec());
2326        assert_eq!(store.get_dotrain_hash("file.rain"), Some(&hash));
2327        assert_eq!(store.get_dotrain_uri(&hash), Some(&"file.rain".to_string()));
2328        assert_eq!(store.get_dotrain_meta("file.rain"), Some(&expected_bytes));
2329        assert_eq!(store.get_dotrain_hash("other.rain"), None);
2330        assert_eq!(store.get_dotrain_uri(&[0u8; 32]), None);
2331        assert_eq!(store.get_dotrain_meta("other.rain"), None);
2332    }
2333
2334    /// set_dotrain branches: same content keeps the meta and reports no old
2335    /// hash; different content remaps the uri and drops or keeps the old
2336    /// meta per keep_old.
2337    #[test]
2338    fn test_store_set_dotrain_branches() {
2339        let mut store = Store::new();
2340        let (hash_one, _) = store.set_dotrain("text one", "a.rain", false).unwrap();
2341
2342        // same content again: same hash, no old hash, meta retained
2343        let (hash_same, old_same) = store.set_dotrain("text one", "a.rain", false).unwrap();
2344        assert_eq!(hash_same, hash_one);
2345        assert!(old_same.is_empty());
2346        assert!(store.get_meta(&hash_one).is_some());
2347
2348        // different content, keep_old = false: remap and drop the old meta
2349        let (hash_two, old_two) = store.set_dotrain("text two", "a.rain", false).unwrap();
2350        assert_ne!(hash_two, hash_one);
2351        assert_eq!(old_two, hash_one);
2352        assert_eq!(store.get_dotrain_hash("a.rain"), Some(&hash_two));
2353        assert!(store.get_meta(&hash_one).is_none());
2354        assert!(store.get_meta(&hash_two).is_some());
2355
2356        // different content, keep_old = true: old meta kept
2357        let (hash_three, old_three) = store.set_dotrain("text three", "a.rain", true).unwrap();
2358        assert_eq!(old_three, hash_two);
2359        assert_eq!(store.get_dotrain_hash("a.rain"), Some(&hash_three));
2360        assert!(store.get_meta(&hash_two).is_some());
2361        assert!(store.get_meta(&hash_three).is_some());
2362    }
2363
2364    /// delete_dotrain removes the uri mapping and honors keep_meta for the
2365    /// cached meta bytes.
2366    #[test]
2367    fn test_store_delete_dotrain_keep_meta() {
2368        let mut store = Store::new();
2369        let (hash, _) = store.set_dotrain("dotrain body", "d.rain", false).unwrap();
2370        store.delete_dotrain("d.rain", false);
2371        assert_eq!(store.get_dotrain_hash("d.rain"), None);
2372        assert!(store.get_meta(&hash).is_none());
2373
2374        let (hash_again, _) = store.set_dotrain("dotrain body", "d.rain", false).unwrap();
2375        store.delete_dotrain("d.rain", true);
2376        assert_eq!(store.get_dotrain_hash("d.rain"), None);
2377        assert!(store.get_meta(&hash_again).is_some());
2378    }
2379
2380    /// merge keeps this store's entry in every map on a key collision, takes
2381    /// the keys it does not already hold, and unions the subgraphs.
2382    #[test]
2383    fn test_store_merge_semantics() {
2384        let mut ours = Store::new();
2385        let mut theirs = Store::new();
2386
2387        // meta cache: two different metas cannot share a key - the key IS
2388        // their digest - so merge takes the other store's entry rather than
2389        // choosing between them
2390        let mine = b"mine".to_vec();
2391        let yours = b"yours".to_vec();
2392        let mine_hash = keccak256(&mine).0.to_vec();
2393        let yours_hash = keccak256(&yours).0.to_vec();
2394        ours.update_with(&mine_hash, &mine).unwrap();
2395        theirs.update_with(&yours_hash, &yours).unwrap();
2396
2397        // same dotrain uri, different content
2398        let (hash_ours, _) = ours.set_dotrain("content a", "x.rain", false).unwrap();
2399        let (_hash_theirs, _) = theirs.set_dotrain("content b", "x.rain", false).unwrap();
2400
2401        theirs.add_subgraphs(&vec!["sg-their".to_string()]);
2402
2403        ours.merge(&theirs);
2404
2405        assert_eq!(ours.get_meta(&mine_hash), Some(&mine));
2406        assert_eq!(ours.get_meta(&yours_hash), Some(&yours));
2407        // dotrain: existing uri mapping wins
2408        assert_eq!(ours.get_dotrain_hash("x.rain"), Some(&hash_ours));
2409        // subgraphs merged
2410        assert!(ours.subgraphs().contains(&"sg-their".to_string()));
2411    }
2412
2413    /// update() stores the fetched bytes under the requested hash and each
2414    /// inner meta item under the keccak of its own encoding; update_check
2415    /// serves a cached hash without any network access.
2416    #[tokio::test]
2417    async fn test_store_update_and_update_check() {
2418        use httpmock::prelude::*;
2419        let authoring_meta: AuthoringMeta = serde_json::from_str(
2420            r#"[{"word":"stack","description":"Copies an existing value from the stack.","operandParserOffset":16}]"#,
2421        )
2422        .unwrap();
2423        let item_one = RainMetaDocumentV1Item {
2424            payload: serde_bytes::ByteBuf::from(authoring_meta.abi_encode_validate().unwrap()),
2425            magic: KnownMagic::AuthoringMetaV1,
2426            content_type: ContentType::Cbor,
2427            content_encoding: ContentEncoding::None,
2428            content_language: ContentLanguage::None,
2429            schema: None,
2430        };
2431        let item_two = sample_dotrain_item();
2432        let doc = RainMetaDocumentV1Item::cbor_encode_seq(
2433            &vec![item_one.clone(), item_two.clone()],
2434            KnownMagic::RainMetaDocumentV1,
2435        )
2436        .unwrap();
2437        let requested = keccak256(&doc).0.to_vec();
2438        let server = MockServer::start();
2439        let _mock = server.mock(|when, then| {
2440            when.method(POST);
2441            then.status(200).json_body(json!({
2442                "data": {"meta": {"__typename": "RainMetaV1", "rawBytes": hex::encode_prefixed(&doc)}}
2443            }));
2444        });
2445        let mut store = Store::new();
2446        store.add_subgraphs(&vec![server.url("/sg")]);
2447        let fetched = store.update(&requested).await.cloned().unwrap();
2448        assert_eq!(fetched, doc);
2449        assert_eq!(store.get_meta(&requested), Some(&doc));
2450        let inner_one = item_one.cbor_encode().unwrap();
2451        let inner_two = item_two.cbor_encode().unwrap();
2452        assert_eq!(
2453            store.get_meta(keccak256(&inner_one).0.as_ref()),
2454            Some(&inner_one)
2455        );
2456        assert_eq!(
2457            store.get_meta(keccak256(&inner_two).0.as_ref()),
2458            Some(&inner_two)
2459        );
2460
2461        // update_check: cached hash short-circuits, no subgraphs needed
2462        let mut cached_store = Store::new();
2463        let bytes = b"standalone meta bytes".to_vec();
2464        let hash = keccak256(&bytes).0.to_vec();
2465        assert!(cached_store.update_with(&hash, &bytes).is_ok());
2466        assert_eq!(cached_store.update_check(&hash).await.unwrap(), &bytes);
2467    }
2468
2469    /// update() applies the same keccak gate as update_with to the subgraph
2470    /// response, so bytes that do not hash to the requested hash poison
2471    /// neither the requested key nor the inner item keys.
2472    #[tokio::test]
2473    async fn test_store_update_rejects_hash_mismatch() {
2474        use httpmock::prelude::*;
2475        let (_, doc) = sample_authoring_doc();
2476        let requested = keccak256(b"the real content").0.to_vec();
2477        let server = MockServer::start();
2478        let _mock = server.mock(|when, then| {
2479            when.method(POST);
2480            then.status(200).json_body(json!({
2481                "data": {"meta": {"__typename": "RainMetaV1", "rawBytes": hex::encode_prefixed(&doc)}}
2482            }));
2483        });
2484        let mut store = Store::new();
2485        store.add_subgraphs(&vec![server.url("/sg")]);
2486        assert!(store.update(&requested).await.is_err());
2487        assert!(store.get_meta(&requested).is_none());
2488        assert!(store.cache().is_empty());
2489        // the miss is not cached either, so update_check retries and misses again
2490        assert!(store.update_check(&requested).await.is_err());
2491    }
2492
2493    /// Store::new() starts with no subgraphs, so every uncached lookup that
2494    /// reaches the network on it resolves to None instead of panicking.
2495    #[tokio::test]
2496    async fn test_store_no_subgraphs_lookups_return_none() {
2497        let hash = [0u8; 32];
2498        let mut store = Store::new();
2499        assert!(store.update(&hash).await.is_err());
2500        assert!(store.update_check(&hash).await.is_err());
2501        assert!(store.cache().is_empty());
2502    }
2503
2504    /// update_with enforces keccak(bytes) == hash, leaves an existing entry
2505    /// untouched, and unpacks inner items only for RainMetaDocumentV1
2506    /// prefixed bytes.
2507    #[test]
2508    fn test_store_update_with_validation_and_content() {
2509        // hash mismatch rejected
2510        let mut store = Store::new();
2511        let bytes = b"payload bytes".to_vec();
2512        let wrong_hash = vec![0x99u8; 32];
2513        // A mismatch is CorruptRecord, not NoRecordFound: the responder
2514        // answered about one hash with bytes that are another, which is not
2515        // the same fact as the hash being absent. #234 and #213 settled that
2516        // distinction for the query layer.
2517        match store.update_with(&wrong_hash, &bytes).unwrap_err() {
2518            Error::CorruptRecord(message) => {
2519                assert!(
2520                    message.contains(&hex::encode_prefixed(&wrong_hash)),
2521                    "{}",
2522                    message
2523                )
2524            }
2525            other => panic!("expected CorruptRecord, got {:?}", other),
2526        }
2527        assert!(store.get_meta(&wrong_hash).is_none());
2528        // valid pair stored
2529        let hash = keccak256(&bytes).0.to_vec();
2530        assert_eq!(store.update_with(&hash, &bytes).unwrap(), &bytes);
2531
2532        // an already cached key returns its entry rather than inserting again.
2533        // Note what is no longer expressible here: the old version of this
2534        // block seeded one hash with unrelated bytes and asserted a later write
2535        // did not overwrite them. Different bytes cannot share a key when the
2536        // key is their digest, so "overwritten with something else" is not a
2537        // state [MetaCache] can be in.
2538        let mut seeded = Store::new();
2539        let planted = b"planted value".to_vec();
2540        let planted_hash = keccak256(&planted).0.to_vec();
2541        seeded.update_with(&planted_hash, &planted).unwrap();
2542        assert_eq!(seeded.cache().len(), 1);
2543        assert_eq!(
2544            seeded.update_with(&planted_hash, &planted).unwrap(),
2545            &planted
2546        );
2547        assert_eq!(seeded.cache().len(), 1);
2548
2549        // prefixed document: inner item stored under keccak of its encoding
2550        let (_, doc) = sample_authoring_doc();
2551        let doc_hash = keccak256(&doc).0.to_vec();
2552        let mut doc_store = Store::new();
2553        assert!(doc_store.update_with(&doc_hash, &doc).is_ok());
2554        let inner = doc[8..].to_vec();
2555        assert_eq!(store_inner_lookup(&doc_store, &inner), Some(inner.clone()));
2556
2557        // bare cbor sequence without the document prefix: no inner extraction
2558        let item_a = sample_dotrain_item().cbor_encode().unwrap();
2559        let (_, doc_b) = sample_authoring_doc();
2560        let item_b = doc_b[8..].to_vec();
2561        let seq = [item_a.clone(), item_b].concat();
2562        let seq_hash = keccak256(&seq).0.to_vec();
2563        let mut seq_store = Store::new();
2564        assert!(seq_store.update_with(&seq_hash, &seq).is_ok());
2565        assert_eq!(store_inner_lookup(&seq_store, &item_a), None);
2566    }
2567
2568    fn store_inner_lookup(store: &Store, inner_encoded: &[u8]) -> Option<Vec<u8>> {
2569        store.get_meta(keccak256(inner_encoded).0.as_ref()).cloned()
2570    }
2571
2572    /// bytes32_to_str propagates invalid utf8 as an error instead of
2573    /// swallowing it.
2574    #[test]
2575    fn test_bytes32_to_str_invalid_utf8() {
2576        let mut bytes = [0u8; 32];
2577        bytes[0] = 0xf0;
2578        bytes[1] = 0x28;
2579        bytes[2] = 0x8c;
2580        bytes[3] = 0x28;
2581        assert!(matches!(bytes32_to_str(&bytes), Err(Error::Utf8Error(_))));
2582        let no_nul = [0xffu8; 32];
2583        assert!(matches!(bytes32_to_str(&no_nul), Err(Error::Utf8Error(_))));
2584    }
2585}