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