Skip to main content

rain_metadata/meta/
normalize.rs

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