From 440183b669d8b31b8fa49aa95d0ec1ecedb47776 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sat, 31 Aug 2024 10:24:01 -0400 Subject: [PATCH] LibJS: Implement Uint8Array.prototype.setFromBase64 --- .../LibJS/Runtime/CommonPropertyNames.h | 3 + .../Libraries/LibJS/Runtime/Uint8Array.cpp | 100 +++++++++++ Userland/Libraries/LibJS/Runtime/Uint8Array.h | 2 + .../Uint8Array.prototype.setFromBase64.js | 169 ++++++++++++++++++ 4 files changed, 274 insertions(+) create mode 100644 Userland/Libraries/LibJS/Tests/builtins/TypedArray/Uint8Array.prototype.setFromBase64.js diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 242a8acdd3b..3f202caa4c1 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -427,6 +427,7 @@ namespace JS { P(race) \ P(random) \ P(raw) \ + P(read) \ P(reason) \ P(reduce) \ P(reduceRight) \ @@ -461,6 +462,7 @@ namespace JS { P(setDate) \ P(setFloat32) \ P(setFloat64) \ + P(setFromBase64) \ P(setFullYear) \ P(setHours) \ P(setInt8) \ @@ -604,6 +606,7 @@ namespace JS { P(withResolvers) \ P(withTimeZone) \ P(writable) \ + P(written) \ P(year) \ P(yearMonthFromFields) \ P(yearOfWeek) \ diff --git a/Userland/Libraries/LibJS/Runtime/Uint8Array.cpp b/Userland/Libraries/LibJS/Runtime/Uint8Array.cpp index bb1791bb822..4e59267317b 100644 --- a/Userland/Libraries/LibJS/Runtime/Uint8Array.cpp +++ b/Userland/Libraries/LibJS/Runtime/Uint8Array.cpp @@ -29,6 +29,7 @@ void Uint8ArrayPrototypeHelpers::initialize(Realm& realm, Object& prototype) static constexpr u8 attr = Attribute::Writable | Attribute::Configurable; prototype.define_native_function(realm, vm.names.toBase64, to_base64, 0, attr); prototype.define_native_function(realm, vm.names.toHex, to_hex, 0, attr); + prototype.define_native_function(realm, vm.names.setFromBase64, set_from_base64, 1, attr); } static ThrowCompletionOr parse_alphabet(VM& vm, Object& options) @@ -193,6 +194,80 @@ JS_DEFINE_NATIVE_FUNCTION(Uint8ArrayConstructorHelpers::from_base64) return typed_array; } +// 4 Uint8Array.prototype.setFromBase64 ( string [ , options ] ), https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.setfrombase64 +JS_DEFINE_NATIVE_FUNCTION(Uint8ArrayPrototypeHelpers::set_from_base64) +{ + auto& realm = *vm.current_realm(); + + auto string_value = vm.argument(0); + auto options_value = vm.argument(1); + + // 1. Let into be the this value. + // 2. Perform ? ValidateUint8Array(into). + auto into = TRY(validate_uint8_array(vm)); + + // 3. If string is not a String, throw a TypeError exception. + if (!string_value.is_string()) + return vm.throw_completion(ErrorType::NotAString, string_value); + + // 4. Let opts be ? GetOptionsObject(options). + auto* options = TRY(Temporal::get_options_object(vm, options_value)); + + // 5. Let alphabet be ? Get(opts, "alphabet"). + // 6. If alphabet is undefined, set alphabet to "base64". + // 7. If alphabet is neither "base64" nor "base64url", throw a TypeError exception. + auto alphabet = TRY(parse_alphabet(vm, *options)); + + // 8. Let lastChunkHandling be ? Get(opts, "lastChunkHandling"). + // 9. If lastChunkHandling is undefined, set lastChunkHandling to "loose". + // 10. If lastChunkHandling is not one of "loose", "strict", or "stop-before-partial", throw a TypeError exception. + auto last_chunk_handling = TRY(parse_last_chunk_handling(vm, *options)); + + // 11. Let taRecord be MakeTypedArrayWithBufferWitnessRecord(into, seq-cst). + auto typed_array_record = make_typed_array_with_buffer_witness_record(into, ArrayBuffer::Order::SeqCst); + + // 12. 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); + + // 13. Let byteLength be TypedArrayLength(taRecord). + auto byte_length = typed_array_length(typed_array_record); + + // 14. Let result be FromBase64(string, alphabet, lastChunkHandling, byteLength). + auto result = JS::from_base64(vm, string_value.as_string().utf8_string_view(), alphabet, last_chunk_handling, byte_length); + + // 15. Let bytes be result.[[Bytes]]. + auto bytes = move(result.bytes); + + // 16. Let written be the length of bytes. + auto written = bytes.size(); + + // 17. NOTE: FromBase64 does not invoke any user code, so the ArrayBuffer backing into cannot have been detached or shrunk. + // 18. Assert: written ≤ byteLength. + VERIFY(written <= byte_length); + + // 19. Perform SetUint8ArrayBytes(into, bytes). + set_uint8_array_bytes(into, bytes); + + // 20. If result.[[Error]] is not none, then + if (result.error.has_value()) { + // a. Throw result.[[Error]]. + return result.error.release_value(); + } + + // 21. Let resultObject be OrdinaryObjectCreate(%Object.prototype%). + auto result_object = Object::create(realm, realm.intrinsics().object_prototype()); + + // 22. Perform ! CreateDataPropertyOrThrow(resultObject, "read", 𝔽(result.[[Read]])). + MUST(result_object->create_data_property(vm.names.read, Value { result.read })); + + // 23. Perform ! CreateDataPropertyOrThrow(resultObject, "written", 𝔽(written)). + MUST(result_object->create_data_property(vm.names.written, Value { written })); + + // 24. Return resultObject. + return result_object; +} + // 7 ValidateUint8Array ( ta ), https://tc39.es/proposal-arraybuffer-base64/spec/#sec-validateuint8array ThrowCompletionOr> validate_uint8_array(VM& vm) { @@ -251,6 +326,31 @@ ThrowCompletionOr get_uint8_array_bytes(VM& vm, TypedArrayBase const return bytes; } +// 9 SetUint8ArrayBytes ( into, bytes ), https://tc39.es/proposal-arraybuffer-base64/spec/#sec-writeuint8arraybytes +void set_uint8_array_bytes(TypedArrayBase& into, ReadonlyBytes bytes) +{ + // 1. Let offset be into.[[ByteOffset]]. + auto offset = into.byte_offset(); + + // 2. Let len be the length of bytes. + auto length = bytes.size(); + + // 3. Let index be 0. + // 4. Repeat, while index < len, + for (u32 index = 0; index < length; ++index) { + // a. Let byte be bytes[index]. + auto byte = bytes[index]; + + // b. Let byteIndexInBuffer be index + offset. + auto byte_index_in_buffer = index + offset; + + // c. Perform SetValueInBuffer(into.[[ViewedArrayBuffer]], byteIndexInBuffer, uint8, 𝔽(byte), true, unordered). + into.set_value_in_buffer(byte_index_in_buffer, Value { byte }, ArrayBuffer::Order::Unordered); + + // d. Set index to index + 1. + } +} + // 10.1 SkipAsciiWhitespace ( string, index ), https://tc39.es/proposal-arraybuffer-base64/spec/#sec-skipasciiwhitespace static size_t skip_ascii_whitespace(StringView string, size_t index) { diff --git a/Userland/Libraries/LibJS/Runtime/Uint8Array.h b/Userland/Libraries/LibJS/Runtime/Uint8Array.h index 5afe989c824..6bb499da370 100644 --- a/Userland/Libraries/LibJS/Runtime/Uint8Array.h +++ b/Userland/Libraries/LibJS/Runtime/Uint8Array.h @@ -31,6 +31,7 @@ public: private: JS_DECLARE_NATIVE_FUNCTION(to_base64); JS_DECLARE_NATIVE_FUNCTION(to_hex); + JS_DECLARE_NATIVE_FUNCTION(set_from_base64); }; enum class Alphabet { @@ -52,6 +53,7 @@ struct DecodeResult { 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 = {}); } diff --git a/Userland/Libraries/LibJS/Tests/builtins/TypedArray/Uint8Array.prototype.setFromBase64.js b/Userland/Libraries/LibJS/Tests/builtins/TypedArray/Uint8Array.prototype.setFromBase64.js new file mode 100644 index 00000000000..214b29e960c --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/TypedArray/Uint8Array.prototype.setFromBase64.js @@ -0,0 +1,169 @@ +describe("errors", () => { + test("called on non-Uint8Array object", () => { + expect(() => { + Uint8Array.prototype.setFromBase64.call(""); + }).toThrowWithMessage(TypeError, "Not an object of type Uint8Array"); + + expect(() => { + Uint8Array.prototype.setFromBase64.call(new Uint16Array()); + }).toThrowWithMessage(TypeError, "Not an object of type Uint8Array"); + }); + + test("detached ArrayBuffer", () => { + let arrayBuffer = new ArrayBuffer(5, { maxByteLength: 10 }); + let typedArray = new Uint8Array(arrayBuffer, Uint8Array.BYTES_PER_ELEMENT, 1); + detachArrayBuffer(arrayBuffer); + + expect(() => { + typedArray.setFromBase64(""); + }).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.setFromBase64(""); + }).toThrowWithMessage( + TypeError, + "TypedArray contains a property which references a value at an index not contained within its buffer's bounds" + ); + }); + + test("invalid string", () => { + expect(() => { + new Uint8Array(10).setFromBase64(3.14); + }).toThrowWithMessage(TypeError, "3.14 is not a string"); + }); + + test("invalid options object", () => { + expect(() => { + new Uint8Array(10).setFromBase64("", 3.14); + }).toThrowWithMessage(TypeError, "Options is not an object"); + }); + + test("invalid alphabet option", () => { + expect(() => { + new Uint8Array(10).setFromBase64("", { alphabet: 3.14 }); + }).toThrowWithMessage(TypeError, "3.14 is not a valid value for option alphabet"); + + expect(() => { + new Uint8Array(10).setFromBase64("", { alphabet: "foo" }); + }).toThrowWithMessage(TypeError, "foo is not a valid value for option alphabet"); + }); + + test("invalid lastChunkHandling option", () => { + expect(() => { + new Uint8Array(10).setFromBase64("", { lastChunkHandling: 3.14 }); + }).toThrowWithMessage(TypeError, "3.14 is not a valid value for option lastChunkHandling"); + + expect(() => { + new Uint8Array(10).setFromBase64("", { lastChunkHandling: "foo" }); + }).toThrowWithMessage(TypeError, "foo is not a valid value for option lastChunkHandling"); + }); + + test("strict mode with trailing data", () => { + expect(() => { + new Uint8Array(10).setFromBase64("Zm9va", { lastChunkHandling: "strict" }); + }).toThrowWithMessage(SyntaxError, "Invalid trailing data"); + }); + + test("invalid padding", () => { + expect(() => { + new Uint8Array(10).setFromBase64("Zm9v=", { lastChunkHandling: "strict" }); + }).toThrowWithMessage(SyntaxError, "Unexpected padding character"); + + expect(() => { + new Uint8Array(10).setFromBase64("Zm9vaa=", { lastChunkHandling: "strict" }); + }).toThrowWithMessage(SyntaxError, "Incomplete number of padding characters"); + + expect(() => { + new Uint8Array(10).setFromBase64("Zm9vaa=a", { lastChunkHandling: "strict" }); + }).toThrowWithMessage(SyntaxError, "Unexpected padding character"); + }); + + test("invalid alphabet", () => { + expect(() => { + new Uint8Array(10).setFromBase64("-", { lastChunkHandling: "strict" }); + }).toThrowWithMessage(SyntaxError, "Invalid character '-'"); + + expect(() => { + new Uint8Array(10).setFromBase64("+", { + alphabet: "base64url", + lastChunkHandling: "strict", + }); + }).toThrowWithMessage(SyntaxError, "Invalid character '+'"); + }); + + test("overlong chunk", () => { + expect(() => { + new Uint8Array(10).setFromBase64("Zh==", { lastChunkHandling: "strict" }); + }).toThrowWithMessage(SyntaxError, "Extra bits found at end of chunk"); + + expect(() => { + new Uint8Array(10).setFromBase64("Zm9=", { lastChunkHandling: "strict" }); + }).toThrowWithMessage(SyntaxError, "Extra bits found at end of chunk"); + }); +}); + +describe("correct behavior", () => { + test("length is 1", () => { + expect(Uint8Array.prototype.setFromBase64).toHaveLength(1); + }); + + const decodeEqual = (input, expected, options, expectedInputBytesRead) => { + expected = toUTF8Bytes(expected); + + let array = new Uint8Array(expected.length); + let result = array.setFromBase64(input, options); + + expect(result.read).toBe(expectedInputBytesRead || input.length); + expect(result.written).toBe(expected.length); + + expect(array).toEqual(expected); + }; + + test("basic functionality", () => { + decodeEqual("", ""); + decodeEqual("Zg==", "f"); + decodeEqual("Zm8=", "fo"); + decodeEqual("Zm9v", "foo"); + decodeEqual("Zm9vYg==", "foob"); + decodeEqual("Zm9vYmE=", "fooba"); + decodeEqual("Zm9vYmFy", "foobar"); + + decodeEqual("8J+kkw==", "🤓"); + decodeEqual("8J+kk2Zvb/CflpY=", "🤓foo🖖"); + }); + + test("base64url alphabet", () => { + const options = { alphabet: "base64url" }; + + decodeEqual("", "", options); + decodeEqual("Zg==", "f", options); + decodeEqual("Zm8=", "fo", options); + decodeEqual("Zm9v", "foo", options); + decodeEqual("Zm9vYg==", "foob", options); + decodeEqual("Zm9vYmE=", "fooba", options); + decodeEqual("Zm9vYmFy", "foobar", options); + + decodeEqual("8J-kkw==", "🤓", options); + decodeEqual("8J-kk2Zvb_CflpY=", "🤓foo🖖", options); + }); + + test("stop-before-partial lastChunkHandling", () => { + const options = { lastChunkHandling: "stop-before-partial" }; + + decodeEqual("Zm9v", "foo", options, 4); + decodeEqual("Zm9va", "foo", options, 4); + decodeEqual("Zm9vaa", "foo", options, 4); + }); +});