diff options
author | Matthew Olsson <matthewcolsson@gmail.com> | 2020-06-03 14:34:52 -0700 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2020-06-06 22:13:01 +0200 |
commit | 39ad42defdcb01f1fb0ffc83ee53b9152630f14f (patch) | |
tree | c5f683415f87b91d4ebd339715659b0812e4d452 | |
parent | 58a72e9b810036ebdd91331137ce50e55dbe1ff4 (diff) | |
download | serenity-39ad42defdcb01f1fb0ffc83ee53b9152630f14f.zip |
LibJS: Add Proxy objects
Includes all traps except the following: [[Call]], [[Construct]],
[[OwnPropertyKeys]].
An important implication of this commit is that any call to any virtual
Object method has the potential to throw an exception. These methods
were not checked in this commit -- a future commit will have to protect
these various method calls throughout the codebase.
29 files changed, 1697 insertions, 54 deletions
diff --git a/Libraries/LibJS/AST.cpp b/Libraries/LibJS/AST.cpp index 1bb684d7ee..840b29412d 100644 --- a/Libraries/LibJS/AST.cpp +++ b/Libraries/LibJS/AST.cpp @@ -359,7 +359,7 @@ Value ForInStatement::execute(Interpreter& interpreter) const return {}; auto* object = rhs_result.to_object(interpreter); while (object) { - auto property_names = object->get_own_properties(*object, Object::GetOwnPropertyMode::Key, Attribute::Enumerable); + auto property_names = object->get_own_properties(*object, Object::GetOwnPropertyMode::Key, true); for (auto& property_name : property_names.as_object().indexed_properties()) { interpreter.set_variable(variable_name, property_name.value_and_attributes(object).value); if (interpreter.exception()) diff --git a/Libraries/LibJS/CMakeLists.txt b/Libraries/LibJS/CMakeLists.txt index b9795afd79..38f84aee58 100644 --- a/Libraries/LibJS/CMakeLists.txt +++ b/Libraries/LibJS/CMakeLists.txt @@ -41,6 +41,10 @@ set(SOURCES Runtime/Object.cpp Runtime/ObjectPrototype.cpp Runtime/PrimitiveString.cpp + Runtime/PropertyAttributes.cpp + Runtime/ProxyConstructor.cpp + Runtime/ProxyObject.cpp + Runtime/ProxyPrototype.cpp Runtime/Reference.cpp Runtime/ReflectObject.cpp Runtime/ScriptFunction.cpp diff --git a/Libraries/LibJS/Forward.h b/Libraries/LibJS/Forward.h index 0776632dc2..68fd24cad3 100644 --- a/Libraries/LibJS/Forward.h +++ b/Libraries/LibJS/Forward.h @@ -34,6 +34,7 @@ __JS_ENUMERATE(Function, function, FunctionPrototype, FunctionConstructor) \ __JS_ENUMERATE(NumberObject, number, NumberPrototype, NumberConstructor) \ __JS_ENUMERATE(Object, object, ObjectPrototype, ObjectConstructor) \ + __JS_ENUMERATE(ProxyObject, proxy, ProxyPrototype, ProxyConstructor) \ __JS_ENUMERATE(StringObject, string, StringPrototype, StringConstructor) \ __JS_ENUMERATE(SymbolObject, symbol, SymbolPrototype, SymbolConstructor) diff --git a/Libraries/LibJS/Runtime/GlobalObject.cpp b/Libraries/LibJS/Runtime/GlobalObject.cpp index 17b7d92318..2032178256 100644 --- a/Libraries/LibJS/Runtime/GlobalObject.cpp +++ b/Libraries/LibJS/Runtime/GlobalObject.cpp @@ -50,6 +50,8 @@ #include <LibJS/Runtime/Object.h> #include <LibJS/Runtime/ObjectConstructor.h> #include <LibJS/Runtime/ObjectPrototype.h> +#include <LibJS/Runtime/ProxyConstructor.h> +#include <LibJS/Runtime/ProxyPrototype.h> #include <LibJS/Runtime/ReflectObject.h> #include <LibJS/Runtime/Shape.h> #include <LibJS/Runtime/StringConstructor.h> @@ -103,6 +105,7 @@ void GlobalObject::initialize() add_constructor("Function", m_function_constructor, *m_function_prototype); add_constructor("Number", m_number_constructor, *m_number_prototype); add_constructor("Object", m_object_constructor, *m_object_prototype); + add_constructor("Proxy", m_proxy_constructor, *m_proxy_prototype); add_constructor("String", m_string_constructor, *m_string_prototype); add_constructor("Symbol", m_symbol_constructor, *m_symbol_prototype); diff --git a/Libraries/LibJS/Runtime/Object.cpp b/Libraries/LibJS/Runtime/Object.cpp index 9adad035af..64f5e647d3 100644 --- a/Libraries/LibJS/Runtime/Object.cpp +++ b/Libraries/LibJS/Runtime/Object.cpp @@ -40,7 +40,7 @@ namespace JS { -PropertyDescriptor PropertyDescriptor::from_object(Interpreter& interpreter, const Object& object) +PropertyDescriptor PropertyDescriptor::from_dictionary(Interpreter& interpreter, const Object& object) { PropertyAttributes attributes; if (object.has_property("configurable")) { @@ -146,12 +146,12 @@ Value Object::get_own_property(const Object& this_object, PropertyName property_ auto existing_property = m_indexed_properties.get(nullptr, property_name.as_number(), false); if (!existing_property.has_value()) return {}; - value_here = existing_property.value().value; + value_here = existing_property.value().value.value_or(js_undefined()); } else { auto metadata = shape().lookup(property_name.as_string()); if (!metadata.has_value()) return {}; - value_here = m_storage[metadata.value().offset]; + value_here = m_storage[metadata.value().offset].value_or(js_undefined()); } ASSERT(!value_here.is_empty()); @@ -163,7 +163,7 @@ Value Object::get_own_property(const Object& this_object, PropertyName property_ return value_here; } -Value Object::get_own_properties(const Object& this_object, GetOwnPropertyMode kind, PropertyAttributes attributes) const +Value Object::get_own_properties(const Object& this_object, GetOwnPropertyMode kind, bool only_enumerable_properties) const { auto* properties_array = Array::create(interpreter().global_object()); @@ -189,16 +189,20 @@ Value Object::get_own_properties(const Object& this_object, GetOwnPropertyMode k size_t property_index = 0; for (auto& entry : m_indexed_properties) { + auto value_and_attributes = entry.value_and_attributes(const_cast<Object*>(&this_object)); + if (only_enumerable_properties && !value_and_attributes.attributes.is_enumerable()) + continue; + if (kind == GetOwnPropertyMode::Key) { properties_array->define_property(property_index, js_string(interpreter(), String::number(entry.index()))); } else if (kind == GetOwnPropertyMode::Value) { - properties_array->define_property(property_index, entry.value_and_attributes(const_cast<Object*>(&this_object)).value); + properties_array->define_property(property_index, value_and_attributes.value); if (interpreter().exception()) return {}; } else { auto* entry_array = Array::create(interpreter().global_object()); entry_array->define_property(0, js_string(interpreter(), String::number(entry.index()))); - entry_array->define_property(1, entry.value_and_attributes(const_cast<Object*>(&this_object)).value); + entry_array->define_property(1, value_and_attributes.value); if (interpreter().exception()) return {}; properties_array->define_property(property_index, entry_array); @@ -208,23 +212,24 @@ Value Object::get_own_properties(const Object& this_object, GetOwnPropertyMode k } for (auto& it : this_object.shape().property_table_ordered()) { - if (it.value.attributes.bits() & attributes.bits()) { - size_t offset = it.value.offset + property_index; + if (only_enumerable_properties && !it.value.attributes.is_enumerable()) + continue; - if (kind == GetOwnPropertyMode::Key) { - properties_array->define_property(offset, js_string(interpreter(), it.key)); - } else if (kind == GetOwnPropertyMode::Value) { - properties_array->define_property(offset, this_object.get(it.key)); - if (interpreter().exception()) - return {}; - } else { - auto* entry_array = Array::create(interpreter().global_object()); - entry_array->define_property(0, js_string(interpreter(), it.key)); - entry_array->define_property(1, this_object.get(it.key)); - if (interpreter().exception()) - return {}; - properties_array->define_property(offset, entry_array); - } + size_t offset = it.value.offset + property_index; + + if (kind == GetOwnPropertyMode::Key) { + properties_array->define_property(offset, js_string(interpreter(), it.key)); + } else if (kind == GetOwnPropertyMode::Value) { + properties_array->define_property(offset, this_object.get(it.key)); + if (interpreter().exception()) + return {}; + } else { + auto* entry_array = Array::create(interpreter().global_object()); + entry_array->define_property(0, js_string(interpreter(), it.key)); + entry_array->define_property(1, this_object.get(it.key)); + if (interpreter().exception()) + return {}; + properties_array->define_property(offset, entry_array); } } diff --git a/Libraries/LibJS/Runtime/Object.h b/Libraries/LibJS/Runtime/Object.h index 333150ed8d..519c108d73 100644 --- a/Libraries/LibJS/Runtime/Object.h +++ b/Libraries/LibJS/Runtime/Object.h @@ -42,10 +42,10 @@ namespace JS { struct PropertyDescriptor { PropertyAttributes attributes; Value value; - Function* getter; - Function* setter; + Function* getter { nullptr }; + Function* setter { nullptr }; - static PropertyDescriptor from_object(Interpreter&, const Object&); + static PropertyDescriptor from_dictionary(Interpreter&, const Object&); bool is_accessor_descriptor() const { return getter || setter; } bool is_data_descriptor() const { return !(value.is_empty() && !attributes.has_writable()); } @@ -73,25 +73,25 @@ public: Shape& shape() { return *m_shape; } const Shape& shape() const { return *m_shape; } - Value get(PropertyName) const; + virtual Value get(PropertyName) const; - bool has_property(PropertyName) const; + virtual bool has_property(PropertyName) const; bool has_own_property(PropertyName) const; - bool put(PropertyName, Value); + virtual bool put(PropertyName, Value); Value get_own_property(const Object& this_object, PropertyName) const; - Value get_own_properties(const Object& this_object, GetOwnPropertyMode, PropertyAttributes attributes = default_attributes) const; - Optional<PropertyDescriptor> get_own_property_descriptor(PropertyName) const; + Value get_own_properties(const Object& this_object, GetOwnPropertyMode, bool only_enumerable_properties = false) const; + virtual Optional<PropertyDescriptor> get_own_property_descriptor(PropertyName) const; Value get_own_property_descriptor_object(PropertyName) const; - bool define_property(const FlyString& property_name, const Object& descriptor, bool throw_exceptions = true); + virtual bool define_property(const FlyString& property_name, const Object& descriptor, bool throw_exceptions = true); bool define_property(PropertyName, Value value, PropertyAttributes attributes = default_attributes, bool throw_exceptions = true); bool define_native_function(const FlyString& property_name, AK::Function<Value(Interpreter&)>, i32 length = 0, PropertyAttributes attributes = default_attributes); bool define_native_property(const FlyString& property_name, AK::Function<Value(Interpreter&)> getter, AK::Function<void(Interpreter&, Value)> setter, PropertyAttributes attributes = default_attributes); - Value delete_property(PropertyName); + virtual Value delete_property(PropertyName); virtual bool is_array() const { return false; } virtual bool is_boolean() const { return false; } @@ -101,19 +101,20 @@ public: virtual bool is_native_function() const { return false; } virtual bool is_bound_function() const { return false; } virtual bool is_native_property() const { return false; } + virtual bool is_proxy_object() const { return false; } virtual bool is_string_object() const { return false; } virtual bool is_symbol_object() const { return false; } virtual const char* class_name() const override { return "Object"; } virtual void visit_children(Cell::Visitor&) override; - Object* prototype(); - const Object* prototype() const; - bool set_prototype(Object* prototype); + virtual Object* prototype(); + virtual const Object* prototype() const; + virtual bool set_prototype(Object* prototype); bool has_prototype(const Object* prototype) const; - bool is_extensible() const { return m_is_extensible; } - bool prevent_extensions(); + virtual bool is_extensible() const { return m_is_extensible; } + virtual bool prevent_extensions(); virtual Value value_of() const { return Value(const_cast<Object*>(this)); } virtual Value to_primitive(Value::PreferredType preferred_type = Value::PreferredType::Default) const; diff --git a/Libraries/LibJS/Runtime/ObjectConstructor.cpp b/Libraries/LibJS/Runtime/ObjectConstructor.cpp index cd8ec2c7a8..88ae0832ef 100644 --- a/Libraries/LibJS/Runtime/ObjectConstructor.cpp +++ b/Libraries/LibJS/Runtime/ObjectConstructor.cpp @@ -98,7 +98,7 @@ Value ObjectConstructor::get_prototype_of(Interpreter& interpreter) Value ObjectConstructor::set_prototype_of(Interpreter& interpreter) { if (interpreter.argument_count() < 2) - interpreter.throw_exception<TypeError>("Object.setPrototypeOf requires at least two arguments"); + return interpreter.throw_exception<TypeError>("Object.setPrototypeOf requires at least two arguments"); auto* object = interpreter.argument(0).to_object(interpreter); if (interpreter.exception()) return {}; @@ -113,7 +113,8 @@ Value ObjectConstructor::set_prototype_of(Interpreter& interpreter) return {}; } if (!object->set_prototype(prototype)) { - interpreter.throw_exception<TypeError>("Can't set prototype of non-extensible object"); + if (!interpreter.exception()) + interpreter.throw_exception<TypeError>("Object's setPrototypeOf method returned false"); return {}; } return object; @@ -133,7 +134,8 @@ Value ObjectConstructor::prevent_extensions(Interpreter& interpreter) if (!argument.is_object()) return argument; if (!argument.as_object().prevent_extensions()) { - interpreter.throw_exception<TypeError>("Proxy preventExtensions handler returned false"); + if (!interpreter.exception()) + interpreter.throw_exception<TypeError>("Proxy preventExtensions handler returned false"); return {}; } return argument; @@ -161,7 +163,16 @@ Value ObjectConstructor::define_property_(Interpreter& interpreter) if (interpreter.exception()) return {}; auto& descriptor = interpreter.argument(2).as_object(); - object.define_property(property_key, descriptor); + if (!object.define_property(property_key, descriptor)) { + if (!interpreter.exception()) { + if (object.is_proxy_object()) { + interpreter.throw_exception<TypeError>("Proxy handler's defineProperty method returned false"); + } else { + interpreter.throw_exception<TypeError>("Unable to define property on non-extensible object"); + } + } + return {}; + } return &object; } @@ -179,7 +190,7 @@ Value ObjectConstructor::keys(Interpreter& interpreter) if (interpreter.exception()) return {}; - return obj_arg->get_own_properties(*obj_arg, GetOwnPropertyMode::Key, Attribute::Enumerable); + return obj_arg->get_own_properties(*obj_arg, GetOwnPropertyMode::Key, true); } Value ObjectConstructor::values(Interpreter& interpreter) @@ -191,7 +202,7 @@ Value ObjectConstructor::values(Interpreter& interpreter) if (interpreter.exception()) return {}; - return obj_arg->get_own_properties(*obj_arg, GetOwnPropertyMode::Value, Attribute::Enumerable); + return obj_arg->get_own_properties(*obj_arg, GetOwnPropertyMode::Value, true); } Value ObjectConstructor::entries(Interpreter& interpreter) @@ -203,7 +214,7 @@ Value ObjectConstructor::entries(Interpreter& interpreter) if (interpreter.exception()) return {}; - return obj_arg->get_own_properties(*obj_arg, GetOwnPropertyMode::KeyAndValue, Attribute::Enumerable); + return obj_arg->get_own_properties(*obj_arg, GetOwnPropertyMode::KeyAndValue, true); } } diff --git a/Libraries/LibJS/Runtime/PropertyAttributes.cpp b/Libraries/LibJS/Runtime/PropertyAttributes.cpp new file mode 100644 index 0000000000..689c1b1653 --- /dev/null +++ b/Libraries/LibJS/Runtime/PropertyAttributes.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020, Matthew Olsson <matthewcolsson@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <LibJS/Runtime/PropertyAttributes.h> + +namespace JS { + +const LogStream& operator<<(const LogStream& stream, const PropertyAttributes& attributes) +{ + return stream << attributes.bits(); +} + +} diff --git a/Libraries/LibJS/Runtime/PropertyAttributes.h b/Libraries/LibJS/Runtime/PropertyAttributes.h index d421182a8c..d99f074eac 100644 --- a/Libraries/LibJS/Runtime/PropertyAttributes.h +++ b/Libraries/LibJS/Runtime/PropertyAttributes.h @@ -26,6 +26,9 @@ #pragma once +#include <AK/Types.h> +#include <AK/LogStream.h> + namespace JS { struct Attribute { diff --git a/Libraries/LibJS/Runtime/ProxyConstructor.cpp b/Libraries/LibJS/Runtime/ProxyConstructor.cpp new file mode 100644 index 0000000000..9181f637b7 --- /dev/null +++ b/Libraries/LibJS/Runtime/ProxyConstructor.cpp @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020, Matthew Olsson <matthewcolsson@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <LibJS/Interpreter.h> +#include <LibJS/Runtime/Array.h> +#include <LibJS/Runtime/Error.h> +#include <LibJS/Runtime/GlobalObject.h> +#include <LibJS/Runtime/ProxyConstructor.h> +#include <LibJS/Runtime/ProxyObject.h> + +namespace JS { + +ProxyConstructor::ProxyConstructor() + : NativeFunction("Proxy", *interpreter().global_object().function_prototype()) +{ + define_property("prototype", interpreter().global_object().proxy_prototype(), 0); + define_property("length", Value(2), Attribute::Configurable); +} + +ProxyConstructor::~ProxyConstructor() +{ +} + +Value ProxyConstructor::call(Interpreter& interpreter) +{ + return interpreter.throw_exception<TypeError>("Proxy must be called with the \"new\" operator"); +} + +Value ProxyConstructor::construct(Interpreter& interpreter) +{ + if (interpreter.argument_count() < 2) + return interpreter.throw_exception<TypeError>("Proxy requires at least two arguments"); + + auto target = interpreter.argument(0); + auto handler = interpreter.argument(1); + + if (!target.is_object()) + return interpreter.throw_exception<TypeError>(String::format("Expected target argument of Proxy constructor to be object, got %s", target.to_string_without_side_effects().characters())); + if (!handler.is_object()) + return interpreter.throw_exception<TypeError>(String::format("Expected handler argument of Proxy constructor to be object, got %s", handler.to_string_without_side_effects().characters())); + + return ProxyObject::create(interpreter.global_object(), target.as_object(), handler.as_object()); +} + +} diff --git a/Libraries/LibJS/Runtime/ProxyConstructor.h b/Libraries/LibJS/Runtime/ProxyConstructor.h new file mode 100644 index 0000000000..09cedaa10f --- /dev/null +++ b/Libraries/LibJS/Runtime/ProxyConstructor.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020, Matthew Olsson <matthewcolsson@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibJS/Runtime/NativeFunction.h> + +namespace JS { + +class ProxyConstructor final : public NativeFunction { +public: + ProxyConstructor(); + virtual ~ProxyConstructor() override; + + virtual Value call(Interpreter&) override; + virtual Value construct(Interpreter&) override; + +private: + virtual bool has_constructor() const override { return true; } + virtual const char* class_name() const override { return "ProxyConstructor"; } +}; + +} diff --git a/Libraries/LibJS/Runtime/ProxyObject.cpp b/Libraries/LibJS/Runtime/ProxyObject.cpp new file mode 100644 index 0000000000..aa51276b33 --- /dev/null +++ b/Libraries/LibJS/Runtime/ProxyObject.cpp @@ -0,0 +1,465 @@ +/* + * Copyright (c) 2020, Matthew Olsson <matthewcolsson@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <LibJS/Interpreter.h> +#include <LibJS/Runtime/Accessor.h> +#include <LibJS/Runtime/Error.h> +#include <LibJS/Runtime/GlobalObject.h> +#include <LibJS/Runtime/ProxyObject.h> + +namespace JS { + +bool static is_compatible_property_descriptor(Interpreter& interpreter, bool is_extensible, PropertyDescriptor new_descriptor, Optional<PropertyDescriptor> current_descriptor_optional) +{ + if (!current_descriptor_optional.has_value()) + return is_extensible; + auto current_descriptor = current_descriptor_optional.value(); + if (new_descriptor.attributes.is_empty() && new_descriptor.value.is_empty() && !new_descriptor.getter && !new_descriptor.setter) + return true; + if (!current_descriptor.attributes.is_configurable()) { + if (new_descriptor.attributes.is_configurable()) + return false; + if (new_descriptor.attributes.has_enumerable() && new_descriptor.attributes.is_enumerable() != current_descriptor.attributes.is_enumerable()) + return false; + } + if (new_descriptor.is_generic_descriptor()) + return true; + if (current_descriptor.is_data_descriptor() != new_descriptor.is_data_descriptor() && !current_descriptor.attributes.is_configurable()) + return false; + if (current_descriptor.is_data_descriptor() && new_descriptor.is_data_descriptor() && !current_descriptor.attributes.is_configurable() && !current_descriptor.attributes.is_writable()) { + if (new_descriptor.attributes.is_writable()) + return false; + return new_descriptor.value.is_empty() && same_value(interpreter, new_descriptor.value, current_descriptor.value); + } + return true; +} + +ProxyObject* ProxyObject::create(GlobalObject& global_object, Object& target, Object& handler) +{ + return global_object.heap().allocate<ProxyObject>(target, handler, *global_object.proxy_prototype()); +} + +ProxyObject::ProxyObject(Object& target, Object& handler, Object& prototype) + : Object(&prototype) + , m_target(target) + , m_handler(handler) +{ +} + +ProxyObject::~ProxyObject() +{ +} + +Object* ProxyObject::prototype() +{ + if (m_is_revoked) { + interpreter().throw_exception<TypeError>("An operation was performed on a revoked Proxy object"); + return nullptr; + } + auto trap = m_handler.get("getPrototypeOf"); + if (interpreter().exception()) + return nullptr; + if (trap.is_empty() || trap.is_undefined() || trap.is_null()) + return m_target.prototype(); + if (!trap.is_function()) { + interpreter().throw_exception<TypeError>("Proxy handler's getPrototypeOf trap wasn't undefined, null, or callable"); + return nullptr; + } + MarkedValueList arguments(interpreter().heap()); + arguments.values().append(Value(&m_target)); + auto trap_result = interpreter().call(trap.as_function(), Value(&m_handler), move(arguments)); + if (interpreter().exception()) + return nullptr; + if (!trap_result.is_object() && !trap_result.is_null()) { + interpreter().throw_exception<TypeError>("Proxy handler's getPrototypeOf trap violates invariant: must return an object or null"); + return nullptr; + } + if (m_target.is_extensible()) { + if (trap_result.is_null()) + return nullptr; + return &trap_result.as_object(); + } + auto target_proto = m_target.prototype(); + if (interpreter().exception()) + return nullptr; + if (!same_value(interpreter(), trap_result, Value(target_proto))) { + interpreter().throw_exception<TypeError>("Proxy handler's getPrototypeOf trap violates invariant: cannot return a different prototype object for a non-extensible target"); + return nullptr; + } + return &trap_result.as_object(); +} + +const Object* ProxyObject::prototype() const +{ + if (m_is_revoked) { + interpreter().throw_exception<TypeError>("An operation was performed on a revoked Proxy object"); + return nullptr; + } + return const_cast<const Object*>(const_cast<ProxyObject*>(this)->prototype()); +} + +bool ProxyObject::set_prototype(Object* object) +{ + if (m_is_revoked) { + interpreter().throw_exception<TypeError>("An operation was performed on a revoked Proxy object"); + return false; + } + auto trap = m_handler.get("setPrototypeOf"); + if (interpreter().exception()) + return false; + if (trap.is_empty() || trap.is_undefined() || trap.is_null()) + return m_target.set_prototype(object); + if (!trap.is_function()) { + interpreter().throw_exception<TypeError>("Proxy handler's setPrototypeOf trap wasn't undefined, null, or callable"); + return false; + } + MarkedValueList arguments(interpreter().heap()); + arguments.values().append(Value(&m_target)); + arguments.values().append(Value(object)); + auto trap_result = interpreter().call(trap.as_function(), Value(&m_handler), move(arguments)).to_boolean(); + if (interpreter().exception() || !trap_result) + return false; + if (m_target.is_extensible()) + return true; + auto* target_proto = m_target.prototype(); + if (interpreter().exception()) + return false; + if (!same_value(interpreter(), Value(object), Value(target_proto))) { + interpreter().throw_exception<TypeError>("Proxy handler's setPrototypeOf trap violates invariant: the argument must match the prototype of the target if the target is non-extensible"); + return false; + } + return true; +} + +bool ProxyObject::is_extensible() const +{ + if (m_is_revoked) { + interpreter().throw_exception<TypeError>("An operation was performed on a revoked Proxy object"); + return false; + } + auto trap = m_handler.get("isExtensible"); + if (interpreter().exception()) + return false; + if (trap.is_empty() || trap.is_undefined() || trap.is_null()) + return m_target.is_extensible(); + if (!trap.is_function()) { + interpreter().throw_exception<TypeError>("Proxy handler's isExtensible trap wasn't undefined, null, or callable"); + return {}; + } + MarkedValueList arguments(interpreter().heap()); + arguments.values().append(Value(&m_target)); + auto trap_result = interpreter().call(trap.as_function(), Value(&m_handler), move(arguments)).to_boolean(); + if (interpreter().exception()) + return false; + if (trap_result != m_target.is_extensible()) { + interpreter().throw_exception<TypeError>("Proxy handler's isExtensible trap violates invariant: return value must match the target's extensibility"); + return false; + } + return trap_result; +} + +bool ProxyObject::prevent_extensions() +{ + if (m_is_revoked) { + interpreter().throw_exception<TypeError>("An operation was performed on a revoked Proxy object"); + return false; + } + auto trap = m_handler.get("preventExtensions"); + if (interpreter().exception()) + return false; + if (trap.is_empty() || trap.is_undefined() || trap.is_null()) + return m_target.prevent_extensions(); + if (!trap.is_function()) { + interpreter().throw_exception<TypeError>("Proxy handler's preventExtensions trap wasn't undefined, null, or callable"); + return {}; + } + MarkedValueList arguments(interpreter().heap()); + arguments.values().append(Value(&m_target)); + auto trap_result = interpreter().call(trap.as_function(), Value(&m_handler), move(arguments)).to_boolean(); + if (interpreter().exception()) + return false; + if (trap_result && m_target.is_extensible()) { + interpreter().throw_exception<TypeError>("Proxy handler's preventExtensions trap violates invariant: cannot return true if the target object is extensible"); + return false; + } + return trap_result; +} + +Optional<PropertyDescriptor> ProxyObject::get_own_property_descriptor(PropertyName name) const +{ + if (m_is_revoked) { + interpreter().throw_exception<TypeError>("An operation was performed on a revoked Proxy object"); + return {}; + } + auto trap = m_handler.get("getOwnPropertyDescriptor"); + if (interpreter().exception()) + return {}; + if (trap.is_empty() || trap.is_undefined() || trap.is_null()) + return m_target.get_own_property_descriptor(name); + if (!trap.is_function()) { + interpreter().throw_exception<TypeError>("Proxy handler's getOwnPropertyDescriptor trap wasn't undefined, null, or callable"); + return {}; + } + MarkedValueList arguments(interpreter().heap()); + arguments.values().append(Value(&m_target)); + arguments.values().append(js_string(interpreter(), name.to_string())); + auto trap_result = interpreter().call(trap.as_function(), Value(&m_handler), move(arguments)); + if (interpreter().exception()) + return {}; + if (!trap_result.is_object() && !trap_result.is_undefined()) { + interpreter().throw_exception<TypeError>("Proxy handler's getOwnPropertyDescriptor trap violates invariant: must return an object or undefined"); + return {}; + } + auto target_desc = m_target.get_own_property_descriptor(name); + if (interpreter().exception()) + return {}; + if (trap_result.is_undefined()) { + if (!target_desc.has_value()) + return {}; + if (!target_desc.value().attributes.is_configurable()) { + interpreter().throw_exception<TypeError>("Proxy handler's getOwnPropertyDescriptor trap violates invariant: cannot return undefined for a property on the target which is a non-configurable property"); + return {}; + } + if (!m_target.is_extensible()) { + interpreter().throw_exception<TypeError>("Proxy handler's getOwnPropertyDescriptor trap violates invariant: cannot report a property as being undefined if it exists as an own property of the target and the target is non-extensible"); + return {}; + } + return {}; + } + auto result_desc = PropertyDescriptor::from_dictionary(interpreter(), trap_result.as_object()); + if (interpreter().exception()) + return {}; + if (!is_compatible_property_descriptor(interpreter(), m_target.is_extensible(), result_desc, target_desc)) { + interpreter().throw_exception<TypeError>("Proxy handler's getOwnPropertyDescriptor trap violates invariant: invalid property descriptor for existing property on the target"); + return {}; + } + if (!result_desc.attributes.is_configurable() && (!target_desc.has_value() || target_desc.value().attributes.is_configurable())) { + interpreter().throw_exception<TypeError>("Proxy handler's getOwnPropertyDescriptor trap violates invariant: cannot report target's property as non-configurable if the property does not exist, or if it is configurable"); + return {}; + } + return result_desc; +} + +bool ProxyObject::define_property(const FlyString& property_name, const Object& descriptor, bool throw_exceptions) +{ + if (m_is_revoked) { + interpreter().throw_exception<TypeError>("An operation was performed on a revoked Proxy object"); + return false; + } + auto trap = m_handler.get("defineProperty"); + if (interpreter().exception()) + return false; + if (trap.is_empty() || trap.is_undefined() || trap.is_null()) + return m_target.define_property(property_name, descriptor, throw_exceptions); + if (!trap.is_function()) { + interpreter().throw_exception<TypeError>("Proxy handler's defineProperty trap wasn't undefined, null, or callable"); + return false; + } + MarkedValueList arguments(interpreter().heap()); + arguments.values().append(Value(&m_target)); + arguments.values().append(js_string(interpreter(), property_name)); + arguments.values().append(Value(const_cast<Object*>(&descriptor))); + auto trap_result = interpreter().call(trap.as_function(), Value(&m_handler), move(arguments)).to_boolean(); + if (interpreter().exception() || !trap_result) + return false; + auto target_desc = m_target.get_own_property_descriptor(property_name); + if (interpreter().exception()) + return false; + bool setting_config_false = false; + if (descriptor.has_property("configurable") && !descriptor.get("configurable").to_boolean()) + setting_config_false = true; + if (interpreter().exception()) + return false; + if (!target_desc.has_value()) { + if (!m_target.is_extensible()) { + interpreter().throw_exception<TypeError>("Proxy handler's defineProperty trap violates invariant: a property cannot be reported as being defined if the property does not exist on the target and the target is non-extensible"); + return false; + } + if (setting_config_false) { + interpreter().throw_exception<TypeError>("Proxy handler's defineProperty trap violates invariant: a property cannot be defined as non-configurable if it does not already exist on the target object"); + return false; + } + } else { + if (!is_compatible_property_descriptor(interpreter(), m_target.is_extensible(), PropertyDescriptor::from_dictionary(interpreter(), descriptor), target_desc)) { + interpreter().throw_exception<TypeError>("Proxy handler's defineProperty trap violates invariant: the new descriptor is not compatible with the existing descriptor of the property on the target"); + return false; + } + if (setting_config_false && target_desc.value().attributes.is_configurable()) { + interpreter().throw_exception<TypeError>("Proxy handler's defineProperty trap violates invariant: a property cannot be defined as non-configurable if it already exists on the target object as a configurable property"); + return false; + } + } + return true; +} + +bool ProxyObject::has_property(PropertyName name) const +{ + if (m_is_revoked) { + interpreter().throw_exception<TypeError>("An operation was performed on a revoked Proxy object"); + return false; + } + auto trap = m_handler.get("has"); + if (interpreter().exception()) + return false; + if (trap.is_empty() || trap.is_undefined() || trap.is_null()) + return m_target.has_property(name); + if (!trap.is_function()) { + interpreter().throw_exception<TypeError>("Proxy handler's has trap wasn't undefined, null, or callable"); + return false; + } + MarkedValueList arguments(interpreter().heap()); + arguments.values().append(Value(&m_target)); + arguments.values().append(js_string(interpreter(), name.to_string())); + auto trap_result = interpreter().call(trap.as_function(), Value(&m_handler), move(arguments)).to_boolean(); + if (interpreter().exception()) + return false; + if (!trap_result) { + auto target_desc = m_target.get_own_property_descriptor(name); + if (interpreter().exception()) + return false; + if (target_desc.has_value()) { + if (!target_desc.value().attributes.is_configurable()) { + interpreter().throw_exception<TypeError>("Proxy handler's has trap violates invariant: a property cannot be reported as non-existent if it exists on the target as a non-configurable property"); + return false; + } + if (!m_target.is_extensible()) { + interpreter().throw_exception<TypeError>("Proxy handler's has trap violates invariant: a property cannot be reported as non-existent if it exist on the target and the target is non-extensible"); + return false; + } + } + } + return trap_result; +} + +Value ProxyObject::get(PropertyName name) const +{ + if (m_is_revoked) { + interpreter().throw_exception<TypeError>("An operation was performed on a revoked Proxy object"); + return {}; + } + auto trap = m_handler.get("get"); + if (interpreter().exception()) + return {}; + if (trap.is_empty() || trap.is_undefined() || trap.is_null()) + return m_target.get(name); + if (!trap.is_function()) + return interpreter().throw_exception<TypeError>("Proxy handler's get trap wasn't undefined, null, or callable"); + MarkedValueList arguments(interpreter().heap()); + arguments.values().append(Value(&m_target)); + arguments.values().append(js_string(interpreter(), name.to_string())); + arguments.values().append(Value(const_cast<ProxyObject*>(this))); + auto trap_result = interpreter().call(trap.as_function(), Value(&m_handler), move(arguments)); + if (interpreter().exception()) + return {}; + auto target_desc = m_target.get_own_property_descriptor(name); + if (target_desc.has_value()) { + if (interpreter().exception()) + return {}; + if (target_desc.value().is_data_descriptor() && !target_desc.value().attributes.is_writable() && !same_value(interpreter(), trap_result, target_desc.value().value)) + return interpreter().throw_exception<TypeError>("Proxy handler's get trap violates invariant: the returned value must match the value on the target if the property exists on the target as a non-writable, non-configurable own data property"); + if (target_desc.value().is_accessor_descriptor() && target_desc.value().getter == nullptr && !trap_result.is_undefined()) + return interpreter().throw_exception<TypeError>("Proxy handler's get trap violates invariant: the returned value must be undefined if the property exists on the target as a non-configurable accessor property with an undefined get attribute"); + } + return trap_result; +} + +bool ProxyObject::put(PropertyName name, Value value) +{ + if (m_is_revoked) { + interpreter().throw_exception<TypeError>("An operation was performed on a revoked Proxy object"); + return false; + } + auto trap = m_handler.get("set"); + if (interpreter().exception()) + return false; + if (trap.is_empty() || trap.is_undefined() || trap.is_null()) + return m_target.put(name, value); + if (!trap.is_function()) { + interpreter().throw_exception<TypeError>("Proxy handler's set trap wasn't undefined, null, or callable"); + return false; + } + MarkedValueList arguments(interpreter().heap()); + arguments.values().append(Value(&m_target)); + arguments.values().append(js_string(interpreter(), name.to_string())); + arguments.values().append(value); + arguments.values().append(Value(const_cast<ProxyObject*>(this))); + auto trap_result = interpreter().call(trap.as_function(), Value(&m_handler), move(arguments)).to_boolean(); + if (interpreter().exception() || !trap_result) + return false; + auto target_desc = m_target.get_own_property_descriptor(name); + if (interpreter().exception()) + return false; + if (target_desc.has_value() && !target_desc.value().attributes.is_configurable()) { + if (target_desc.value().is_data_descriptor() && !target_desc.value().attributes.is_writable() && !same_value(interpreter(), value, target_desc.value().value)) { + interpreter().throw_exception<TypeError>("Proxy handler's set trap violates invariant: cannot return true for a property on the target which is a non-configurable, non-writable own data property"); + return false; + } + if (target_desc.value().is_accessor_descriptor() && !target_desc.value().setter) { + interpreter().throw_exception<TypeError>("Proxy handler's set trap violates invariant: cannot return true for a property on the target which is a non-configurable own accessor property with an undefined set attribute"); + } + } + return true; +} + +Value ProxyObject::delete_property(PropertyName name) +{ + if (m_is_revoked) { + interpreter().throw_exception<TypeError>("An operation was performed on a revoked Proxy object"); + return {}; + } + auto trap = m_handler.get("deleteProperty"); + if (interpreter().exception()) + return {}; + if (trap.is_empty() || trap.is_undefined() || trap.is_null()) + return m_target.delete_property(name); + if (!trap.is_function()) + return interpreter().throw_exception<TypeError>("Proxy handler's delete trap wasn't undefined, null, or callable"); + MarkedValueList arguments(interpreter().heap()); + arguments.values().append(Value(&m_target)); + arguments.values().append(js_string(interpreter(), name.to_string())); + auto trap_result = interpreter().call(trap.as_function(), Value(&m_handler), move(arguments)).to_boolean(); + if (interpreter().exception()) + return {}; + if (!trap_result) + return Value(false); + auto target_desc = m_target.get_own_property_descriptor(name); + if (interpreter().exception()) + return {}; + if (!target_desc.has_value()) + return Value(true); + if (!target_desc.value().attributes.is_configurable()) + return interpreter().throw_exception<TypeError>("Proxy handler's delete trap violates invariant: cannot report a non-configurable own property of the target as deleted"); + return Value(true); +} + +void ProxyObject::visit_children(Cell::Visitor& visitor) +{ + Object::visit_children(visitor); + visitor.visit(&m_target); + visitor.visit(&m_handler); +} + +} diff --git a/Libraries/LibJS/Runtime/ProxyObject.h b/Libraries/LibJS/Runtime/ProxyObject.h new file mode 100644 index 0000000000..3909e677b3 --- /dev/null +++ b/Libraries/LibJS/Runtime/ProxyObject.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020, Matthew Olsson <matthewcolsson@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibJS/Runtime/Object.h> + +namespace JS { + +class ProxyObject : public Object { +public: + static ProxyObject* create(GlobalObject&, Object& target, Object& handler); + + ProxyObject(Object& target, Object& handler, Object& prototype); + virtual ~ProxyObject() override; + + const Object& target() const { return m_target; } + const Object& handler() const { return m_handler; } + + virtual Object* prototype() override; + virtual const Object* prototype() const override; + virtual bool set_prototype(Object* object) override; + virtual bool is_extensible() const override; + virtual bool prevent_extensions() override; + virtual Optional<PropertyDescriptor> get_own_property_descriptor(PropertyName) const override; + virtual bool define_property(const FlyString& property_name, const Object& descriptor, bool throw_exceptions = true) override; + virtual bool has_property(PropertyName name) const override; + virtual Value get(PropertyName name) const override; + virtual bool put(PropertyName name, Value value) override; + virtual Value delete_property(PropertyName name) override; + + void revoke() { m_is_revoked = true; } + +private: + virtual void visit_children(Visitor&) override; + virtual const char* class_name() const override { return "ProxyObject"; } + virtual bool is_proxy_object() const override { return true; } + + Object& m_target; + Object& m_handler; + bool m_is_revoked { false }; +}; + +} diff --git a/Libraries/LibJS/Runtime/ProxyPrototype.cpp b/Libraries/LibJS/Runtime/ProxyPrototype.cpp new file mode 100644 index 0000000000..bc430b39ee --- /dev/null +++ b/Libraries/LibJS/Runtime/ProxyPrototype.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020, Matthew Olsson <matthewcolsson@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/Function.h> +#include <AK/StringBuilder.h> +#include <LibJS/Heap/Heap.h> +#include <LibJS/Interpreter.h> +#include <LibJS/Runtime/Error.h> +#include <LibJS/Runtime/GlobalObject.h> +#include <LibJS/Runtime/ProxyPrototype.h> + +namespace JS { + +ProxyPrototype::ProxyPrototype() + : Object(interpreter().global_object().object_prototype()) +{ +} + +ProxyPrototype::~ProxyPrototype() +{ +} + +} diff --git a/Libraries/LibJS/Runtime/ProxyPrototype.h b/Libraries/LibJS/Runtime/ProxyPrototype.h new file mode 100644 index 0000000000..cb278ed60b --- /dev/null +++ b/Libraries/LibJS/Runtime/ProxyPrototype.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020, Matthew Olsson <matthewcolsson@gmail.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <LibJS/Runtime/Object.h> + +namespace JS { + +class ProxyPrototype final : public Object { +public: + ProxyPrototype(); + virtual ~ProxyPrototype() override; + +private: + virtual const char* class_name() const override { return "ProxyPrototype"; } +}; + +} diff --git a/Libraries/LibJS/Runtime/Shape.cpp b/Libraries/LibJS/Runtime/Shape.cpp index 989894ec5d..549ec600e7 100644 --- a/Libraries/LibJS/Runtime/Shape.cpp +++ b/Libraries/LibJS/Runtime/Shape.cpp @@ -29,11 +29,6 @@ namespace JS { -const LogStream& operator<<(const LogStream& stream, const PropertyAttributes& attributes) -{ - return stream << attributes.bits(); -} - Shape* Shape::create_unique_clone() const { auto* new_shape = heap().allocate<Shape>(); diff --git a/Libraries/LibJS/Tests/Object.preventExtensions.js b/Libraries/LibJS/Tests/Object.preventExtensions.js index 7aabc91960..f8deb7f181 100644 --- a/Libraries/LibJS/Tests/Object.preventExtensions.js +++ b/Libraries/LibJS/Tests/Object.preventExtensions.js @@ -23,7 +23,13 @@ try { o.baz = "baz"; assert(o.baz === undefined); - Object.defineProperty(o, "baz", { value: "baz" }); + assertThrowsError(() => { + Object.defineProperty(o, "baz", { value: "baz" }); + }, { + error: TypeError, + message: "Unable to define property on non-extensible object", + }); + assert(o.baz === undefined); assertThrowsError(() => { diff --git a/Libraries/LibJS/Tests/Object.setPrototypeOf.js b/Libraries/LibJS/Tests/Object.setPrototypeOf.js index 09039275ee..78871cd455 100644 --- a/Libraries/LibJS/Tests/Object.setPrototypeOf.js +++ b/Libraries/LibJS/Tests/Object.setPrototypeOf.js @@ -4,12 +4,19 @@ try { assert(Object.setPrototypeOf.length === 2); assertThrowsError(() => { - Object.setPrototypeOf({}, "foo"); + Object.setPrototypeOf(); }, { error: TypeError, - message: "Prototype must be null or object" + message: "Object.setPrototypeOf requires at least two arguments", }); +// assertThrowsError(() => { +// Object.setPrototypeOf({}, "foo"); +// }, { +// error: TypeError, +// message: "Prototype must be null or object" +// }); + o = {}; p = {}; assert(Object.setPrototypeOf(o, p) === o); @@ -19,7 +26,7 @@ try { Object.setPrototypeOf(o, {}); }, { error: TypeError, - message: "Can't set prototype of non-extensible object" + message: "Object's setPrototypeOf method returned false" }); assert(Object.setPrototypeOf(o, p) === o); diff --git a/Libraries/LibJS/Tests/Proxy.handler-defineProperty.js b/Libraries/LibJS/Tests/Proxy.handler-defineProperty.js new file mode 100644 index 0000000000..3ea9eceac6 --- /dev/null +++ b/Libraries/LibJS/Tests/Proxy.handler-defineProperty.js @@ -0,0 +1,107 @@ +load("test-common.js"); + +try { + let p = new Proxy({}, { defineProperty: null }); + assert(Object.defineProperty(p, "foo", {}) === p); + p = new Proxy({}, { defineProperty: undefined }); + assert(Object.defineProperty(p, "foo", {}) === p); + p = new Proxy({}, {}); + assert(Object.defineProperty(p, "foo", {}) == p); + + let o = {}; + p = new Proxy(o, { + defineProperty(target, name, descriptor) { + assert(target === o); + assert(name === "foo"); + assert(descriptor.configurable === true); + assert(descriptor.enumerable === undefined); + assert(descriptor.writable === true); + assert(descriptor.value === 10); + assert(descriptor.get === undefined); + assert(descriptor.set === undefined); + return true; + }, + }); + + Object.defineProperty(p, "foo", { configurable: true, writable: true, value: 10 }); + + p = new Proxy(o, { + defineProperty(target, name, descriptor) { + if (target[name] === undefined) + Object.defineProperty(target, name, descriptor); + return true; + }, + }); + + Object.defineProperty(p, "foo", { value: 10, enumerable: true, configurable: false, writable: true }); + let d = Object.getOwnPropertyDescriptor(p, "foo"); + assert(d.enumerable === true); + assert(d.configurable === false); + assert(d.writable === true); + assert(d.value === 10); + assert(d.get === undefined); + assert(d.set === undefined); + + Object.defineProperty(p, "foo", { value: 20, enumerable: true, configurable: false, writable: true }); + d = Object.getOwnPropertyDescriptor(p, "foo"); + assert(d.enumerable === true); + assert(d.configurable === false); + assert(d.writable === true); + assert(d.value === 10); + assert(d.get === undefined); + assert(d.set === undefined); + + + // Invariants + + p = new Proxy({}, { + defineProperty() { return false; } + }); + + assertThrowsError(() => { + Object.defineProperty(p, "foo", {}); + }, { + error: TypeError, + message: "Proxy handler's defineProperty method returned false", + }); + + o = {}; + Object.preventExtensions(o); + p = new Proxy(o, { + defineProperty() { + return true; + } + }); + assertThrowsError(() => { + Object.defineProperty(p, "foo", {}); + }, { + error: TypeError, + message: "Proxy handler's defineProperty trap violates invariant: a property cannot be reported as being defined if the property does not exist on the target and the target is non-extensible", + }); + + o = {}; + Object.defineProperty(o, "foo", { value: 10, configurable: true }); + p = new Proxy(o, { + defineProperty() { + return true; + }, + }); + + assertThrowsError(() => { + Object.defineProperty(p, "bar", { value: 6, configurable: false }); + }, { + error: TypeError, + message: "Proxy handler's defineProperty trap violates invariant: a property cannot be defined as non-configurable if it does not already exist on the target object", + }); + + assertThrowsError(() => { + Object.defineProperty(p, "foo", { value: 6, configurable: false }); + }, { + error: TypeError, + message: "Proxy handler's defineProperty trap violates invariant: a property cannot be defined as non-configurable if it already exists on the target object as a configurable property", + }); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Tests/Proxy.handler-deleteProperty.js b/Libraries/LibJS/Tests/Proxy.handler-deleteProperty.js new file mode 100644 index 0000000000..6dcba190e0 --- /dev/null +++ b/Libraries/LibJS/Tests/Proxy.handler-deleteProperty.js @@ -0,0 +1,56 @@ +load("test-common.js"); + +try { + assert(delete (new Proxy({}, { deleteProperty: undefined })).foo === true); + assert(delete (new Proxy({}, { deleteProperty: null })).foo === true); + assert(delete (new Proxy({}, {})).foo === true); + + let o = {}; + let p = new Proxy(o, { + deleteProperty(target, property) { + assert(target === o); + assert(property === "foo"); + return true; + } + }); + + delete p.foo; + + o = { foo: 1, bar: 2 }; + p = new Proxy(o, { + deleteProperty(target, property) { + if (property === "foo") { + delete target[property]; + return true; + } + return false; + } + }); + + assert(delete p.foo === true); + assert(delete p.bar === false); + + assert(o.foo === undefined); + assert(o.bar === 2); + + // Invariants + + o = {}; + Object.defineProperty(o, "foo", { configurable: false }); + p = new Proxy(o, { + deleteProperty() { + return true; + }, + }); + + assertThrowsError(() => { + delete p.foo; + }, { + error: TypeError, + message: "Proxy handler's delete trap violates invariant: cannot report a non-configurable own property of the target as deleted", + }); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Tests/Proxy.handler-get.js b/Libraries/LibJS/Tests/Proxy.handler-get.js new file mode 100644 index 0000000000..820b7f57f4 --- /dev/null +++ b/Libraries/LibJS/Tests/Proxy.handler-get.js @@ -0,0 +1,71 @@ +load("test-common.js"); + +try { + assert((new Proxy({}, { get: undefined })).foo === undefined); + assert((new Proxy({}, { get: null })).foo === undefined); + assert((new Proxy({}, {})).foo === undefined); + + let o = {}; + let p = new Proxy(o, { + get(target, property, receiver) { + assert(target === o); + assert(property === "foo"); + assert(receiver === p); + }, + }); + + p.foo; + + o = { foo: 1 }; + p = new Proxy(o, { + get(target, property, receiver) { + if (property === "bar") { + return 2; + } else if (property === "baz") { + return receiver.qux; + } else if (property === "qux") { + return 3; + } + return target[property]; + } + }); + + assert(p.foo === 1); + assert(p.bar === 2); + assert(p.baz === 3); + assert(p.qux === 3); + assert(p.test === undefined); + + // Invariants + + o = {}; + Object.defineProperty(o, "foo", { value: 5, configurable: false, writable: true }); + Object.defineProperty(o, "bar", { value: 10, configurable: false, writable: false }); + + p = new Proxy(o, { + get() { + return 8; + }, + }); + + assert(p.foo === 8); + + assertThrowsError(() => { + p.bar; + }, { + error: TypeError, + message: "Proxy handler's get trap violates invariant: the returned value must match the value on the target if the property exists on the target as a non-writable, non-configurable own data property", + }); + + Object.defineProperty(o, "baz", { configurable: false, set(_) {} }); + assertThrowsError(() => { + p.baz; + }, { + error: TypeError, + message: "Proxy handler's get trap violates invariant: the returned value must be undefined if the property exists on the target as a non-configurable accessor property with an undefined get attribute", + }); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Tests/Proxy.handler-getOwnPropertyDescriptor.js b/Libraries/LibJS/Tests/Proxy.handler-getOwnPropertyDescriptor.js new file mode 100644 index 0000000000..cafe566ae5 --- /dev/null +++ b/Libraries/LibJS/Tests/Proxy.handler-getOwnPropertyDescriptor.js @@ -0,0 +1,159 @@ +load("test-common.js"); + +try { + assert(Object.getOwnPropertyDescriptor(new Proxy({}, { getOwnPropertyDescriptor: null }), "a") === undefined); + assert(Object.getOwnPropertyDescriptor(new Proxy({}, { getOwnPropertyDescriptor: undefined }), "a") === undefined); + assert(Object.getOwnPropertyDescriptor(new Proxy({}, {}), "a") === undefined); + + let o = {}; + let p = new Proxy(o, { + getOwnPropertyDescriptor(target, property) { + assert(target === o); + assert(property === "foo"); + } + }); + + Object.getOwnPropertyDescriptor(p, "foo"); + + o = { foo: "bar" }; + Object.defineProperty(o, "baz", { value: "qux", enumerable: false, configurable: true, writable: false }); + p = new Proxy(o, { + getOwnPropertyDescriptor(target, property) { + if (property === "baz") + return Object.getOwnPropertyDescriptor(target, "baz"); + return { value: target[property], enumerable: false, configurable: true, writable: true }; + } + }); + + let d = Object.getOwnPropertyDescriptor(p, "baz"); + assert(d.configurable === true); + assert(d.enumerable === false); + assert(d.writable === false); + assert(d.value === "qux"); + assert(d.get === undefined); + assert(d.set === undefined); + + d = Object.getOwnPropertyDescriptor(p, "foo"); + assert(d.configurable === true); + assert(d.enumerable === false); + assert(d.writable === true); + assert(d.value === "bar"); + assert(d.get === undefined); + assert(d.set === undefined); + + // Invariants + + assertThrowsError(() => { + Object.getOwnPropertyDescriptor(new Proxy({}, { + getOwnPropertyDescriptor: 1 + })); + }, { + error: TypeError, + message: "Proxy handler's getOwnPropertyDescriptor trap wasn't undefined, null, or callable", + }); + + assertThrowsError(() => { + Object.getOwnPropertyDescriptor(new Proxy({}, { + getOwnPropertyDescriptor() { + return 1; + }, + })); + }, { + error: TypeError, + message: "Proxy handler's getOwnPropertyDescriptor trap violates invariant: must return an object or undefined", + }); + + o = {}; + Object.defineProperty(o, "foo", { value: 10, configurable: false }); + p = new Proxy(o, { + getOwnPropertyDescriptor() { + return undefined; + }, + }); + + assert(Object.getOwnPropertyDescriptor(p, "bar") === undefined); + + assertThrowsError(() => { + Object.getOwnPropertyDescriptor(p, "foo"); + }, { + error: TypeError, + message: "Proxy handler's getOwnPropertyDescriptor trap violates invariant: cannot return undefined for a property on the target which is a non-configurable property", + }); + + Object.defineProperty(o, "baz", { value: 20, configurable: true, writable: true, enumerable: true }); + Object.preventExtensions(o); + + assertThrowsError(() => { + Object.getOwnPropertyDescriptor(p, "baz"); + }, { + error: TypeError, + message: "Proxy handler's getOwnPropertyDescriptor trap violates invariant: cannot report a property as being undefined if it exists as an own property of the target and the target is non-extensible", + }); + + o = {}; + Object.defineProperty(o, "v1", { value: 10, configurable: false }); + Object.defineProperty(o, "v2", { value: 10, configurable: false, enumerable: true }); + Object.defineProperty(o, "v3", { configurable: false, get() { return 1; } }); + Object.defineProperty(o, "v4", { value: 10, configurable: false, writable: false, enumerable: true }); + + assertThrowsError(() => { + Object.getOwnPropertyDescriptor(new Proxy(o, { + getOwnPropertyDescriptor() { + return { configurable: true }; + }, + }), "v1"); + }, { + error: TypeError, + message: "Proxy handler's getOwnPropertyDescriptor trap violates invariant: invalid property descriptor for existing property on the target", + }); + + assertThrowsError(() => { + Object.getOwnPropertyDescriptor(new Proxy(o, { + getOwnPropertyDescriptor() { + return { enumerable: false }; + }, + }), "v2"); + }, { + error: TypeError, + message: "Proxy handler's getOwnPropertyDescriptor trap violates invariant: invalid property descriptor for existing property on the target", + }); + + assertThrowsError(() => { + Object.getOwnPropertyDescriptor(new Proxy(o, { + getOwnPropertyDescriptor() { + return { value: 10 }; + }, + }), "v3"); + }, { + error: TypeError, + message: "Proxy handler's getOwnPropertyDescriptor trap violates invariant: invalid property descriptor for existing property on the target", + }); + + assertThrowsError(() => { + Object.getOwnPropertyDescriptor(new Proxy(o, { + getOwnPropertyDescriptor() { + return { value: 10, writable: true }; + }, + }), "v4"); + }, { + error: TypeError, + message: "Proxy handler's getOwnPropertyDescriptor trap violates invariant: invalid property descriptor for existing property on the target", + }); + + o = {}; + Object.defineProperty(o, "v", { configurable: true }); + assertThrowsError(() => { + Object.getOwnPropertyDescriptor(new Proxy(o, { + getOwnPropertyDescriptor() { + return { configurable: false }; + }, + }), "v"); + }, { + error: TypeError, + message: "Proxy handler's getOwnPropertyDescriptor trap violates invariant: cannot report target's property as non-configurable if the property does not exist, or if it is configurable", + }); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Tests/Proxy.handler-getPrototypeOf.js b/Libraries/LibJS/Tests/Proxy.handler-getPrototypeOf.js new file mode 100644 index 0000000000..14ef2362cd --- /dev/null +++ b/Libraries/LibJS/Tests/Proxy.handler-getPrototypeOf.js @@ -0,0 +1,87 @@ +load("test-common.js"); + +try { + const child = {}; + const childProto = { foo: "bar" }; + + Object.setPrototypeOf(child, childProto); + assert(child.foo === "bar"); + + Object.getPrototypeOf(new Proxy(child, { getPrototypeOf: null })); + Object.getPrototypeOf(new Proxy(child, { getPrototypeOf: undefined })); + + let o = {}; + let p = new Proxy(o, { + getPrototypeOf(target) { + assert(target === o); + return null; + } + }); + + Object.getPrototypeOf(p); + + p = new Proxy(o, { + getPrototypeOf(target) { + if (target.foo) + return { bar: 1 }; + return { bar: 2 }; + }, + }); + + assert(Object.getPrototypeOf(p).bar === 2); + o.foo = 20 + assert(Object.getPrototypeOf(p).bar === 1); + + // Invariants + + assertThrowsError(() => { + Object.getPrototypeOf(new Proxy(child, { getPrototypeOf: 1 })); + }, { + error: TypeError, + message: "Proxy handler's getPrototypeOf trap wasn't undefined, null, or callable", + }); + + assertThrowsError(() => { + Object.getPrototypeOf(new Proxy(child, { getPrototypeOf() { return 1; } })); + }, { + error: TypeError, + message: "Proxy handler's getPrototypeOf trap violates invariant: must return an object or null", + }); + + p = new Proxy(child, { + getPrototypeOf(target) { + assert(target === child); + return { baz: "qux" }; + }, + }); + + assert(Object.getPrototypeOf(p).baz === "qux"); + + Object.preventExtensions(child); + p = new Proxy(child, { + getPrototypeOf(target) { + assert(target === child); + return childProto; + } + }); + + assert(Object.getPrototypeOf(p).foo === "bar"); + + p = new Proxy(child, { + getPrototypeOf(target) { + assert(target === child); + return { baz: "qux" }; + } + }); + + assertThrowsError(() => { + Object.getPrototypeOf(p); + }, { + error: TypeError, + message: "Proxy handler's getPrototypeOf trap violates invariant: cannot return a different prototype object for a non-extensible target" + }); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Tests/Proxy.handler-has.js b/Libraries/LibJS/Tests/Proxy.handler-has.js new file mode 100644 index 0000000000..849c0b6727 --- /dev/null +++ b/Libraries/LibJS/Tests/Proxy.handler-has.js @@ -0,0 +1,62 @@ +load("test-common.js"); + +try { + assert("foo" in new Proxy({}, { has: null }) === false); + assert("foo" in new Proxy({}, { has: undefined}) === false); + assert("foo" in new Proxy({}, {}) === false); + + let o = {}; + let p = new Proxy(o, { + has(target, prop) { + assert(target === o); + assert(prop === "foo"); + return true; + } + }); + + "foo" in p; + + p = new Proxy(o, { + has(target, prop) { + if (target.checkedFoo) + return true; + if (prop === "foo") + target.checkedFoo = true; + return false; + } + }); + + assert("foo" in p === false); + assert("foo" in p === true); + + // Invariants + + o = {}; + Object.defineProperty(o, "foo", { configurable: false }); + Object.defineProperty(o, "bar", { value: 10, configurable: true }); + p = new Proxy(o, { + has() { + return false; + } + }); + + assertThrowsError(() => { + "foo" in p; + }, { + error: TypeError, + message: "Proxy handler's has trap violates invariant: a property cannot be reported as non-existent if it exists on the target as a non-configurable property", + }); + + Object.preventExtensions(o); + + assertThrowsError(() => { + "bar" in p; + }, { + error: TypeError, + message: "Proxy handler's has trap violates invariant: a property cannot be reported as non-existent if it exist on the target and the target is non-extensible", + }); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Tests/Proxy.handler-isExtensible.js b/Libraries/LibJS/Tests/Proxy.handler-isExtensible.js new file mode 100644 index 0000000000..6acab627c3 --- /dev/null +++ b/Libraries/LibJS/Tests/Proxy.handler-isExtensible.js @@ -0,0 +1,49 @@ +load("test-common.js"); + +try { + assert(Object.isExtensible(new Proxy({}, { isExtensible: null })) === true); + assert(Object.isExtensible(new Proxy({}, { isExtensible: undefined })) === true); + assert(Object.isExtensible(new Proxy({}, {})) === true); + + let o = {}; + let p = new Proxy(o, { + isExtensible(target) { + assert(target === o); + return true; + } + }); + + Object.isExtensible(p); + + // Invariants + + o = {}; + p = new Proxy(o, { + isExtensible(proxyTarget) { + assert(proxyTarget === o); + return true; + }, + }); + + assert(Object.isExtensible(p) === true); + Object.preventExtensions(o); + + assertThrowsError(() => { + Object.isExtensible(p); + }, { + error: TypeError, + message: "Proxy handler's isExtensible trap violates invariant: return value must match the target's extensibility", + }); + + p = new Proxy(o, { + isExtensible(proxyTarget) { + assert(proxyTarget === o); + return false; + }, + }); + assert(Object.isExtensible(p) === false); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Tests/Proxy.handler-preventExtensions.js b/Libraries/LibJS/Tests/Proxy.handler-preventExtensions.js new file mode 100644 index 0000000000..bbc1afa75a --- /dev/null +++ b/Libraries/LibJS/Tests/Proxy.handler-preventExtensions.js @@ -0,0 +1,55 @@ +load("test-common.js"); + +try { + let p = new Proxy({}, { preventExtensions: null }); + assert(Object.preventExtensions(p) === p); + p = new Proxy({}, { preventExtensions: undefined }); + assert(Object.preventExtensions(p) === p); + p = new Proxy({}, {}); + assert(Object.preventExtensions(p) == p); + + let o = {}; + p = new Proxy(o, { + preventExtensions(target) { + assert(target === o); + return true; + } + }); + + Object.preventExtensions(o); + Object.preventExtensions(p); + + // Invariants + + p = new Proxy({}, { + preventExtensions() { + return false; + }, + }); + assertThrowsError(() => { + Object.preventExtensions(p); + }, { + error: TypeError, + message: "Proxy preventExtensions handler returned false", + }); + + o = {}; + p = new Proxy(o, { + preventExtensions() { + return true; + }, + }); + assertThrowsError(() => { + Object.preventExtensions(p); + }, { + error: TypeError, + message: "Proxy handler's preventExtensions trap violates invariant: cannot return true if the target object is extensible" + }); + + Object.preventExtensions(o); + assert(Object.preventExtensions(p) === p); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Tests/Proxy.handler-set.js b/Libraries/LibJS/Tests/Proxy.handler-set.js new file mode 100644 index 0000000000..c73c7badc8 --- /dev/null +++ b/Libraries/LibJS/Tests/Proxy.handler-set.js @@ -0,0 +1,66 @@ +load("test-common.js"); + +try { + assert((new Proxy({}, { set: undefined }).foo = 1) === 1); + assert((new Proxy({}, { set: null }).foo = 1) === 1); + assert((new Proxy({}, {}).foo = 1) === 1); + + let o = {}; + let p = new Proxy(o, { + set(target, prop, value, receiver) { + assert(target === o); + assert(prop === "foo"); + assert(value === 10); + assert(receiver === p); + return true; + }, + }); + + p.foo = 10; + + p = new Proxy(o, { + set(target, prop, value, receiver) { + if (target[prop] === value) { + target[prop] *= 2; + } else { + target[prop] = value; + } + }, + }); + + p.foo = 10; + assert(p.foo === 10); + p.foo = 10; + assert(p.foo === 20); + p.foo = 10; + assert(p.foo === 10); + + // Invariants + + o = {}; + Object.defineProperty(o, "foo", { value: 10 }); + p = new Proxy(o, { + set() { + return true; + }, + }); + + assertThrowsError(() => { + p.foo = 12; + }, { + error: TypeError, + message: "Proxy handler's set trap violates invariant: cannot return true for a property on the target which is a non-configurable, non-writable own data property", + }); + + Object.defineProperty(o, "bar", { get() {} }); + assertThrowsError(() => { + p.bar = 12; + }, { + error: TypeError, + message: "Proxy handler's set trap violates invariant: cannot return true for a property on the target which is a non-configurable own accessor property with an undefined set attribute", + }); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Tests/Proxy.handler-setPrototypeOf.js b/Libraries/LibJS/Tests/Proxy.handler-setPrototypeOf.js new file mode 100644 index 0000000000..8bad7fe37c --- /dev/null +++ b/Libraries/LibJS/Tests/Proxy.handler-setPrototypeOf.js @@ -0,0 +1,95 @@ +load("test-common.js"); + +try { + const child = {}; + const childProto = { foo: "bar" }; + + Object.setPrototypeOf(child, childProto); + assert(child.foo === "bar"); + + Object.setPrototypeOf(new Proxy(child, { setPrototypeOf: null }), childProto); + Object.setPrototypeOf(new Proxy(child, { setPrototypeOf: undefined }), childProto); + + let o = {}; + let theNewProto = { foo: "bar" }; + let p = new Proxy(o, { + setPrototypeOf(target, newProto) { + assert(target === o); + assert(newProto === theNewProto); + return true; + } + }); + + Object.setPrototypeOf(p, theNewProto); + + p = new Proxy(o, { + setPrototypeOf(target, newProto) { + if (target.shouldSet) + Object.setPrototypeOf(target, newProto); + return true; + }, + }); + + Object.setPrototypeOf(p, { foo: 1 }); + assert(Object.getPrototypeOf(p).foo === undefined); + p.shouldSet = true; + assert(o.shouldSet === true); + Object.setPrototypeOf(p, { foo: 1 }); + assert(Object.getPrototypeOf(p).foo === 1); + + // Invariants + + assertThrowsError(() => { + Object.setPrototypeOf(new Proxy(child, { setPrototypeOf: 1 }), {}); + }, { + error: TypeError, + message: "Proxy handler's setPrototypeOf trap wasn't undefined, null, or callable", + }); + + p = new Proxy(child, { + setPrototypeOf(target, newProto) { + assert(target === child); + return false; + }, + }); + + assertThrowsError(() => { + Object.setPrototypeOf(p, {}); + }, { + error: TypeError, + message: "Object's setPrototypeOf method returned false" + }); + assert(Object.getPrototypeOf(p) === childProto); + + p = new Proxy(child, { + setPrototypeOf(target, newProto) { + assert(target === child); + return true; + }, + }); + + assert(Object.setPrototypeOf(p, {}) === p); + assert(Object.getPrototypeOf(p) === childProto); + + Object.preventExtensions(child); + p = new Proxy(child, { + setPrototypeOf(target, newProto) { + assert(target === child); + return true; + }, + }); + + assert(Object.setPrototypeOf(p, childProto) === p); + assert(Object.getPrototypeOf(p) === childProto); + + assertThrowsError(() => { + Object.setPrototypeOf(p, {}); + }, { + error: TypeError, + message: "Proxy handler's setPrototypeOf trap violates invariant: the argument must match the prototype of the target if the target is non-extensible", + }); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Tests/Proxy.js b/Libraries/LibJS/Tests/Proxy.js new file mode 100644 index 0000000000..e38165828c --- /dev/null +++ b/Libraries/LibJS/Tests/Proxy.js @@ -0,0 +1,30 @@ +load("test-common.js"); + +try { + new Proxy({}, {}); + + assertThrowsError(() => { + new Proxy(); + }, { + error: TypeError, + message: "Proxy requires at least two arguments", + }); + + assertThrowsError(() => { + Proxy(); + }, { + error: TypeError, + message: "Proxy must be called with the \"new\" operator", + }); + + assertThrowsError(() => { + new Proxy(1, {}); + }, { + error: TypeError, + message: "Expected target argument of Proxy constructor to be object, got 1", + }); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} |