use super::{BuiltInBuilder, BuiltInConstructor, IntrinsicObject, OrdinaryObject};
use crate::object::internal_methods::InternalMethodCallContext;
use crate::value::JsVariant;
use crate::{
Context, JsArgs, JsResult, JsString, JsValue,
builtins::{BuiltInObject, array},
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
error::JsNativeError,
js_string,
native_function::NativeFunction,
object::{
JsData, JsFunction, JsObject, JsPrototype,
internal_methods::{
CallValue, InternalMethodPropertyContext, InternalObjectMethods,
ORDINARY_INTERNAL_METHODS, is_compatible_property_descriptor,
},
shape::slot::SlotAttributes,
},
property::{PropertyDescriptor, PropertyKey},
realm::Realm,
string::StaticJsStrings,
value::Type,
};
use boa_gc::{Finalize, GcRefCell, Trace};
use rustc_hash::FxHashSet;
#[derive(Debug, Clone, Trace, Finalize)]
pub struct Proxy {
data: Option<(JsObject, JsObject)>,
}
impl JsData for Proxy {
fn internal_methods(&self) -> &'static InternalObjectMethods {
static BASIC: InternalObjectMethods = InternalObjectMethods {
__get_prototype_of__: proxy_exotic_get_prototype_of,
__set_prototype_of__: proxy_exotic_set_prototype_of,
__is_extensible__: proxy_exotic_is_extensible,
__prevent_extensions__: proxy_exotic_prevent_extensions,
__get_own_property__: proxy_exotic_get_own_property,
__define_own_property__: proxy_exotic_define_own_property,
__has_property__: proxy_exotic_has_property,
__try_get__: proxy_exotic_try_get,
__get__: proxy_exotic_get,
__set__: proxy_exotic_set,
__delete__: proxy_exotic_delete,
__own_property_keys__: proxy_exotic_own_property_keys,
..ORDINARY_INTERNAL_METHODS
};
static CALLABLE: InternalObjectMethods = InternalObjectMethods {
__call__: proxy_exotic_call,
..BASIC
};
static CONSTRUCTOR: InternalObjectMethods = InternalObjectMethods {
__call__: proxy_exotic_call,
__construct__: proxy_exotic_construct,
..BASIC
};
let Some(data) = &self.data else {
return &BASIC;
};
if data.0.is_constructor() {
&CONSTRUCTOR
} else if data.0.is_callable() {
&CALLABLE
} else {
&BASIC
}
}
}
impl IntrinsicObject for Proxy {
fn init(realm: &Realm) {
BuiltInBuilder::from_standard_constructor::<Self>(realm)
.static_method(Self::revocable, js_string!("revocable"), 2)
.build_without_prototype();
}
fn get(intrinsics: &Intrinsics) -> JsObject {
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
}
}
impl BuiltInObject for Proxy {
const NAME: JsString = StaticJsStrings::PROXY;
}
impl BuiltInConstructor for Proxy {
const CONSTRUCTOR_ARGUMENTS: usize = 2;
const PROTOTYPE_STORAGE_SLOTS: usize = 0;
const CONSTRUCTOR_STORAGE_SLOTS: usize = 1;
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
StandardConstructors::proxy;
fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
if new_target.is_undefined() {
return Err(JsNativeError::typ()
.with_message("Proxy constructor called on undefined new target")
.into());
}
Self::create(args.get_or_undefined(0), args.get_or_undefined(1), context).map(JsValue::from)
}
}
impl Proxy {
pub(crate) fn new(target: JsObject, handler: JsObject) -> Self {
Self {
data: Some((target, handler)),
}
}
pub(crate) fn try_data(&self) -> JsResult<(JsObject, JsObject)> {
self.data.clone().ok_or_else(|| {
JsNativeError::typ()
.with_message("Proxy object has empty handler and target")
.into()
})
}
pub(crate) fn create(
target: &JsValue,
handler: &JsValue,
context: &mut Context,
) -> JsResult<JsObject> {
let target = target.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("Proxy constructor called with non-object target")
})?;
let handler = handler.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("Proxy constructor called with non-object handler")
})?;
let p = JsObject::from_proto_and_data_with_shared_shape(
context.root_shape(),
context.intrinsics().constructors().object().prototype(),
Self::new(target.clone(), handler.clone()),
);
Ok(p)
}
pub(crate) fn revoker(proxy: JsObject, context: &mut Context) -> JsFunction {
NativeFunction::from_copy_closure_with_captures(
|_, _, revocable_proxy, _| {
if let Some(p) = std::mem::take(&mut *revocable_proxy.borrow_mut()) {
p.downcast_mut::<Proxy>()
.expect("[[RevocableProxy]] must be a proxy object")
.data = None;
}
Ok(JsValue::undefined())
},
GcRefCell::new(Some(proxy)),
)
.to_js_function(context.realm())
}
fn revocable(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let p = Self::create(args.get_or_undefined(0), args.get_or_undefined(1), context)?;
let revoker = Self::revoker(p.clone(), context);
let result = JsObject::with_object_proto(context.intrinsics());
result
.create_data_property_or_throw(js_string!("proxy"), p, context)
.expect("CreateDataPropertyOrThrow cannot fail here");
result
.create_data_property_or_throw(js_string!("revoke"), revoker, context)
.expect("CreateDataPropertyOrThrow cannot fail here");
Ok(result.into())
}
}
pub(crate) fn proxy_exotic_get_prototype_of(
obj: &JsObject,
context: &mut Context,
) -> JsResult<JsPrototype> {
let (target, handler) = obj
.downcast_ref::<Proxy>()
.expect("Proxy object internal internal method called on non-proxy object")
.try_data()?;
let Some(trap) = handler.get_method(js_string!("getPrototypeOf"), context)? else {
return target.__get_prototype_of__(context);
};
let handler_proto = trap.call(&handler.into(), &[target.clone().into()], context)?;
let handler_proto = match handler_proto.variant() {
JsVariant::Object(obj) => Some(obj.clone()),
JsVariant::Null => None,
_ => {
return Err(JsNativeError::typ()
.with_message("Proxy trap result is neither object nor null")
.into());
}
};
if target.is_extensible(context)? {
return Ok(handler_proto);
}
let target_proto = target.__get_prototype_of__(context)?;
if handler_proto != target_proto {
return Err(JsNativeError::typ()
.with_message("Proxy trap returned unexpected prototype")
.into());
}
Ok(handler_proto)
}
pub(crate) fn proxy_exotic_set_prototype_of(
obj: &JsObject,
val: JsPrototype,
context: &mut Context,
) -> JsResult<bool> {
let (target, handler) = obj
.downcast_ref::<Proxy>()
.expect("Proxy object internal internal method called on non-proxy object")
.try_data()?;
let Some(trap) = handler.get_method(js_string!("setPrototypeOf"), context)? else {
return target.__set_prototype_of__(val, context);
};
if !trap
.call(
&handler.into(),
&[
target.clone().into(),
val.clone().map_or(JsValue::null(), Into::into),
],
context,
)?
.to_boolean()
{
return Ok(false);
}
if target.is_extensible(context)? {
return Ok(true);
}
let target_proto = target.__get_prototype_of__(context)?;
if val != target_proto {
return Err(JsNativeError::typ()
.with_message("Proxy trap failed to set prototype")
.into());
}
Ok(true)
}
pub(crate) fn proxy_exotic_is_extensible(obj: &JsObject, context: &mut Context) -> JsResult<bool> {
let (target, handler) = obj
.downcast_ref::<Proxy>()
.expect("Proxy object internal internal method called on non-proxy object")
.try_data()?;
let Some(trap) = handler.get_method(js_string!("isExtensible"), context)? else {
return target.is_extensible(context);
};
let boolean_trap_result = trap
.call(&handler.into(), &[target.clone().into()], context)?
.to_boolean();
let target_result = target.is_extensible(context)?;
if boolean_trap_result != target_result {
return Err(JsNativeError::typ()
.with_message("Proxy trap returned unexpected extensible value")
.into());
}
Ok(boolean_trap_result)
}
pub(crate) fn proxy_exotic_prevent_extensions(
obj: &JsObject,
context: &mut Context,
) -> JsResult<bool> {
let (target, handler) = obj
.downcast_ref::<Proxy>()
.expect("Proxy object internal internal method called on non-proxy object")
.try_data()?;
let Some(trap) = handler.get_method(js_string!("preventExtensions"), context)? else {
return target.__prevent_extensions__(context);
};
let boolean_trap_result = trap
.call(&handler.into(), &[target.clone().into()], context)?
.to_boolean();
if boolean_trap_result && target.is_extensible(context)? {
return Err(JsNativeError::typ()
.with_message("Proxy trap failed to set extensible")
.into());
}
Ok(boolean_trap_result)
}
pub(crate) fn proxy_exotic_get_own_property(
obj: &JsObject,
key: &PropertyKey,
context: &mut InternalMethodPropertyContext<'_>,
) -> JsResult<Option<PropertyDescriptor>> {
context.slot().attributes |= SlotAttributes::NOT_CACHABLE;
let (target, handler) = obj
.downcast_ref::<Proxy>()
.expect("Proxy object internal internal method called on non-proxy object")
.try_data()?;
let Some(trap) = handler.get_method(js_string!("getOwnPropertyDescriptor"), context)? else {
return target.__get_own_property__(key, context);
};
let trap_result_obj = trap.call(
&handler.into(),
&[target.clone().into(), key.clone().into()],
context,
)?;
if !trap_result_obj.is_object() && !trap_result_obj.is_undefined() {
return Err(JsNativeError::typ()
.with_message("Proxy trap result is neither object nor undefined")
.into());
}
let target_desc = target.__get_own_property__(key, context)?;
if trap_result_obj.is_undefined() {
if let Some(desc) = target_desc {
if !desc.expect_configurable() {
return Err(JsNativeError::typ()
.with_message(
"Proxy trap result is undefined adn target result is not configurable",
)
.into());
}
if !target.is_extensible(context)? {
return Err(JsNativeError::typ()
.with_message("Proxy trap result is undefined and target is not extensible")
.into());
}
return Ok(None);
}
return Ok(None);
}
let extensible_target = target.is_extensible(context)?;
let result_desc = trap_result_obj.to_property_descriptor(context)?;
let result_desc = result_desc.complete_property_descriptor();
if !is_compatible_property_descriptor(
extensible_target,
result_desc.clone(),
target_desc.clone(),
) {
return Err(JsNativeError::typ()
.with_message("Proxy trap returned unexpected property")
.into());
}
if !result_desc.expect_configurable() {
match &target_desc {
Some(desc) if !desc.expect_configurable() => {
if result_desc.writable() == Some(false) {
if desc.expect_writable() {
return
Err(JsNativeError::typ().with_message("Proxy trap result is writable and not configurable while target result is not configurable").into())
;
}
}
}
_ => {
return Err(JsNativeError::typ()
.with_message(
"Proxy trap result is not configurable and target result is undefined",
)
.into());
}
}
}
Ok(Some(result_desc))
}
pub(crate) fn proxy_exotic_define_own_property(
obj: &JsObject,
key: &PropertyKey,
desc: PropertyDescriptor,
context: &mut InternalMethodPropertyContext<'_>,
) -> JsResult<bool> {
context.slot().attributes |= SlotAttributes::NOT_CACHABLE;
let (target, handler) = obj
.downcast_ref::<Proxy>()
.expect("Proxy object internal internal method called on non-proxy object")
.try_data()?;
let Some(trap) = handler.get_method(js_string!("defineProperty"), context)? else {
return target.__define_own_property__(key, desc, context);
};
let desc_obj = OrdinaryObject::from_property_descriptor(Some(desc.clone()), context);
if !trap
.call(
&handler.into(),
&[target.clone().into(), key.clone().into(), desc_obj],
context,
)?
.to_boolean()
{
return Ok(false);
}
let target_desc = target.__get_own_property__(key, context)?;
let extensible_target = target.is_extensible(context)?;
let setting_config_false = matches!(desc.configurable(), Some(false));
match target_desc {
None => {
if !extensible_target {
return Err(JsNativeError::typ()
.with_message("Proxy trap failed to set property")
.into());
}
if setting_config_false {
return Err(JsNativeError::typ()
.with_message("Proxy trap failed to set property")
.into());
}
}
Some(target_desc) => {
if !is_compatible_property_descriptor(
extensible_target,
desc.clone(),
Some(target_desc.clone()),
) {
return Err(JsNativeError::typ()
.with_message("Proxy trap set property to unexpected value")
.into());
}
if setting_config_false && target_desc.expect_configurable() {
return Err(JsNativeError::typ()
.with_message("Proxy trap set property with unexpected configurable field")
.into());
}
if target_desc.is_data_descriptor()
&& !target_desc.expect_configurable()
&& target_desc.expect_writable()
{
if let Some(writable) = desc.writable()
&& !writable
{
return Err(JsNativeError::typ()
.with_message("Proxy trap set property with unexpected writable field")
.into());
}
}
}
}
Ok(true)
}
pub(crate) fn proxy_exotic_has_property(
obj: &JsObject,
key: &PropertyKey,
context: &mut InternalMethodPropertyContext<'_>,
) -> JsResult<bool> {
context.slot().attributes |= SlotAttributes::NOT_CACHABLE;
let (target, handler) = obj
.downcast_ref::<Proxy>()
.expect("Proxy object internal internal method called on non-proxy object")
.try_data()?;
let Some(trap) = handler.get_method(js_string!("has"), context)? else {
return target.has_property(key.clone(), context);
};
let boolean_trap_result = trap
.call(
&handler.into(),
&[target.clone().into(), key.clone().into()],
context,
)?
.to_boolean();
if !boolean_trap_result {
let target_desc = target.__get_own_property__(key, context)?;
if let Some(target_desc) = target_desc {
if !target_desc.expect_configurable() {
return Err(JsNativeError::typ()
.with_message("Proxy trap returned unexpected property")
.into());
}
if !target.is_extensible(context)? {
return Err(JsNativeError::typ()
.with_message("Proxy trap returned unexpected property")
.into());
}
}
}
Ok(boolean_trap_result)
}
pub(crate) fn proxy_exotic_try_get(
obj: &JsObject,
key: &PropertyKey,
receiver: JsValue,
context: &mut InternalMethodPropertyContext<'_>,
) -> JsResult<Option<JsValue>> {
if proxy_exotic_has_property(obj, key, context)? {
Ok(Some(proxy_exotic_get(obj, key, receiver, context)?))
} else {
Ok(None)
}
}
pub(crate) fn proxy_exotic_get(
obj: &JsObject,
key: &PropertyKey,
receiver: JsValue,
context: &mut InternalMethodPropertyContext<'_>,
) -> JsResult<JsValue> {
context.slot().attributes |= SlotAttributes::NOT_CACHABLE;
let (target, handler) = obj
.downcast_ref::<Proxy>()
.expect("Proxy object internal internal method called on non-proxy object")
.try_data()?;
let Some(trap) = handler.get_method(js_string!("get"), context)? else {
return target.__get__(key, receiver, context);
};
let trap_result = trap.call(
&handler.into(),
&[target.clone().into(), key.clone().into(), receiver],
context,
)?;
let target_desc = target.__get_own_property__(key, context)?;
if let Some(target_desc) = target_desc
&& !target_desc.expect_configurable()
{
if target_desc.is_data_descriptor() && !target_desc.expect_writable() {
if !JsValue::same_value(&trap_result, target_desc.expect_value()) {
return Err(JsNativeError::typ()
.with_message("Proxy trap returned unexpected data descriptor")
.into());
}
}
if target_desc.is_accessor_descriptor() && target_desc.expect_get().is_undefined() {
if !trap_result.is_undefined() {
return Err(JsNativeError::typ()
.with_message("Proxy trap returned unexpected accessor descriptor")
.into());
}
}
}
Ok(trap_result)
}
pub(crate) fn proxy_exotic_set(
obj: &JsObject,
key: PropertyKey,
value: JsValue,
receiver: JsValue,
context: &mut InternalMethodPropertyContext<'_>,
) -> JsResult<bool> {
context.slot().attributes |= SlotAttributes::NOT_CACHABLE;
let (target, handler) = obj
.downcast_ref::<Proxy>()
.expect("Proxy object internal internal method called on non-proxy object")
.try_data()?;
let Some(trap) = handler.get_method(js_string!("set"), context)? else {
return target.__set__(key, value, receiver, context);
};
if !trap
.call(
&handler.into(),
&[
target.clone().into(),
key.clone().into(),
value.clone(),
receiver,
],
context,
)?
.to_boolean()
{
return Ok(false);
}
let target_desc = target.__get_own_property__(&key, context)?;
if let Some(target_desc) = target_desc
&& !target_desc.expect_configurable()
{
if target_desc.is_data_descriptor() && !target_desc.expect_writable() {
if !JsValue::same_value(&value, target_desc.expect_value()) {
return Err(JsNativeError::typ()
.with_message("Proxy trap set unexpected data descriptor")
.into());
}
}
if target_desc.is_accessor_descriptor() {
match target_desc.set().map(JsValue::is_undefined) {
None | Some(true) => {
return Err(JsNativeError::typ()
.with_message("Proxy trap set unexpected accessor descriptor")
.into());
}
_ => {}
}
}
}
Ok(true)
}
pub(crate) fn proxy_exotic_delete(
obj: &JsObject,
key: &PropertyKey,
context: &mut InternalMethodPropertyContext<'_>,
) -> JsResult<bool> {
let (target, handler) = obj
.downcast_ref::<Proxy>()
.expect("Proxy object internal internal method called on non-proxy object")
.try_data()?;
let Some(trap) = handler.get_method(js_string!("deleteProperty"), context)? else {
return target.__delete__(key, context);
};
if !trap
.call(
&handler.into(),
&[target.clone().into(), key.clone().into()],
context,
)?
.to_boolean()
{
return Ok(false);
}
match target.__get_own_property__(key, context)? {
None => return Ok(true),
Some(target_desc) => {
if !target_desc.expect_configurable() {
return Err(JsNativeError::typ()
.with_message("Proxy trap failed to delete property")
.into());
}
}
}
if !target.is_extensible(context)? {
return Err(JsNativeError::typ()
.with_message("Proxy trap failed to delete property")
.into());
}
Ok(true)
}
pub(crate) fn proxy_exotic_own_property_keys(
obj: &JsObject,
context: &mut Context,
) -> JsResult<Vec<PropertyKey>> {
let (target, handler) = obj
.downcast_ref::<Proxy>()
.expect("Proxy object internal internal method called on non-proxy object")
.try_data()?;
let Some(trap) = handler.get_method(js_string!("ownKeys"), context)? else {
return target.__own_property_keys__(context);
};
let trap_result_array = trap.call(&handler.into(), &[target.clone().into()], context)?;
let trap_result_raw =
trap_result_array.create_list_from_array_like(&[Type::String, Type::Symbol], context)?;
let mut unchecked_result_keys: FxHashSet<PropertyKey> = FxHashSet::default();
let mut trap_result = Vec::new();
for value in &trap_result_raw {
match value.variant() {
JsVariant::String(s) => {
if !unchecked_result_keys.insert(s.clone().into()) {
return Err(JsNativeError::typ()
.with_message("Proxy trap result contains duplicate string property keys")
.into());
}
trap_result.push(s.clone().into());
}
JsVariant::Symbol(s) => {
if !unchecked_result_keys.insert(s.clone().into()) {
return Err(JsNativeError::typ()
.with_message("Proxy trap result contains duplicate symbol property keys")
.into());
}
trap_result.push(s.clone().into());
}
_ => {}
}
}
let extensible_target = target.is_extensible(context)?;
let target_keys = target.__own_property_keys__(context)?;
let mut target_configurable_keys = Vec::new();
let mut target_nonconfigurable_keys = Vec::new();
for key in target_keys {
match target.__get_own_property__(&key, &mut context.into())? {
Some(desc) if !desc.expect_configurable() => {
target_nonconfigurable_keys.push(key);
}
_ => {
target_configurable_keys.push(key);
}
}
}
if extensible_target && target_nonconfigurable_keys.is_empty() {
return Ok(trap_result);
}
for key in target_nonconfigurable_keys {
if !unchecked_result_keys.remove(&key) {
return Err(JsNativeError::typ()
.with_message("Proxy trap failed to return all non-configurable property keys")
.into());
}
}
if extensible_target {
return Ok(trap_result);
}
for key in target_configurable_keys {
if !unchecked_result_keys.remove(&key) {
return Err(JsNativeError::typ()
.with_message("Proxy trap failed to return all configurable property keys")
.into());
}
}
if !unchecked_result_keys.is_empty() {
return Err(JsNativeError::typ()
.with_message("Proxy trap failed to return all property keys")
.into());
}
Ok(trap_result)
}
fn proxy_exotic_call(
obj: &JsObject,
argument_count: usize,
context: &mut InternalMethodCallContext<'_>,
) -> JsResult<CallValue> {
let (target, handler) = obj
.downcast_ref::<Proxy>()
.expect("Proxy object internal internal method called on non-proxy object")
.try_data()?;
let Some(trap) = handler.get_method(js_string!("apply"), context)? else {
return Ok(target.__call__(argument_count));
};
let args = context
.vm
.stack
.calling_convention_pop_arguments(argument_count);
let arg_array = array::Array::create_array_from_list(args, context);
let _func = context.vm.stack.pop();
let this = context.vm.stack.pop();
context.vm.stack.push(handler); context.vm.stack.push(trap.clone());
context.vm.stack.push(target);
context.vm.stack.push(this);
context.vm.stack.push(arg_array);
Ok(trap.__call__(3))
}
fn proxy_exotic_construct(
obj: &JsObject,
argument_count: usize,
context: &mut InternalMethodCallContext<'_>,
) -> JsResult<CallValue> {
let (target, handler) = obj
.downcast_ref::<Proxy>()
.expect("Proxy object internal internal method called on non-proxy object")
.try_data()?;
assert!(target.is_constructor());
let Some(trap) = handler.get_method(js_string!("construct"), context)? else {
return Ok(target.__construct__(argument_count));
};
let new_target = context.vm.stack.pop();
let args = context
.vm
.stack
.calling_convention_pop_arguments(argument_count);
let _func = context.vm.stack.pop();
let _this = context.vm.stack.pop();
let arg_array = array::Array::create_array_from_list(args, context);
let new_obj = trap.call(
&handler.into(),
&[target.into(), arg_array.into(), new_target],
context,
)?;
let new_obj = new_obj.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("Proxy trap constructor returned non-object value")
})?;
context.vm.stack.push(new_obj);
Ok(CallValue::Complete)
}