use std::collections::HashSet; // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 use rquickjs::{ atom::PredefinedAtom, function::{Constructor, Opt, This}, Array, ArrayBuffer, Ctx, Exception, Function, IntoJs, Null, Object, Result, Type, Value, }; use super::{ hash, object::ObjectExt, primordials::{BasePrimordials, Primordial}, }; #[derive(Debug)] enum StackItem<'js> { Value(usize, Value<'js>, Option, Option), ObjectEnd, } #[derive(Debug)] enum ObjectType { Set, Map, } #[derive(Debug)] enum TapeValue<'js> { Array(Array<'js>), Object(Object<'js>), Value(Value<'js>), Collection(Option>, ObjectType), } #[derive(Debug)] struct TapeItem<'js> { parent: usize, object_key: Option, array_index: Option, value: TapeValue<'js>, } pub fn structured_clone<'js>( ctx: &Ctx<'js>, value: Value<'js>, options: Opt>, ) -> Result> { let primordials = BasePrimordials::get(ctx)?; let mut transfer_set = None; if let Some(options) = options.0 { if let Some(transfer_array) = options.get_optional::<_, Array>("transfer")? { let mut set = HashSet::with_capacity(transfer_array.len()); for item in transfer_array.iter::() { set.insert(item?); } transfer_set = Some(set); } } let mut tape = Vec::::with_capacity(10); let mut stack = Vec::with_capacity(10); let mut visited = Vec::<(usize, usize)>::with_capacity(10); let mut index = 0usize; stack.push(StackItem::Value(0, value, None, None)); while let Some(item) = stack.pop() { match item { StackItem::Value(parent, value, mut object_key, array_index) => { if let Some(set) = &transfer_set { if let Some(value) = set.get(&value) { append_transfer_value(&mut tape, value, parent, object_key, array_index)?; index += 1; continue; } } match value.type_of() { Type::Proxy => { return Err(Exception::throw_type( ctx, "A Proxy value could not be cloned", )); }, Type::Object => { if check_circular( &mut tape, &mut visited, &value, parent, &mut object_key, array_index, index, ) { index += 1; continue; } //unsafe OK since we're guaranteed to be object by the match let object = unsafe { value.as_object().unwrap_unchecked() }; if object.is_instance_of(&primordials.constructor_date) { append_ctor_value( &mut tape, object, &primordials.constructor_date, parent, object_key, array_index, )?; index += 1; continue; } if object.is_instance_of(&primordials.constructor_regexp) { append_ctor_value( &mut tape, object, &primordials.constructor_regexp, parent, object_key, array_index, )?; index += 1; continue; } let is_collection = if object.is_instance_of(&primordials.constructor_set) { Some(ObjectType::Set) } else if object.is_instance_of(&primordials.constructor_map) { Some(ObjectType::Map) } else { None }; if let Some(collection_type) = is_collection { append_collection( &mut tape, &primordials.function_array_from, object, parent, object_key, array_index, collection_type, &mut stack, index, )?; index += 1; continue; } if primordials .function_array_buffer_is_view .call::<_, bool>((value.clone(),))? { append_buffer(&mut tape, object, parent, object_key, array_index)?; index += 1; continue; } let new: Object<'_> = if object.is_instance_of(&primordials.constructor_error) { primordials.constructor_error.construct(("",)) } else { Object::new(ctx.clone()) }?; tape.push(TapeItem { parent, object_key, array_index, value: TapeValue::Object(new), }); stack.push(StackItem::ObjectEnd); for key in object.keys::() { let key = key?; let value = object.get(&key)?; stack.push(StackItem::Value(index, value, Some(key), None)); } }, Type::Array => { if check_circular( &mut tape, &mut visited, &value, parent, &mut object_key, array_index, index, ) { index += 1; continue; } let new = Array::new(ctx.clone())?; tape.push(TapeItem { parent, object_key, array_index, value: TapeValue::Array(new), }); stack.push(StackItem::ObjectEnd); //unsafe OK since we're guaranteed to be object by the match let array = unsafe { value.as_array().unwrap_unchecked() }; //reverse for loop of items in array for array_index in (0usize..array.len()).rev() { stack.push(StackItem::Value( index, array.get(array_index)?, None, Some(array_index), )); } }, _ => { tape.push(TapeItem { parent, object_key, array_index, value: TapeValue::Value(value), }); }, } index += 1; }, StackItem::ObjectEnd => { visited.pop(); }, } } while let Some(item) = tape.pop() { let value = match item.value { TapeValue::Array(array) => array.into_value(), TapeValue::Object(object) => object.into_value(), TapeValue::Value(value) => value, TapeValue::Collection(mut value, _) => value.take().unwrap(), }; if tape.is_empty() { return Ok(value); } let parent = &mut tape[item.parent]; let array_index = item.array_index; let object_key = item.object_key; match &mut parent.value { TapeValue::Array(array) => { array.set(array_index.unwrap(), value)?; }, TapeValue::Object(object) => { let string = object_key.unwrap(); object.set(string, value)?; }, TapeValue::Collection(collection_value, collection_type) => { match collection_type { ObjectType::Set => { collection_value.replace(primordials.constructor_set.construct((value,))?); }, ObjectType::Map => { collection_value.replace(primordials.constructor_map.construct((value,))?); }, }; }, _ => {}, }; } Null.into_js(ctx) } #[inline(always)] #[cold] fn append_buffer<'js>( tape: &mut Vec>, object: &Object<'js>, parent: usize, object_key: Option, array_index: Option, ) -> Result<()> { let ctor: Constructor = object.get(PredefinedAtom::Constructor)?; let slice: Function = object.get("slice")?; let clone: Value = slice.call((This(object.clone()),))?; let new = ctor.construct((clone,))?; tape.push(TapeItem { parent, object_key, array_index, value: TapeValue::Value(new), }); Ok(()) } #[inline(always)] #[cold] #[allow(clippy::too_many_arguments)] fn append_collection<'js>( tape: &mut Vec>, array_from: &Function<'js>, object: &Object<'js>, parent: usize, object_key: Option, array_index: Option, collection_type: ObjectType, stack: &mut Vec>, index: usize, ) -> Result<()> { let array: Array = array_from.call((object.clone(),))?; tape.push(TapeItem { parent, object_key, array_index, value: TapeValue::Collection(None, collection_type), }); stack.push(StackItem::ObjectEnd); stack.push(StackItem::Value(index, array.into(), None, None)); Ok(()) } #[inline(always)] fn check_circular( tape: &mut Vec, visited: &mut Vec<(usize, usize)>, value: &Value<'_>, parent: usize, object_key: &mut Option, array_index: Option, index: usize, ) -> bool { let hash = hash::default_hash(value); if let Some(visited) = visited.iter().find(|v| v.0 == hash) { append_circular(tape, visited, object_key, parent, array_index); return true; } visited.push((hash, index)); false } #[inline(always)] #[cold] fn append_transfer_value<'js>( tape: &mut Vec>, value: &Value<'js>, parent: usize, object_key: Option, array_index: Option, ) -> Result<()> { let value = if let Some(ab) = ArrayBuffer::from_value(value.clone()) { ab.get::<_, Function>("transfer")?.call((This(ab),))? } else { value.clone() }; tape.push(TapeItem { parent, object_key, array_index, value: TapeValue::Value(value), }); Ok(()) } #[inline(always)] #[cold] fn append_circular( tape: &mut Vec>, visited: &(usize, usize), object_key: &mut Option, parent: usize, array_index: Option, ) { let value = match &tape[visited.1].value { TapeValue::Array(array) => array.clone().into_value(), TapeValue::Object(object) => object.clone().into_value(), TapeValue::Value(value) => value.clone(), TapeValue::Collection(value, _) => value.clone().unwrap(), }; let object_key = object_key.take(); tape.push(TapeItem { parent, object_key, array_index, value: TapeValue::Value(value), }); } #[inline(always)] #[cold] fn append_ctor_value<'js>( tape: &mut Vec>, object: &Object<'js>, ctor: &Constructor<'js>, parent: usize, object_key: Option, array_index: Option, ) -> Result<()> { let clone: Value = ctor.construct((object.clone(),))?; tape.push(TapeItem { parent, object_key, array_index, value: TapeValue::Value(clone), }); Ok(()) } #[cfg(test)] mod tests { use llrt_test::test_sync_with; use rquickjs::{function::Opt, Object, Value}; use crate::primordials::{BasePrimordials, Primordial}; use super::structured_clone; #[tokio::test] async fn clone() { test_sync_with(|ctx| { BasePrimordials::init(&ctx)?; let value: Object = ctx.eval( r#" const a = { "foo":{ "bar":"baz" }, "foo1":{ "bar1":"baz1", "bar11":"baz11" } }; a "#, )?; let cloned = structured_clone(&ctx, value.clone().into_value(), Opt(None))? .into_object() .unwrap(); let json = ctx .json_stringify(value.clone())? .unwrap() .to_string()? .to_string(); let clone_json = ctx .json_stringify(cloned.clone())? .unwrap() .to_string()? .to_string(); assert_eq!(json, clone_json); assert_ne!( value.get::<_, Value>("foo")?, cloned.get::<_, Value>("foo")? ); Ok(()) }) .await } #[tokio::test] async fn clone_circular() { test_sync_with(|ctx| { BasePrimordials::init(&ctx)?; let _value: Object = ctx.eval( r#" const originalObject = { foo: { bar: "baz",arr: [1,2,3] } }; originalObject.foo.circularRef = originalObject; originalObject.foo.circularRef2 = originalObject; originalObject.foo.circularRef3 = originalObject.foo; originalObject.ref2 = originalObject; "#, )?; Ok(()) }) .await } }