LibJS: Use Utf16StringBuilder for more strings

Port more UTF-16 string construction sites to Utf16StringBuilder.
This covers JSON serialization, URI escaping, RegExp replacement, date
and Temporal formatting, Uint8Array hex conversion, and stack string
formatting. Keep byte-oriented debug, bytecode, parser, and print
plumbing on StringBuilder.

Add a checked JS string length sum helper and use it for accumulation
paths that append JS-observable string pieces.
This commit is contained in:
Andreas Kling 2026-06-22 01:59:18 +02:00 committed by Andreas Kling
parent 595ae25e74
commit 7c86352f34
11 changed files with 146 additions and 60 deletions

View file

@ -52,6 +52,15 @@ size_t max_js_string_length()
return NumericLimits<u32>::max();
}
ThrowCompletionOr<size_t> checked_js_string_length_sum(VM& vm, size_t addend_a, size_t addend_b, ErrorType const& error_type)
{
Checked<size_t> sum = addend_a;
sum += addend_b;
if (sum.has_overflow() || sum.value() > max_js_string_length())
return vm.throw_completion<RangeError>(error_type);
return sum.value();
}
ThrowCompletionOr<size_t> checked_js_string_length_product(VM& vm, size_t factor_a, size_t factor_b, ErrorType const& error_type)
{
Checked<size_t> product = factor_a;

View file

@ -79,6 +79,7 @@ bool all_import_attributes_supported(VM& vm, Vector<ImportAttribute> const& attr
ThrowCompletionOr<Value> perform_import_call(VM&, Value specifier, Value options_value);
size_t max_js_string_length();
ThrowCompletionOr<size_t> checked_js_string_length_sum(VM&, size_t, size_t, ErrorType const&);
ThrowCompletionOr<size_t> checked_js_string_length_product(VM&, size_t, size_t, ErrorType const&);
enum class CanonicalIndexMode {

View file

@ -6,7 +6,7 @@
*/
#include <AK/NeverDestroyed.h>
#include <AK/StringBuilder.h>
#include <AK/Utf16StringBuilder.h>
#include <LibJS/Runtime/ErrorData.h>
#include <LibJS/Runtime/ExecutionContext.h>
#include <LibJS/Runtime/ExternalMemory.h>
@ -65,7 +65,7 @@ Utf16String ErrorData::stack_string(CompactTraceback compact) const
if (m_traceback.is_empty())
return {};
StringBuilder stack_string_builder(StringBuilder::Mode::UTF16);
Utf16StringBuilder stack_string_builder;
// Note: We roughly follow V8's formatting
auto append_frame = [&](TracebackFrame const& frame) {
@ -75,11 +75,11 @@ Utf16String ErrorData::stack_string(CompactTraceback compact) const
if (!source_range.filename().is_empty() || source_range.start.line != 0 || source_range.start.column != 0) {
if (function_name.is_empty())
stack_string_builder.appendff(" at {}:{}:{}\n", source_range.filename(), source_range.start.line, source_range.start.column);
stack_string_builder.append(Utf16String::formatted(" at {}:{}:{}\n", source_range.filename(), source_range.start.line, source_range.start.column));
else
stack_string_builder.appendff(" at {} ({}:{}:{})\n", function_name, source_range.filename(), source_range.start.line, source_range.start.column);
stack_string_builder.append(Utf16String::formatted(" at {} ({}:{}:{})\n", function_name, source_range.filename(), source_range.start.line, source_range.start.column));
} else {
stack_string_builder.appendff(" at {}\n", function_name.is_empty() ? "<unknown>"_utf16 : function_name);
stack_string_builder.append(Utf16String::formatted(" at {}\n", function_name.is_empty() ? "<unknown>"_utf16 : function_name));
}
};
@ -110,7 +110,7 @@ Utf16String ErrorData::stack_string(CompactTraceback compact) const
// the name only once and show the number of repetitions instead. This prevents
// printing ridiculously large call stacks of recursive functions.
append_frame(frame);
stack_string_builder.appendff(" {} more calls\n", repetitions);
stack_string_builder.append(Utf16String::formatted(" {} more calls\n", repetitions));
} else {
for (size_t j = 0; j < repetitions + 1; j++)
append_frame(frame);
@ -120,7 +120,7 @@ Utf16String ErrorData::stack_string(CompactTraceback compact) const
for (size_t j = 0; j < repetitions; j++)
append_frame(m_traceback[used_frames - 1]);
return stack_string_builder.to_utf16_string();
return stack_string_builder.to_string();
}
}

View file

@ -244,6 +244,7 @@
M(StringNonGlobalRegExp, "RegExp argument is non-global") \
M(StringRepeatCountMustBe, "repeat count must be a {} number") \
M(StringRepeatCountMustNotOverflow, "repeat count must not overflow") \
M(StringSizeMustNotOverflow, "string size must not overflow") \
M(TemporalDifferentCalendars, "Cannot compare dates from two different calendars") \
M(TemporalDifferentTimeZones, "Cannot compare dates from two different time zones") \
M(TemporalDisambiguatePossibleEpochNSRejectMoreThanOne, "Cannot disambiguate two or more possible epoch nanoseconds") \

View file

@ -5,7 +5,6 @@
*/
#include <AK/Function.h>
#include <AK/StringBuilder.h>
#include <AK/TypeCasts.h>
#include <LibGC/RootVector.h>
#include <LibJS/Runtime/AbstractOperations.h>

View file

@ -7,7 +7,6 @@
#include <AK/GenericShorthands.h>
#include <AK/NeverDestroyed.h>
#include <AK/StringBuilder.h>
#include <AK/Utf16String.h>
#include <AK/Utf16StringBuilder.h>
#include <LibJS/Runtime/AbstractOperations.h>

View file

@ -9,6 +9,7 @@
#include <AK/StringBuilder.h>
#include <AK/StringConversions.h>
#include <AK/TypeCasts.h>
#include <AK/Utf16StringBuilder.h>
#include <AK/Utf16View.h>
#include <AK/Utf8View.h>
#include <LibJS/Runtime/AbstractOperations.h>
@ -487,7 +488,7 @@ JS_DEFINE_NATIVE_FUNCTION(JSONObject::parse)
// Returns {} on malformed escape sequences.
static Optional<Utf16String> unescape_json_string(StringView raw)
{
StringBuilder builder(StringBuilder::Mode::UTF16, raw.length());
Utf16StringBuilder builder(raw.length());
GenericLexer lexer { raw };
@ -586,7 +587,7 @@ static Optional<Utf16String> unescape_json_string(StringView raw)
}
}
return builder.to_utf16_string();
return builder.to_string();
}
template<typename T>

