mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-06-08 05:27:14 +09:00
AK+LibJS: Use simdutf for all base64 operations
We were previously unable to use simdutf for base64 decoding operations other than "loose". Upstream has added support for the "strict" and "stop-before-partial" operations, so let's make use of them!
This commit is contained in:
parent
e04545979f
commit
8a80ff7b3b
Notes:
github-actions[bot]
2025-05-03 15:22:03 +00:00
Author: https://github.com/trflynn89
Commit: 8a80ff7b3b
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/4568
6 changed files with 88 additions and 370 deletions
|
@ -17,38 +17,70 @@ size_t size_required_to_decode_base64(StringView input)
|
|||
return simdutf::maximal_binary_length_from_base64(input.characters_without_null_termination(), input.length());
|
||||
}
|
||||
|
||||
static ErrorOr<size_t, InvalidBase64> decode_base64_into_impl(StringView input, ByteBuffer& output, simdutf::base64_options options)
|
||||
static constexpr simdutf::last_chunk_handling_options to_simdutf_last_chunk_handling(LastChunkHandling last_chunk_handling)
|
||||
{
|
||||
size_t output_length = output.size();
|
||||
switch (last_chunk_handling) {
|
||||
case LastChunkHandling::Loose:
|
||||
return simdutf::last_chunk_handling_options::loose;
|
||||
case LastChunkHandling::Strict:
|
||||
return simdutf::last_chunk_handling_options::strict;
|
||||
case LastChunkHandling::StopBeforePartial:
|
||||
return simdutf::last_chunk_handling_options::stop_before_partial;
|
||||
}
|
||||
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
static ErrorOr<size_t, InvalidBase64> decode_base64_into_impl(StringView input, ByteBuffer& output, LastChunkHandling last_chunk_handling, simdutf::base64_options options)
|
||||
{
|
||||
static constexpr auto decode_up_to_bad_character = true;
|
||||
auto output_length = output.size();
|
||||
|
||||
auto result = simdutf::base64_to_binary_safe(
|
||||
input.characters_without_null_termination(),
|
||||
input.length(),
|
||||
reinterpret_cast<char*>(output.data()),
|
||||
output_length,
|
||||
options);
|
||||
options,
|
||||
to_simdutf_last_chunk_handling(last_chunk_handling),
|
||||
decode_up_to_bad_character);
|
||||
|
||||
if (result.error != simdutf::SUCCESS && result.error != simdutf::OUTPUT_BUFFER_TOO_SMALL) {
|
||||
if (result.error == simdutf::BASE64_INPUT_REMAINDER && last_chunk_handling == LastChunkHandling::StopBeforePartial) {
|
||||
result.error = simdutf::SUCCESS;
|
||||
} else if (result.error != simdutf::SUCCESS && result.error != simdutf::OUTPUT_BUFFER_TOO_SMALL) {
|
||||
output.resize((result.count / 4) * 3);
|
||||
|
||||
return InvalidBase64 {
|
||||
.error = Error::from_string_literal("Invalid base64-encoded data"),
|
||||
.valid_input_bytes = result.count,
|
||||
};
|
||||
auto error = [&]() {
|
||||
switch (result.error) {
|
||||
case simdutf::BASE64_EXTRA_BITS:
|
||||
return Error::from_string_literal("Extra bits found at end of chunk");
|
||||
case simdutf::BASE64_INPUT_REMAINDER:
|
||||
return Error::from_string_literal("Invalid trailing data");
|
||||
case simdutf::INVALID_BASE64_CHARACTER:
|
||||
return Error::from_string_literal("Invalid base64 character");
|
||||
default:
|
||||
return Error::from_string_literal("Invalid base64-encoded data");
|
||||
}
|
||||
}();
|
||||
|
||||
return InvalidBase64 { .error = move(error), .valid_input_bytes = result.count };
|
||||
}
|
||||
|
||||
VERIFY(output_length <= output.size());
|
||||
output.resize(output_length);
|
||||
|
||||
if (last_chunk_handling == LastChunkHandling::StopBeforePartial)
|
||||
return input.length() - (input.length() % 4);
|
||||
|
||||
return result.error == simdutf::SUCCESS ? input.length() : result.count;
|
||||
}
|
||||
|
||||
static ErrorOr<ByteBuffer> decode_base64_impl(StringView input, simdutf::base64_options options)
|
||||
static ErrorOr<ByteBuffer> decode_base64_impl(StringView input, LastChunkHandling last_chunk_handling, simdutf::base64_options options)
|
||||
{
|
||||
ByteBuffer output;
|
||||
TRY(output.try_resize(size_required_to_decode_base64(input)));
|
||||
|
||||
if (auto result = decode_base64_into_impl(input, output, options); result.is_error())
|
||||
if (auto result = decode_base64_into_impl(input, output, last_chunk_handling, options); result.is_error())
|
||||
return result.release_error().error;
|
||||
|
||||
return output;
|
||||
|
@ -68,24 +100,24 @@ static ErrorOr<String> encode_base64_impl(StringView input, simdutf::base64_opti
|
|||
return String::from_utf8_without_validation(output);
|
||||
}
|
||||
|
||||
ErrorOr<ByteBuffer> decode_base64(StringView input)
|
||||
ErrorOr<ByteBuffer> decode_base64(StringView input, LastChunkHandling last_chunk_handling)
|
||||
{
|
||||
return decode_base64_impl(input, simdutf::base64_default);
|
||||
return decode_base64_impl(input, last_chunk_handling, simdutf::base64_default);
|
||||
}
|
||||
|
||||
ErrorOr<ByteBuffer> decode_base64url(StringView input)
|
||||
ErrorOr<ByteBuffer> decode_base64url(StringView input, LastChunkHandling last_chunk_handling)
|
||||
{
|
||||
return decode_base64_impl(input, simdutf::base64_url);
|
||||
return decode_base64_impl(input, last_chunk_handling, simdutf::base64_url);
|
||||
}
|
||||
|
||||
ErrorOr<size_t, InvalidBase64> decode_base64_into(StringView input, ByteBuffer& output)
|
||||
ErrorOr<size_t, InvalidBase64> decode_base64_into(StringView input, ByteBuffer& output, LastChunkHandling last_chunk_handling)
|
||||
{
|
||||
return decode_base64_into_impl(input, output, simdutf::base64_default);
|
||||
return decode_base64_into_impl(input, output, last_chunk_handling, simdutf::base64_default);
|
||||
}
|
||||
|
||||
ErrorOr<size_t, InvalidBase64> decode_base64url_into(StringView input, ByteBuffer& output)
|
||||
ErrorOr<size_t, InvalidBase64> decode_base64url_into(StringView input, ByteBuffer& output, LastChunkHandling last_chunk_handling)
|
||||
{
|
||||
return decode_base64_into_impl(input, output, simdutf::base64_url);
|
||||
return decode_base64_into_impl(input, output, last_chunk_handling, simdutf::base64_url);
|
||||
}
|
||||
|
||||
ErrorOr<String> encode_base64(ReadonlyBytes input, OmitPadding omit_padding)
|
||||
|
|
14
AK/Base64.h
14
AK/Base64.h
|
@ -15,8 +15,14 @@ namespace AK {
|
|||
|
||||
size_t size_required_to_decode_base64(StringView);
|
||||
|
||||
ErrorOr<ByteBuffer> decode_base64(StringView);
|
||||
ErrorOr<ByteBuffer> decode_base64url(StringView);
|
||||
enum class LastChunkHandling {
|
||||
Loose,
|
||||
Strict,
|
||||
StopBeforePartial,
|
||||
};
|
||||
|
||||
ErrorOr<ByteBuffer> decode_base64(StringView, LastChunkHandling = LastChunkHandling::Loose);
|
||||
ErrorOr<ByteBuffer> decode_base64url(StringView, LastChunkHandling = LastChunkHandling::Loose);
|
||||
|
||||
struct InvalidBase64 {
|
||||
Error error;
|
||||
|
@ -25,8 +31,8 @@ struct InvalidBase64 {
|
|||
|
||||
// On success, these return the number of input bytes that were decoded. This might be less than the
|
||||
// string length if the output buffer was not large enough.
|
||||
ErrorOr<size_t, InvalidBase64> decode_base64_into(StringView, ByteBuffer&);
|
||||
ErrorOr<size_t, InvalidBase64> decode_base64url_into(StringView, ByteBuffer&);
|
||||
ErrorOr<size_t, InvalidBase64> decode_base64_into(StringView, ByteBuffer&, LastChunkHandling = LastChunkHandling::Loose);
|
||||
ErrorOr<size_t, InvalidBase64> decode_base64url_into(StringView, ByteBuffer&, LastChunkHandling = LastChunkHandling::Loose);
|
||||
|
||||
enum class OmitPadding {
|
||||
No,
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Base64.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <AK/StringUtils.h>
|
||||
#include <LibJS/Runtime/AbstractOperations.h>
|
||||
|
@ -55,23 +54,23 @@ static ThrowCompletionOr<Alphabet> parse_alphabet(VM& vm, Object& options)
|
|||
return vm.throw_completion<TypeError>(ErrorType::OptionIsNotValidValue, alphabet, "alphabet"sv);
|
||||
}
|
||||
|
||||
static ThrowCompletionOr<LastChunkHandling> parse_last_chunk_handling(VM& vm, Object& options)
|
||||
static ThrowCompletionOr<AK::LastChunkHandling> parse_last_chunk_handling(VM& vm, Object& options)
|
||||
{
|
||||
// Let lastChunkHandling be ? Get(opts, "lastChunkHandling").
|
||||
auto last_chunk_handling = TRY(options.get(vm.names.lastChunkHandling));
|
||||
|
||||
// If lastChunkHandling is undefined, set lastChunkHandling to "loose".
|
||||
if (last_chunk_handling.is_undefined())
|
||||
return LastChunkHandling::Loose;
|
||||
return AK::LastChunkHandling::Loose;
|
||||
|
||||
// If lastChunkHandling is not one of "loose", "strict", or "stop-before-partial", throw a TypeError exception.
|
||||
if (last_chunk_handling.is_string()) {
|
||||
if (last_chunk_handling.as_string().utf8_string_view() == "loose"sv)
|
||||
return LastChunkHandling::Loose;
|
||||
return AK::LastChunkHandling::Loose;
|
||||
if (last_chunk_handling.as_string().utf8_string_view() == "strict"sv)
|
||||
return LastChunkHandling::Strict;
|
||||
return AK::LastChunkHandling::Strict;
|
||||
if (last_chunk_handling.as_string().utf8_string_view() == "stop-before-partial"sv)
|
||||
return LastChunkHandling::StopBeforePartial;
|
||||
return AK::LastChunkHandling::StopBeforePartial;
|
||||
}
|
||||
|
||||
return vm.throw_completion<TypeError>(ErrorType::OptionIsNotValidValue, last_chunk_handling, "lastChunkHandling"sv);
|
||||
|
@ -451,337 +450,23 @@ void set_uint8_array_bytes(TypedArrayBase& into, ReadonlyBytes bytes)
|
|||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
// 1. Let length be the length of string.
|
||||
auto length = string.length();
|
||||
|
||||
// 2. Repeat, while index < length,
|
||||
while (index < length) {
|
||||
// a. Let char be the code unit at index index of string.
|
||||
auto ch = string[index];
|
||||
|
||||
// b. If char is neither 0x0009 (TAB), 0x000A (LF), 0x000C (FF), 0x000D (CR), nor 0x0020 (SPACE), then
|
||||
if (ch != '\t' && ch != '\n' && ch != '\f' && ch != '\r' && ch != ' ') {
|
||||
// i. Return index.
|
||||
return index;
|
||||
}
|
||||
|
||||
// c. Set index to index + 1.
|
||||
++index;
|
||||
}
|
||||
|
||||
// 3. Return index.
|
||||
return index;
|
||||
}
|
||||
|
||||
// 10.2 DecodeBase64Chunk ( chunk [ , throwOnExtraBits ] ), https://tc39.es/proposal-arraybuffer-base64/spec/#sec-frombase64
|
||||
static ThrowCompletionOr<ByteBuffer> decode_base64_chunk(VM& vm, StringBuilder& chunk, Optional<bool> throw_on_extra_bits = {})
|
||||
{
|
||||
// 1. Let chunkLength be the length of chunk.
|
||||
auto chunk_length = chunk.length();
|
||||
|
||||
// 2. If chunkLength is 2, then
|
||||
if (chunk_length == 2) {
|
||||
// a. Set chunk to the string-concatenation of chunk and "AA".
|
||||
chunk.append("AA"sv);
|
||||
}
|
||||
// 3. Else if chunkLength is 3, then
|
||||
else if (chunk_length == 3) {
|
||||
// a. Set chunk to the string-concatenation of chunk and "A".
|
||||
chunk.append("A"sv);
|
||||
}
|
||||
// 4. Else,
|
||||
else {
|
||||
// a. Assert: chunkLength is 4.
|
||||
VERIFY(chunk_length == 4);
|
||||
}
|
||||
|
||||
// 5. Let byteSequence be the unique sequence of 3 bytes resulting from decoding chunk as base64 (such that applying
|
||||
// the base64 encoding specified in section 4 of RFC 4648 to byteSequence would produce chunk).
|
||||
// 6. Let bytes be a List whose elements are the elements of byteSequence, in order.
|
||||
auto bytes = MUST(decode_base64(chunk.string_view()));
|
||||
|
||||
// 7. If chunkLength is 2, then
|
||||
if (chunk_length == 2) {
|
||||
// a. Assert: throwOnExtraBits is present.
|
||||
VERIFY(throw_on_extra_bits.has_value());
|
||||
|
||||
// b. If throwOnExtraBits is true and bytes[1] ≠ 0, then
|
||||
if (*throw_on_extra_bits && bytes[1] != 0) {
|
||||
// i. Throw a SyntaxError exception.
|
||||
return vm.throw_completion<SyntaxError>("Extra bits found at end of chunk"sv);
|
||||
}
|
||||
|
||||
// c. Return « bytes[0] ».
|
||||
return MUST(bytes.slice(0, 1));
|
||||
}
|
||||
|
||||
// 8. Else if chunkLength is 3, then
|
||||
if (chunk_length == 3) {
|
||||
// a. Assert: throwOnExtraBits is present.
|
||||
VERIFY(throw_on_extra_bits.has_value());
|
||||
|
||||
// b. If throwOnExtraBits is true and bytes[2] ≠ 0, then
|
||||
if (*throw_on_extra_bits && bytes[2] != 0) {
|
||||
// i. Throw a SyntaxError exception.
|
||||
return vm.throw_completion<SyntaxError>("Extra bits found at end of chunk"sv);
|
||||
}
|
||||
|
||||
// c. Return « bytes[0], bytes[1] ».
|
||||
return MUST(bytes.slice(0, 2));
|
||||
}
|
||||
|
||||
// 9. Else,
|
||||
// a. Return bytes.
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// 10.3 FromBase64 ( string, alphabet, lastChunkHandling [ , maxLength ] ), https://tc39.es/proposal-arraybuffer-base64/spec/#sec-frombase64
|
||||
DecodeResult from_base64(VM& vm, StringView string, Alphabet alphabet, LastChunkHandling last_chunk_handling, Optional<size_t> max_length)
|
||||
DecodeResult from_base64(VM& vm, StringView string, Alphabet alphabet, AK::LastChunkHandling last_chunk_handling, Optional<size_t> max_length)
|
||||
{
|
||||
// FIXME: We can only use simdutf when the last-chunk-handling parameter is "loose". Upstream is planning to implement
|
||||
// the remaining options. When that is complete, we should be able to remove the slow implementation below. See:
|
||||
// https://github.com/simdutf/simdutf/issues/440
|
||||
if (last_chunk_handling == LastChunkHandling::Loose) {
|
||||
auto output = MUST(ByteBuffer::create_uninitialized(max_length.value_or_lazy_evaluated([&]() {
|
||||
return AK::size_required_to_decode_base64(string);
|
||||
})));
|
||||
auto output = MUST(ByteBuffer::create_uninitialized(max_length.value_or_lazy_evaluated([&]() {
|
||||
return AK::size_required_to_decode_base64(string);
|
||||
})));
|
||||
|
||||
auto result = alphabet == Alphabet::Base64
|
||||
? AK::decode_base64_into(string, output)
|
||||
: AK::decode_base64url_into(string, output);
|
||||
auto result = alphabet == Alphabet::Base64
|
||||
? AK::decode_base64_into(string, output, last_chunk_handling)
|
||||
: AK::decode_base64url_into(string, output, last_chunk_handling);
|
||||
|
||||
if (result.is_error()) {
|
||||
auto error = vm.throw_completion<SyntaxError>(result.error().error.string_literal());
|
||||
return { .read = result.error().valid_input_bytes, .bytes = move(output), .error = move(error) };
|
||||
}
|
||||
|
||||
return { .read = result.value(), .bytes = move(output), .error = {} };
|
||||
if (result.is_error()) {
|
||||
auto error = vm.throw_completion<SyntaxError>(result.error().error.string_literal());
|
||||
return { .read = result.error().valid_input_bytes, .bytes = move(output), .error = move(error) };
|
||||
}
|
||||
|
||||
// 1. If maxLength is not present, then
|
||||
if (!max_length.has_value()) {
|
||||
// a. Let maxLength be 2**53 - 1.
|
||||
max_length = MAX_ARRAY_LIKE_INDEX;
|
||||
|
||||
// b. NOTE: Because the input is a string, the length of strings is limited to 2**53 - 1 characters, and the
|
||||
// output requires no more bytes than the input has characters, this limit can never be reached. However, it
|
||||
// is editorially convenient to use a finite value here.
|
||||
}
|
||||
|
||||
// 2. NOTE: The order of validation and decoding in the algorithm below is not observable. Implementations are
|
||||
// encouraged to perform them in whatever order is most efficient, possibly interleaving validation with decoding,
|
||||
// as long as the behaviour is observably equivalent.
|
||||
|
||||
// 3. If maxLength is 0, then
|
||||
if (max_length == 0uz) {
|
||||
// a. Return the Record { [[Read]]: 0, [[Bytes]]: « », [[Error]]: none }.
|
||||
return { .read = 0, .bytes = {}, .error = {} };
|
||||
}
|
||||
|
||||
// 4. Let read be 0.
|
||||
size_t read = 0;
|
||||
|
||||
// 5. Let bytes be « ».
|
||||
ByteBuffer bytes;
|
||||
|
||||
// 6. Let chunk be the empty String.
|
||||
StringBuilder chunk;
|
||||
|
||||
// 7. Let chunkLength be 0.
|
||||
size_t chunk_length = 0;
|
||||
|
||||
// 8. Let index be 0.
|
||||
size_t index = 0;
|
||||
|
||||
// 9. Let length be the length of string.
|
||||
auto length = string.length();
|
||||
|
||||
// 10. Repeat,
|
||||
while (true) {
|
||||
// a. Set index to SkipAsciiWhitespace(string, index).
|
||||
index = skip_ascii_whitespace(string, index);
|
||||
|
||||
// b. If index = length, then
|
||||
if (index == length) {
|
||||
// i. If chunkLength > 0, then
|
||||
if (chunk_length > 0) {
|
||||
// 1. If lastChunkHandling is "stop-before-partial", then
|
||||
if (last_chunk_handling == LastChunkHandling::StopBeforePartial) {
|
||||
// a. Return the Record { [[Read]]: read, [[Bytes]]: bytes, [[Error]]: none }.
|
||||
return { .read = read, .bytes = move(bytes), .error = {} };
|
||||
}
|
||||
// 2. Else if lastChunkHandling is "loose", then
|
||||
else if (last_chunk_handling == LastChunkHandling::Loose) {
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
// 3. Else,
|
||||
else {
|
||||
// a. Assert: lastChunkHandling is "strict".
|
||||
VERIFY(last_chunk_handling == LastChunkHandling::Strict);
|
||||
|
||||
// b. Let error be a new SyntaxError exception.
|
||||
auto error = vm.throw_completion<SyntaxError>("Invalid trailing data"sv);
|
||||
|
||||
// c. Return the Record { [[Read]]: read, [[Bytes]]: bytes, [[Error]]: error }.
|
||||
return { .read = read, .bytes = move(bytes), .error = move(error) };
|
||||
}
|
||||
}
|
||||
|
||||
// ii. Return the Record { [[Read]]: length, [[Bytes]]: bytes, [[Error]]: none }.
|
||||
return { .read = length, .bytes = move(bytes), .error = {} };
|
||||
}
|
||||
|
||||
// c. Let char be the substring of string from index to index + 1.
|
||||
auto ch = string[index];
|
||||
|
||||
// d. Set index to index + 1.
|
||||
++index;
|
||||
|
||||
// e. If char is "=", then
|
||||
if (ch == '=') {
|
||||
// i. If chunkLength < 2, then
|
||||
if (chunk_length < 2) {
|
||||
// 1. Let error be a new SyntaxError exception.
|
||||
auto error = vm.throw_completion<SyntaxError>("Unexpected padding character"sv);
|
||||
|
||||
// 2. Return the Record { [[Read]]: read, [[Bytes]]: bytes, [[Error]]: error }.
|
||||
return { .read = read, .bytes = move(bytes), .error = move(error) };
|
||||
}
|
||||
|
||||
// ii. Set index to SkipAsciiWhitespace(string, index).
|
||||
index = skip_ascii_whitespace(string, index);
|
||||
|
||||
// iii. If chunkLength = 2, then
|
||||
if (chunk_length == 2) {
|
||||
// 1. If index = length, then
|
||||
if (index == length) {
|
||||
// a. If lastChunkHandling is "stop-before-partial", then
|
||||
if (last_chunk_handling == LastChunkHandling::StopBeforePartial) {
|
||||
// i. Return the Record { [[Read]]: read, [[Bytes]]: bytes, [[Error]]: none }.
|
||||
return { .read = read, .bytes = move(bytes), .error = {} };
|
||||
}
|
||||
|
||||
// b. Let error be a new SyntaxError exception.
|
||||
auto error = vm.throw_completion<SyntaxError>("Incomplete number of padding characters"sv);
|
||||
|
||||
// c. Return the Record { [[Read]]: read, [[Bytes]]: bytes, [[Error]]: error }.
|
||||
return { .read = read, .bytes = move(bytes), .error = move(error) };
|
||||
}
|
||||
|
||||
// 2. Set char to the substring of string from index to index + 1.
|
||||
ch = string[index];
|
||||
|
||||
// 3. If char is "=", then
|
||||
if (ch == '=') {
|
||||
// a. Set index to SkipAsciiWhitespace(string, index + 1).
|
||||
index = skip_ascii_whitespace(string, index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// iv. If index < length, then
|
||||
if (index < length) {
|
||||
// 1. Let error be a new SyntaxError exception.
|
||||
auto error = vm.throw_completion<SyntaxError>("Unexpected padding character"sv);
|
||||
|
||||
// 2. Return the Record { [[Read]]: read, [[Bytes]]: bytes, [[Error]]: error }.
|
||||
return { .read = read, .bytes = move(bytes), .error = move(error) };
|
||||
}
|
||||
|
||||
// v. If lastChunkHandling is "strict", let throwOnExtraBits be true.
|
||||
// vi. Else, let throwOnExtraBits be false.
|
||||
auto throw_on_extra_bits = last_chunk_handling == LastChunkHandling::Strict;
|
||||
|
||||
// vii. Let decodeResult be Completion(DecodeBase64Chunk(chunk, throwOnExtraBits)).
|
||||
auto decode_result = decode_base64_chunk(vm, chunk, throw_on_extra_bits);
|
||||
|
||||
// viii. If decodeResult is an abrupt completion, then
|
||||
if (decode_result.is_error()) {
|
||||
// 1. Let error be decodeResult.[[Value]].
|
||||
auto error = decode_result.release_error();
|
||||
|
||||
// 2. Return the Record { [[Read]]: read, [[Bytes]]: bytes, [[Error]]: error }.
|
||||
return { .read = read, .bytes = move(bytes), .error = move(error) };
|
||||
}
|
||||
|
||||
// ix. Set bytes to the list-concatenation of bytes and ! decodeResult.
|
||||
bytes.append(decode_result.release_value());
|
||||
|
||||
// x. Return the Record { [[Read]]: length, [[Bytes]]: bytes, [[Error]]: none }.
|
||||
return { .read = length, .bytes = move(bytes), .error = {} };
|
||||
}
|
||||
|
||||
// f. If alphabet is "base64url", then
|
||||
if (alphabet == Alphabet::Base64URL) {
|
||||
// i. If char is either "+" or "/", then
|
||||
if (ch == '+' || ch == '/') {
|
||||
// 1. Let error be a new SyntaxError exception.
|
||||
auto error = vm.throw_completion<SyntaxError>(MUST(String::formatted("Invalid character '{}'", ch)));
|
||||
|
||||
// 2. Return the Record { [[Read]]: read, [[Bytes]]: bytes, [[Error]]: error }.
|
||||
return { .read = read, .bytes = move(bytes), .error = move(error) };
|
||||
}
|
||||
// ii. Else if char is "-", then
|
||||
else if (ch == '-') {
|
||||
// 1. Set char to "+".
|
||||
ch = '+';
|
||||
}
|
||||
// iii. Else if char is "_", then
|
||||
else if (ch == '_') {
|
||||
// 1. Set char to "/".
|
||||
ch = '/';
|
||||
}
|
||||
}
|
||||
|
||||
// g. If the sole code unit of char is not an element of the standard base64 alphabet, then
|
||||
static constexpr auto standard_base64_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"sv;
|
||||
|
||||
if (!standard_base64_alphabet.contains(ch)) {
|
||||
// i. Let error be a new SyntaxError exception.
|
||||
auto error = vm.throw_completion<SyntaxError>(MUST(String::formatted("Invalid character '{}'", ch)));
|
||||
|
||||
// ii. Return the Record { [[Read]]: read, [[Bytes]]: bytes, [[Error]]: error }.
|
||||
return { .read = read, .bytes = move(bytes), .error = move(error) };
|
||||
}
|
||||
|
||||
// h. Let remaining be maxLength - the length of bytes.
|
||||
auto remaining = *max_length - bytes.size();
|
||||
|
||||
// i. If remaining = 1 and chunkLength = 2, or if remaining = 2 and chunkLength = 3, then
|
||||
if ((remaining == 1 && chunk_length == 2) || (remaining == 2 && chunk_length == 3)) {
|
||||
// i. Return the Record { [[Read]]: read, [[Bytes]]: bytes, [[Error]]: none }.
|
||||
return { .read = read, .bytes = move(bytes), .error = {} };
|
||||
}
|
||||
|
||||
// j. Set chunk to the string-concatenation of chunk and char.
|
||||
chunk.append(ch);
|
||||
|
||||
// k. Set chunkLength to the length of chunk.
|
||||
chunk_length = chunk.length();
|
||||
|
||||
// l. If chunkLength = 4, then
|
||||
if (chunk_length == 4) {
|
||||
// i. Set bytes to the list-concatenation of bytes and ! DecodeBase64Chunk(chunk).
|
||||
bytes.append(MUST(decode_base64_chunk(vm, chunk)));
|
||||
|
||||
// ii. Set chunk to the empty String.
|
||||
chunk.clear();
|
||||
|
||||
// iii. Set chunkLength to 0.
|
||||
chunk_length = 0;
|
||||
|
||||
// iv. Set read to index.
|
||||
read = index;
|
||||
|
||||
// v. If the length of bytes = maxLength, then
|
||||
if (bytes.size() == max_length) {
|
||||
// 1. Return the Record { [[Read]]: read, [[Bytes]]: bytes, [[Error]]: none }.
|
||||
return { .read = read, .bytes = move(bytes), .error = {} };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { .read = result.value(), .bytes = move(output), .error = {} };
|
||||
}
|
||||
|
||||
// 10.4 FromHex ( string [ , maxLength ] ), https://tc39.es/proposal-arraybuffer-base64/spec/#sec-fromhex
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Base64.h>
|
||||
#include <AK/ByteBuffer.h>
|
||||
#include <AK/Optional.h>
|
||||
#include <AK/StringView.h>
|
||||
|
@ -41,12 +42,6 @@ enum class Alphabet {
|
|||
Base64URL,
|
||||
};
|
||||
|
||||
enum class LastChunkHandling {
|
||||
Loose,
|
||||
Strict,
|
||||
StopBeforePartial,
|
||||
};
|
||||
|
||||
struct DecodeResult {
|
||||
size_t read { 0 }; // [[Read]]
|
||||
ByteBuffer bytes; // [[Bytes]]
|
||||
|
@ -56,7 +51,7 @@ struct DecodeResult {
|
|||
ThrowCompletionOr<GC::Ref<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 = {});
|
||||
DecodeResult from_base64(VM&, StringView string, Alphabet alphabet, AK::LastChunkHandling last_chunk_handling, Optional<size_t> max_length = {});
|
||||
DecodeResult from_hex(VM&, StringView string, Optional<size_t> max_length = {});
|
||||
|
||||
}
|
||||
|
|
|
@ -40,25 +40,25 @@ describe("errors", () => {
|
|||
test("invalid padding", () => {
|
||||
expect(() => {
|
||||
Uint8Array.fromBase64("Zm9v=", { lastChunkHandling: "strict" });
|
||||
}).toThrowWithMessage(SyntaxError, "Unexpected padding character");
|
||||
}).toThrowWithMessage(SyntaxError, "Invalid trailing data");
|
||||
|
||||
expect(() => {
|
||||
Uint8Array.fromBase64("Zm9vaa=", { lastChunkHandling: "strict" });
|
||||
}).toThrowWithMessage(SyntaxError, "Incomplete number of padding characters");
|
||||
}).toThrowWithMessage(SyntaxError, "Invalid trailing data");
|
||||
|
||||
expect(() => {
|
||||
Uint8Array.fromBase64("Zm9vaa=a", { lastChunkHandling: "strict" });
|
||||
}).toThrowWithMessage(SyntaxError, "Unexpected padding character");
|
||||
}).toThrowWithMessage(SyntaxError, "Invalid base64 character");
|
||||
});
|
||||
|
||||
test("invalid alphabet", () => {
|
||||
expect(() => {
|
||||
Uint8Array.fromBase64("-", { lastChunkHandling: "strict" });
|
||||
}).toThrowWithMessage(SyntaxError, "Invalid character '-'");
|
||||
}).toThrowWithMessage(SyntaxError, "Invalid base64 character");
|
||||
|
||||
expect(() => {
|
||||
Uint8Array.fromBase64("+", { alphabet: "base64url", lastChunkHandling: "strict" });
|
||||
}).toThrowWithMessage(SyntaxError, "Invalid character '+'");
|
||||
}).toThrowWithMessage(SyntaxError, "Invalid base64 character");
|
||||
});
|
||||
|
||||
test("overlong chunk", () => {
|
||||
|
|
|
@ -79,28 +79,28 @@ describe("errors", () => {
|
|||
test("invalid padding", () => {
|
||||
expect(() => {
|
||||
new Uint8Array(10).setFromBase64("Zm9v=", { lastChunkHandling: "strict" });
|
||||
}).toThrowWithMessage(SyntaxError, "Unexpected padding character");
|
||||
}).toThrowWithMessage(SyntaxError, "Invalid trailing data");
|
||||
|
||||
expect(() => {
|
||||
new Uint8Array(10).setFromBase64("Zm9vaa=", { lastChunkHandling: "strict" });
|
||||
}).toThrowWithMessage(SyntaxError, "Incomplete number of padding characters");
|
||||
}).toThrowWithMessage(SyntaxError, "Invalid trailing data");
|
||||
|
||||
expect(() => {
|
||||
new Uint8Array(10).setFromBase64("Zm9vaa=a", { lastChunkHandling: "strict" });
|
||||
}).toThrowWithMessage(SyntaxError, "Unexpected padding character");
|
||||
}).toThrowWithMessage(SyntaxError, "Invalid base64 character");
|
||||
});
|
||||
|
||||
test("invalid alphabet", () => {
|
||||
expect(() => {
|
||||
new Uint8Array(10).setFromBase64("-", { lastChunkHandling: "strict" });
|
||||
}).toThrowWithMessage(SyntaxError, "Invalid character '-'");
|
||||
}).toThrowWithMessage(SyntaxError, "Invalid base64 character");
|
||||
|
||||
expect(() => {
|
||||
new Uint8Array(10).setFromBase64("+", {
|
||||
alphabet: "base64url",
|
||||
lastChunkHandling: "strict",
|
||||
});
|
||||
}).toThrowWithMessage(SyntaxError, "Invalid character '+'");
|
||||
}).toThrowWithMessage(SyntaxError, "Invalid base64 character");
|
||||
});
|
||||
|
||||
test("overlong chunk", () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue