LibJS: Implement Uint8Array.prototype.toBase64

This commit is contained in:
Timothy Flynn 2024-07-15 10:59:29 -04:00 committed by Andreas Kling
commit b97f9f2c55
Notes: github-actions[bot] 2024-09-03 15:47:03 +00:00
6 changed files with 294 additions and 0 deletions

View file

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

View file

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

View file

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

View file

@ -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;
}
}

View file

@ -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&);
}

View file

@ -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);
});
});