diff --git a/Libraries/LibJS/Runtime/ErrorTypes.h b/Libraries/LibJS/Runtime/ErrorTypes.h index 501cad3af7..e406d4377f 100644 --- a/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Libraries/LibJS/Runtime/ErrorTypes.h @@ -156,6 +156,10 @@ M(ThisHasNotBeenInitialized, "|this| has not been initialized") \ M(ThisIsAlreadyInitialized, "|this| is already initialized") \ M(ToObjectNullOrUndef, "ToObject on null or undefined") \ + M(TypedArrayInvalidBufferLength, "Invalid buffer length for {}: must be a multiple of {}, got {}") \ + M(TypedArrayInvalidByteOffset, "Invalid byte offset for {}: must be a multiple of {}, got {}") \ + M(TypedArrayOutOfRangeByteOffset, "Typed array byte offset {} is out of range for buffer with length {}") \ + M(TypedArrayOutOfRangeByteOffsetOrLength, "Typed array range {}:{} is out of range for buffer with length {}") \ M(UnknownIdentifier, "'{}' is not defined") \ /* LibWeb bindings */ \ M(NotAByteString, "Argument to {}() must be a byte string") \ diff --git a/Libraries/LibJS/Runtime/TypedArray.cpp b/Libraries/LibJS/Runtime/TypedArray.cpp index d184925e52..1339e6925c 100644 --- a/Libraries/LibJS/Runtime/TypedArray.cpp +++ b/Libraries/LibJS/Runtime/TypedArray.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2020, Andreas Kling + * Copyright (c) 2020, Linus Groh * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -24,68 +25,133 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +#include #include #include #include namespace JS { -#define JS_DEFINE_TYPED_ARRAY(ClassName, snake_name, PrototypeName, ConstructorName, Type) \ - ClassName* ClassName::create(GlobalObject& global_object, u32 length) \ - { \ - return global_object.heap().allocate(global_object, length, *global_object.snake_name##_prototype()); \ - } \ - \ - ClassName::ClassName(u32 length, Object& prototype) \ - : TypedArray(length, prototype) \ - { \ - } \ - ClassName::~ClassName() { } \ - \ - PrototypeName::PrototypeName(GlobalObject& global_object) \ - : Object(*global_object.typed_array_prototype()) \ - { \ - } \ - PrototypeName::~PrototypeName() { } \ - \ - ConstructorName::ConstructorName(GlobalObject& global_object) \ - : TypedArrayConstructor(vm().names.ClassName, *global_object.typed_array_constructor()) \ - { \ - } \ - ConstructorName::~ConstructorName() { } \ - void ConstructorName::initialize(GlobalObject& global_object) \ - { \ - auto& vm = this->vm(); \ - NativeFunction::initialize(global_object); \ - define_property(vm.names.prototype, global_object.snake_name##_prototype(), 0); \ - define_property(vm.names.length, Value(1), Attribute::Configurable); \ - define_property(vm.names.BYTES_PER_ELEMENT, Value((i32)sizeof(Type)), 0); \ - } \ - Value ConstructorName::call() \ - { \ - auto& vm = this->vm(); \ - vm.throw_exception(global_object(), ErrorType::ConstructorWithoutNew, vm.names.ClassName); \ - return {}; \ - } \ - Value ConstructorName::construct(Function&) \ - { \ - auto& vm = this->vm(); \ - if (vm.argument_count() == 0) \ - return ClassName::create(global_object(), 0); \ - \ - if (vm.argument(0).is_object()) { \ - /* FIXME: Initialize from TypedArray, ArrayBuffer, Iterable or Array-like object */ \ - TODO(); \ - } \ - auto array_length = vm.argument(0).to_index(global_object()); \ - if (vm.exception()) { \ - /* Re-throw more specific RangeError */ \ - vm.clear_exception(); \ - vm.throw_exception(global_object(), ErrorType::InvalidLength, "typed array"); \ - return {}; \ - } \ - auto* array = ClassName::create(global_object(), array_length); \ - return array; \ +static void initialize_typed_array_from_array_buffer(GlobalObject& global_object, TypedArrayBase& typed_array, ArrayBuffer& array_buffer, Value byte_offset, Value length) +{ + // 22.2.5.1.3 InitializeTypedArrayFromArrayBuffer, https://tc39.es/ecma262/#sec-initializetypedarrayfromarraybuffer + + auto& vm = global_object.vm(); + auto element_size = typed_array.element_size(); + auto offset = byte_offset.to_index(global_object); + if (vm.exception()) + return; + if (offset % element_size != 0) { + vm.throw_exception(global_object, ErrorType::TypedArrayInvalidByteOffset, typed_array.class_name(), element_size, offset); + return; + } + size_t new_length { 0 }; + if (!length.is_undefined()) { + new_length = length.to_index(global_object); + if (vm.exception()) + return; + } + // FIXME: 8. If IsDetachedBuffer(buffer) is true, throw a TypeError exception. + auto buffer_byte_length = array_buffer.byte_length(); + size_t new_byte_length; + if (length.is_undefined()) { + if (buffer_byte_length % element_size != 0) { + vm.throw_exception(global_object, ErrorType::TypedArrayInvalidBufferLength, typed_array.class_name(), element_size, buffer_byte_length); + return; + } + if (offset > buffer_byte_length) { + vm.throw_exception(global_object, ErrorType::TypedArrayOutOfRangeByteOffset, offset, buffer_byte_length); + return; + } + new_byte_length = buffer_byte_length - offset; + } else { + new_byte_length = new_length * element_size; + if (offset + new_byte_length > buffer_byte_length) { + vm.throw_exception(global_object, ErrorType::TypedArrayOutOfRangeByteOffsetOrLength, offset, offset + new_byte_length, buffer_byte_length); + return; + } + } + typed_array.set_viewed_array_buffer(&array_buffer); + typed_array.set_byte_length(new_byte_length); + typed_array.set_byte_offset(offset); + typed_array.set_array_length(new_byte_length / element_size); +} + +void TypedArrayBase::visit_edges(Visitor& visitor) +{ + Object::visit_edges(visitor); + visitor.visit(m_viewed_array_buffer); +} + +#define JS_DEFINE_TYPED_ARRAY(ClassName, snake_name, PrototypeName, ConstructorName, Type) \ + ClassName* ClassName::create(GlobalObject& global_object, u32 length) \ + { \ + return global_object.heap().allocate(global_object, length, *global_object.snake_name##_prototype()); \ + } \ + \ + ClassName::ClassName(u32 length, Object& prototype) \ + : TypedArray(length, prototype) \ + { \ + } \ + ClassName::~ClassName() { } \ + \ + PrototypeName::PrototypeName(GlobalObject& global_object) \ + : Object(*global_object.typed_array_prototype()) \ + { \ + } \ + PrototypeName::~PrototypeName() { } \ + \ + ConstructorName::ConstructorName(GlobalObject& global_object) \ + : TypedArrayConstructor(vm().names.ClassName, *global_object.typed_array_constructor()) \ + { \ + } \ + ConstructorName::~ConstructorName() { } \ + void ConstructorName::initialize(GlobalObject& global_object) \ + { \ + auto& vm = this->vm(); \ + NativeFunction::initialize(global_object); \ + define_property(vm.names.prototype, global_object.snake_name##_prototype(), 0); \ + define_property(vm.names.length, Value(1), Attribute::Configurable); \ + define_property(vm.names.BYTES_PER_ELEMENT, Value((i32)sizeof(Type)), 0); \ + } \ + Value ConstructorName::call() \ + { \ + auto& vm = this->vm(); \ + vm.throw_exception(global_object(), ErrorType::ConstructorWithoutNew, vm.names.ClassName); \ + return {}; \ + } \ + Value ConstructorName::construct(Function&) \ + { \ + auto& vm = this->vm(); \ + if (vm.argument_count() == 0) \ + return ClassName::create(global_object(), 0); \ + \ + auto first_argument = vm.argument(0); \ + if (first_argument.is_object()) { \ + auto* typed_array = ClassName::create(global_object(), 0); \ + if (first_argument.as_object().is_typed_array()) { \ + /* FIXME: Initialize from TypedArray */ \ + TODO(); \ + } else if (first_argument.as_object().is_array_buffer()) { \ + auto& array_buffer = static_cast(first_argument.as_object()); \ + initialize_typed_array_from_array_buffer(global_object(), *typed_array, array_buffer, vm.argument(1), vm.argument(2)); \ + if (vm.exception()) \ + return {}; \ + } else { \ + /* FIXME: Initialize from Iterator or Array-like object */ \ + TODO(); \ + } \ + return typed_array; \ + } \ + \ + auto array_length = first_argument.to_index(global_object()); \ + if (vm.exception()) { \ + /* Re-throw more specific RangeError */ \ + vm.clear_exception(); \ + vm.throw_exception(global_object(), ErrorType::InvalidLength, "typed array"); \ + return {}; \ + } \ + return ClassName::create(global_object(), array_length); \ } #undef __JS_ENUMERATE diff --git a/Libraries/LibJS/Runtime/TypedArray.h b/Libraries/LibJS/Runtime/TypedArray.h index e7f98dfba8..fccf72b4c2 100644 --- a/Libraries/LibJS/Runtime/TypedArray.h +++ b/Libraries/LibJS/Runtime/TypedArray.h @@ -26,6 +26,7 @@ #pragma once +#include #include #include #include @@ -36,16 +37,31 @@ class TypedArrayBase : public Object { JS_OBJECT(TypedArrayBase, Object); public: - u32 length() const { return m_length; } + u32 array_length() const { return m_array_length; } + u32 byte_length() const { return m_byte_length; } + u32 byte_offset() const { return m_byte_offset; } + ArrayBuffer* viewed_array_buffer() const { return m_viewed_array_buffer; } + + void set_array_length(u32 length) { m_array_length = length; } + void set_byte_length(u32 length) { m_byte_length = length; } + void set_byte_offset(u32 offset) { m_byte_offset = offset; } + void set_viewed_array_buffer(ArrayBuffer* array_buffer) { m_viewed_array_buffer = array_buffer; } + + virtual size_t element_size() const = 0; protected: - TypedArrayBase(u32 length, Object& prototype) + TypedArrayBase(Object& prototype) : Object(prototype) - , m_length(length) { } - u32 m_length { 0 }; + u32 m_array_length { 0 }; + u32 m_byte_length { 0 }; + u32 m_byte_offset { 0 }; + ArrayBuffer* m_viewed_array_buffer { nullptr }; + +private: + virtual void visit_edges(Visitor&) override; }; template @@ -53,28 +69,22 @@ class TypedArray : public TypedArrayBase { JS_OBJECT(TypedArray, TypedArrayBase); public: - virtual ~TypedArray() override - { - ASSERT(m_data); - free(m_data); - m_data = nullptr; - } - virtual bool put_by_index(u32 property_index, Value value) override { - if (property_index >= m_length) + property_index += m_byte_offset / sizeof(T); + if (property_index >= m_array_length) return Base::put_by_index(property_index, value); if constexpr (sizeof(T) < 4) { auto number = value.to_i32(global_object()); if (vm().exception()) return {}; - m_data[property_index] = number; + data()[property_index] = number; } else if constexpr (sizeof(T) == 4) { auto number = value.to_double(global_object()); if (vm().exception()) return {}; - m_data[property_index] = number; + data()[property_index] = number; } else { static_assert(DependentFalse, "TypedArray::put_by_index with unhandled type size"); } @@ -83,13 +93,14 @@ public: virtual Value get_by_index(u32 property_index) const override { - if (property_index >= m_length) + property_index += m_byte_offset / sizeof(T); + if (property_index >= m_array_length) return Base::get_by_index(property_index); if constexpr (sizeof(T) < 4) { - return Value((i32)m_data[property_index]); + return Value((i32)data()[property_index]); } else if constexpr (sizeof(T) == 4) { - auto value = m_data[property_index]; + auto value = data()[property_index]; if constexpr (NumericLimits::is_signed()) { if (value > NumericLimits::max() || value < NumericLimits::min()) return Value((double)value); @@ -103,20 +114,29 @@ public: } } - T* data() { return m_data; } - const T* data() const { return m_data; } + T* data() const { return reinterpret_cast(m_viewed_array_buffer->buffer().data()); } + + virtual size_t element_size() const override { return sizeof(T); }; protected: - TypedArray(u32 length, Object& prototype) - : TypedArrayBase(length, prototype) + TypedArray(ArrayBuffer& array_buffer, u32 array_length, Object& prototype) + : TypedArrayBase(prototype) { - m_data = (T*)calloc(m_length, sizeof(T)); + m_viewed_array_buffer = &array_buffer; + m_array_length = array_length; + m_byte_length = m_viewed_array_buffer->byte_length(); + } + + TypedArray(u32 array_length, Object& prototype) + : TypedArrayBase(prototype) + { + m_viewed_array_buffer = ArrayBuffer::create(global_object(), array_length * sizeof(T)); + m_array_length = array_length; + m_byte_length = m_viewed_array_buffer->byte_length(); } private: virtual bool is_typed_array() const final { return true; } - - T* m_data { nullptr }; }; #define JS_DECLARE_TYPED_ARRAY(ClassName, snake_name, PrototypeName, ConstructorName, Type) \ diff --git a/Libraries/LibJS/Runtime/TypedArrayPrototype.cpp b/Libraries/LibJS/Runtime/TypedArrayPrototype.cpp index 80547d3bfe..bb94db292d 100644 --- a/Libraries/LibJS/Runtime/TypedArrayPrototype.cpp +++ b/Libraries/LibJS/Runtime/TypedArrayPrototype.cpp @@ -64,7 +64,7 @@ JS_DEFINE_NATIVE_GETTER(TypedArrayPrototype::length_getter) auto typed_array = typed_array_from(vm, global_object); if (!typed_array) return {}; - return Value(typed_array->length()); + return Value(typed_array->array_length()); } } diff --git a/Libraries/LibJS/Tests/builtins/TypedArray/TypedArray.js b/Libraries/LibJS/Tests/builtins/TypedArray/TypedArray.js index c78fba3a85..09884a6f81 100644 --- a/Libraries/LibJS/Tests/builtins/TypedArray/TypedArray.js +++ b/Libraries/LibJS/Tests/builtins/TypedArray/TypedArray.js @@ -45,6 +45,70 @@ test("typed arrays inherit from TypedArray", () => { }); }); +test("typed array can share the same ArrayBuffer", () => { + const arrayBuffer = new ArrayBuffer(2); + const uint8Array = new Uint8Array(arrayBuffer); + const uint16Array = new Uint16Array(arrayBuffer); + expect(uint8Array[0]).toBe(0); + expect(uint8Array[1]).toBe(0); + expect(uint16Array[0]).toBe(0); + expect(uint16Array[1]).toBeUndefined(); + uint16Array[0] = 54321; + expect(uint8Array[0]).toBe(0x31); + expect(uint8Array[1]).toBe(0xd4); + expect(uint16Array[0]).toBe(54321); + expect(uint16Array[1]).toBeUndefined(); +}); + +test("typed array from ArrayBuffer with custom length and offset", () => { + const arrayBuffer = new ArrayBuffer(10); + const uint8ArrayAll = new Uint8Array(arrayBuffer); + const uint16ArrayPartial = new Uint16Array(arrayBuffer, 2, 4); + // Affects two bytes of the buffer, beginning at offset + uint16ArrayPartial[0] = 52651 + // Out of relative bounds, doesn't affect buffer + uint16ArrayPartial[4] = 123 + expect(uint8ArrayAll[0]).toBe(0); + expect(uint8ArrayAll[1]).toBe(0); + expect(uint8ArrayAll[2]).toBe(0xab); + expect(uint8ArrayAll[3]).toBe(0xcd); + expect(uint8ArrayAll[5]).toBe(0); + expect(uint8ArrayAll[6]).toBe(0); + expect(uint8ArrayAll[7]).toBe(0); + expect(uint8ArrayAll[8]).toBe(0); + expect(uint8ArrayAll[9]).toBe(0); +}); + +test("typed array from ArrayBuffer errors", () => { + expect(() => { + new Uint16Array(new ArrayBuffer(1)); + }).toThrowWithMessage( + RangeError, + "Invalid buffer length for Uint16Array: must be a multiple of 2, got 1" + ); + + expect(() => { + new Uint16Array(new ArrayBuffer(), 1); + }).toThrowWithMessage( + RangeError, + "Invalid byte offset for Uint16Array: must be a multiple of 2, got 1" + ); + + expect(() => { + new Uint16Array(new ArrayBuffer(), 2); + }).toThrowWithMessage( + RangeError, + "Typed array byte offset 2 is out of range for buffer with length 0" + ); + + expect(() => { + new Uint16Array(new ArrayBuffer(7), 2, 3); + }).toThrowWithMessage( + RangeError, + "Typed array range 2:8 is out of range for buffer with length 7" + ); +}); + test("TypedArray is not exposed on the global object", () => { expect(globalThis.TypedArray).toBeUndefined(); });