Skip to main content

mhome_script_api/
value.rs

1use crate::ContractError;
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4
5/// Owned values at the native script boundary. Domain objects retain their
6/// `_type`/unit/value or URI fields; adapters interpret them without losing units.
7/// The tagged wire representation is only for process boundaries, not local calls.
8#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
9#[serde(
10    tag = "type",
11    content = "value",
12    rename_all = "camelCase",
13    deny_unknown_fields
14)]
15pub enum ScriptValue {
16    Null,
17    Bool(bool),
18    Number(f64),
19    I64(#[serde(with = "signed")] i64),
20    U64(#[serde(with = "unsigned")] u64),
21    String(String),
22    Bytes(#[serde(with = "binary")] Vec<u8>),
23    List(Vec<ScriptValue>),
24    Object(BTreeMap<String, ScriptValue>),
25}
26
27macro_rules! decimal {
28    ($module:ident, $ty:ty) => {
29        mod $module {
30            use serde::{Deserialize, Deserializer, Serializer};
31            pub fn serialize<S: Serializer>(value: &$ty, serializer: S) -> Result<S::Ok, S::Error> {
32                serializer.serialize_str(&value.to_string())
33            }
34            pub fn deserialize<'de, D: Deserializer<'de>>(
35                deserializer: D,
36            ) -> Result<$ty, D::Error> {
37                let text = String::deserialize(deserializer)?;
38                let value: $ty = text.parse().map_err(serde::de::Error::custom)?;
39                if value.to_string() != text {
40                    return Err(serde::de::Error::custom(
41                        "integer must use canonical decimal text",
42                    ));
43                }
44                Ok(value)
45            }
46        }
47    };
48}
49decimal!(signed, i64);
50decimal!(unsigned, u64);
51
52mod binary {
53    use base64::{engine::general_purpose::STANDARD, Engine};
54    use serde::{Deserialize, Deserializer, Serializer};
55    pub fn serialize<S: Serializer>(value: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
56        serializer.serialize_str(&STANDARD.encode(value))
57    }
58    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
59        STANDARD
60            .decode(String::deserialize(deserializer)?)
61            .map_err(serde::de::Error::custom)
62    }
63}
64
65#[derive(Clone, Copy, Debug)]
66pub struct ValueLimits {
67    pub bytes: usize,
68    pub nodes: usize,
69    pub depth: usize,
70}
71
72impl Default for ValueLimits {
73    fn default() -> Self {
74        Self {
75            bytes: 4 * 1024 * 1024,
76            nodes: 65_536,
77            depth: 64,
78        }
79    }
80}
81
82impl ScriptValue {
83    /// Enforce aggregate bounds before allocation in the JS engine. HTTP adapters
84    /// must also bound the incoming body before deserializing it.
85    pub fn validate(&self, mut limits: ValueLimits) -> Result<(), ContractError> {
86        self.check(&mut limits, 0)
87    }
88
89    fn check(&self, limits: &mut ValueLimits, depth: usize) -> Result<(), ContractError> {
90        if depth > limits.depth || limits.nodes == 0 {
91            return Err(ContractError(
92                "script value exceeds node/depth limit".into(),
93            ));
94        }
95        limits.nodes -= 1;
96        let size = match self {
97            Self::String(value) => value.len().saturating_add(8),
98            Self::Bytes(value) => value.len().saturating_add(8),
99            Self::Number(value) if !value.is_finite() => {
100                return Err(ContractError("script number must be finite".into()));
101            }
102            _ => 8,
103        };
104        limits.bytes = limits
105            .bytes
106            .checked_sub(size)
107            .ok_or_else(|| ContractError("script value exceeds byte limit".into()))?;
108        match self {
109            Self::List(values) => {
110                for value in values {
111                    value.check(limits, depth + 1)?;
112                }
113            }
114            Self::Object(values) => {
115                for (name, value) in values {
116                    limits.bytes = limits
117                        .bytes
118                        .checked_sub(name.len())
119                        .ok_or_else(|| ContractError("script value exceeds byte limit".into()))?;
120                    value.check(limits, depth + 1)?;
121                }
122            }
123            _ => {}
124        }
125        Ok(())
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn integer_wire_values_never_use_json_numbers_or_accept_rounding() {
135        for value in [
136            ScriptValue::I64(i64::MIN),
137            ScriptValue::I64(i64::MAX),
138            ScriptValue::U64(u64::MAX),
139        ] {
140            let json = serde_json::to_value(&value).unwrap();
141            assert!(json["value"].is_string());
142            assert_eq!(serde_json::from_value::<ScriptValue>(json).unwrap(), value);
143        }
144        for invalid in [
145            r#"{"type":"u64","value":18446744073709551615}"#,
146            r#"{"type":"i64","value":"01"}"#,
147            r#"{"type":"i64","value":"+1"}"#,
148            r#"{"type":"u64","value":"18446744073709551616"}"#,
149        ] {
150            assert!(serde_json::from_str::<ScriptValue>(invalid).is_err());
151        }
152    }
153
154    #[test]
155    fn values_have_aggregate_bounds_and_reject_nonfinite_numbers() {
156        let value = ScriptValue::List(vec![ScriptValue::String("abcdef".into()); 3]);
157        assert!(value
158            .validate(ValueLimits {
159                bytes: 25,
160                ..ValueLimits::default()
161            })
162            .is_err());
163        assert!(value
164            .validate(ValueLimits {
165                nodes: 3,
166                ..ValueLimits::default()
167            })
168            .is_err());
169        assert!(value
170            .validate(ValueLimits {
171                depth: 0,
172                ..ValueLimits::default()
173            })
174            .is_err());
175        assert!(value.validate(ValueLimits::default()).is_ok());
176        assert!(ScriptValue::Number(f64::NAN)
177            .validate(ValueLimits::default())
178            .is_err());
179    }
180}