From 29ac6e3689fe6492aa931584eb31bc88e08aae41 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sat, 14 Oct 2023 19:10:55 -0400 Subject: [PATCH] LibJS: Partially implement resizable ArrayBuffer objects This is (part of) a normative change in the ECMA-262 spec. See: https://github.com/tc39/ecma262/commit/a9ae96e This implements just support for resizing ArrayBuffer objects. This does not implement the SharedArrayBuffer changes, as we do not have enough support to do so. --- .../Libraries/LibJS/Runtime/ArrayBuffer.cpp | 71 +++++++++- .../Libraries/LibJS/Runtime/ArrayBuffer.h | 21 ++- .../LibJS/Runtime/ArrayBufferConstructor.cpp | 12 +- .../LibJS/Runtime/ArrayBufferPrototype.cpp | 125 +++++++++++++++++- .../LibJS/Runtime/ArrayBufferPrototype.h | 3 + .../LibJS/Runtime/CommonPropertyNames.h | 3 + Userland/Libraries/LibJS/Runtime/ErrorTypes.h | 2 + Userland/Libraries/LibJS/Runtime/VM.cpp | 20 +++ Userland/Libraries/LibJS/Runtime/VM.h | 6 + .../Tests/builtins/ArrayBuffer/ArrayBuffer.js | 15 +++ .../ArrayBuffer.prototype.maxByteLength.js | 29 ++++ .../ArrayBuffer.prototype.resizable.js | 19 +++ .../ArrayBuffer.prototype.resize.js | 57 ++++++++ 13 files changed, 369 insertions(+), 14 deletions(-) create mode 100644 Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.maxByteLength.js create mode 100644 Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.resizable.js create mode 100644 Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.resize.js diff --git a/Userland/Libraries/LibJS/Runtime/ArrayBuffer.cpp b/Userland/Libraries/LibJS/Runtime/ArrayBuffer.cpp index e4a2dcb7e9..5788de6c2b 100644 --- a/Userland/Libraries/LibJS/Runtime/ArrayBuffer.cpp +++ b/Userland/Libraries/LibJS/Runtime/ArrayBuffer.cpp @@ -139,23 +139,64 @@ void copy_data_block_bytes(ByteBuffer& to_block, u64 to_index, ByteBuffer const& } // 25.1.3.1 AllocateArrayBuffer ( constructor, byteLength [ , maxByteLength ] ), https://tc39.es/ecma262/#sec-allocatearraybuffer -ThrowCompletionOr allocate_array_buffer(VM& vm, FunctionObject& constructor, size_t byte_length) +ThrowCompletionOr allocate_array_buffer(VM& vm, FunctionObject& constructor, size_t byte_length, Optional const& max_byte_length) { - // 1. Let obj be ? OrdinaryCreateFromConstructor(constructor, "%ArrayBuffer.prototype%", « [[ArrayBufferData]], [[ArrayBufferByteLength]], [[ArrayBufferDetachKey]] »). + // 1. Let slots be « [[ArrayBufferData]], [[ArrayBufferByteLength]], [[ArrayBufferDetachKey]] ». + + // 2. If maxByteLength is present and maxByteLength is not empty, let allocatingResizableBuffer be true; otherwise let allocatingResizableBuffer be false. + auto allocating_resizable_buffer = max_byte_length.has_value(); + + // 3. If allocatingResizableBuffer is true, then + if (allocating_resizable_buffer) { + // a. If byteLength > maxByteLength, throw a RangeError exception. + if (byte_length > *max_byte_length) + return vm.throw_completion(ErrorType::ByteLengthExceedsMaxByteLength, byte_length, *max_byte_length); + + // b. Append [[ArrayBufferMaxByteLength]] to slots. + } + + // 4. Let obj be ? OrdinaryCreateFromConstructor(constructor, "%ArrayBuffer.prototype%", slots). auto obj = TRY(ordinary_create_from_constructor(vm, constructor, &Intrinsics::array_buffer_prototype, nullptr)); - // 2. Let block be ? CreateByteDataBlock(byteLength). + // 5. Let block be ? CreateByteDataBlock(byteLength). auto block = TRY(create_byte_data_block(vm, byte_length)); - // 3. Set obj.[[ArrayBufferData]] to block. + // 6. Set obj.[[ArrayBufferData]] to block. obj->set_data_block(move(block)); - // 4. Set obj.[[ArrayBufferByteLength]] to byteLength. + // 7. Set obj.[[ArrayBufferByteLength]] to byteLength. - // 5. Return obj. + // 8. If allocatingResizableBuffer is true, then + if (allocating_resizable_buffer) { + // a. If it is not possible to create a Data Block block consisting of maxByteLength bytes, throw a RangeError exception. + // b. NOTE: Resizable ArrayBuffers are designed to be implementable with in-place growth. Implementations may throw if, for example, virtual memory cannot be reserved up front. + if (auto result = obj->buffer().try_ensure_capacity(*max_byte_length); result.is_error()) + return vm.throw_completion(ErrorType::NotEnoughMemoryToAllocate, *max_byte_length); + + // c. Set obj.[[ArrayBufferMaxByteLength]] to maxByteLength. + obj->set_max_byte_length(*max_byte_length); + } + + // 9. Return obj. return obj.ptr(); } +// 25.1.3.2 ArrayBufferByteLength ( arrayBuffer, order ), https://tc39.es/ecma262/#sec-arraybufferbytelength +size_t array_buffer_byte_length(ArrayBuffer const& array_buffer, ArrayBuffer::Order) +{ + // FIXME: 1. If IsSharedArrayBuffer(arrayBuffer) is true and arrayBuffer has an [[ArrayBufferByteLengthData]] internal slot, then + // FIXME: a. Let bufferByteLengthBlock be arrayBuffer.[[ArrayBufferByteLengthData]]. + // FIXME: b. Let rawLength be GetRawBytesFromSharedBlock(bufferByteLengthBlock, 0, biguint64, true, order). + // FIXME: c. Let isLittleEndian be the value of the [[LittleEndian]] field of the surrounding agent's Agent Record. + // FIXME: d. Return ℝ(RawBytesToNumeric(biguint64, rawLength, isLittleEndian)). + + // 2. Assert: IsDetachedBuffer(arrayBuffer) is false. + VERIFY(!array_buffer.is_detached()); + + // 3. Return arrayBuffer.[[ArrayBufferByteLength]]. + return array_buffer.byte_length(); +} + // 25.1.3.4 DetachArrayBuffer ( arrayBuffer [ , key ] ), https://tc39.es/ecma262/#sec-detacharraybuffer ThrowCompletionOr detach_array_buffer(VM& vm, ArrayBuffer& array_buffer, Optional key) { @@ -202,6 +243,24 @@ ThrowCompletionOr clone_array_buffer(VM& vm, ArrayBuffer& source_b return target_buffer; } +// 25.1.3.6 GetArrayBufferMaxByteLengthOption ( options ), https://tc39.es/ecma262/#sec-getarraybuffermaxbytelengthoption +ThrowCompletionOr> get_array_buffer_max_byte_length_option(VM& vm, Value options) +{ + // 1. If options is not an Object, return empty. + if (!options.is_object()) + return OptionalNone {}; + + // 2. Let maxByteLength be ? Get(options, "maxByteLength"). + auto max_byte_length = TRY(options.as_object().get(vm.names.maxByteLength)); + + // 3. If maxByteLength is undefined, return empty. + if (max_byte_length.is_undefined()) + return OptionalNone {}; + + // 4. Return ? ToIndex(maxByteLength). + return TRY(max_byte_length.to_index(vm)); +} + // 25.1.2.14 ArrayBufferCopyAndDetach ( arrayBuffer, newLength, preserveResizability ), https://tc39.es/proposal-arraybuffer-transfer/#sec-arraybuffer.prototype.transfertofixedlength ThrowCompletionOr array_buffer_copy_and_detach(VM& vm, ArrayBuffer& array_buffer, Value new_length, PreserveResizability) { diff --git a/Userland/Libraries/LibJS/Runtime/ArrayBuffer.h b/Userland/Libraries/LibJS/Runtime/ArrayBuffer.h index a642485b2c..fc5d6837d4 100644 --- a/Userland/Libraries/LibJS/Runtime/ArrayBuffer.h +++ b/Userland/Libraries/LibJS/Runtime/ArrayBuffer.h @@ -69,6 +69,10 @@ public: ByteBuffer& buffer() { return m_data_block.buffer(); } ByteBuffer const& buffer() const { return m_data_block.buffer(); } + // [[ArrayBufferMaxByteLength]] + size_t max_byte_length() const { return m_max_byte_length.value(); } + void set_max_byte_length(size_t max_byte_length) { m_max_byte_length = max_byte_length; } + // Used by allocate_array_buffer() to attach the data block after construction void set_data_block(DataBlock block) { m_data_block = move(block); } @@ -87,6 +91,17 @@ public: return false; } + // 25.1.3.8 IsFixedLengthArrayBuffer ( arrayBuffer ), https://tc39.es/ecma262/#sec-isfixedlengtharraybuffer + bool is_fixed_length() const + { + // 1. If arrayBuffer has an [[ArrayBufferMaxByteLength]] internal slot, return false. + if (m_max_byte_length.has_value()) + return false; + + // 2. Return true. + return true; + } + // 25.2.2.2 IsSharedArrayBuffer ( obj ), https://tc39.es/ecma262/#sec-issharedarraybuffer bool is_shared_array_buffer() const { @@ -121,6 +136,8 @@ private: virtual void visit_edges(Visitor&) override; DataBlock m_data_block; + Optional m_max_byte_length; + // The various detach related members of ArrayBuffer are not used by any ECMA262 functionality, // but are required to be available for the use of various harnesses like the Test262 test runner. Value m_detach_key; @@ -128,8 +145,10 @@ private: ThrowCompletionOr create_byte_data_block(VM& vm, size_t size); void copy_data_block_bytes(ByteBuffer& to_block, u64 to_index, ByteBuffer const& from_block, u64 from_index, u64 count); -ThrowCompletionOr allocate_array_buffer(VM&, FunctionObject& constructor, size_t byte_length); +ThrowCompletionOr allocate_array_buffer(VM&, FunctionObject& constructor, size_t byte_length, Optional const& max_byte_length = {}); +size_t array_buffer_byte_length(ArrayBuffer const&, ArrayBuffer::Order); ThrowCompletionOr detach_array_buffer(VM&, ArrayBuffer& array_buffer, Optional key = {}); +ThrowCompletionOr> get_array_buffer_max_byte_length_option(VM&, Value options); ThrowCompletionOr clone_array_buffer(VM&, ArrayBuffer& source_buffer, size_t source_byte_offset, size_t source_length); ThrowCompletionOr array_buffer_copy_and_detach(VM&, ArrayBuffer& array_buffer, Value new_length, PreserveResizability preserve_resizability); ThrowCompletionOr> allocate_shared_array_buffer(VM&, FunctionObject& constructor, size_t byte_length); diff --git a/Userland/Libraries/LibJS/Runtime/ArrayBufferConstructor.cpp b/Userland/Libraries/LibJS/Runtime/ArrayBufferConstructor.cpp index 3ba70067ff..42bafef891 100644 --- a/Userland/Libraries/LibJS/Runtime/ArrayBufferConstructor.cpp +++ b/Userland/Libraries/LibJS/Runtime/ArrayBufferConstructor.cpp @@ -52,8 +52,11 @@ ThrowCompletionOr> ArrayBufferConstructor::construct(Functi { auto& vm = this->vm(); + auto length = vm.argument(0); + auto options = vm.argument(1); + // 2. Let byteLength be ? ToIndex(length). - auto byte_length_or_error = vm.argument(0).to_index(vm); + auto byte_length_or_error = length.to_index(vm); if (byte_length_or_error.is_error()) { auto error = byte_length_or_error.release_error(); @@ -64,8 +67,11 @@ ThrowCompletionOr> ArrayBufferConstructor::construct(Functi return error; } - // 3. Return ? AllocateArrayBuffer(NewTarget, byteLength). - return *TRY(allocate_array_buffer(vm, new_target, byte_length_or_error.release_value())); + // 3. Let requestedMaxByteLength be ? GetArrayBufferMaxByteLengthOption(options). + auto requested_max_byte_length = TRY(get_array_buffer_max_byte_length_option(vm, options)); + + // 3. Return ? AllocateArrayBuffer(NewTarget, byteLength, requestedMaxByteLength). + return *TRY(allocate_array_buffer(vm, new_target, byte_length_or_error.release_value(), requested_max_byte_length)); } // 25.1.5.1 ArrayBuffer.isView ( arg ), https://tc39.es/ecma262/#sec-arraybuffer.isview diff --git a/Userland/Libraries/LibJS/Runtime/ArrayBufferPrototype.cpp b/Userland/Libraries/LibJS/Runtime/ArrayBufferPrototype.cpp index 615b918386..e71aa4b5fb 100644 --- a/Userland/Libraries/LibJS/Runtime/ArrayBufferPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/ArrayBufferPrototype.cpp @@ -27,6 +27,9 @@ void ArrayBufferPrototype::initialize(Realm& realm) Base::initialize(realm); u8 attr = Attribute::Writable | Attribute::Configurable; define_native_accessor(realm, vm.names.byteLength, byte_length_getter, {}, Attribute::Configurable); + define_native_accessor(realm, vm.names.maxByteLength, max_byte_length, {}, Attribute::Configurable); + define_native_accessor(realm, vm.names.resizable, resizable, {}, Attribute::Configurable); + define_native_function(realm, vm.names.resize, resize, 1, attr); define_native_function(realm, vm.names.slice, slice, 2, attr); define_native_accessor(realm, vm.names.detached, detached_getter, {}, Attribute::Configurable); define_native_function(realm, vm.names.transfer, transfer, 0, attr); @@ -54,6 +57,110 @@ JS_DEFINE_NATIVE_FUNCTION(ArrayBufferPrototype::byte_length_getter) return Value(array_buffer_object->byte_length()); } +// 25.1.6.3 get ArrayBuffer.prototype.maxByteLength, https://tc39.es/ecma262/#sec-get-arraybuffer.prototype.maxbytelength +JS_DEFINE_NATIVE_FUNCTION(ArrayBufferPrototype::max_byte_length) +{ + // 1. Let O be the this value. + // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]). + auto array_buffer_object = TRY(typed_this_value(vm)); + + // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception. + if (array_buffer_object->is_shared_array_buffer()) + return vm.throw_completion(ErrorType::ThisCannotBeSharedArrayBuffer); + + // 4. If IsDetachedBuffer(O) is true, return +0𝔽. + if (array_buffer_object->is_detached()) + return Value { 0 }; + + size_t length = 0; + + // 5. If IsFixedLengthArrayBuffer(O) is true, then + if (array_buffer_object->is_fixed_length()) { + // a. Let length be O.[[ArrayBufferByteLength]]. + length = array_buffer_object->byte_length(); + } + // 6. Else, + else { + // a. Let length be O.[[ArrayBufferMaxByteLength]]. + length = array_buffer_object->max_byte_length(); + } + + // 7. Return 𝔽(length). + return Value { length }; +} + +// 25.1.6.4 get ArrayBuffer.prototype.resizable, https://tc39.es/ecma262/#sec-get-arraybuffer.prototype.resizable +JS_DEFINE_NATIVE_FUNCTION(ArrayBufferPrototype::resizable) +{ + // 1. Let O be the this value. + // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]). + auto array_buffer_object = TRY(typed_this_value(vm)); + + // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception. + if (array_buffer_object->is_shared_array_buffer()) + return vm.throw_completion(ErrorType::ThisCannotBeSharedArrayBuffer); + + // 4. If IsFixedLengthArrayBuffer(O) is false, return true; otherwise return false. + return Value { !array_buffer_object->is_fixed_length() }; +} + +// 25.1.6.5 ArrayBuffer.prototype.resize ( newLength ), https://tc39.es/ecma262/#sec-arraybuffer.prototype.resize +JS_DEFINE_NATIVE_FUNCTION(ArrayBufferPrototype::resize) +{ + auto new_length = vm.argument(0); + + // 1. Let O be the this value. + auto array_buffer_object = TRY(typed_this_value(vm)); + + // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferMaxByteLength]]). + if (array_buffer_object->is_fixed_length()) + return vm.throw_completion(ErrorType::FixedArrayBuffer); + + // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception. + if (array_buffer_object->is_shared_array_buffer()) + return vm.throw_completion(ErrorType::ThisCannotBeSharedArrayBuffer); + + // 4. Let newByteLength be ? ToIndex(newLength). + auto new_byte_length = TRY(new_length.to_index(vm)); + + // 5. If IsDetachedBuffer(O) is true, throw a TypeError exception. + if (array_buffer_object->is_detached()) + return vm.throw_completion(ErrorType::DetachedArrayBuffer); + + // 6. If newByteLength > O.[[ArrayBufferMaxByteLength]], throw a RangeError exception. + if (new_byte_length > array_buffer_object->max_byte_length()) + return vm.throw_completion(ErrorType::ByteLengthExceedsMaxByteLength, new_byte_length, array_buffer_object->max_byte_length()); + + // 7. Let hostHandled be ? HostResizeArrayBuffer(O, newByteLength). + auto host_handled = TRY(vm.host_resize_array_buffer(array_buffer_object, new_byte_length)); + + // 8. If hostHandled is handled, return undefined. + if (host_handled == HandledByHost::Handled) + return js_undefined(); + + // 9. Let oldBlock be O.[[ArrayBufferData]]. + auto const& old_block = array_buffer_object->buffer(); + + // 10. Let newBlock be ? CreateByteDataBlock(newByteLength). + auto new_block = TRY(create_byte_data_block(vm, new_byte_length)); + + // 11. Let copyLength be min(newByteLength, O.[[ArrayBufferByteLength]]). + auto copy_length = min(new_byte_length, array_buffer_object->byte_length()); + + // 12. Perform CopyDataBlockBytes(newBlock, 0, oldBlock, 0, copyLength). + copy_data_block_bytes(new_block.buffer(), 0, old_block, 0, copy_length); + + // 13. NOTE: Neither creation of the new Data Block nor copying from the old Data Block are observable. Implementations may implement this method as in-place growth or shrinkage. + + // 14. Set O.[[ArrayBufferData]] to newBlock. + array_buffer_object->set_data_block(move(new_block)); + + // 15. Set O.[[ArrayBufferByteLength]] to newByteLength. + + // 16. Return undefined. + return js_undefined(); +} + // 25.1.6.6 ArrayBuffer.prototype.slice ( start, end ), https://tc39.es/ecma262/#sec-arraybuffer.prototype.slice JS_DEFINE_NATIVE_FUNCTION(ArrayBufferPrototype::slice) { @@ -133,7 +240,8 @@ JS_DEFINE_NATIVE_FUNCTION(ArrayBufferPrototype::slice) if (new_array_buffer_object->byte_length() < new_length) return vm.throw_completion(ErrorType::SpeciesConstructorReturned, "an ArrayBuffer smaller than requested"); - // 22. NOTE: Side-effects of the above steps may have detached O. + // 22. NOTE: Side-effects of the above steps may have detached or resized O. + // 23. If IsDetachedBuffer(O) is true, throw a TypeError exception. if (array_buffer_object->is_detached()) return vm.throw_completion(ErrorType::DetachedArrayBuffer); @@ -144,10 +252,19 @@ JS_DEFINE_NATIVE_FUNCTION(ArrayBufferPrototype::slice) // 25. Let toBuf be new.[[ArrayBufferData]]. auto& to_buf = new_array_buffer_object->buffer(); - // 26. Perform CopyDataBlockBytes(toBuf, 0, fromBuf, first, newLen). - copy_data_block_bytes(to_buf, 0, from_buf, first, new_length); + // 26. Let currentLen be O.[[ArrayBufferByteLength]]. + auto current_length = array_buffer_object->byte_length(); - // 27. Return new. + // 27. If first < currentLen, then + if (first < current_length) { + // a. Let count be min(newLen, currentLen - first). + auto count = min(new_length, current_length - first); + + // b. Perform CopyDataBlockBytes(toBuf, 0, fromBuf, first, count). + copy_data_block_bytes(to_buf, 0, from_buf, first, count); + } + + // 28. Return new. return new_array_buffer_object; } diff --git a/Userland/Libraries/LibJS/Runtime/ArrayBufferPrototype.h b/Userland/Libraries/LibJS/Runtime/ArrayBufferPrototype.h index db3251f2e7..c859623b2b 100644 --- a/Userland/Libraries/LibJS/Runtime/ArrayBufferPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/ArrayBufferPrototype.h @@ -23,6 +23,9 @@ private: explicit ArrayBufferPrototype(Realm&); JS_DECLARE_NATIVE_FUNCTION(byte_length_getter); + JS_DECLARE_NATIVE_FUNCTION(max_byte_length); + JS_DECLARE_NATIVE_FUNCTION(resizable); + JS_DECLARE_NATIVE_FUNCTION(resize); JS_DECLARE_NATIVE_FUNCTION(slice); JS_DECLARE_NATIVE_FUNCTION(detached_getter); JS_DECLARE_NATIVE_FUNCTION(transfer); diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 280affeb91..6f38f7f9c4 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -351,6 +351,7 @@ namespace JS { P(leftContext) \ P(map) \ P(max) \ + P(maxByteLength) \ P(maximize) \ P(mergeFields) \ P(message) \ @@ -426,6 +427,8 @@ namespace JS { P(reject) \ P(relativeTo) \ P(repeat) \ + P(resizable) \ + P(resize) \ P(resolve) \ P(resolvedOptions) \ P(reverse) \ diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index b22c1fba0c..873255f8fd 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -18,6 +18,7 @@ M(BigIntFromNonIntegral, "Cannot convert non-integral number to BigInt") \ M(BigIntInvalidValue, "Invalid value for BigInt: {}") \ M(BindingNotInitialized, "Binding {} is not initialized") \ + M(ByteLengthExceedsMaxByteLength, "ArrayBuffer byte length of {} exceeds the max byte length of {}") \ M(CallStackSizeExceeded, "Call stack size limit exceeded") \ M(CannotDeclareGlobalFunction, "Cannot declare global function of name '{}'") \ M(CannotDeclareGlobalVariable, "Cannot declare global variable of name '{}'") \ @@ -37,6 +38,7 @@ M(DivisionByZero, "Division by zero") \ M(DynamicImportNotAllowed, "Dynamic Imports are not allowed") \ M(FinalizationRegistrySameTargetAndValue, "Target and held value must not be the same") \ + M(FixedArrayBuffer, "ArrayBuffer is not resizable") \ M(GetCapabilitiesExecutorCalledMultipleTimes, "GetCapabilitiesExecutor was called multiple times") \ M(GeneratorAlreadyExecuting, "Generator is already executing") \ M(GeneratorBrandMismatch, "Generator brand '{}' does not match generator brand '{}')") \ diff --git a/Userland/Libraries/LibJS/Runtime/VM.cpp b/Userland/Libraries/LibJS/Runtime/VM.cpp index 3a35127b5b..1970aeeef5 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.cpp +++ b/Userland/Libraries/LibJS/Runtime/VM.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -134,6 +135,25 @@ VM::VM(OwnPtr custom_data, ErrorMessages error_messages) // NOTE: Since LibJS has no way of knowing whether the current environment is a browser we always // call HostEnsureCanAddPrivateElement when needed. }; + + // 25.1.3.7 HostResizeArrayBuffer ( buffer, newByteLength ), https://tc39.es/ecma262/#sec-hostresizearraybuffer + host_resize_array_buffer = [this](ArrayBuffer& buffer, size_t new_byte_length) -> ThrowCompletionOr { + // The host-defined abstract operation HostResizeArrayBuffer takes arguments buffer (an ArrayBuffer) and + // newByteLength (a non-negative integer) and returns either a normal completion containing either handled or + // unhandled, or a throw completion. It gives the host an opportunity to perform implementation-defined resizing + // of buffer. If the host chooses not to handle resizing of buffer, it may return unhandled for the default behaviour. + + // The implementation of HostResizeArrayBuffer must conform to the following requirements: + // - The abstract operation does not detach buffer. + // - If the abstract operation completes normally with handled, buffer.[[ArrayBufferByteLength]] is newByteLength. + + // The default implementation of HostResizeArrayBuffer is to return NormalCompletion(unhandled). + + if (auto result = buffer.buffer().try_resize(new_byte_length); result.is_error()) + return throw_completion(ErrorType::NotEnoughMemoryToAllocate, new_byte_length); + + return HandledByHost::Handled; + }; } VM::~VM() = default; diff --git a/Userland/Libraries/LibJS/Runtime/VM.h b/Userland/Libraries/LibJS/Runtime/VM.h index f0a217c524..9e835b9339 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.h +++ b/Userland/Libraries/LibJS/Runtime/VM.h @@ -32,6 +32,11 @@ namespace JS { class Identifier; struct BindingPattern; +enum class HandledByHost { + Handled, + Unhandled, +}; + class VM : public RefCounted { public: struct CustomData { @@ -252,6 +257,7 @@ public: Function host_make_job_callback; Function(Realm&)> host_ensure_can_compile_strings; Function(Object&)> host_ensure_can_add_private_element; + Function(ArrayBuffer&, size_t)> host_resize_array_buffer; // Execute a specific AST node either in AST or BC interpreter, depending on which one is enabled by default. // NOTE: This is meant as a temporary stopgap until everything is bytecode. diff --git a/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.js b/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.js index a09d591680..7befe5aa57 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.js +++ b/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.js @@ -17,3 +17,18 @@ test("ArrayBuffer size limit", () => { new ArrayBuffer(2 ** 53); }).toThrowWithMessage(RangeError, "Invalid array buffer length"); }); + +test("invalid ArrayBuffer maximum size option", () => { + expect(() => { + new ArrayBuffer(10, { maxByteLength: -1 }); + }).toThrowWithMessage(RangeError, "Index must be a positive integer"); +}); + +test("ArrayBuffer size exceeds maximum size", () => { + expect(() => { + new ArrayBuffer(10, { maxByteLength: 5 }); + }).toThrowWithMessage( + RangeError, + "ArrayBuffer byte length of 10 exceeds the max byte length of 5" + ); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.maxByteLength.js b/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.maxByteLength.js new file mode 100644 index 0000000000..54776fa778 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.maxByteLength.js @@ -0,0 +1,29 @@ +describe("errors", () => { + test("called on non-ArrayBuffer object", () => { + expect(() => { + ArrayBuffer.prototype.maxByteLength; + }).toThrowWithMessage(TypeError, "Not an object of type ArrayBuffer"); + }); +}); + +describe("normal behavior", () => { + test("detached buffer", () => { + let buffer = new ArrayBuffer(5); + detachArrayBuffer(buffer); + expect(buffer.maxByteLength).toBe(0); + + buffer = new ArrayBuffer(5, { maxByteLength: 10 }); + detachArrayBuffer(buffer); + expect(buffer.maxByteLength).toBe(0); + }); + + test("fixed buffer", () => { + let buffer = new ArrayBuffer(5); + expect(buffer.maxByteLength).toBe(5); + }); + + test("resizable buffer", () => { + let buffer = new ArrayBuffer(5, { maxByteLength: 10 }); + expect(buffer.maxByteLength).toBe(10); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.resizable.js b/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.resizable.js new file mode 100644 index 0000000000..1e85e5ff80 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.resizable.js @@ -0,0 +1,19 @@ +describe("errors", () => { + test("called on non-ArrayBuffer object", () => { + expect(() => { + ArrayBuffer.prototype.resizable; + }).toThrowWithMessage(TypeError, "Not an object of type ArrayBuffer"); + }); +}); + +describe("normal behavior", () => { + test("fixed buffer", () => { + let buffer = new ArrayBuffer(5); + expect(buffer.resizable).toBeFalse(); + }); + + test("resizable buffer", () => { + let buffer = new ArrayBuffer(5, { maxByteLength: 10 }); + expect(buffer.resizable).toBeTrue(); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.resize.js b/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.resize.js new file mode 100644 index 0000000000..0c62e80e0b --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.resize.js @@ -0,0 +1,57 @@ +describe("errors", () => { + test("called on non-ArrayBuffer object", () => { + expect(() => { + ArrayBuffer.prototype.resize(10); + }).toThrowWithMessage(TypeError, "Not an object of type ArrayBuffer"); + }); + + test("fixed buffer", () => { + let buffer = new ArrayBuffer(5); + detachArrayBuffer(buffer); + + expect(() => { + buffer.resize(10); + }).toThrowWithMessage(TypeError, "ArrayBuffer is not resizable"); + }); + + test("detached buffer", () => { + let buffer = new ArrayBuffer(5, { maxByteLength: 10 }); + detachArrayBuffer(buffer); + + expect(() => { + buffer.resize(10); + }).toThrowWithMessage(TypeError, "ArrayBuffer is detached"); + }); + + test("invalid new byte length", () => { + let buffer = new ArrayBuffer(5, { maxByteLength: 10 }); + + expect(() => { + buffer.resize(-1); + }).toThrowWithMessage(RangeError, "Index must be a positive integer"); + }); + + test("new byte length exceeds maximum size", () => { + let buffer = new ArrayBuffer(5, { maxByteLength: 10 }); + + expect(() => { + buffer.resize(11); + }).toThrowWithMessage( + RangeError, + "ArrayBuffer byte length of 11 exceeds the max byte length of 10" + ); + }); +}); + +describe("normal behavior", () => { + test("resizable buffer", () => { + let buffer = new ArrayBuffer(5, { maxByteLength: 10 }); + expect(buffer.byteLength).toBe(5); + expect(buffer.maxByteLength).toBe(10); + + for (let i = 0; i <= buffer.maxByteLength; ++i) { + buffer.resize(i); + expect(buffer.byteLength).toBe(i); + } + }); +});