mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-09-29 08:11:13 +00:00
LibJS: Implement Uint8Array.prototype.setFromBase64
This commit is contained in:
parent
e8f27160bf
commit
440183b669
Notes:
github-actions[bot]
2024-09-03 15:46:43 +00:00
Author: https://github.com/trflynn89 Commit: https://github.com/LadybirdBrowser/ladybird/commit/440183b669d Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/1258 Reviewed-by: https://github.com/awesomekling
|
@ -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) \
|
||||
|
|
|
@ -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<Alphabet> 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<TypeError>(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<TypeError>(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<NonnullGCPtr<TypedArrayBase>> validate_uint8_array(VM& vm)
|
||||
{
|
||||
|
@ -251,6 +326,31 @@ ThrowCompletionOr<ByteBuffer> 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)
|
||||
{
|
||||
|
|
|
@ -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<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 = {});
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue