From c7afd175bc366fc52238fabf279d673824afd378 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sun, 1 Sep 2024 18:37:12 -0400 Subject: [PATCH] LibJS: Implement Uint8Array.fromHex --- .../LibJS/Runtime/CommonPropertyNames.h | 1 + .../Libraries/LibJS/Runtime/Uint8Array.cpp | 94 +++++++++++++++++++ Userland/Libraries/LibJS/Runtime/Uint8Array.h | 2 + .../builtins/TypedArray/Uint8Array.fromHex.js | 38 ++++++++ 4 files changed, 135 insertions(+) create mode 100644 Userland/Libraries/LibJS/Tests/builtins/TypedArray/Uint8Array.fromHex.js diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 3f202caa4c1..3f30e59ccce 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -220,6 +220,7 @@ namespace JS { P(fromEpochMilliseconds) \ P(fromEpochNanoseconds) \ P(fromEpochSeconds) \ + P(fromHex) \ P(fround) \ P(gc) \ P(get) \ diff --git a/Userland/Libraries/LibJS/Runtime/Uint8Array.cpp b/Userland/Libraries/LibJS/Runtime/Uint8Array.cpp index 4e59267317b..a38b63d14cd 100644 --- a/Userland/Libraries/LibJS/Runtime/Uint8Array.cpp +++ b/Userland/Libraries/LibJS/Runtime/Uint8Array.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -20,6 +21,7 @@ void Uint8ArrayConstructorHelpers::initialize(Realm& realm, Object& constructor) static constexpr u8 attr = Attribute::Writable | Attribute::Configurable; constructor.define_native_function(realm, vm.names.fromBase64, from_base64, 1, attr); + constructor.define_native_function(realm, vm.names.fromHex, from_hex, 1, attr); } void Uint8ArrayPrototypeHelpers::initialize(Realm& realm, Object& prototype) @@ -268,6 +270,43 @@ JS_DEFINE_NATIVE_FUNCTION(Uint8ArrayPrototypeHelpers::set_from_base64) return result_object; } +// 5 Uint8Array.fromHex ( string ), https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.fromhex +JS_DEFINE_NATIVE_FUNCTION(Uint8ArrayConstructorHelpers::from_hex) +{ + auto& realm = *vm.current_realm(); + + auto string_value = vm.argument(0); + + // 1. If string is not a String, throw a TypeError exception. + if (!string_value.is_string()) + return vm.throw_completion(ErrorType::NotAString, string_value); + + // 2. Let result be FromHex(string). + auto result = JS::from_hex(vm, string_value.as_string().utf8_string_view()); + + // 3. If result.[[Error]] is not none, then + if (result.error.has_value()) { + // a. Throw result.[[Error]]. + return result.error.release_value(); + } + + // 4. Let resultLength be the length of result.[[Bytes]]. + auto result_length = result.bytes.size(); + + // 5. Let ta be ? AllocateTypedArray("Uint8Array", %Uint8Array%, "%Uint8Array.prototype%", resultLength). + auto typed_array = TRY(Uint8Array::create(realm, result_length)); + + // 6. Set the value at each index of ta.[[ViewedArrayBuffer]].[[ArrayBufferData]] to the value at the corresponding + // index of result.[[Bytes]]. + auto& array_buffer_data = typed_array->viewed_array_buffer()->buffer(); + + for (size_t index = 0; index < result_length; ++index) + array_buffer_data[index] = result.bytes[index]; + + // 7. Return ta. + return typed_array; +} + // 7 ValidateUint8Array ( ta ), https://tc39.es/proposal-arraybuffer-base64/spec/#sec-validateuint8array ThrowCompletionOr> validate_uint8_array(VM& vm) { @@ -684,4 +723,59 @@ DecodeResult from_base64(VM& vm, StringView string, Alphabet alphabet, LastChunk } } +// 10.4 FromHex ( string [ , maxLength ] ), https://tc39.es/proposal-arraybuffer-base64/spec/#sec-fromhex +DecodeResult from_hex(VM& vm, StringView string, Optional max_length) +{ + // 1. If maxLength is not present, let maxLength be 2**53 - 1. + if (!max_length.has_value()) + max_length = MAX_ARRAY_LIKE_INDEX; + + // 2. Let length be the length of string. + auto length = string.length(); + + // 3. Let bytes be « ». + ByteBuffer bytes; + + // 4. Let read be 0. + size_t read = 0; + + // 5. If length modulo 2 is not 0, then + if (length % 2 != 0) { + // a. Let error be a new SyntaxError exception. + auto error = vm.throw_completion("Hex string must have an even length"sv); + + // b. Return the Record { [[Read]]: read, [[Bytes]]: bytes, [[Error]]: error }. + return { .read = read, .bytes = move(bytes), .error = move(error) }; + } + + // 6. Repeat, while read < length and the length of bytes < maxLength, + while (read < length && bytes.size() < *max_length) { + // a. Let hexits be the substring of string from read to read + 2. + auto hexits = string.substring_view(read, 2); + + // d. Let byte be the integer value represented by hexits in base-16 notation, using the letters A-F and a-f + // for digits with values 10 through 15. + // NOTE: We do this early so that we don't have to effectively parse hexits twice. + auto byte = AK::StringUtils::convert_to_uint_from_hex(hexits, AK::TrimWhitespace::No); + + // b. If hexits contains any code units which are not in "0123456789abcdefABCDEF", then + if (!byte.has_value()) { + // i. Let error be a new SyntaxError exception. + auto error = vm.throw_completion("Hex string must only contain hex characters"sv); + + // ii. Return the Record { [[Read]]: read, [[Bytes]]: bytes, [[Error]]: error }. + return { .read = read, .bytes = move(bytes), .error = move(error) }; + } + + // c. Set read to read + 2. + read += 2; + + // e. Append byte to bytes. + bytes.append(*byte); + } + + // 7. Return the Record { [[Read]]: read, [[Bytes]]: bytes, [[Error]]: none }. + return { .read = read, .bytes = move(bytes), .error = {} }; +} + } diff --git a/Userland/Libraries/LibJS/Runtime/Uint8Array.h b/Userland/Libraries/LibJS/Runtime/Uint8Array.h index 6bb499da370..d96b6483277 100644 --- a/Userland/Libraries/LibJS/Runtime/Uint8Array.h +++ b/Userland/Libraries/LibJS/Runtime/Uint8Array.h @@ -22,6 +22,7 @@ public: private: JS_DECLARE_NATIVE_FUNCTION(from_base64); + JS_DECLARE_NATIVE_FUNCTION(from_hex); }; class Uint8ArrayPrototypeHelpers { @@ -55,5 +56,6 @@ ThrowCompletionOr> validate_uint8_array(VM&); ThrowCompletionOr get_uint8_array_bytes(VM&, TypedArrayBase const&); void set_uint8_array_bytes(TypedArrayBase&, ReadonlyBytes); DecodeResult from_base64(VM&, StringView string, Alphabet alphabet, LastChunkHandling last_chunk_handling, Optional max_length = {}); +DecodeResult from_hex(VM&, StringView string, Optional max_length = {}); } diff --git a/Userland/Libraries/LibJS/Tests/builtins/TypedArray/Uint8Array.fromHex.js b/Userland/Libraries/LibJS/Tests/builtins/TypedArray/Uint8Array.fromHex.js new file mode 100644 index 00000000000..de04ca6aa7e --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/TypedArray/Uint8Array.fromHex.js @@ -0,0 +1,38 @@ +describe("errors", () => { + test("invalid string", () => { + expect(() => { + Uint8Array.fromHex(3.14); + }).toThrowWithMessage(TypeError, "3.14 is not a string"); + }); + + test("odd number of characters", () => { + expect(() => { + Uint8Array.fromHex("a"); + }).toThrowWithMessage(SyntaxError, "Hex string must have an even length"); + }); + + test("invalid alphabet", () => { + expect(() => { + Uint8Array.fromHex("qq"); + }).toThrowWithMessage(SyntaxError, "Hex string must only contain hex characters"); + }); +}); + +describe("correct behavior", () => { + test("length is 1", () => { + expect(Uint8Array.fromHex).toHaveLength(1); + }); + + const decodeEqual = (input, expected, options) => { + const decoded = Uint8Array.fromHex(input, options); + expect(decoded).toEqual(toUTF8Bytes(expected)); + }; + + test("basic functionality", () => { + decodeEqual("", ""); + decodeEqual("61", "a"); + decodeEqual("616263646566303132333435", "abcdef012345"); + decodeEqual("f09fa493", "🤓"); + decodeEqual("f09fa493666f6ff09f9696", "🤓foo🖖"); + }); +});