View file

@ -755,6 +755,7 @@ ThrowCompletionOr<Value> RegExpPrototype::symbol_replace_impl(VM& vm, Object& re
&& &realm == &typed_regexp->realm();
Utf16StringBuilder accumulated_result;
size_t accumulated_result_length = 0;
size_t next_source_position = 0;
bool had_match = false;
size_t last_match_start = 0;
@ -775,7 +776,10 @@ ThrowCompletionOr<Value> RegExpPrototype::symbol_replace_impl(VM& vm, Object& re
for (int i = 0; i < num_matches; ++i) {
auto [match_start, match_end] = compiled_regex->find_all_match(i);
if (static_cast<size_t>(match_start) >= next_source_position) {
accumulated_result.append(utf16_view.substring_view(next_source_position, match_start - next_source_position));
auto substring = utf16_view.substring_view(next_source_position, match_start - next_source_position);
accumulated_result_length = TRY(checked_js_string_length_sum(vm, accumulated_result_length, substring.length_in_code_units(), ErrorType::StringSizeMustNotOverflow));
accumulated_result.append(substring);
accumulated_result_length = TRY(checked_js_string_length_sum(vm, accumulated_result_length, replace_string.length_in_code_units(), ErrorType::StringSizeMustNotOverflow));
accumulated_result.append(replace_string);
next_source_position = match_end;
}
@ -826,7 +830,10 @@ ThrowCompletionOr<Value> RegExpPrototype::symbol_replace_impl(VM& vm, Object& re
// Append the part of the string before this match + the replacement.
if (match_start >= next_source_position) {
accumulated_result.append(utf16_view.substring_view(next_source_position, match_start - next_source_position));
auto substring = utf16_view.substring_view(next_source_position, match_start - next_source_position);
accumulated_result_length = TRY(checked_js_string_length_sum(vm, accumulated_result_length, substring.length_in_code_units(), ErrorType::StringSizeMustNotOverflow));
accumulated_result.append(substring);
accumulated_result_length = TRY(checked_js_string_length_sum(vm, accumulated_result_length, replace_string.length_in_code_units(), ErrorType::StringSizeMustNotOverflow));
accumulated_result.append(replace_string);
next_source_position = match_start + match_length;
}
@ -874,8 +881,11 @@ ThrowCompletionOr<Value> RegExpPrototype::symbol_replace_impl(VM& vm, Object& re
return string;
// Append the trailing portion of the string.
if (next_source_position < length_s)
accumulated_result.append(utf16_view.substring_view(next_source_position));
if (next_source_position < length_s) {
auto substring = utf16_view.substring_view(next_source_position);
accumulated_result_length = TRY(checked_js_string_length_sum(vm, accumulated_result_length, substring.length_in_code_units(), ErrorType::StringSizeMustNotOverflow));
accumulated_result.append(substring);
}
return PrimitiveString::create(vm, accumulated_result.to_string());
}
@ -948,6 +958,7 @@ ThrowCompletionOr<Value> RegExpPrototype::symbol_replace_impl(VM& vm, Object& re
// 13. Let accumulatedResult be the empty String.
Utf16StringBuilder accumulated_result;
size_t accumulated_result_length = 0;
// 14. Let nextSourcePosition be 0.
size_t next_source_position = 0;
@ -1042,7 +1053,9 @@ ThrowCompletionOr<Value> RegExpPrototype::symbol_replace_impl(VM& vm, Object& re
// ii. Set accumulatedResult to the string-concatenation of accumulatedResult, the substring of S from nextSourcePosition to position, and replacement.
auto substring = string->utf16_string_view().substring_view(next_source_position, position - next_source_position);
accumulated_result_length = TRY(checked_js_string_length_sum(vm, accumulated_result_length, substring.length_in_code_units(), ErrorType::StringSizeMustNotOverflow));
accumulated_result.append(substring);
accumulated_result_length = TRY(checked_js_string_length_sum(vm, accumulated_result_length, replacement.length_in_code_units(), ErrorType::StringSizeMustNotOverflow));
accumulated_result.append(replacement);
// iii. Set nextSourcePosition to position + matchLength.
@ -1056,6 +1069,7 @@ ThrowCompletionOr<Value> RegExpPrototype::symbol_replace_impl(VM& vm, Object& re
// 17. Return the string-concatenation of accumulatedResult and the substring of S from nextSourcePosition.
auto substring = string->utf16_string_view().substring_view(next_source_position);
accumulated_result_length = TRY(checked_js_string_length_sum(vm, accumulated_result_length, substring.length_in_code_units(), ErrorType::StringSizeMustNotOverflow));
accumulated_result.append(substring);
return PrimitiveString::create(vm, accumulated_result.to_string());

View file

@ -7,6 +7,7 @@
*/
#include <AK/TypeCasts.h>
#include <AK/Utf16StringBuilder.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Array.h>
#include <LibJS/Runtime/ArrayIterator.h>
@ -1043,42 +1044,46 @@ JS_DEFINE_NATIVE_FUNCTION(TypedArrayPrototype::join)
// 3. Let len be TypedArrayLength(taRecord).
auto length = typed_array_length(typed_array_record);
String sep {};
Utf16String sep {};
// 4. If separator is undefined, let sep be ",".
if (separator.is_undefined())
sep = String::from_code_point(',');
sep = ","_utf16;
// 5. Else, let sep be ? ToString(separator).
else
sep = TRY(separator.to_string(vm));
sep = TRY(separator.to_utf16_string(vm));
// 6. Let R be the empty String.
StringBuilder builder;
Utf16StringBuilder builder;
size_t result_length = 0;
// 7. Let k be 0.
// 8. Repeat, while k < len,
for (size_t k = 0; k < length; ++k) {
// a. If k > 0, set R to the string-concatenation of R and sep.
if (k > 0)
if (k > 0) {
result_length = TRY(checked_js_string_length_sum(vm, result_length, sep.length_in_code_units(), ErrorType::StringSizeMustNotOverflow));
builder.append(sep);
}
// b. Let element be ! Get(O, ! ToString(𝔽(k))).
auto element = MUST(typed_array->get(k));
String next {};
Utf16String next {};
// c. If element is undefined, let next be the empty String; otherwise, let next be ! ToString(element).
if (!element.is_undefined())
next = MUST(element.to_string(vm));
next = MUST(element.to_utf16_string(vm));
// d. Set R to the string-concatenation of R and next.
result_length = TRY(checked_js_string_length_sum(vm, result_length, next.length_in_code_units(), ErrorType::StringSizeMustNotOverflow));
builder.append(next);
// e. Set k to k + 1.
}
// 9. Return R.
return PrimitiveString::create(vm, MUST(builder.to_string()));
return PrimitiveString::create(vm, builder.to_string());
}
// 23.2.3.19 %TypedArray%.prototype.keys ( ), https://tc39.es/ecma262/#sec-%typedarray%.prototype.keys
@ -2014,7 +2019,8 @@ JS_DEFINE_NATIVE_FUNCTION(TypedArrayPrototype::to_locale_string)
constexpr auto separator = ',';
// 4. Let R be the empty String.
StringBuilder builder;
Utf16StringBuilder builder;
size_t result_length = 0;
// 5. Let k be 0.
// 6. Repeat, while k < len,
@ -2022,7 +2028,8 @@ JS_DEFINE_NATIVE_FUNCTION(TypedArrayPrototype::to_locale_string)
// a. If k > 0, then
if (k > 0) {
// i. Set R to the string-concatenation of R and separator.
builder.append(separator);
result_length = TRY(checked_js_string_length_sum(vm, result_length, 1, ErrorType::StringSizeMustNotOverflow));
builder.append_ascii(separator);
}
// b. Let nextElement be ? Get(array, ! ToString(k)).
@ -2032,9 +2039,10 @@ JS_DEFINE_NATIVE_FUNCTION(TypedArrayPrototype::to_locale_string)
if (!next_element.is_nullish()) {
// i. Let S be ? ToString(? Invoke(nextElement, "toLocaleString", « locales, options »)).
auto locale_string_value = TRY(next_element.invoke(vm, vm.names.toLocaleString, locales, options));
auto locale_string = TRY(locale_string_value.to_string(vm));
auto locale_string = TRY(locale_string_value.to_utf16_string(vm));
// ii. Set R to the string-concatenation of R and S.
result_length = TRY(checked_js_string_length_sum(vm, result_length, locale_string.length_in_code_units(), ErrorType::StringSizeMustNotOverflow));
builder.append(locale_string);
}
@ -2042,7 +2050,7 @@ JS_DEFINE_NATIVE_FUNCTION(TypedArrayPrototype::to_locale_string)
}
// 7. Return R.
return PrimitiveString::create(vm, builder.to_string_without_validation());
return PrimitiveString::create(vm, builder.to_string());
}
// 23.2.3.32 %TypedArray%.prototype.toReversed ( ), https://tc39.es/ecma262/#sec-%typedarray%.prototype.toreversed

View file

@ -4,9 +4,10 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/StringBuilder.h>
#include <AK/StringConversions.h>
#include <AK/StringUtils.h>
#include <AK/Utf16String.h>
#include <AK/Utf16StringBuilder.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/TypedArray.h>
#include <LibJS/Runtime/Uint8Array.h>
@ -15,6 +16,13 @@
namespace JS {
static void append_lowercase_hex_byte(Utf16StringBuilder& builder, u8 byte)
{
constexpr auto hex_digits = "0123456789abcdef"sv;
builder.append_ascii(hex_digits[byte >> 4]);
builder.append_ascii(hex_digits[byte & 0xf]);
}
void Uint8ArrayConstructorHelpers::initialize(Realm& realm, Object& constructor)
{
auto& vm = constructor.vm();
@ -377,18 +385,18 @@ JS_DEFINE_NATIVE_FUNCTION(Uint8ArrayPrototypeHelpers::to_hex)
auto to_encode = TRY(get_uint8_array_bytes(vm, typed_array));
// 4. Let out be the empty String.
StringBuilder out;
Utf16StringBuilder out(to_encode.bytes().size() * 2);
// 5. For each byte byte of toEncode, do
for (auto byte : to_encode.bytes()) {
// a. Let hex be Number::toString(𝔽(byte), 16).
// b. Set hex to StringPad(hex, 2, "0", START).
// c. Set out to the string-concatenation of out and hex.
out.appendff("{:02x}", byte);
append_lowercase_hex_byte(out, byte);
}
// 6. Return out.
return PrimitiveString::create(vm, MUST(out.to_string()));
return PrimitiveString::create(vm, out.to_string());
}
// 23.3.3.1 ValidateUint8Array ( ta ), https://tc39.es/ecma262/#sec-validateuint8array

View file

@ -14,7 +14,7 @@
#include <AK/StringBuilder.h>
#include <AK/StringConversions.h>
#include <AK/Utf16String.h>
#include <AK/Utf8View.h>
#include <AK/Utf16StringBuilder.h>
#include <LibCrypto/BigInt/SignedBigInteger.h>
#include <LibJS/Bytecode/PropertyAccess.h>
#include <LibJS/Runtime/AbstractOperations.h>
@ -75,9 +75,50 @@ static ALWAYS_INLINE bool both_bigint(Value const& lhs, Value const& rhs)
return lhs.is_bigint() && rhs.is_bigint();
}
static void append_ascii_for_number(StringBuilder& builder, char code_unit)
{
builder.append(code_unit);
}
static void append_ascii_for_number(Utf16StringBuilder& builder, char code_unit)
{
builder.append_ascii(code_unit);
}
static void append_ascii_for_number(StringBuilder& builder, StringView string)
{
builder.append(string);
}
static void append_ascii_for_number(Utf16StringBuilder& builder, StringView string)
{
builder.append_ascii(string);
}
static void append_ascii_for_number(StringBuilder& builder, char const* string, size_t length)
{
builder.append(string, length);
}
static void append_ascii_for_number(Utf16StringBuilder& builder, char const* string, size_t length)
{
builder.append_ascii(StringView { string, length });
}
static void append_repeated_ascii_for_number(StringBuilder& builder, char code_unit, size_t count)
{
builder.append_repeated(code_unit, count);
}
static void append_repeated_ascii_for_number(Utf16StringBuilder& builder, char code_unit, size_t count)
{
builder.append_repeated_ascii(code_unit, count);
}
// 6.1.6.1.20 Number::toString ( x ), https://tc39.es/ecma262/#sec-numeric-types-number-tostring
// Implementation for radix = 10
void number_to_string(StringBuilder& builder, double d, NumberToStringMode mode)
template<typename Builder>
static void number_to_string_impl(Builder& builder, double d, NumberToStringMode mode)
{
auto convert_to_decimal_digits_array = [](auto x, auto& digits, auto& length) {
for (; x; x /= 10)
@ -88,24 +129,24 @@ void number_to_string(StringBuilder& builder, double d, NumberToStringMode mode)
// 1. If x is NaN, return "NaN".
if (isnan(d)) {
builder.append("NaN"sv);
append_ascii_for_number(builder, "NaN"sv);
return;
}
// 2. If x is +0𝔽 or -0𝔽, return "0".
if (d == +0.0 || d == -0.0) {
builder.append("0"sv);
append_ascii_for_number(builder, "0"sv);
return;
}
// 4. If x is +∞𝔽, return "Infinity".
if (isinf(d)) {
if (d > 0) {
builder.append("Infinity"sv);
append_ascii_for_number(builder, "Infinity"sv);
return;
}
builder.append("-Infinity"sv);
append_ascii_for_number(builder, "-Infinity"sv);
return;
}
@ -124,7 +165,7 @@ void number_to_string(StringBuilder& builder, double d, NumberToStringMode mode)
// 3. If x < -0𝔽, return the string-concatenation of "-" and Number::toString(-x, radix).
if (sign)
builder.append('-');
append_ascii_for_number(builder, '-');
// Non-standard: Intl needs number-to-string conversions for extremely large numbers without any
// exponential formatting, as it will handle such formatting itself in a locale-aware way.
@ -136,31 +177,31 @@ void number_to_string(StringBuilder& builder, double d, NumberToStringMode mode)
if (n >= k) {
// i. Return the string-concatenation of:
// the code units of the k digits of the representation of s using radix radix
builder.append(mantissa_digits.data(), k);
append_ascii_for_number(builder, mantissa_digits.data(), k);
// n - k occurrences of the code unit 0x0030 (DIGIT ZERO)
builder.append_repeated('0', n - k);
append_repeated_ascii_for_number(builder, '0', n - k);
// b. Else if n > 0, then
} else if (n > 0) {
// i. Return the string-concatenation of:
// the code units of the most significant n digits of the representation of s using radix radix
builder.append(mantissa_digits.data(), n);
append_ascii_for_number(builder, mantissa_digits.data(), n);
// the code unit 0x002E (FULL STOP)
builder.append('.');
append_ascii_for_number(builder, '.');
// the code units of the remaining k - n digits of the representation of s using radix radix
builder.append(mantissa_digits.data() + n, k - n);
append_ascii_for_number(builder, mantissa_digits.data() + n, k - n);
// c. Else,
} else {
// i. Assert: n ≤ 0.
VERIFY(n <= 0);
// ii. Return the string-concatenation of:
// the code unit 0x0030 (DIGIT ZERO)
builder.append('0');
append_ascii_for_number(builder, '0');
// the code unit 0x002E (FULL STOP)
builder.append('.');
append_ascii_for_number(builder, '.');
// -n occurrences of the code unit 0x0030 (DIGIT ZERO)
builder.append_repeated('0', -n);
append_repeated_ascii_for_number(builder, '0', -n);
// the code units of the k digits of the representation of s using radix radix
builder.append(mantissa_digits.data(), k);
append_ascii_for_number(builder, mantissa_digits.data(), k);
}
return;
@ -182,30 +223,35 @@ void number_to_string(StringBuilder& builder, double d, NumberToStringMode mode)
if (k == 1) {
// a. Return the string-concatenation of:
// the code unit of the single digit of s
builder.append(mantissa_digits[0]);
append_ascii_for_number(builder, mantissa_digits[0]);
// the code unit 0x0065 (LATIN SMALL LETTER E)
builder.append('e');
append_ascii_for_number(builder, 'e');
// exponentSign
builder.append(exponent_sign);
append_ascii_for_number(builder, exponent_sign);
// the code units of the decimal representation of abs(n - 1)
builder.append(exponent_digits.data(), exponent_length);
append_ascii_for_number(builder, exponent_digits.data(), exponent_length);
return;
}
// 12. Return the string-concatenation of:
// the code unit of the most significant digit of the decimal representation of s
builder.append(mantissa_digits[0]);
append_ascii_for_number(builder, mantissa_digits[0]);
// the code unit 0x002E (FULL STOP)
builder.append('.');
append_ascii_for_number(builder, '.');
// the code units of the remaining k - 1 digits of the decimal representation of s
builder.append(mantissa_digits.data() + 1, k - 1);
append_ascii_for_number(builder, mantissa_digits.data() + 1, k - 1);
// the code unit 0x0065 (LATIN SMALL LETTER E)
builder.append('e');
append_ascii_for_number(builder, 'e');
// exponentSign
builder.append(exponent_sign);
append_ascii_for_number(builder, exponent_sign);
// the code units of the decimal representation of abs(n - 1)
builder.append(exponent_digits.data(), exponent_length);
append_ascii_for_number(builder, exponent_digits.data(), exponent_length);
}
void number_to_string(StringBuilder& builder, double d, NumberToStringMode mode)
{
number_to_string_impl(builder, d, mode);
}
String number_to_string(double d, NumberToStringMode mode)
@ -217,9 +263,9 @@ String number_to_string(double d, NumberToStringMode mode)
Utf16String number_to_utf16_string(double d, NumberToStringMode mode)
{
StringBuilder builder(StringBuilder::Mode::UTF16);
number_to_string(builder, d, mode);
return builder.to_utf16_string();
Utf16StringBuilder builder;
number_to_string_impl(builder, d, mode);
return builder.to_string();
}
ByteString number_to_byte_string(double d, NumberToStringMode mode)