From 3f3686cf7bce2f9cae6c19da5ca69150f7b2073a Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Thu, 28 Dec 2023 08:55:38 -0500 Subject: [PATCH] LibJS: Implement missing steps from the ArrayBuffer transfer proposal We can now implement steps related to resizable ArrayBuffer objects. We can also implement a couple of missing SharedArrayBuffer checks. The original implementation of this proposal did not have any tests, so tests are added here for the whole implementation. --- .../Libraries/LibJS/Runtime/ArrayBuffer.cpp | 24 ++++-- .../LibJS/Runtime/ArrayBufferPrototype.cpp | 3 +- .../ArrayBuffer.prototype.detached.js | 30 ++++++++ .../ArrayBuffer.prototype.transfer.js | 76 +++++++++++++++++++ ...yBuffer.prototype.transferToFixedLength.js | 76 +++++++++++++++++++ 5 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.detached.js create mode 100644 Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.transfer.js create mode 100644 Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.transferToFixedLength.js diff --git a/Userland/Libraries/LibJS/Runtime/ArrayBuffer.cpp b/Userland/Libraries/LibJS/Runtime/ArrayBuffer.cpp index 0aeabb68a7..d238b3e70c 100644 --- a/Userland/Libraries/LibJS/Runtime/ArrayBuffer.cpp +++ b/Userland/Libraries/LibJS/Runtime/ArrayBuffer.cpp @@ -262,13 +262,15 @@ ThrowCompletionOr> get_array_buffer_max_byte_length_option(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) +ThrowCompletionOr array_buffer_copy_and_detach(VM& vm, ArrayBuffer& array_buffer, Value new_length, PreserveResizability preserve_resizability) { auto& realm = *vm.current_realm(); // 1. Perform ? RequireInternalSlot(arrayBuffer, [[ArrayBufferData]]). - // FIXME: 2. If IsSharedArrayBuffer(arrayBuffer) is true, throw a TypeError exception. + // 2. If IsSharedArrayBuffer(arrayBuffer) is true, throw a TypeError exception. + if (array_buffer.is_shared_array_buffer()) + return vm.throw_completion(ErrorType::SharedArrayBuffer); // 3. If newLength is undefined, then // a. Let newByteLength be arrayBuffer.[[ArrayBufferByteLength]]. @@ -280,17 +282,25 @@ ThrowCompletionOr array_buffer_copy_and_detach(VM& vm, ArrayBuffer if (array_buffer.is_detached()) return vm.throw_completion(ErrorType::DetachedArrayBuffer); - // FIXME: 6. If preserveResizability is preserve-resizability and IsResizableArrayBuffer(arrayBuffer) is true, then - // a. Let newMaxByteLength be arrayBuffer.[[ArrayBufferMaxByteLength]]. + Optional new_max_byte_length; + + // 6. If preserveResizability is preserve-resizability and IsResizableArrayBuffer(arrayBuffer) is true, then + // FIXME: The ArrayBuffer transfer spec is a bit out-of-date. IsResizableArrayBuffer no longer exists, we now have IsFixedLengthArrayBuffer. + if (preserve_resizability == PreserveResizability::PreserveResizability && !array_buffer.is_fixed_length()) { + // a. Let newMaxByteLength be arrayBuffer.[[ArrayBufferMaxByteLength]]. + new_max_byte_length = array_buffer.max_byte_length(); + } // 7. Else, - // a. Let newMaxByteLength be empty. + else { + // a. Let newMaxByteLength be empty. + } // 8. If arrayBuffer.[[ArrayBufferDetachKey]] is not undefined, throw a TypeError exception. if (!array_buffer.detach_key().is_undefined()) return vm.throw_completion(ErrorType::DetachKeyMismatch, array_buffer.detach_key(), js_undefined()); - // 9. Let newBuffer be ? AllocateArrayBuffer(%ArrayBuffer%, newByteLength, FIXME: newMaxByteLength). - auto* new_buffer = TRY(allocate_array_buffer(vm, realm.intrinsics().array_buffer_constructor(), new_byte_length)); + // 9. Let newBuffer be ? AllocateArrayBuffer(%ArrayBuffer%, newByteLength, newMaxByteLength). + auto* new_buffer = TRY(allocate_array_buffer(vm, realm.intrinsics().array_buffer_constructor(), new_byte_length, new_max_byte_length)); // 10. Let copyLength be min(newByteLength, arrayBuffer.[[ArrayBufferByteLength]]). auto copy_length = min(new_byte_length, array_buffer.byte_length()); diff --git a/Userland/Libraries/LibJS/Runtime/ArrayBufferPrototype.cpp b/Userland/Libraries/LibJS/Runtime/ArrayBufferPrototype.cpp index 7b63f3599d..ff80b739e8 100644 --- a/Userland/Libraries/LibJS/Runtime/ArrayBufferPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/ArrayBufferPrototype.cpp @@ -278,7 +278,8 @@ JS_DEFINE_NATIVE_FUNCTION(ArrayBufferPrototype::detached_getter) auto array_buffer_object = TRY(typed_this_value(vm)); // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception. - // FIXME: Check for shared buffer + if (array_buffer_object->is_shared_array_buffer()) + return vm.throw_completion(ErrorType::SharedArrayBuffer); // 4. Return IsDetachedBuffer(O). return Value(array_buffer_object->is_detached()); diff --git a/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.detached.js b/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.detached.js new file mode 100644 index 0000000000..49cb2a87da --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.detached.js @@ -0,0 +1,30 @@ +describe("errors", () => { + test("called on non-ArrayBuffer object", () => { + expect(() => { + ArrayBuffer.prototype.detached; + }).toThrowWithMessage(TypeError, "Not an object of type ArrayBuffer"); + }); + + test("called on SharedArrayBuffer object", () => { + let detached = Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, "detached"); + let getter = detached.get; + + expect(() => { + getter.call(new SharedArrayBuffer()); + }).toThrowWithMessage(TypeError, "The array buffer object cannot be a SharedArrayBuffer"); + }); +}); + +describe("normal behavior", () => { + test("not detached", () => { + let buffer = new ArrayBuffer(); + expect(buffer.detached).toBeFalse(); + }); + + test("detached", () => { + let buffer = new ArrayBuffer(); + detachArrayBuffer(buffer); + + expect(buffer.detached).toBeTrue(); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.transfer.js b/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.transfer.js new file mode 100644 index 0000000000..cfcc41412f --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.transfer.js @@ -0,0 +1,76 @@ +describe("errors", () => { + test("called on non-ArrayBuffer object", () => { + expect(() => { + ArrayBuffer.prototype.transfer(Symbol.hasInstance); + }).toThrowWithMessage(TypeError, "Not an object of type ArrayBuffer"); + }); + + test("called on SharedArrayBuffer object", () => { + expect(() => { + ArrayBuffer.prototype.transfer.call(new SharedArrayBuffer()); + }).toThrowWithMessage(TypeError, "The array buffer object cannot be a SharedArrayBuffer"); + }); + + test("detached buffer", () => { + let buffer = new ArrayBuffer(5); + detachArrayBuffer(buffer); + + expect(() => { + buffer.transfer(); + }).toThrowWithMessage(TypeError, "ArrayBuffer is detached"); + }); +}); + +const readBuffer = buffer => { + let array = new Uint8Array(buffer, 0, buffer.byteLength / Uint8Array.BYTES_PER_ELEMENT); + let values = []; + + for (let value of array) { + values.push(Number(value)); + } + + return values; +}; + +const writeBuffer = (buffer, values) => { + let array = new Uint8Array(buffer, 0, buffer.byteLength / Uint8Array.BYTES_PER_ELEMENT); + array.set(values); +}; + +describe("normal behavior", () => { + test("old buffer is detached", () => { + let buffer = new ArrayBuffer(5); + let newBuffer = buffer.transfer(); + + expect(buffer.detached).toBeTrue(); + expect(newBuffer.detached).toBeFalse(); + }); + + test("resizability is preserved", () => { + let buffer = new ArrayBuffer(5, { maxByteLength: 10 }); + let newBuffer = buffer.transfer(); + + expect(buffer.resizable).toBeTrue(); + expect(newBuffer.resizable).toBeTrue(); + }); + + test("data is transferred", () => { + let buffer = new ArrayBuffer(5); + writeBuffer(buffer, [1, 2, 3, 4, 5]); + + let newBuffer = buffer.transfer(); + const values = readBuffer(newBuffer); + + expect(values).toEqual([1, 2, 3, 4, 5]); + }); + + test("length may be limited", () => { + let buffer = new ArrayBuffer(5); + writeBuffer(buffer, [1, 2, 3, 4, 5]); + + let newBuffer = buffer.transfer(3); + const values = readBuffer(newBuffer); + + expect(values).toEqual([1, 2, 3]); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.transferToFixedLength.js b/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.transferToFixedLength.js new file mode 100644 index 0000000000..56ce9c4fba --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/ArrayBuffer/ArrayBuffer.prototype.transferToFixedLength.js @@ -0,0 +1,76 @@ +describe("errors", () => { + test("called on non-ArrayBuffer object", () => { + expect(() => { + ArrayBuffer.prototype.transferToFixedLength(Symbol.hasInstance); + }).toThrowWithMessage(TypeError, "Not an object of type ArrayBuffer"); + }); + + test("called on SharedArrayBuffer object", () => { + expect(() => { + ArrayBuffer.prototype.transferToFixedLength.call(new SharedArrayBuffer()); + }).toThrowWithMessage(TypeError, "The array buffer object cannot be a SharedArrayBuffer"); + }); + + test("detached buffer", () => { + let buffer = new ArrayBuffer(5); + detachArrayBuffer(buffer); + + expect(() => { + buffer.transferToFixedLength(); + }).toThrowWithMessage(TypeError, "ArrayBuffer is detached"); + }); +}); + +const readBuffer = buffer => { + let array = new Uint8Array(buffer, 0, buffer.byteLength / Uint8Array.BYTES_PER_ELEMENT); + let values = []; + + for (let value of array) { + values.push(Number(value)); + } + + return values; +}; + +const writeBuffer = (buffer, values) => { + let array = new Uint8Array(buffer, 0, buffer.byteLength / Uint8Array.BYTES_PER_ELEMENT); + array.set(values); +}; + +describe("normal behavior", () => { + test("old buffer is detached", () => { + let buffer = new ArrayBuffer(5); + let newBuffer = buffer.transferToFixedLength(); + + expect(buffer.detached).toBeTrue(); + expect(newBuffer.detached).toBeFalse(); + }); + + test("resizability is not preserved", () => { + let buffer = new ArrayBuffer(5, { maxByteLength: 10 }); + let newBuffer = buffer.transferToFixedLength(); + + expect(buffer.resizable).toBeTrue(); + expect(newBuffer.resizable).toBeFalse(); + }); + + test("data is transferred", () => { + let buffer = new ArrayBuffer(5); + writeBuffer(buffer, [1, 2, 3, 4, 5]); + + let newBuffer = buffer.transferToFixedLength(); + const values = readBuffer(newBuffer); + + expect(values).toEqual([1, 2, 3, 4, 5]); + }); + + test("length may be limited", () => { + let buffer = new ArrayBuffer(5); + writeBuffer(buffer, [1, 2, 3, 4, 5]); + + let newBuffer = buffer.transferToFixedLength(3); + const values = readBuffer(newBuffer); + + expect(values).toEqual([1, 2, 3]); + }); +});