From f350c153e82a0c35da8377cab53be49014b68bf9 Mon Sep 17 00:00:00 2001 From: ForLoveOfCats Date: Wed, 2 Mar 2022 11:24:46 -0500 Subject: [PATCH] LibJS: Implement and test ArrayBuffer.prototype.resize --- .../LibJS/Runtime/ArrayBufferPrototype.cpp | 55 +++++++++++++++++++ .../LibJS/Runtime/ArrayBufferPrototype.h | 1 + .../LibJS/Runtime/CommonPropertyNames.h | 1 + Userland/Libraries/LibJS/Runtime/ErrorTypes.h | 2 + Userland/Libraries/LibJS/Runtime/VM.cpp | 18 ++++++ Userland/Libraries/LibJS/Runtime/VM.h | 6 ++ .../ArrayBuffer.prototype.resize.js | 35 ++++++++++++ 7 files changed, 118 insertions(+) create mode 100644 Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.resize.js diff --git a/Userland/Libraries/LibJS/Runtime/ArrayBufferPrototype.cpp b/Userland/Libraries/LibJS/Runtime/ArrayBufferPrototype.cpp index f9fda290d9..49581da9b5 100644 --- a/Userland/Libraries/LibJS/Runtime/ArrayBufferPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/ArrayBufferPrototype.cpp @@ -26,6 +26,7 @@ void ArrayBufferPrototype::initialize(GlobalObject& global_object) Object::initialize(global_object); u8 attr = Attribute::Writable | Attribute::Configurable; define_native_function(vm.names.slice, slice, 2, attr); + define_native_function(vm.names.resize, resize, 1, attr); define_native_accessor(vm.names.byteLength, byte_length_getter, {}, Attribute::Configurable); define_native_accessor(vm.names.maxByteLength, max_byte_length_getter, {}, Attribute::Configurable); define_native_accessor(vm.names.resizable, resizable_getter, {}, Attribute::Configurable); @@ -127,6 +128,60 @@ JS_DEFINE_NATIVE_FUNCTION(ArrayBufferPrototype::slice) return new_array_buffer_object; } +// 1.3.5 ArrayBuffer.prototype.resize ( newLength ), https://tc39.es/proposal-resizablearraybuffer/#sec-arraybuffer.prototype.resize +JS_DEFINE_NATIVE_FUNCTION(ArrayBufferPrototype::resize) +{ + // 1. Let O be the this value. + auto* array_buffer_object = TRY(typed_this_value(global_object)); + + // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferMaxByteLength]]). + if (!array_buffer_object->is_resizable_array_buffer()) + return vm.throw_completion(global_object, ErrorType::NotAResizableArrayBuffer); + + // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception. + // FIXME: Check for shared buffer + + // 4. If IsDetachedBuffer(O) is true, throw a TypeError exception. + if (array_buffer_object->is_detached()) + return vm.throw_completion(global_object, ErrorType::DetachedArrayBuffer); + + // 5. Let newByteLength be ? ToIntegerOrInfinity(newLength). + auto new_byte_length = TRY(vm.argument(0).to_integer_or_infinity(global_object)); + + // 6. If newByteLength < 0 or newByteLength > O.[[ArrayBufferMaxByteLength]], throw a RangeError exception. + if (new_byte_length < 0 || new_byte_length > array_buffer_object->max_byte_length()) + return vm.throw_completion(global_object, ErrorType::NewByteLengthOutOfRange); + + // 7. Let hostHandled be ? HostResizeArrayBuffer(O, newByteLength). + auto host_handled = TRY(vm.host_resize_array_buffer(global_object, (size_t)new_byte_length)); + + // 8. If hostHandled is handled, return undefined. + if (host_handled == VM::HostResizeArrayBufferResult::Handled) + return js_undefined(); + + // 9. Let oldBlock be O.[[ArrayBufferData]]. + // 10. Let newBlock be ? CreateByteDataBlock(newByteLength). + // 11. Let copyLength be min(newByteLength, O.[[ArrayBufferByteLength]]). + // 12. Perform CopyDataBlockBytes(newBlock, 0, oldBlock, 0, copyLength). + // 13. NOTE: Neither creation of the new Data Block nor copying from the old Data Block are observable. Implementations reserve the right to implement this method as in-place growth or shrinkage. + // 14. Set O.[[ArrayBufferData]] to newBlock. + auto old_byte_length = array_buffer_object->byte_length(); + if (array_buffer_object->buffer().try_resize((size_t)new_byte_length).is_error()) + return global_object.vm().throw_completion(global_object, ErrorType::NotEnoughMemoryToAllocate, new_byte_length); + + // Resizing an `AK::ByteBuffer` does not zero initialize any new capacity, but we want it to be anyway + if (new_byte_length > old_byte_length) { + // This performs bounds checking whereas a raw `memset` call would not be + array_buffer_object->buffer().bytes().slice(old_byte_length).fill(0); + } + + // 15. Set O.[[ArrayBufferByteLength]] to newLength. + // This value is managed by the `AK::ByteBuffer` internally, it already has its new value + + // 16. Return undefined. + return js_undefined(); +} + // 25.1.5.1 get ArrayBuffer.prototype.byteLength, https://tc39.es/ecma262/#sec-get-arraybuffer.prototype.bytelength JS_DEFINE_NATIVE_FUNCTION(ArrayBufferPrototype::byte_length_getter) { diff --git a/Userland/Libraries/LibJS/Runtime/ArrayBufferPrototype.h b/Userland/Libraries/LibJS/Runtime/ArrayBufferPrototype.h index eb50c333ab..71a6592039 100644 --- a/Userland/Libraries/LibJS/Runtime/ArrayBufferPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/ArrayBufferPrototype.h @@ -21,6 +21,7 @@ public: private: JS_DECLARE_NATIVE_FUNCTION(slice); + JS_DECLARE_NATIVE_FUNCTION(resize); JS_DECLARE_NATIVE_FUNCTION(byte_length_getter); JS_DECLARE_NATIVE_FUNCTION(max_byte_length_getter); JS_DECLARE_NATIVE_FUNCTION(resizable_getter); diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 53f078d299..f9a45402e8 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -380,6 +380,7 @@ namespace JS { P(reject) \ P(relativeTo) \ P(repeat) \ + P(resize) \ P(resizable) \ P(resolve) \ P(resolvedOptions) \ diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 3ac7f5fc9a..74c8f38b7e 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -75,6 +75,7 @@ M(ModuleNotFound, "Cannot find/open module: '{}'") \ M(ModuleNotFoundNoReferencingScript, "Cannot resolve module {} without any active script or module") \ M(NegativeExponent, "Exponent must be positive") \ + M(NewByteLengthOutOfRange, "New byte length outside range supported by ArrayBuffer instance") \ M(NonExtensibleDefine, "Cannot define property {} on non-extensible object") \ M(NotAConstructor, "{} is not a constructor") \ M(NotAFunction, "{} is not a function") \ @@ -83,6 +84,7 @@ M(NotAnObjectOfType, "Not an object of type {}") \ M(NotAnObjectOrNull, "{} is neither an object nor null") \ M(NotAnObjectOrString, "{} is neither an object nor a string") \ + M(NotAResizableArrayBuffer, "ArrayBuffer instance not resizable") \ M(NotAString, "{} is not a string") \ M(NotASymbol, "{} is not a symbol") \ M(NotImplemented, "TODO({} is not implemented in LibJS)") \ diff --git a/Userland/Libraries/LibJS/Runtime/VM.cpp b/Userland/Libraries/LibJS/Runtime/VM.cpp index 2511e185ef..66edf44e72 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.cpp +++ b/Userland/Libraries/LibJS/Runtime/VM.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -113,6 +114,23 @@ VM::VM(OwnPtr custom_data) return Vector { "type" }; }; + // 1.1.7 HostResizeArrayBuffer ( buffer, newByteLength ), https://tc39.es/proposal-resizablearraybuffer/#sec-hostresizearraybuffer + host_resize_array_buffer = [](GlobalObject& global_object, size_t new_byte_length) { + // The host-defined abstract operation HostResizeArrayBuffer takes arguments buffer (an ArrayBuffer) and newByteLength (a non-negative integer). + // The host-defined abstract operation HostResizeArrayBuffer takes arguments buffer (an ArrayBuffer object) and newByteLength. + // 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 behavior. + // The implementation of HostResizeArrayBuffer must conform to the following requirements: + // * The abstract operation must return either NormalCompletion(handled), NormalCompletion(unhandled), or an abrupt throw completion. + // * 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 unhandled. + + (void)global_object; + (void)new_byte_length; + + return HostResizeArrayBufferResult::Unhandled; + }; + #define __JS_ENUMERATE(SymbolName, snake_name) \ m_well_known_symbol_##snake_name = js_symbol(*this, "Symbol." #SymbolName, false); JS_ENUMERATE_WELL_KNOWN_SYMBOLS diff --git a/Userland/Libraries/LibJS/Runtime/VM.h b/Userland/Libraries/LibJS/Runtime/VM.h index c2fead7512..0ac8ed6c55 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.h +++ b/Userland/Libraries/LibJS/Runtime/VM.h @@ -39,6 +39,11 @@ public: static NonnullRefPtr create(OwnPtr = {}); ~VM(); + enum class HostResizeArrayBufferResult { + Unhandled, + Handled, + }; + Heap& heap() { return m_heap; } const Heap& heap() const { return m_heap; } @@ -223,6 +228,7 @@ public: Function host_enqueue_finalization_registry_cleanup_job; Function()>, Realm*)> host_enqueue_promise_job; Function host_make_job_callback; + Function(GlobalObject&, size_t)> host_resize_array_buffer; private: explicit VM(OwnPtr); 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..e8b67f16bc --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.resize.js @@ -0,0 +1,35 @@ +test("length is 1", () => { + expect(ArrayBuffer.prototype.resize).toHaveLength(1); +}); + +test("resize up to max", () => { + let a = new ArrayBuffer(0, { maxByteLength: 10 }); + a.resize(10); + expect(a.byteLength).toEqual(10); +}); + +test("resize less than max", () => { + let a = new ArrayBuffer(10, { maxByteLength: 10 }); + a.resize(5); + expect(a.byteLength).toEqual(5); +}); + +test("resize with negative length", () => { + let a = new ArrayBuffer(10, { maxByteLength: 10 }); + expect(() => { + a.resize(-1); + }).toThrowWithMessage( + RangeError, + "New byte length outside range supported by ArrayBuffer instance" + ); +}); + +test("resize past max length", () => { + let a = new ArrayBuffer(10, { maxByteLength: 10 }); + expect(() => { + a.resize(11); + }).toThrowWithMessage( + RangeError, + "New byte length outside range supported by ArrayBuffer instance" + ); +});