From b97f9f2c555ed937be54626f311adcb230ff52da Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Mon, 15 Jul 2024 10:59:29 -0400 Subject: [PATCH] LibJS: Implement Uint8Array.prototype.toBase64 --- Userland/Libraries/LibJS/CMakeLists.txt | 1 + .../LibJS/Runtime/CommonPropertyNames.h | 3 + .../Libraries/LibJS/Runtime/TypedArray.cpp | 4 + .../Libraries/LibJS/Runtime/Uint8Array.cpp | 146 ++++++++++++++++++ Userland/Libraries/LibJS/Runtime/Uint8Array.h | 30 ++++ .../Uint8Array.prototype.toBase64.js | 110 +++++++++++++ 6 files changed, 294 insertions(+) create mode 100644 Userland/Libraries/LibJS/Runtime/Uint8Array.cpp create mode 100644 Userland/Libraries/LibJS/Runtime/Uint8Array.h create mode 100644 Userland/Libraries/LibJS/Tests/builtins/TypedArray/Uint8Array.prototype.toBase64.js diff --git a/Userland/Libraries/LibJS/CMakeLists.txt b/Userland/Libraries/LibJS/CMakeLists.txt index f1d0bf40d63..bc7ef9dac06 100644 --- a/Userland/Libraries/LibJS/CMakeLists.txt +++ b/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 diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 959234bd60f..af5a85508eb 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/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) \ diff --git a/Userland/Libraries/LibJS/Runtime/TypedArray.cpp b/Userland/Libraries/LibJS/Runtime/TypedArray.cpp index af5c6232d20..adf826b6009 100644 --- a/Userland/Libraries/LibJS/Runtime/TypedArray.cpp +++ b/Userland/Libraries/LibJS/Runtime/TypedArray.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include 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) \ + Uint8ArrayPrototypeHelpers::initialize(realm, *this); \ } \ \ ConstructorName::ConstructorName(Realm& realm, Object& prototype) \ diff --git a/Userland/Libraries/LibJS/Runtime/Uint8Array.cpp b/Userland/Libraries/LibJS/Runtime/Uint8Array.cpp new file mode 100644 index 00000000000..deb83229f19 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/Uint8Array.cpp @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +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 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(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> 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(ErrorType::NotAnObjectOfType, "Uint8Array"); + + auto& typed_array = static_cast(*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(ErrorType::NotAnObjectOfType, "Uint8Array"); + + // 3. Return UNUSED. + return typed_array; +} + +// 8 GetUint8ArrayBytes ( ta ), https://tc39.es/proposal-arraybuffer-base64/spec/#sec-getuint8arraybytes +ThrowCompletionOr 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(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; +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/Uint8Array.h b/Userland/Libraries/LibJS/Runtime/Uint8Array.h new file mode 100644 index 00000000000..aab3e01309e --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/Uint8Array.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace JS { + +class Uint8ArrayPrototypeHelpers { +public: + static void initialize(Realm&, Object& prototype); + +private: + JS_DECLARE_NATIVE_FUNCTION(to_base64); +}; + +enum class Alphabet { + Base64, + Base64URL, +}; + +ThrowCompletionOr> validate_uint8_array(VM&); +ThrowCompletionOr get_uint8_array_bytes(VM&, TypedArrayBase const&); + +} diff --git a/Userland/Libraries/LibJS/Tests/builtins/TypedArray/Uint8Array.prototype.toBase64.js b/Userland/Libraries/LibJS/Tests/builtins/TypedArray/Uint8Array.prototype.toBase64.js new file mode 100644 index 00000000000..96aaf9eb67f --- /dev/null +++ b/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); + }); +});