Преглед изворни кода

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.
Timothy Flynn пре 1 година
родитељ
комит
3f3686cf7b

+ 17 - 7
Userland/Libraries/LibJS/Runtime/ArrayBuffer.cpp

@@ -262,13 +262,15 @@ ThrowCompletionOr<Optional<size_t>> 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<ArrayBuffer*> array_buffer_copy_and_detach(VM& vm, ArrayBuffer& array_buffer, Value new_length, PreserveResizability)
+ThrowCompletionOr<ArrayBuffer*> 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<TypeError>(ErrorType::SharedArrayBuffer);
 
     // 3. If newLength is undefined, then
     // a. Let newByteLength be arrayBuffer.[[ArrayBufferByteLength]].
@@ -280,17 +282,25 @@ ThrowCompletionOr<ArrayBuffer*> array_buffer_copy_and_detach(VM& vm, ArrayBuffer
     if (array_buffer.is_detached())
         return vm.throw_completion<TypeError>(ErrorType::DetachedArrayBuffer);
 
-    // FIXME: 6. If preserveResizability is preserve-resizability and IsResizableArrayBuffer(arrayBuffer) is true, then
-    // a. Let newMaxByteLength be arrayBuffer.[[ArrayBufferMaxByteLength]].
+    Optional<size_t> 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<TypeError>(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());

+ 2 - 1
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<TypeError>(ErrorType::SharedArrayBuffer);
 
     // 4. Return IsDetachedBuffer(O).
     return Value(array_buffer_object->is_detached());

+ 30 - 0
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();
+    });
+});

+ 76 - 0
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]);
+    });
+});

+ 76 - 0
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]);
+    });
+});