فهرست منبع

LibJS: Implement Uint8Array.prototype.toBase64

Timothy Flynn 1 سال پیش
والد
کامیت
b97f9f2c55

+ 1 - 0
Userland/Libraries/LibJS/CMakeLists.txt

@@ -249,6 +249,7 @@ set(SOURCES
     Runtime/TypedArray.cpp
     Runtime/TypedArrayConstructor.cpp
     Runtime/TypedArrayPrototype.cpp
+    Runtime/Uint8Array.cpp
     Runtime/Utf16String.cpp
     Runtime/Value.cpp
     Runtime/VM.cpp

+ 3 - 0
Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h

@@ -68,6 +68,7 @@ namespace JS {
     P(adopt)                                 \
     P(all)                                   \
     P(allSettled)                            \
+    P(alphabet)                              \
     P(anchor)                                \
     P(any)                                   \
     P(apply)                                 \
@@ -398,6 +399,7 @@ namespace JS {
     P(of)                                    \
     P(offset)                                \
     P(offsetNanoseconds)                     \
+    P(omitPadding)                           \
     P(overflow)                              \
     P(ownKeys)                               \
     P(padEnd)                                \
@@ -526,6 +528,7 @@ namespace JS {
     P(timeZone)                              \
     P(timeZoneName)                          \
     P(toArray)                               \
+    P(toBase64)                              \
     P(toDateString)                          \
     P(toExponential)                         \
     P(toFixed)                               \

+ 4 - 0
Userland/Libraries/LibJS/Runtime/TypedArray.cpp

@@ -15,6 +15,7 @@
 #include <LibJS/Runtime/Iterator.h>
 #include <LibJS/Runtime/TypedArray.h>
 #include <LibJS/Runtime/TypedArrayConstructor.h>
+#include <LibJS/Runtime/Uint8Array.h>
 #include <LibJS/Runtime/ValueInlines.h>
 
 namespace JS {
@@ -500,6 +501,9 @@ void TypedArrayBase::visit_edges(Visitor& visitor)
         auto& vm = this->vm();                                                                                              \
         Base::initialize(realm);                                                                                            \
         define_direct_property(vm.names.BYTES_PER_ELEMENT, Value((i32)sizeof(Type)), 0);                                    \
+                                                                                                                            \
+        if constexpr (IsSame<PrototypeName, Uint8ArrayPrototype>)                                                           \
+            Uint8ArrayPrototypeHelpers::initialize(realm, *this);                                                           \
     }                                                                                                                       \
                                                                                                                             \
     ConstructorName::ConstructorName(Realm& realm, Object& prototype)                                                       \

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

@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <AK/Base64.h>
+#include <LibJS/Runtime/Temporal/AbstractOperations.h>
+#include <LibJS/Runtime/TypedArray.h>
+#include <LibJS/Runtime/Uint8Array.h>
+#include <LibJS/Runtime/VM.h>
+#include <LibJS/Runtime/ValueInlines.h>
+
+namespace JS {
+
+void Uint8ArrayPrototypeHelpers::initialize(Realm& realm, Object& prototype)
+{
+    auto& vm = prototype.vm();
+
+    static constexpr u8 attr = Attribute::Writable | Attribute::Configurable;
+    prototype.define_native_function(realm, vm.names.toBase64, to_base64, 0, attr);
+}
+
+static ThrowCompletionOr<Alphabet> parse_alphabet(VM& vm, Object& options)
+{
+    // Let alphabet be ? Get(opts, "alphabet").
+    auto alphabet = TRY(options.get(vm.names.alphabet));
+
+    // If alphabet is undefined, set alphabet to "base64".
+    if (alphabet.is_undefined())
+        return Alphabet::Base64;
+
+    // If alphabet is neither "base64" nor "base64url", throw a TypeError exception.
+    if (alphabet.is_string()) {
+        if (alphabet.as_string().utf8_string_view() == "base64"sv)
+            return Alphabet::Base64;
+        if (alphabet.as_string().utf8_string_view() == "base64url"sv)
+            return Alphabet::Base64URL;
+    }
+
+    return vm.throw_completion<TypeError>(ErrorType::OptionIsNotValidValue, alphabet, "alphabet"sv);
+}
+
+// 1 Uint8Array.prototype.toBase64 ( [ options ] ), https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.tobase64
+JS_DEFINE_NATIVE_FUNCTION(Uint8ArrayPrototypeHelpers::to_base64)
+{
+    auto options_value = vm.argument(0);
+
+    // 1. Let O be the this value.
+    // 2. Perform ? ValidateUint8Array(O).
+    auto typed_array = TRY(validate_uint8_array(vm));
+
+    // 3. Let opts be ? GetOptionsObject(options).
+    auto* options = TRY(Temporal::get_options_object(vm, options_value));
+
+    // 4. Let alphabet be ? Get(opts, "alphabet").
+    // 5. If alphabet is undefined, set alphabet to "base64".
+    // 6. If alphabet is neither "base64" nor "base64url", throw a TypeError exception.
+    auto alphabet = TRY(parse_alphabet(vm, *options));
+
+    // 7. Let omitPadding be ToBoolean(? Get(opts, "omitPadding")).
+    auto omit_padding_value = TRY(options->get(vm.names.omitPadding)).to_boolean();
+    auto omit_padding = omit_padding_value ? AK::OmitPadding::Yes : AK::OmitPadding::No;
+
+    // 8. Let toEncode be ? GetUint8ArrayBytes(O).
+    auto to_encode = TRY(get_uint8_array_bytes(vm, typed_array));
+
+    String out_ascii;
+
+    // 9. If alphabet is "base64", then
+    if (alphabet == Alphabet::Base64) {
+        // a. Let outAscii be the sequence of code points which results from encoding toEncode according to the base64
+        //    encoding specified in section 4 of RFC 4648. Padding is included if and only if omitPadding is false.
+        out_ascii = MUST(encode_base64(to_encode, omit_padding));
+    }
+    // 10. Else,
+    else {
+        // a. Assert: alphabet is "base64url".
+        // b. Let outAscii be the sequence of code points which results from encoding toEncode according to the base64url
+        //    encoding specified in section 5 of RFC 4648. Padding is included if and only if omitPadding is false.
+        out_ascii = MUST(encode_base64url(to_encode, omit_padding));
+    }
+
+    // 11. Return CodePointsToString(outAscii).
+    return PrimitiveString::create(vm, move(out_ascii));
+}
+
+// 7 ValidateUint8Array ( ta ), https://tc39.es/proposal-arraybuffer-base64/spec/#sec-validateuint8array
+ThrowCompletionOr<NonnullGCPtr<TypedArrayBase>> validate_uint8_array(VM& vm)
+{
+    auto this_object = TRY(vm.this_value().to_object(vm));
+
+    // 1. Perform ? RequireInternalSlot(ta, [[TypedArrayName]]).
+    if (!this_object->is_typed_array())
+        return vm.throw_completion<TypeError>(ErrorType::NotAnObjectOfType, "Uint8Array");
+
+    auto& typed_array = static_cast<TypedArrayBase&>(*this_object.ptr());
+
+    // 2. If ta.[[TypedArrayName]] is not "Uint8Array", throw a TypeError exception.
+    if (typed_array.kind() != TypedArrayBase::Kind::Uint8Array)
+        return vm.throw_completion<TypeError>(ErrorType::NotAnObjectOfType, "Uint8Array");
+
+    // 3. Return UNUSED.
+    return typed_array;
+}
+
+// 8 GetUint8ArrayBytes ( ta ), https://tc39.es/proposal-arraybuffer-base64/spec/#sec-getuint8arraybytes
+ThrowCompletionOr<ByteBuffer> get_uint8_array_bytes(VM& vm, TypedArrayBase const& typed_array)
+{
+    // 1. Let buffer be ta.[[ViewedArrayBuffer]].
+    // 2. Let taRecord be MakeTypedArrayWithBufferWitnessRecord(ta, SEQ-CST).
+    auto typed_array_record = make_typed_array_with_buffer_witness_record(typed_array, ArrayBuffer::Order::SeqCst);
+
+    // 3. If IsTypedArrayOutOfBounds(taRecord) is true, throw a TypeError exception.
+    if (is_typed_array_out_of_bounds(typed_array_record))
+        return vm.throw_completion<TypeError>(ErrorType::BufferOutOfBounds, "TypedArray"sv);
+
+    // 4. Let len be TypedArrayLength(taRecord).
+    auto length = typed_array_length(typed_array_record);
+
+    // 5. Let byteOffset be ta.[[ByteOffset]].
+    auto byte_offset = typed_array.byte_offset();
+
+    // 6. Let bytes be a new empty List.
+    ByteBuffer bytes;
+
+    // 7. Let index be 0.
+    // 8. Repeat, while index < len,
+    for (u32 index = 0; index < length; ++index) {
+        // a. Let byteIndex be byteOffset + index.
+        auto byte_index = byte_offset + index;
+
+        // b. Let byte be ℝ(GetValueFromBuffer(buffer, byteIndex, UINT8, true, UNORDERED)).
+        auto byte = typed_array.get_value_from_buffer(byte_index, ArrayBuffer::Order::Unordered);
+
+        // c. Append byte to bytes.
+        bytes.append(MUST(byte.to_u8(vm)));
+
+        // d. Set index to index + 1.
+    }
+
+    // 9. Return bytes.
+    return bytes;
+}
+
+}

+ 30 - 0
Userland/Libraries/LibJS/Runtime/Uint8Array.h

@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibJS/Forward.h>
+#include <LibJS/Heap/GCPtr.h>
+
+namespace JS {
+
+class Uint8ArrayPrototypeHelpers {
+public:
+    static void initialize(Realm&, Object& prototype);
+
+private:
+    JS_DECLARE_NATIVE_FUNCTION(to_base64);
+};
+
+enum class Alphabet {
+    Base64,
+    Base64URL,
+};
+
+ThrowCompletionOr<NonnullGCPtr<TypedArrayBase>> validate_uint8_array(VM&);
+ThrowCompletionOr<ByteBuffer> get_uint8_array_bytes(VM&, TypedArrayBase const&);
+
+}

+ 110 - 0
Userland/Libraries/LibJS/Tests/builtins/TypedArray/Uint8Array.prototype.toBase64.js

@@ -0,0 +1,110 @@
+describe("errors", () => {
+    test("called on non-Uint8Array object", () => {
+        expect(() => {
+            Uint8Array.prototype.toBase64.call(1);
+        }).toThrowWithMessage(TypeError, "Not an object of type Uint8Array");
+
+        expect(() => {
+            Uint8Array.prototype.toBase64.call(new Uint16Array());
+        }).toThrowWithMessage(TypeError, "Not an object of type Uint8Array");
+    });
+
+    test("invalid options object", () => {
+        expect(() => {
+            new Uint8Array().toBase64(3.14);
+        }).toThrowWithMessage(TypeError, "Options is not an object");
+    });
+
+    test("invalid alphabet option", () => {
+        expect(() => {
+            new Uint8Array().toBase64({ alphabet: 3.14 });
+        }).toThrowWithMessage(TypeError, "3.14 is not a valid value for option alphabet");
+
+        expect(() => {
+            new Uint8Array().toBase64({ alphabet: "foo" });
+        }).toThrowWithMessage(TypeError, "foo is not a valid value for option alphabet");
+    });
+
+    test("detached ArrayBuffer", () => {
+        let arrayBuffer = new ArrayBuffer(5, { maxByteLength: 10 });
+        let typedArray = new Uint8Array(arrayBuffer, Uint8Array.BYTES_PER_ELEMENT, 1);
+        detachArrayBuffer(arrayBuffer);
+
+        expect(() => {
+            typedArray.toBase64();
+        }).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.toBase64();
+        }).toThrowWithMessage(
+            TypeError,
+            "TypedArray contains a property which references a value at an index not contained within its buffer's bounds"
+        );
+    });
+});
+
+describe("correct behavior", () => {
+    test("length is 0", () => {
+        expect(Uint8Array.prototype.toBase64).toHaveLength(0);
+    });
+
+    const encodeEqual = (input, expected, options) => {
+        const encoded = toUTF8Bytes(input).toBase64(options);
+        expect(encoded).toBe(expected);
+    };
+
+    test("basic functionality", () => {
+        encodeEqual("", "");
+        encodeEqual("f", "Zg==");
+        encodeEqual("fo", "Zm8=");
+        encodeEqual("foo", "Zm9v");
+        encodeEqual("foob", "Zm9vYg==");
+        encodeEqual("fooba", "Zm9vYmE=");
+        encodeEqual("foobar", "Zm9vYmFy");
+
+        encodeEqual("🤓", "8J+kkw==");
+        encodeEqual("🤓foo🖖", "8J+kk2Zvb/CflpY=");
+    });
+
+    test("omit padding", () => {
+        const options = { omitPadding: true };
+
+        encodeEqual("", "", options);
+        encodeEqual("f", "Zg", options);
+        encodeEqual("fo", "Zm8", options);
+        encodeEqual("foo", "Zm9v", options);
+        encodeEqual("foob", "Zm9vYg", options);
+        encodeEqual("fooba", "Zm9vYmE", options);
+        encodeEqual("foobar", "Zm9vYmFy", options);
+
+        encodeEqual("🤓", "8J+kkw", options);
+        encodeEqual("🤓foo🖖", "8J+kk2Zvb/CflpY", options);
+    });
+
+    test("base64url alphabet", () => {
+        const options = { alphabet: "base64url" };
+
+        encodeEqual("", "", options);
+        encodeEqual("f", "Zg==", options);
+        encodeEqual("fo", "Zm8=", options);
+        encodeEqual("foo", "Zm9v", options);
+        encodeEqual("foob", "Zm9vYg==", options);
+        encodeEqual("fooba", "Zm9vYmE=", options);
+        encodeEqual("foobar", "Zm9vYmFy", options);
+
+        encodeEqual("🤓", "8J-kkw==", options);
+        encodeEqual("🤓foo🖖", "8J-kk2Zvb_CflpY=", options);
+    });
+});