forest/rpc/
json_validator.rs1use ahash::HashSet;
16use serde::de::DeserializeOwned;
17
18pub const STRICT_JSON_ENV: &str = "FOREST_STRICT_JSON";
19
20crate::def_is_env_truthy!(is_strict_mode, STRICT_JSON_ENV);
21
22pub fn validate_json_for_duplicates(json_str: &str) -> Result<(), String> {
24 if !is_strict_mode() {
25 return Ok(());
26 }
27
28 fn check_value(value: &sonic_rs::Value) -> Result<(), String> {
29 match value.as_ref() {
30 sonic_rs::ValueRef::Object(obj) => {
31 let mut seen = HashSet::default();
32 for (key, value) in obj.iter() {
33 if !seen.insert(key) {
34 return Err(format!(
35 "duplicate key '{key}' in JSON object - this likely indicates malformed input. \
36 Set {STRICT_JSON_ENV}=0 to disable this check"
37 ));
38 }
39 check_value(value)?;
40 }
41 Ok(())
42 }
43 sonic_rs::ValueRef::Array(arr) => {
44 for item in arr.iter() {
45 check_value(item)?;
46 }
47 Ok(())
48 }
49 _ => Ok(()),
50 }
51 }
52 let value: sonic_rs::Value = match sonic_rs::from_str(json_str) {
54 Ok(v) => v,
55 Err(_) => return Ok(()),
56 };
57 check_value(&value)
58}
59
60pub fn from_value_rejecting_unknown_fields<T: DeserializeOwned>(
63 value: serde_json::Value,
64) -> Result<T, serde_json::Error> {
65 if !is_strict_mode() {
66 return serde_json::from_value(value);
67 }
68 let mut unknown = Vec::new();
69 let result: T = serde_ignored::deserialize(value, |path| {
70 unknown.push(path.to_string());
71 })?;
72 if !unknown.is_empty() {
73 return Err(serde::de::Error::custom(format!(
74 "unknown field(s): {}. Set {STRICT_JSON_ENV}=0 to disable this check",
75 unknown.join(", ")
76 )));
77 }
78 Ok(result)
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use serde::Deserialize;
85 use serde_json::json;
86 use serial_test::serial;
87
88 fn with_strict_mode<F>(enabled: bool, f: F)
89 where
90 F: FnOnce(),
91 {
92 let original = std::env::var(STRICT_JSON_ENV).ok();
93
94 if enabled {
95 unsafe {
96 std::env::set_var(STRICT_JSON_ENV, "1");
97 }
98 } else {
99 unsafe {
100 std::env::remove_var(STRICT_JSON_ENV);
101 }
102 }
103
104 f();
105
106 unsafe {
107 match original {
108 Some(val) => std::env::set_var(STRICT_JSON_ENV, val),
109 None => std::env::remove_var(STRICT_JSON_ENV),
110 }
111 }
112 }
113
114 #[test]
115 #[serial]
116 fn test_no_duplicates() {
117 with_strict_mode(true, || {
118 let json = r#"{"a": 1, "b": 2, "c": 3}"#;
119 assert!(validate_json_for_duplicates(json).is_ok());
120 });
121 }
122
123 #[test]
124 #[serial]
125 fn test_duplicate_keys_detected() {
126 with_strict_mode(true, || {
127 let json = r#"{"/":"cid1", "/":"cid2"}"#;
128 let result = validate_json_for_duplicates(json);
129 assert!(result.is_err(), "Should have detected duplicate key");
130 assert!(result.unwrap_err().contains("duplicate key"));
131 });
132 }
133
134 #[test]
135 #[serial]
136 fn test_strict_mode_disabled() {
137 with_strict_mode(false, || {
138 let json = r#"{"/":"cid1", "/":"cid2"}"#;
140 assert!(validate_json_for_duplicates(json).is_ok());
141 });
142 }
143
144 #[test]
145 #[serial]
146 fn test_duplicate_cid_keys() {
147 with_strict_mode(true, || {
148 let json = r#"{
149 "jsonrpc": "2.0",
150 "id": 1,
151 "method": "Filecoin.ChainGetMessagesInTipset",
152 "params": [[{
153 "/":"bafy2bzacea43254b5x6c4l22ynpjfoct5qvabbbk2abcfspfcjkiltivrlyqi",
154 "/":"bafy2bzacea4viqyaozpfk57lnemwufryb76llxzmebxc7it2rnssqz2ljdl6a",
155 "/":"bafy2bzaceav6j67epppz5ib55v5ty26dhkq4jinbsizq2olb3azbzxvfmc73o"
156 }]]
157 }"#;
158
159 let result = validate_json_for_duplicates(json);
160 assert!(result.is_err());
161 assert!(result.unwrap_err().contains("duplicate key '/'"));
162 });
163 }
164
165 #[derive(Debug, Deserialize, PartialEq)]
166 struct RpcTestReq {
167 name: String,
168 value: i32,
169 }
170
171 #[test]
172 #[serial]
173 fn test_unknown_fields_known_only() {
174 with_strict_mode(true, || {
175 let val = json!({"name": "alice", "value": 42});
176 let result = from_value_rejecting_unknown_fields::<RpcTestReq>(val);
177 assert_eq!(
178 result.unwrap(),
179 RpcTestReq {
180 name: "alice".into(),
181 value: 42
182 }
183 );
184 });
185 }
186
187 #[test]
188 #[serial]
189 fn test_unknown_fields_detected() {
190 with_strict_mode(true, || {
191 let val = json!({"name": "alice", "value": 42, "extra": true});
192 let err = from_value_rejecting_unknown_fields::<RpcTestReq>(val)
193 .expect_err("expected Err when unknown JSON field is present under strict mode");
194 let msg = err.to_string();
195 assert!(
196 msg.contains("unknown field(s)") && msg.contains("extra"),
197 "got: {msg}"
198 );
199 });
200 }
201
202 #[test]
203 #[serial]
204 fn test_unknown_fields_strict_mode_off() {
205 with_strict_mode(false, || {
206 let val = json!({"name": "alice", "value": 42, "extra": true});
207 let result = from_value_rejecting_unknown_fields::<RpcTestReq>(val);
208 assert!(
209 result.is_ok(),
210 "unknown fields should be allowed when strict mode is off"
211 );
212 });
213 }
214}