Bläddra i källkod

LibJS: Implement Uint8Array.fromHex

Timothy Flynn 11 månader sedan
förälder
incheckning
c7afd175bc

+ 1 - 0
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)                                   \

+ 94 - 0
Userland/Libraries/LibJS/Runtime/Uint8Array.cpp

@@ -6,6 +6,7 @@
 
 #include <AK/Base64.h>
 #include <AK/StringBuilder.h>
+#include <AK/StringUtils.h>
 #include <LibJS/Runtime/Temporal/AbstractOperations.h>
 #include <LibJS/Runtime/TypedArray.h>
 #include <LibJS/Runtime/Uint8Array.h>
@@ -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<TypeError>(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<NonnullGCPtr<TypedArrayBase>> 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<size_t> 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<SyntaxError>("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<u8>(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<SyntaxError>("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 = {} };
+}
+
 }

+ 2 - 0
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<NonnullGCPtr<TypedArrayBase>> validate_uint8_array(VM&);
 ThrowCompletionOr<ByteBuffer> 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<size_t> max_length = {});
+DecodeResult from_hex(VM&, StringView string, Optional<size_t> max_length = {});
 
 }

+ 38 - 0
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🖖");
+    });
+});