Files
sausage/host/cred-script/vendor/llrt_utils_watts/src/clone.rs
T

488 lines
15 KiB
Rust

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<String>, Option<usize>),
ObjectEnd,
}
#[derive(Debug)]
enum ObjectType {
Set,
Map,
}
#[derive(Debug)]
enum TapeValue<'js> {
Array(Array<'js>),
Object(Object<'js>),
Value(Value<'js>),
Collection(Option<Value<'js>>, ObjectType),
}
#[derive(Debug)]
struct TapeItem<'js> {
parent: usize,
object_key: Option<String>,
array_index: Option<usize>,
value: TapeValue<'js>,
}
pub fn structured_clone<'js>(
ctx: &Ctx<'js>,
value: Value<'js>,
options: Opt<Object<'js>>,
) -> Result<Value<'js>> {
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::<Value>() {
set.insert(item?);
}
transfer_set = Some(set);
}
}
let mut tape = Vec::<TapeItem>::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::<String>() {
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<TapeItem<'js>>,
object: &Object<'js>,
parent: usize,
object_key: Option<String>,
array_index: Option<usize>,
) -> 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<TapeItem<'js>>,
array_from: &Function<'js>,
object: &Object<'js>,
parent: usize,
object_key: Option<String>,
array_index: Option<usize>,
collection_type: ObjectType,
stack: &mut Vec<StackItem<'js>>,
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<TapeItem>,
visited: &mut Vec<(usize, usize)>,
value: &Value<'_>,
parent: usize,
object_key: &mut Option<String>,
array_index: Option<usize>,
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<TapeItem<'js>>,
value: &Value<'js>,
parent: usize,
object_key: Option<String>,
array_index: Option<usize>,
) -> 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<TapeItem<'_>>,
visited: &(usize, usize),
object_key: &mut Option<String>,
parent: usize,
array_index: Option<usize>,
) {
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<TapeItem<'js>>,
object: &Object<'js>,
ctor: &Constructor<'js>,
parent: usize,
object_key: Option<String>,
array_index: Option<usize>,
) -> 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
}
}