Skip to content

Commit c863b8c

Browse files
authored
Fix JsValue::to_json with cyclic values (#4176)
* Fix JsValue::to_json with cyclic values * use IndexSet * use hashset * remove redundant reference
1 parent 4e31c15 commit c863b8c

File tree

1 file changed

+43
-7
lines changed

1 file changed

+43
-7
lines changed

core/engine/src/value/conversions/serde_json.rs

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::{
1010
Context, JsResult, JsVariant,
1111
};
1212
use serde_json::{Map, Value};
13+
use std::collections::HashSet;
1314

1415
impl JsValue {
1516
/// Converts a [`serde_json::Value`] to a `JsValue`.
@@ -113,6 +114,15 @@ impl JsValue {
113114
///
114115
/// Panics if the `JsValue` is `Undefined`.
115116
pub fn to_json(&self, context: &mut Context) -> JsResult<Value> {
117+
let mut seen_objects = HashSet::new();
118+
self.to_json_inner(context, &mut seen_objects)
119+
}
120+
121+
fn to_json_inner(
122+
&self,
123+
context: &mut Context,
124+
seen_objects: &mut HashSet<JsObject>,
125+
) -> JsResult<Value> {
116126
match self.variant() {
117127
JsVariant::Null => Ok(Value::Null),
118128
JsVariant::Undefined => todo!("undefined to JSON"),
@@ -124,11 +134,20 @@ impl JsValue {
124134
.with_message("cannot convert bigint to JSON")
125135
.into()),
126136
JsVariant::Object(obj) => {
127-
let value_by_prop_key = |property_key, context: &mut Context| {
137+
if seen_objects.contains(obj) {
138+
return Err(JsNativeError::typ()
139+
.with_message("cyclic object value")
140+
.into());
141+
}
142+
seen_objects.insert(obj.clone());
143+
let mut value_by_prop_key = |property_key, context: &mut Context| {
128144
obj.borrow()
129145
.properties()
130146
.get(&property_key)
131-
.and_then(|x| x.value().map(|val| val.to_json(context)))
147+
.and_then(|x| {
148+
x.value()
149+
.map(|val| val.to_json_inner(context, seen_objects))
150+
})
132151
.unwrap_or(Ok(Value::Null))
133152
};
134153

@@ -140,7 +159,9 @@ impl JsValue {
140159
let val = value_by_prop_key(k.into(), context)?;
141160
arr.push(val);
142161
}
143-
162+
// Passing the object rather than its clone that was inserted to the set should be fine
163+
// as they hash to the same value and therefore HashSet can still remove the clone
164+
seen_objects.remove(obj);
144165
Ok(Value::Array(arr))
145166
} else {
146167
let mut map = Map::new();
@@ -164,7 +185,7 @@ impl JsValue {
164185
let value = value_by_prop_key(property_key, context)?;
165186
map.insert(key, value);
166187
}
167-
188+
seen_objects.remove(obj);
168189
Ok(Value::Object(map))
169190
}
170191
}
@@ -181,9 +202,9 @@ mod tests {
181202
use indoc::indoc;
182203
use serde_json::json;
183204

184-
use crate::object::JsArray;
185-
use crate::{js_string, JsValue};
186-
use crate::{run_test_actions, TestAction};
205+
use crate::{
206+
js_string, object::JsArray, run_test_actions, Context, JsObject, JsValue, TestAction,
207+
};
187208

188209
#[test]
189210
fn json_conversions() {
@@ -272,4 +293,19 @@ mod tests {
272293
}),
273294
]);
274295
}
296+
297+
#[test]
298+
fn to_json_cyclic() {
299+
let mut context = Context::default();
300+
let obj = JsObject::with_null_proto();
301+
obj.create_data_property(js_string!("a"), obj.clone(), &mut context)
302+
.expect("should create data property");
303+
assert_eq!(
304+
JsValue::from(obj)
305+
.to_json(&mut context)
306+
.unwrap_err()
307+
.to_string(),
308+
"TypeError: cyclic object value"
309+
);
310+
}
275311
}

0 commit comments

Comments
 (0)