Skip to main content

rain_metadata/meta/
normalize.rs

1use super::{
2    KnownMeta,
3    super::error::Error,
4    types::{authoring::v1::AuthoringMeta, authoring::v2::AuthoringMetaV2},
5};
6
7impl KnownMeta {
8    /// normalizes meta types and also performs validation on those that need validation
9    pub fn normalize(&self, data: &[u8]) -> Result<Vec<u8>, Error> {
10        Ok(match self {
11            KnownMeta::AuthoringMetaV1 => {
12                // for AuthoringMeta since it can be a json or abi encoded bytes, we try to abi
13                // decode first and then json deserialize if that fails, if either succeeds
14                // then the result of that will be abi encoded with validation
15                match AuthoringMeta::abi_decode(data) {
16                    Ok(am) => am.abi_encode_validate()?,
17                    _ => AuthoringMeta::abi_encode_validate(
18                        &serde_json::from_str::<AuthoringMeta>(std::str::from_utf8(data)?)?,
19                    )?,
20                }
21            }
22            KnownMeta::AuthoringMetaV2 => {
23                // v2 is abi encoded onchain and this crate has no encoder for
24                // it, so validation is a decode gate over the input as is,
25                // carrying the rain word grammar
26                AuthoringMetaV2::abi_decode_validate(data)
27                    .map_err(|e| Error::InvalidInput(e.to_string()))?;
28                data.to_vec()
29            }
30            // rest of meta types are only pure bytes (ut8 strings or binary)
31            // so no normalization/validation can happen for them at this level
32            _ => data.to_vec(),
33        })
34    }
35}
36
37#[cfg(all(test, not(target_family = "wasm")))]
38mod tests {
39    use alloy::sol_types::SolValue;
40    use crate::error::Error;
41    use crate::meta::types::authoring::v1::{AuthoringMeta, AuthoringMetaItem};
42    use crate::meta::types::authoring::v2::AuthoringMetaV2Sol;
43    use crate::meta::KnownMeta;
44
45    fn authoring_meta_v2_abi(word: [u8; 32], description: &str) -> Vec<u8> {
46        vec![AuthoringMetaV2Sol {
47            word: word.into(),
48            description: description.to_string(),
49        }]
50        .abi_encode()
51    }
52
53    /// OpV1 is a known meta this crate does not model, so normalize passes
54    /// its bytes through rather than validating them. It reaches the same
55    /// fallthrough as every other unmodelled type.
56    #[test]
57    fn test_normalize_op_v1_is_a_passthrough() {
58        let bytes = b"{  \"name\" : \"add\" }";
59        assert_eq!(KnownMeta::OpV1.normalize(bytes).unwrap(), bytes.to_vec());
60    }
61
62    /// SolidityAbiV2 is a known meta this crate no longer models (#317), so
63    /// normalize passes its bytes through: input the deleted model rewrote or
64    /// rejected comes back unchanged.
65    #[test]
66    fn test_normalize_solidity_abi_v2_is_a_passthrough() {
67        let inputs: [&[u8]; 3] = [b"[ ]", b"not json at all", &[0xff, 0xfe]];
68        for bytes in inputs {
69            assert_eq!(
70                KnownMeta::SolidityAbiV2.normalize(bytes).unwrap(),
71                bytes.to_vec()
72            );
73        }
74    }
75
76    /// InterpreterCallerMetaV1 is a known meta this crate no longer models
77    /// (#317), so normalize passes its bytes through: a payload the deleted
78    /// model rejected for having no methods comes back unchanged.
79    #[test]
80    fn test_normalize_interpreter_caller_meta_v1_is_a_passthrough() {
81        let bytes = br#"{"name":"Test Caller","abiName":"TestCaller","methods":[]}"#;
82        assert_eq!(
83            KnownMeta::InterpreterCallerMetaV1.normalize(bytes).unwrap(),
84            bytes.to_vec()
85        );
86    }
87
88    /// Meta types with no json schema at this level pass through untouched.
89    #[test]
90    fn test_normalize_passthrough_for_binary_metas() {
91        let data = vec![0x00, 0x01, 0xff];
92        assert_eq!(
93            KnownMeta::ExpressionDeployerV2BytecodeV1
94                .normalize(&data)
95                .unwrap(),
96            data
97        );
98    }
99
100    fn sample_authoring_meta() -> AuthoringMeta {
101        serde_json::from_str(
102            r#"[{"word":"stack","description":"Copies an existing value from the stack.","operandParserOffset":16}]"#,
103        )
104        .unwrap()
105    }
106
107    /// Valid abi encoded input takes the abi-decode path and is re-encoded
108    /// with validation, byte identically.
109    #[test]
110    fn test_normalize_authoring_meta_v1_abi_path() {
111        let authoring_meta = sample_authoring_meta();
112        let abi = authoring_meta.abi_encode_validate().unwrap();
113        let normalized = KnownMeta::AuthoringMetaV1.normalize(&abi).unwrap();
114        assert_eq!(normalized, abi);
115    }
116
117    /// Abi-decodable input that fails validation must be rejected: the abi
118    /// path re-encodes via abi_encode_validate, not a passthrough.
119    #[test]
120    fn test_normalize_authoring_meta_v1_abi_invalid_rejected() {
121        let invalid = AuthoringMeta(vec![AuthoringMetaItem {
122            word: "NOTKEBAB".to_string(),
123            operand_parser_offset: 0,
124            description: "some description".to_string(),
125        }]);
126        // encode WITHOUT validation so the bytes are decodable but invalid
127        let abi = invalid.abi_encode().unwrap();
128        let result = KnownMeta::AuthoringMetaV1.normalize(&abi);
129        assert!(matches!(result, Err(Error::ValidationErrors(_))));
130    }
131
132    /// Json input falls back to serde parse and is abi encoded with
133    /// validation: output is the abi encoding, not the raw json bytes.
134    #[test]
135    fn test_normalize_authoring_meta_v1_json_fallback() {
136        let json = r#"[{"word":"stack","description":"Copies an existing value from the stack.","operandParserOffset":16}]"#;
137        let expected = sample_authoring_meta().abi_encode_validate().unwrap();
138        let normalized = KnownMeta::AuthoringMetaV1
139            .normalize(json.as_bytes())
140            .unwrap();
141        assert_eq!(normalized, expected);
142        assert_ne!(normalized, json.as_bytes().to_vec());
143    }
144
145    /// AuthoringMetaV2 has a concrete abi encoding, so a decodable payload is
146    /// valid and passes through byte identically.
147    #[test]
148    fn test_normalize_authoring_meta_v2_abi_passthrough() {
149        let mut word = [0u8; 32];
150        word[..5].copy_from_slice(b"stack");
151        let abi = authoring_meta_v2_abi(word, "Copies an existing value from the stack.");
152        assert_eq!(KnownMeta::AuthoringMetaV2.normalize(&abi).unwrap(), abi);
153    }
154
155    /// AuthoringMetaV2 is not pure bytes: bytes that cannot abi decode as
156    /// AuthoringMetaV2Sol[] are rejected rather than passed through.
157    #[test]
158    fn test_normalize_authoring_meta_v2_rejects_arbitrary_bytes() {
159        assert!(matches!(
160            KnownMeta::AuthoringMetaV2.normalize(&[0xde, 0xad]),
161            Err(Error::InvalidInput(_))
162        ));
163        assert!(matches!(
164            KnownMeta::AuthoringMetaV2.normalize(b"[]"),
165            Err(Error::InvalidInput(_))
166        ));
167    }
168
169    /// The decode gate carries the word utf8 requirement: abi shaped bytes
170    /// whose word is not utf8 before its first NUL are rejected.
171    #[test]
172    fn test_normalize_authoring_meta_v2_rejects_non_utf8_word() {
173        let mut word = [0u8; 32];
174        // 0xc3 followed by 0x28 is an invalid utf8 sequence, before any NUL
175        word[0] = 0xc3;
176        word[1] = 0x28;
177        let abi = authoring_meta_v2_abi(word, "bad word bytes");
178        assert!(matches!(
179            KnownMeta::AuthoringMetaV2.normalize(&abi),
180            Err(Error::InvalidInput(_))
181        ));
182    }
183
184    /// The decode gate carries the rain word grammar: a word the grammar
185    /// rejects does not reach the board through this crate.
186    #[test]
187    fn test_normalize_authoring_meta_v2_rejects_a_word_outside_the_grammar() {
188        let mut word = [0u8; 32];
189        word[..3].copy_from_slice(b"BAD");
190        let abi = authoring_meta_v2_abi(word, "fine");
191        assert!(matches!(
192            KnownMeta::AuthoringMetaV2.normalize(&abi),
193            Err(Error::InvalidInput(_))
194        ));
195    }
196
197    /// Meta types without a structured normal form pass raw bytes through
198    /// unchanged.
199    #[test]
200    fn test_normalize_default_arm_passthrough() {
201        let data = b"some dotrain text".to_vec();
202        assert_eq!(KnownMeta::DotrainV1.normalize(&data).unwrap(), data);
203        let binary = vec![0xffu8, 0x00, 0x01];
204        assert_eq!(KnownMeta::RainlangV1.normalize(&binary).unwrap(), binary);
205    }
206}