From 955ce10a5372bd7e52391ae608a5a6aa53ebb4bb Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sun, 1 Sep 2024 18:44:39 -0400 Subject: [PATCH] LibJS: Implement Uint8Array.prototype.setFromHex --- .../LibJS/Runtime/CommonPropertyNames.h | 1 + .../Libraries/LibJS/Runtime/Uint8Array.cpp | 61 ++++++++++++++ Userland/Libraries/LibJS/Runtime/Uint8Array.h | 1 + .../Uint8Array.prototype.setFromHex.js | 84 +++++++++++++++++++ 4 files changed, 147 insertions(+) create mode 100644 Userland/Libraries/LibJS/Tests/builtins/TypedArray/Uint8Array.prototype.setFromHex.js diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 3f30e59ccce8..cce23f2654ed 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -464,6 +464,7 @@ namespace JS { P(setFloat32) \ P(setFloat64) \ P(setFromBase64) \ + P(setFromHex) \ P(setFullYear) \ P(setHours) \ P(setInt8) \ diff --git a/Userland/Libraries/LibJS/Runtime/Uint8Array.cpp b/Userland/Libraries/LibJS/Runtime/Uint8Array.cpp index a38b63d14cd5..b6b3ed4df826 100644 --- a/Userland/Libraries/LibJS/Runtime/Uint8Array.cpp +++ b/Userland/Libraries/LibJS/Runtime/Uint8Array.cpp @@ -32,6 +32,7 @@ void Uint8ArrayPrototypeHelpers::initialize(Realm& realm, Object& prototype) prototype.define_native_function(realm, vm.names.toBase64, to_base64, 0, attr); prototype.define_native_function(realm, vm.names.toHex, to_hex, 0, attr); prototype.define_native_function(realm, vm.names.setFromBase64, set_from_base64, 1, attr); + prototype.define_native_function(realm, vm.names.setFromHex, set_from_hex, 1, attr); } static ThrowCompletionOr parse_alphabet(VM& vm, Object& options) @@ -307,6 +308,66 @@ JS_DEFINE_NATIVE_FUNCTION(Uint8ArrayConstructorHelpers::from_hex) return typed_array; } +// 6 Uint8Array.prototype.setFromHex ( string ), https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.setfromhex +JS_DEFINE_NATIVE_FUNCTION(Uint8ArrayPrototypeHelpers::set_from_hex) +{ + auto& realm = *vm.current_realm(); + + auto string_value = vm.argument(0); + + // 1. Let into be the this value. + // 2. Perform ? ValidateUint8Array(into). + auto into = TRY(validate_uint8_array(vm)); + + // 3. If string is not a String, throw a TypeError exception. + if (!string_value.is_string()) + return vm.throw_completion(ErrorType::NotAString, string_value); + + // 4. Let taRecord be MakeTypedArrayWithBufferWitnessRecord(into, seq-cst). + auto typed_array_record = make_typed_array_with_buffer_witness_record(into, ArrayBuffer::Order::SeqCst); + + // 5. If IsTypedArrayOutOfBounds(taRecord) is true, throw a TypeError exception. + if (is_typed_array_out_of_bounds(typed_array_record)) + return vm.throw_completion(ErrorType::BufferOutOfBounds, "TypedArray"sv); + + // 6. Let byteLength be TypedArrayLength(taRecord). + auto byte_length = typed_array_length(typed_array_record); + + // 7. Let result be FromHex(string, byteLength). + auto result = JS::from_hex(vm, string_value.as_string().utf8_string_view(), byte_length); + + // 8. Let bytes be result.[[Bytes]]. + auto bytes = move(result.bytes); + + // 9. Let written be the length of bytes. + auto written = bytes.size(); + + // 10. NOTE: FromHex does not invoke any user code, so the ArrayBuffer backing into cannot have been detached or shrunk. + // 11. Assert: written ≤ byteLength. + VERIFY(written <= byte_length); + + // 12. Perform SetUint8ArrayBytes(into, bytes). + set_uint8_array_bytes(into, bytes); + + // 13. If result.[[Error]] is not none, then + if (result.error.has_value()) { + // a. Throw result.[[Error]]. + return result.error.release_value(); + } + + // 14. Let resultObject be OrdinaryObjectCreate(%Object.prototype%). + auto result_object = Object::create(realm, realm.intrinsics().object_prototype()); + + // 15. Perform ! CreateDataPropertyOrThrow(resultObject, "read", 𝔽(result.[[Read]])). + MUST(result_object->create_data_property(vm.names.read, Value { result.read })); + + // 16. Perform ! CreateDataPropertyOrThrow(resultObject, "written", 𝔽(written)). + MUST(result_object->create_data_property(vm.names.written, Value { written })); + + // 17. Return resultObject. + return result_object; +} + // 7 ValidateUint8Array ( ta ), https://tc39.es/proposal-arraybuffer-base64/spec/#sec-validateuint8array ThrowCompletionOr> validate_uint8_array(VM& vm) { diff --git a/Userland/Libraries/LibJS/Runtime/Uint8Array.h b/Userland/Libraries/LibJS/Runtime/Uint8Array.h index d96b64832776..f5e8929ea8fc 100644 --- a/Userland/Libraries/LibJS/Runtime/Uint8Array.h +++ b/Userland/Libraries/LibJS/Runtime/Uint8Array.h @@ -33,6 +33,7 @@ class Uint8ArrayPrototypeHelpers { JS_DECLARE_NATIVE_FUNCTION(to_base64); JS_DECLARE_NATIVE_FUNCTION(to_hex); JS_DECLARE_NATIVE_FUNCTION(set_from_base64); + JS_DECLARE_NATIVE_FUNCTION(set_from_hex); }; enum class Alphabet { diff --git a/Userland/Libraries/LibJS/Tests/builtins/TypedArray/Uint8Array.prototype.setFromHex.js b/Userland/Libraries/LibJS/Tests/builtins/TypedArray/Uint8Array.prototype.setFromHex.js new file mode 100644 index 000000000000..0dd73ce3ee01 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/TypedArray/Uint8Array.prototype.setFromHex.js @@ -0,0 +1,84 @@ +describe("errors", () => { + test("called on non-Uint8Array object", () => { + expect(() => { + Uint8Array.prototype.setFromHex.call(""); + }).toThrowWithMessage(TypeError, "Not an object of type Uint8Array"); + + expect(() => { + Uint8Array.prototype.setFromHex.call(new Uint16Array()); + }).toThrowWithMessage(TypeError, "Not an object of type Uint8Array"); + }); + + test("detached ArrayBuffer", () => { + let arrayBuffer = new ArrayBuffer(5, { maxByteLength: 10 }); + let typedArray = new Uint8Array(arrayBuffer, Uint8Array.BYTES_PER_ELEMENT, 1); + detachArrayBuffer(arrayBuffer); + + expect(() => { + typedArray.setFromHex(""); + }).toThrowWithMessage( + TypeError, + "TypedArray contains a property which references a value at an index not contained within its buffer's bounds" + ); + }); + + test("ArrayBuffer out of bounds", () => { + let arrayBuffer = new ArrayBuffer(Uint8Array.BYTES_PER_ELEMENT * 2, { + maxByteLength: Uint8Array.BYTES_PER_ELEMENT * 4, + }); + + let typedArray = new Uint8Array(arrayBuffer, Uint8Array.BYTES_PER_ELEMENT, 1); + arrayBuffer.resize(Uint8Array.BYTES_PER_ELEMENT); + + expect(() => { + typedArray.setFromHex(""); + }).toThrowWithMessage( + TypeError, + "TypedArray contains a property which references a value at an index not contained within its buffer's bounds" + ); + }); + + test("invalid string", () => { + expect(() => { + new Uint8Array(10).setFromHex(3.14); + }).toThrowWithMessage(TypeError, "3.14 is not a string"); + }); + + test("odd number of characters", () => { + expect(() => { + new Uint8Array(10).setFromHex("a"); + }).toThrowWithMessage(SyntaxError, "Hex string must have an even length"); + }); + + test("invalid alphabet", () => { + expect(() => { + new Uint8Array(10).setFromHex("qq"); + }).toThrowWithMessage(SyntaxError, "Hex string must only contain hex characters"); + }); +}); + +describe("correct behavior", () => { + test("length is 1", () => { + expect(Uint8Array.prototype.setFromHex).toHaveLength(1); + }); + + const decodeEqual = (input, expected) => { + expected = toUTF8Bytes(expected); + + let array = new Uint8Array(expected.length); + let result = array.setFromHex(input); + + expect(result.read).toBe(input.length); + expect(result.written).toBe(expected.length); + + expect(array).toEqual(expected); + }; + + test("basic functionality", () => { + decodeEqual("", ""); + decodeEqual("61", "a"); + decodeEqual("616263646566303132333435", "abcdef012345"); + decodeEqual("f09fa493", "🤓"); + decodeEqual("f09fa493666f6ff09f9696", "🤓foo🖖"); + }); +});