Skip to main content

rain_metadata/meta/
normalize.rs

1use super::{
2    KnownMeta,
3    super::error::Error,
4    types::{
5        op::v1::OpMeta, authoring::v1::AuthoringMeta, solidity_abi::v2::SolidityAbiMeta,
6        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::OpV1 => normalize_json::<OpMeta>(data)?,
24            KnownMeta::SolidityAbiV2 => normalize_json::<SolidityAbiMeta>(data)?,
25            KnownMeta::InterpreterCallerMetaV1 => normalize_json::<InterpreterCallerMeta>(data)?,
26            KnownMeta::AuthoringMetaV1 => {
27                // for AuthoringMeta since it can be a json or abi encoded bytes, we try to abi
28                // decode first and then json deserialize if that fails, if either succeeds
29                // then the result of that will be abi encoded with validation
30                match AuthoringMeta::abi_decode(data) {
31                    Ok(am) => am.abi_encode_validate()?,
32                    _ => AuthoringMeta::abi_encode_validate(
33                        &serde_json::from_str::<AuthoringMeta>(std::str::from_utf8(data)?)?,
34                    )?,
35                }
36            }
37            // rest of meta types are only pure bytes (ut8 strings or binary)
38            // so no normalization/validation can happen for them at this level
39            _ => data.to_vec(),
40        })
41    }
42}
43
44#[cfg(all(test, not(target_family = "wasm")))]
45mod tests {
46    use crate::error::Error;
47    use crate::meta::types::authoring::v1::{AuthoringMeta, AuthoringMetaItem};
48    use crate::meta::KnownMeta;
49
50    /// OpV1 normalizes valid metadata to its canonical compact json form.
51    #[test]
52    fn test_normalize_op_v1_canonicalizes() {
53        let spaced = b"{  \"name\" : \"add\" ,\n  \"desc\" : \"adds numbers\" }";
54        let normalized = KnownMeta::OpV1.normalize(spaced).unwrap();
55        assert_eq!(
56            String::from_utf8(normalized).unwrap(),
57            r#"{"name":"add","desc":"adds numbers","operand":[],"inputs":[],"outputs":[],"aliases":[]}"#
58        );
59    }
60
61    /// OpV1 rejects metadata that parses but fails validation: opcode names
62    /// must be lower-kebab-case rain symbols.
63    #[test]
64    fn test_normalize_op_v1_rejects_invalid_symbol() {
65        let invalid = br#"{"name":"NOT_A_RAIN_SYMBOL"}"#;
66        assert!(matches!(
67            KnownMeta::OpV1.normalize(invalid),
68            Err(Error::ValidationErrors(_))
69        ));
70    }
71
72    /// SolidityAbiV2 rejects data that is not json and data that is not utf8.
73    #[test]
74    fn test_normalize_solidity_abi_v2_rejects_bad_input() {
75        assert!(matches!(
76            KnownMeta::SolidityAbiV2.normalize(b"not json at all"),
77            Err(Error::SerdeJsonError(_))
78        ));
79        assert!(matches!(
80            KnownMeta::SolidityAbiV2.normalize(&[0xff, 0xfe]),
81            Err(Error::Utf8Error(_))
82        ));
83    }
84
85    /// SolidityAbiV2 normalizes whitespace away to the canonical compact form.
86    #[test]
87    fn test_normalize_solidity_abi_v2_canonicalizes() {
88        assert_eq!(
89            KnownMeta::SolidityAbiV2.normalize(b"[ ]").unwrap(),
90            b"[]".to_vec()
91        );
92    }
93
94    /// InterpreterCallerMetaV1 parses but rejects metadata failing validation:
95    /// at least one method is required.
96    #[test]
97    fn test_normalize_interpreter_caller_rejects_empty_methods() {
98        let invalid = br#"{"name":"Test Caller","abiName":"TestCaller","methods":[]}"#;
99        assert!(matches!(
100            KnownMeta::InterpreterCallerMetaV1.normalize(invalid),
101            Err(Error::ValidationErrors(_))
102        ));
103    }
104
105    /// Meta types with no json schema at this level pass through untouched.
106    #[test]
107    fn test_normalize_passthrough_for_binary_metas() {
108        let data = vec![0x00, 0x01, 0xff];
109        assert_eq!(
110            KnownMeta::ExpressionDeployerV2BytecodeV1
111                .normalize(&data)
112                .unwrap(),
113            data
114        );
115    }
116
117    fn sample_authoring_meta() -> AuthoringMeta {
118        serde_json::from_str(
119            r#"[{"word":"stack","description":"Copies an existing value from the stack.","operandParserOffset":16}]"#,
120        )
121        .unwrap()
122    }
123
124    /// Valid abi encoded input takes the abi-decode path and is re-encoded
125    /// with validation, byte identically.
126    #[test]
127    fn test_normalize_authoring_meta_v1_abi_path() {
128        let authoring_meta = sample_authoring_meta();
129        let abi = authoring_meta.abi_encode_validate().unwrap();
130        let normalized = KnownMeta::AuthoringMetaV1.normalize(&abi).unwrap();
131        assert_eq!(normalized, abi);
132    }
133
134    /// Abi-decodable input that fails validation must be rejected: the abi
135    /// path re-encodes via abi_encode_validate, not a passthrough.
136    #[test]
137    fn test_normalize_authoring_meta_v1_abi_invalid_rejected() {
138        let invalid = AuthoringMeta(vec![AuthoringMetaItem {
139            word: "NOTKEBAB".to_string(),
140            operand_parser_offset: 0,
141            description: "some description".to_string(),
142        }]);
143        // encode WITHOUT validation so the bytes are decodable but invalid
144        let abi = invalid.abi_encode().unwrap();
145        let result = KnownMeta::AuthoringMetaV1.normalize(&abi);
146        assert!(matches!(result, Err(Error::ValidationErrors(_))));
147    }
148
149    /// Json input falls back to serde parse and is abi encoded with
150    /// validation: output is the abi encoding, not the raw json bytes.
151    #[test]
152    fn test_normalize_authoring_meta_v1_json_fallback() {
153        let json = r#"[{"word":"stack","description":"Copies an existing value from the stack.","operandParserOffset":16}]"#;
154        let expected = sample_authoring_meta().abi_encode_validate().unwrap();
155        let normalized = KnownMeta::AuthoringMetaV1
156            .normalize(json.as_bytes())
157            .unwrap();
158        assert_eq!(normalized, expected);
159        assert_ne!(normalized, json.as_bytes().to_vec());
160    }
161
162    /// Meta types without a structured normal form pass raw bytes through
163    /// unchanged.
164    #[test]
165    fn test_normalize_default_arm_passthrough() {
166        let data = b"some dotrain text".to_vec();
167        assert_eq!(KnownMeta::DotrainV1.normalize(&data).unwrap(), data);
168        let binary = vec![0xffu8, 0x00, 0x01];
169        assert_eq!(KnownMeta::RainlangV1.normalize(&binary).unwrap(), binary);
170    }
171}