@@ -10,6 +10,7 @@ use crate::{
1010 Context , JsResult , JsVariant ,
1111} ;
1212use serde_json:: { Map , Value } ;
13+ use std:: collections:: HashSet ;
1314
1415impl 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