From 595ae25e744fa233ffa14137648e3281070c12a5 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Mon, 22 Jun 2026 01:34:34 +0200 Subject: [PATCH] LibJS: Use Utf16StringBuilder for JS string construction Port straightforward JS string construction sites to Utf16StringBuilder. Make ASCII appends explicit with append_ascii(), and leave byte-output, formatted-output, and printer plumbing on StringBuilder where that API still matches the caller. Add a small checked JS string length product helper. Use it for String.prototype.repeat() before constructing the repeated result. Keep the maximum string length policy in LibJS while allowing the UTF-16 builder to remain purpose-built and infallible. Cover overflow from the final repeated code-unit length in the repeat tests. --- Libraries/LibJS/Console.cpp | 7 +- Libraries/LibJS/ParserError.cpp | 1 + .../LibJS/Runtime/AbstractOperations.cpp | 23 +++++- Libraries/LibJS/Runtime/AbstractOperations.h | 6 +- Libraries/LibJS/Runtime/ArrayPrototype.cpp | 26 +++---- .../LibJS/Runtime/Intl/DurationFormat.cpp | 8 +- .../Runtime/Intl/DurationFormatPrototype.cpp | 8 +- Libraries/LibJS/Runtime/PrimitiveString.cpp | 13 ++-- Libraries/LibJS/Runtime/RegExpObject.cpp | 9 ++- Libraries/LibJS/Runtime/RegExpPrototype.cpp | 25 +++--- Libraries/LibJS/Runtime/StringConstructor.cpp | 22 +++--- Libraries/LibJS/Runtime/StringPrototype.cpp | 76 ++++++++++--------- .../Runtime/Temporal/AbstractOperations.cpp | 7 +- .../Runtime/Temporal/DurationPrototype.cpp | 7 +- Libraries/LibJS/SourceCode.cpp | 8 +- .../String/String.prototype.repeat.js | 4 + .../wpt-import/url/a-element-origin.txt | 2 +- .../wpt-import/url/url-constructor.any.txt | 2 +- .../wpt-import/url/url-origin.any.txt | 2 +- 19 files changed, 150 insertions(+), 106 deletions(-) diff --git a/Libraries/LibJS/Console.cpp b/Libraries/LibJS/Console.cpp index f64448c0ed..cc9024cae0 100644 --- a/Libraries/LibJS/Console.cpp +++ b/Libraries/LibJS/Console.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -77,7 +78,11 @@ ThrowCompletionOr Console::assert_() // 3. Otherwise: else { // 1. Let concat be the concatenation of message, U+003A (:), U+0020 SPACE, and first. - auto concat = TRY_OR_THROW_OOM(vm, String::formatted("{}: {}", message->utf8_string(), MUST(first.to_string(vm)))); + Utf16StringBuilder builder; + builder.append(message->utf16_string_view()); + builder.append_ascii(": "sv); + builder.append(first.as_string().utf16_string_view()); + auto concat = builder.to_string(); // 2. Set data[0] to concat. data[0] = PrimitiveString::create(vm, move(concat)); } diff --git a/Libraries/LibJS/ParserError.cpp b/Libraries/LibJS/ParserError.cpp index fd52319162..cdc3f0816d 100644 --- a/Libraries/LibJS/ParserError.cpp +++ b/Libraries/LibJS/ParserError.cpp @@ -5,6 +5,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include diff --git a/Libraries/LibJS/Runtime/AbstractOperations.cpp b/Libraries/LibJS/Runtime/AbstractOperations.cpp index 28936a8c92..7c659dafe5 100644 --- a/Libraries/LibJS/Runtime/AbstractOperations.cpp +++ b/Libraries/LibJS/Runtime/AbstractOperations.cpp @@ -6,8 +6,11 @@ */ #include +#include #include +#include #include +#include #include #include #include @@ -44,6 +47,20 @@ namespace JS { +size_t max_js_string_length() +{ + return NumericLimits::max(); +} + +ThrowCompletionOr checked_js_string_length_product(VM& vm, size_t factor_a, size_t factor_b, ErrorType const& error_type) +{ + Checked product = factor_a; + product *= factor_b; + if (product.has_overflow() || product.value() > max_js_string_length()) + return vm.throw_completion(error_type); + return product.value(); +} + // 7.2.1 RequireObjectCoercible ( argument ), https://tc39.es/ecma262/#sec-requireobjectcoercible ThrowCompletionOr require_object_coercible(VM& vm, Value value) { @@ -1258,7 +1275,7 @@ CanonicalIndex canonical_numeric_index_string(PropertyKey const& property_key, C } // 22.1.3.19.1 GetSubstitution ( matched, str, position, captures, namedCaptures, replacementTemplate ), https://tc39.es/ecma262/#sec-getsubstitution -ThrowCompletionOr get_substitution(VM& vm, Utf16View const& matched, Utf16View const& str, size_t position, Span captures, Value named_captures, Value replacement_template) +ThrowCompletionOr get_substitution(VM& vm, Utf16View const& matched, Utf16View const& str, size_t position, Span captures, Value named_captures, Value replacement_template) { // 1. Let stringLength be the length of str. auto string_length = str.length_in_code_units(); @@ -1267,7 +1284,7 @@ ThrowCompletionOr get_substitution(VM& vm, Utf16View const& matched, Utf VERIFY(position <= string_length); // 3. Let result be the empty String. - StringBuilder result(StringBuilder::Mode::UTF16); + Utf16StringBuilder result; // 4. Let templateRemainder be replacementTemplate. auto replace_template_string = TRY(replacement_template.to_utf16_string(vm)); @@ -1446,7 +1463,7 @@ ThrowCompletionOr get_substitution(VM& vm, Utf16View const& matched, Utf } // 6. Return result. - return MUST(result.utf16_string_view().to_utf8()); + return result.to_string(); } void DisposeCapability::visit_edges(GC::Cell::Visitor& visitor) const diff --git a/Libraries/LibJS/Runtime/AbstractOperations.h b/Libraries/LibJS/Runtime/AbstractOperations.h index a62d253b80..2fba0a234d 100644 --- a/Libraries/LibJS/Runtime/AbstractOperations.h +++ b/Libraries/LibJS/Runtime/AbstractOperations.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -77,12 +78,15 @@ bool all_import_attributes_supported(VM& vm, Vector const& attr ThrowCompletionOr perform_import_call(VM&, Value specifier, Value options_value); +size_t max_js_string_length(); +ThrowCompletionOr checked_js_string_length_product(VM&, size_t, size_t, ErrorType const&); + enum class CanonicalIndexMode { DetectNumericRoundtrip, IgnoreNumericRoundtrip, }; [[nodiscard]] CanonicalIndex canonical_numeric_index_string(PropertyKey const&, CanonicalIndexMode needs_numeric); -ThrowCompletionOr get_substitution(VM&, Utf16View const& matched, Utf16View const& str, size_t position, Span captures, Value named_captures, Value replacement); +ThrowCompletionOr get_substitution(VM&, Utf16View const& matched, Utf16View const& str, size_t position, Span captures, Value named_captures, Value replacement); enum class CallerMode { Strict, diff --git a/Libraries/LibJS/Runtime/ArrayPrototype.cpp b/Libraries/LibJS/Runtime/ArrayPrototype.cpp index e7ba59cf06..3135f044a3 100644 --- a/Libraries/LibJS/Runtime/ArrayPrototype.cpp +++ b/Libraries/LibJS/Runtime/ArrayPrototype.cpp @@ -11,7 +11,7 @@ #include #include #include -#include +#include #include #include #include @@ -931,21 +931,21 @@ JS_DEFINE_NATIVE_FUNCTION(ArrayPrototype::join) }; auto length = TRY(length_of_array_like(vm, this_object)); - String separator = ","_string; + Utf16String separator = ","_utf16; if (!vm.argument(0).is_undefined()) - separator = TRY(vm.argument(0).to_string(vm)); - StringBuilder builder; + separator = TRY(vm.argument(0).to_utf16_string(vm)); + Utf16StringBuilder builder; for (size_t i = 0; i < length; ++i) { if (i > 0) - builder.append(separator); + builder.append(separator.utf16_view()); auto value = TRY(this_object->get(i)); if (value.is_nullish()) continue; - auto string = TRY(value.to_string(vm)); - builder.append(string); + auto string = TRY(value.to_utf16_string(vm)); + builder.append(string.utf16_view()); } - return PrimitiveString::create(vm, builder.to_string_without_validation()); + return PrimitiveString::create(vm, builder.to_string()); } // 23.1.3.19 Array.prototype.keys ( ), https://tc39.es/ecma262/#sec-array.prototype.keys @@ -1822,7 +1822,7 @@ JS_DEFINE_NATIVE_FUNCTION(ArrayPrototype::to_locale_string) constexpr auto separator = ","sv; // 4. Let R be the empty String. - StringBuilder builder; + Utf16StringBuilder builder; // 5. Let k be 0. // 6. Repeat, while k < len, @@ -1830,7 +1830,7 @@ JS_DEFINE_NATIVE_FUNCTION(ArrayPrototype::to_locale_string) // a. If k > 0, then if (i > 0) { // i. Set R to the string-concatenation of R and separator. - builder.append(separator); + builder.append_ascii(separator); } // b. Let nextElement be ? Get(array, ! ToString(k)). @@ -1842,15 +1842,15 @@ JS_DEFINE_NATIVE_FUNCTION(ArrayPrototype::to_locale_string) auto locale_string_result = TRY(value.invoke(vm, vm.names.toLocaleString, locales, options)); // ii. Set R to the string-concatenation of R and S. - auto string = TRY(locale_string_result.to_string(vm)); - builder.append(string); + auto string = TRY(locale_string_result.to_utf16_string(vm)); + builder.append(string.utf16_view()); } // d. Increase k by 1. } // 7. Return R. - return PrimitiveString::create(vm, builder.to_string_without_validation()); + return PrimitiveString::create(vm, builder.to_string()); } // 23.1.3.33 Array.prototype.toReversed ( ), https://tc39.es/ecma262/#sec-array.prototype.toreversed diff --git a/Libraries/LibJS/Runtime/Intl/DurationFormat.cpp b/Libraries/LibJS/Runtime/Intl/DurationFormat.cpp index 9859ffd1e1..479b80ffec 100644 --- a/Libraries/LibJS/Runtime/Intl/DurationFormat.cpp +++ b/Libraries/LibJS/Runtime/Intl/DurationFormat.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include #include #include @@ -741,16 +743,16 @@ Vector list_format_parts(VM& vm, DurationFormat const& durat // 8. For each element parts of partitionedPartsList, do for (auto const& parts : partitioned_parts_list) { // a. Let string be the empty String. - StringBuilder string(StringBuilder::Mode::UTF16); + Utf16StringBuilder string; // b. For each Record { [[Type]], [[Value]], [[Unit]] } part in parts, do for (auto const& part : parts) { // i. Set string to the string-concatenation of string and part.[[Value]]. - string.append(part.value); + string.append(part.value.utf16_view()); } // c. Append string to strings. - strings.unchecked_append(string.to_utf16_string()); + strings.unchecked_append(string.to_string()); } // 9. Let formattedPartsList be CreatePartsFromList(lf, strings). diff --git a/Libraries/LibJS/Runtime/Intl/DurationFormatPrototype.cpp b/Libraries/LibJS/Runtime/Intl/DurationFormatPrototype.cpp index 1d8e5b2eb3..faa379a8e1 100644 --- a/Libraries/LibJS/Runtime/Intl/DurationFormatPrototype.cpp +++ b/Libraries/LibJS/Runtime/Intl/DurationFormatPrototype.cpp @@ -6,7 +6,7 @@ */ #include -#include +#include #include #include #include @@ -124,16 +124,16 @@ JS_DEFINE_NATIVE_FUNCTION(DurationFormatPrototype::format) auto parts = partition_duration_format_pattern(vm, duration_format, duration); // 5. Let result be a new empty String. - StringBuilder result; + Utf16StringBuilder result; // 6. For each Record { [[Type]], [[Value]], [[Unit]] } part in parts, do for (auto const& part : parts) { // a. Set result to the string-concatenation of result and part.[[Value]]. - result.append(part.value); + result.append(part.value.utf16_view()); } // 7. Return result. - return PrimitiveString::create(vm, MUST(result.to_string())); + return PrimitiveString::create(vm, result.to_string()); } // 13.3.4 Intl.DurationFormat.prototype.formatToParts ( duration ), https://tc39.es/ecma402/#sec-Intl.DurationFormat.prototype.formatToParts diff --git a/Libraries/LibJS/Runtime/PrimitiveString.cpp b/Libraries/LibJS/Runtime/PrimitiveString.cpp index 288e431155..2fb3cef1e1 100644 --- a/Libraries/LibJS/Runtime/PrimitiveString.cpp +++ b/Libraries/LibJS/Runtime/PrimitiveString.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -410,16 +411,12 @@ void RopeString::resolve(EncodingPreference preference) const if (preference == EncodingPreference::UTF16) { // The caller wants a UTF-16 string, so we can simply concatenate all the pieces // into a UTF-16 code unit buffer and create a Utf16String from it. - StringBuilder builder(StringBuilder::Mode::UTF16, length_in_utf16_code_units); + Utf16StringBuilder builder(length_in_utf16_code_units); - for (auto const* current : pieces) { - if (current->has_utf16_string()) - builder.append(current->utf16_string_view()); - else - builder.append(current->utf8_string_view()); - } + for (auto const* current : pieces) + builder.append(current->utf16_string_view()); - m_utf16_string = builder.to_utf16_string(); + m_utf16_string = builder.to_string(); m_deferred_kind = DeferredKind::None; m_lhs = nullptr; m_rhs = nullptr; diff --git a/Libraries/LibJS/Runtime/RegExpObject.cpp b/Libraries/LibJS/Runtime/RegExpObject.cpp index 4c8c0c961a..7590fd14e8 100644 --- a/Libraries/LibJS/Runtime/RegExpObject.cpp +++ b/Libraries/LibJS/Runtime/RegExpObject.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -474,14 +475,14 @@ String RegExpObject::escape_regexp_pattern() const for (auto code_point : m_pattern) { if (escaped) { escaped = false; - builder.append_code_point('\\'); + builder.append('\\'); switch (code_point) { case '\n': - builder.append_code_point('n'); + builder.append('n'); break; case '\r': - builder.append_code_point('r'); + builder.append('r'); break; case LINE_SEPARATOR: builder.append("u2028"sv); @@ -510,7 +511,7 @@ String RegExpObject::escape_regexp_pattern() const switch (code_point) { case '/': if (in_character_class) - builder.append_code_point('/'); + builder.append('/'); else builder.append("\\/"sv); break; diff --git a/Libraries/LibJS/Runtime/RegExpPrototype.cpp b/Libraries/LibJS/Runtime/RegExpPrototype.cpp index 5883ad6cf5..d0c07da9fb 100644 --- a/Libraries/LibJS/Runtime/RegExpPrototype.cpp +++ b/Libraries/LibJS/Runtime/RegExpPrototype.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -499,7 +500,7 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::flags) auto regexp_object = TRY(this_object(vm)); // 3. Let result be the empty String. - StringBuilder builder(8); + Utf16StringBuilder builder(8); // 4. Let hasIndices be ToBoolean(? Get(R, "hasIndices")). // 5. If hasIndices is true, append the code unit 0x0064 (LATIN SMALL LETTER D) as the last code unit of result. @@ -522,13 +523,13 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::flags) static auto& cache = *new Bytecode::StaticPropertyLookupCache; \ auto flag_##flag_name = TRY(regexp_object->get(vm.names.flagName, cache)); \ if (flag_##flag_name.to_boolean()) \ - builder.append(#flag_char##sv); \ + builder.append_ascii(#flag_char##sv); \ } JS_ENUMERATE_REGEXP_FLAGS #undef __JS_ENUMERATE // 20. Return result. - return PrimitiveString::create(vm, builder.to_string_without_validation()); + return PrimitiveString::create(vm, builder.to_string()); } // 22.2.6.8 RegExp.prototype [ @@match ] ( string ), https://tc39.es/ecma262/#sec-regexp.prototype-@@match @@ -704,8 +705,8 @@ ThrowCompletionOr RegExpPrototype::symbol_replace_impl(VM& vm, Object& re && !regexp_object.storage_has(vm.names.global) && !regexp_object.storage_has(vm.names.unicode) && !regexp_object.storage_has(vm.names.flags)) { - auto replace_string = TRY(replace_value.to_string(vm)); - bool has_dollar = replace_string.contains('$'); + auto replace_string = TRY(replace_value.to_utf16_string(vm)); + bool has_dollar = replace_string.utf16_view().contains('$'); if (!has_dollar) { auto flag_bits = typed_regexp->flag_bits(); @@ -753,7 +754,7 @@ ThrowCompletionOr RegExpPrototype::symbol_replace_impl(VM& vm, Object& re bool need_legacy = typed_regexp->legacy_features_enabled() && &realm == &typed_regexp->realm(); - StringBuilder accumulated_result; + Utf16StringBuilder accumulated_result; size_t next_source_position = 0; bool had_match = false; size_t last_match_start = 0; @@ -876,7 +877,7 @@ ThrowCompletionOr RegExpPrototype::symbol_replace_impl(VM& vm, Object& re if (next_source_position < length_s) accumulated_result.append(utf16_view.substring_view(next_source_position)); - return PrimitiveString::create(vm, accumulated_result.to_string_without_validation()); + return PrimitiveString::create(vm, accumulated_result.to_string()); } } } @@ -946,7 +947,7 @@ ThrowCompletionOr RegExpPrototype::symbol_replace_impl(VM& vm, Object& re } // 13. Let accumulatedResult be the empty String. - StringBuilder accumulated_result; + Utf16StringBuilder accumulated_result; // 14. Let nextSourcePosition be 0. size_t next_source_position = 0; @@ -1000,7 +1001,7 @@ ThrowCompletionOr RegExpPrototype::symbol_replace_impl(VM& vm, Object& re static auto& cache3 = *new Bytecode::StaticPropertyLookupCache; auto named_captures = TRY(result->get(vm.names.groups, cache3)); - String replacement; + Utf16String replacement; // k. If functionalReplace is true, then if (replace_value.is_function()) { @@ -1021,7 +1022,7 @@ ThrowCompletionOr RegExpPrototype::symbol_replace_impl(VM& vm, Object& re auto replace_result = TRY(call(vm, replace_value.as_function(), js_undefined(), replacer_args.span())); // iv. Let replacement be ? ToString(replValue). - replacement = TRY(replace_result.to_string(vm)); + replacement = TRY(replace_result.to_utf16_string(vm)); } // l. Else, else { @@ -1051,13 +1052,13 @@ ThrowCompletionOr RegExpPrototype::symbol_replace_impl(VM& vm, Object& re // 16. If nextSourcePosition ≥ lengthS, return accumulatedResult. if (next_source_position >= string->length_in_utf16_code_units()) - return PrimitiveString::create(vm, accumulated_result.to_string_without_validation()); + return PrimitiveString::create(vm, accumulated_result.to_string()); // 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.append(substring); - return PrimitiveString::create(vm, accumulated_result.to_string_without_validation()); + return PrimitiveString::create(vm, accumulated_result.to_string()); } // 22.2.6.12 RegExp.prototype [ @@search ] ( string ), https://tc39.es/ecma262/#sec-regexp.prototype-@@search diff --git a/Libraries/LibJS/Runtime/StringConstructor.cpp b/Libraries/LibJS/Runtime/StringConstructor.cpp index dc6f098ea1..f3972e45f6 100644 --- a/Libraries/LibJS/Runtime/StringConstructor.cpp +++ b/Libraries/LibJS/Runtime/StringConstructor.cpp @@ -4,9 +4,9 @@ * SPDX-License-Identifier: BSD-2-Clause */ -#include #include #include +#include #include #include #include @@ -99,7 +99,7 @@ JS_DEFINE_NATIVE_FUNCTION(StringConstructor::from_char_code) return from_char_code_impl(vm, vm.argument(0)); // 1. Let result be the empty String. - StringBuilder builder(StringBuilder::Mode::UTF16, vm.argument_count()); + Utf16StringBuilder builder(vm.argument_count()); // 2. For each element next of codeUnits, do for (size_t i = 0; i < vm.argument_count(); ++i) { @@ -111,7 +111,7 @@ JS_DEFINE_NATIVE_FUNCTION(StringConstructor::from_char_code) } // 3. Return result. - return PrimitiveString::create(vm, builder.to_utf16_string()); + return PrimitiveString::create(vm, builder.to_string()); } // 22.1.2.2 String.fromCodePoint ( ...codePoints ), https://tc39.es/ecma262/#sec-string.fromcodepoint @@ -119,7 +119,7 @@ JS_DEFINE_NATIVE_FUNCTION(StringConstructor::from_code_point) { // 1. Let result be the empty String. // NOTE: This will be an under-estimate if any code point is > 0xffff. - StringBuilder builder(StringBuilder::Mode::UTF16, vm.argument_count()); + Utf16StringBuilder builder(vm.argument_count()); // 2. For each element next of codePoints, do for (size_t i = 0; i < vm.argument_count(); ++i) { @@ -147,7 +147,7 @@ JS_DEFINE_NATIVE_FUNCTION(StringConstructor::from_code_point) VERIFY(builder.is_empty()); // 4. Return result. - return PrimitiveString::create(vm, builder.to_utf16_string()); + return PrimitiveString::create(vm, builder.to_string()); } // 22.1.2.4 String.raw ( template, ...substitutions ), https://tc39.es/ecma262/#sec-string.raw @@ -172,7 +172,7 @@ JS_DEFINE_NATIVE_FUNCTION(StringConstructor::raw) return PrimitiveString::create(vm, String {}); // 6. Let R be the empty String. - StringBuilder builder; + Utf16StringBuilder builder; // 7. Let nextIndex be 0. // 8. Repeat, @@ -181,10 +181,10 @@ JS_DEFINE_NATIVE_FUNCTION(StringConstructor::raw) auto next_literal_value = TRY(literals->get(PropertyKey(i))); // b. Let nextLiteral be ? ToString(nextLiteralVal). - auto next_literal = TRY(next_literal_value.to_string(vm)); + auto next_literal = TRY(next_literal_value.to_utf16_string(vm)); // c. Set R to the string-concatenation of R and nextLiteral. - builder.append(next_literal); + builder.append(next_literal.utf16_view()); // d. If nextIndex + 1 = literalCount, return R. if (i + 1 == literal_count) @@ -196,15 +196,15 @@ JS_DEFINE_NATIVE_FUNCTION(StringConstructor::raw) auto next_substitution_value = vm.argument(i + 1); // ii. Let nextSub be ? ToString(nextSubVal). - auto next_substitution = TRY(next_substitution_value.to_string(vm)); + auto next_substitution = TRY(next_substitution_value.to_utf16_string(vm)); // iii. Set R to the string-concatenation of R and nextSub. - builder.append(next_substitution); + builder.append(next_substitution.utf16_view()); } // f. Set nextIndex to nextIndex + 1. } - return PrimitiveString::create(vm, builder.to_byte_string()); + return PrimitiveString::create(vm, builder.to_string()); } } diff --git a/Libraries/LibJS/Runtime/StringPrototype.cpp b/Libraries/LibJS/Runtime/StringPrototype.cpp index 4d0f73530a..dca8874a02 100644 --- a/Libraries/LibJS/Runtime/StringPrototype.cpp +++ b/Libraries/LibJS/Runtime/StringPrototype.cpp @@ -7,8 +7,8 @@ #include #include -#include #include +#include #include #include #include @@ -763,14 +763,14 @@ static ThrowCompletionOr pad_string(VM& vm, GC::Ref stri // 8. Let fillLen be intMaxLength - stringLength. auto fill_length = int_max_length - string_length; - StringBuilder truncated_string_filler_builder; + Utf16StringBuilder truncated_string_filler_builder; auto fill_code_units = filler.length_in_code_units(); for (size_t i = 0; i < fill_length / fill_code_units; ++i) truncated_string_filler_builder.append(filler); // 9. Let truncatedStringFiller be the String value consisting of repeated concatenations of filler truncated to length fillLen. truncated_string_filler_builder.append(filler.substring_view(0, fill_length % fill_code_units)); - auto truncated_string_filler = MUST(truncated_string_filler_builder.to_string()); + auto truncated_string_filler = truncated_string_filler_builder.to_string(); // 10. If placement is start, return the string-concatenation of truncatedStringFiller and S. // 11. Else, return the string-concatenation of S and truncatedStringFiller. @@ -811,7 +811,7 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::repeat) { // 1. Let O be ? RequireObjectCoercible(this value). // 2. Let S be ? ToString(O). - auto string = TRY(utf8_string_from(vm)); + auto string = TRY(primitive_string_from(vm)); // 3. Let n be ? ToIntegerOrInfinity(count). auto n = TRY(vm.argument(0).to_integer_or_infinity(vm)); @@ -827,15 +827,21 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::repeat) return PrimitiveString::create(vm, String {}); // OPTIMIZATION: If the string is empty, the result will be empty as well. - if (string.is_empty()) + if (string->is_empty()) return PrimitiveString::create(vm, String {}); - auto repeated = String::repeated(string, n); - if (repeated.is_error()) + if (n > static_cast(NumericLimits::max())) return vm.throw_completion(ErrorType::StringRepeatCountMustNotOverflow); + auto count = static_cast(n); + auto string_view = string->utf16_string_view(); + + TRY(checked_js_string_length_product(vm, string_view.length_in_code_units(), count, ErrorType::StringRepeatCountMustNotOverflow)); + // 6. Return the String value that is made from n copies of S appended together. - return PrimitiveString::create(vm, repeated.release_value()); + Utf16StringBuilder builder; + builder.append_repeated(string_view, count); + return PrimitiveString::create(vm, builder.to_string()); } // 22.1.3.19 String.prototype.replace ( searchValue, replaceValue ), https://tc39.es/ecma262/#sec-string.prototype.replace @@ -892,7 +898,7 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::replace) // 10. Let preceding be the substring of string from 0 to position. auto preceding = string->utf16_string_view().substring_view(0, *position); - String replacement; + Utf16String replacement; // 11. Let following be the substring of string from position + searchLength. auto following = string->utf16_string_view().substring_view(*position + search_length); @@ -901,7 +907,7 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::replace) if (replace_value.is_function()) { // a. Let replacement be ? ToString(? Call(replaceValue, undefined, « searchString, 𝔽(position), string »)). auto result = TRY(call(vm, replace_value.as_function(), js_undefined(), search_string, Value(*position), string)); - replacement = TRY(result.to_string(vm)); + replacement = TRY(result.to_utf16_string(vm)); } // 13. Else, else { @@ -916,12 +922,12 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::replace) } // 14. Return the string-concatenation of preceding, replacement, and following. - StringBuilder builder; + Utf16StringBuilder builder; builder.append(preceding); - builder.append(replacement); + builder.append(replacement.utf16_view()); builder.append(following); - return PrimitiveString::create(vm, MUST(builder.to_string())); + return PrimitiveString::create(vm, builder.to_string()); } // 22.1.3.20 String.prototype.replaceAll ( searchValue, replaceValue ), https://tc39.es/ecma262/#sec-string.prototype.replaceall @@ -1007,18 +1013,18 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::replace_all) size_t end_of_last_match = 0; // 13. Let result be the empty String. - StringBuilder result; + Utf16StringBuilder result; // 14. For each element p of matchPositions, do for (auto position : match_positions) { // a. Let preserved be the substring of string from endOfLastMatch to p. auto preserved = string->utf16_string_view().substring_view(end_of_last_match, position - end_of_last_match); - String replacement; + Utf16String replacement; // b. If functionalReplace is true, then if (replace_value.is_function()) { // i. Let replacement be ? ToString(? Call(replaceValue, undefined, « searchString, 𝔽(p), string »)). - replacement = TRY(TRY(call(vm, replace_value.as_function(), js_undefined(), search_string, Value(position), string)).to_string(vm)); + replacement = TRY(TRY(call(vm, replace_value.as_function(), js_undefined(), search_string, Value(position), string)).to_utf16_string(vm)); } // c. Else, else { @@ -1030,7 +1036,7 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::replace_all) // d. Set result to the string-concatenation of result, preserved, and replacement. result.append(preserved); - result.append(replacement); + result.append(replacement.utf16_view()); // e. Set endOfLastMatch to p + searchLength. end_of_last_match = position + search_length; @@ -1045,7 +1051,7 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::replace_all) } // 16. Return result. - return PrimitiveString::create(vm, MUST(result.to_string())); + return PrimitiveString::create(vm, result.to_string()); } // 22.1.3.21 String.prototype.search ( regexp ), https://tc39.es/ecma262/#sec-string.prototype.search @@ -1586,49 +1592,49 @@ static ThrowCompletionOr create_html(VM& vm, Value string, StringView tag TRY(require_object_coercible(vm, string)); // 2. Let S be ? ToString(str). - auto str = TRY(string.to_string(vm)); + auto str = TRY(string.to_utf16_string(vm)); // 3. Let p1 be the string-concatenation of "<" and tag. - StringBuilder builder; - builder.append('<'); - builder.append(tag); + Utf16StringBuilder builder; + builder.append_ascii('<'); + builder.append_ascii(tag); // 4. If attribute is not the empty String, then if (!attribute.is_empty()) { // a. Let V be ? ToString(value). - auto value_string = TRY(value.to_string(vm)); + auto value_string = TRY(value.to_utf16_string(vm)); // b. Let escapedV be the String value that is the same as V except that each occurrence of the code unit 0x0022 (QUOTATION MARK) in V has been replaced with the six code unit sequence """. - auto escaped_value_string = MUST(value_string.replace("\""sv, """sv, ReplaceMode::All)); + auto escaped_value_string = value_string.replace("\""sv, """sv, ReplaceMode::All); // c. Set p1 to the string-concatenation of: // - p1 // - the code unit 0x0020 (SPACE) - builder.append(' '); + builder.append_ascii(' '); // - attribute - builder.append(attribute); + builder.append_ascii(attribute); // - the code unit 0x003D (EQUALS SIGN) // - the code unit 0x0022 (QUOTATION MARK) - builder.append("=\""sv); + builder.append_ascii("=\""sv); // - escapedV - builder.append(escaped_value_string); + builder.append(escaped_value_string.utf16_view()); // - the code unit 0x0022 (QUOTATION MARK) - builder.append('"'); + builder.append_ascii('"'); } // 5. Let p2 be the string-concatenation of p1 and ">". - builder.append('>'); + builder.append_ascii('>'); // 6. Let p3 be the string-concatenation of p2 and S. - builder.append(str); + builder.append(str.utf16_view()); // 7. Let p4 be the string-concatenation of p3, "". - builder.append("'); + builder.append_ascii("'); // 8. Return p4. - return PrimitiveString::create(vm, MUST(builder.to_string())); + return PrimitiveString::create(vm, builder.to_string()); } // B.2.2.2 String.prototype.anchor ( name ), https://tc39.es/ecma262/#sec-string.prototype.anchor diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index 75bd374f45..de27ecc8a9 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -8,6 +8,8 @@ */ #include +#include +#include #include #include #include @@ -785,7 +787,10 @@ String format_fractional_seconds(u64 sub_second_nanoseconds, Precision precision } // 3. Return the string-concatenation of the code unit 0x002E (FULL STOP) and fractionString. - return MUST(String::formatted(".{}", fraction_string)); + StringBuilder builder; + builder.append('.'); + builder.append(fraction_string); + return MUST(builder.to_string()); } // 13.26 FormatTimeString ( hour, minute, second, subSecondNanoseconds, precision [ , style ] ), https://tc39.es/proposal-temporal/#sec-temporal-formattimestring diff --git a/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp index 67ce679006..98bbb9201c 100644 --- a/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp @@ -5,6 +5,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -653,16 +654,16 @@ JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::to_locale_string) auto parts = partition_duration_format_pattern(vm, formatter, duration); // 5. Let result be the empty String. - StringBuilder result; + Utf16StringBuilder result; // 6. For each Record { [[Type]], [[Value]], [[Unit]] } part in parts, do for (auto const& part : parts) { // a. Set result to the string-concatenation of result and part.[[Value]]. - result.append(part.value); + result.append(part.value.utf16_view()); } // 7. Return result. - return PrimitiveString::create(vm, MUST(result.to_string())); + return PrimitiveString::create(vm, result.to_string()); } // 7.3.25 Temporal.Duration.prototype.valueOf ( ), https://tc39.es/proposal-temporal/#sec-temporal.duration.prototype.valueof diff --git a/Libraries/LibJS/SourceCode.cpp b/Libraries/LibJS/SourceCode.cpp index 3fe4ffd587..35a5a44aa8 100644 --- a/Libraries/LibJS/SourceCode.cpp +++ b/Libraries/LibJS/SourceCode.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include #include #include @@ -291,7 +291,7 @@ Utf16String SourceCode::decode_source_range(size_t start_offset, size_t length) input = input.substring_view(byte_order_mark_size); } - StringBuilder builder(StringBuilder::Mode::UTF16, length); + Utf16StringBuilder builder(length); size_t current_offset = 0; auto result = actual_decoder->process_code_points(input, [&](auto code_point) -> ErrorOr { char16_t code_units[2]; @@ -303,7 +303,7 @@ Utf16String SourceCode::decode_source_range(size_t start_offset, size_t length) for (size_t i = 0; i < code_point_length_in_code_units; ++i) { auto code_unit_offset = current_offset + i; if (code_unit_offset >= start_offset && code_unit_offset < end_offset) - TRY(builder.try_append_code_unit(code_units[i])); + builder.append_code_unit(code_units[i]); } current_offset += code_point_length_in_code_units; @@ -311,7 +311,7 @@ Utf16String SourceCode::decode_source_range(size_t start_offset, size_t length) }); result.release_value_but_fixme_should_propagate_errors(); - return builder.to_utf16_string(); + return builder.to_string(); } void SourceCode::fill_position_cache() const diff --git a/Tests/LibJS/Runtime/builtins/String/String.prototype.repeat.js b/Tests/LibJS/Runtime/builtins/String/String.prototype.repeat.js index ba819eda9e..4c7803fc5d 100644 --- a/Tests/LibJS/Runtime/builtins/String/String.prototype.repeat.js +++ b/Tests/LibJS/Runtime/builtins/String/String.prototype.repeat.js @@ -26,6 +26,10 @@ test("throws correct range errors", () => { expect(() => { "foo".repeat(0xffffffffff); }).toThrowWithMessage(RangeError, "repeat count must not overflow"); + + expect(() => { + "aa".repeat(0x80000000); + }).toThrowWithMessage(RangeError, "repeat count must not overflow"); }); test("UTF-16", () => { diff --git a/Tests/LibWeb/Text/expected/wpt-import/url/a-element-origin.txt b/Tests/LibWeb/Text/expected/wpt-import/url/a-element-origin.txt index ecd2611ed6..b4f50e9178 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/url/a-element-origin.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/url/a-element-origin.txt @@ -413,4 +413,4 @@ Pass Parsing origin: against Pass Parsing origin: against Pass Parsing origin: against Pass Parsing origin: against -Pass Parsing origin: against \ No newline at end of file +Pass Parsing origin: against \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/url/url-constructor.any.txt b/Tests/LibWeb/Text/expected/wpt-import/url/url-constructor.any.txt index f2ca7b92e7..9299dcfaf4 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/url/url-constructor.any.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/url/url-constructor.any.txt @@ -906,4 +906,4 @@ Pass Parsing: ?@AZ[\]^_`az{|}~€Éé> without base Pass Parsing: ?@AZ[\]^_`az{|}~€Éé> without base -Pass Parsing: without base \ No newline at end of file +Pass Parsing: without base \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/url/url-origin.any.txt b/Tests/LibWeb/Text/expected/wpt-import/url/url-origin.any.txt index ac27280984..041631d3f1 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/url/url-origin.any.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/url/url-origin.any.txt @@ -414,4 +414,4 @@ Pass Origin parsing: without base Pass Origin parsing: without base Pass Origin parsing: without base Pass Origin parsing: without base -Pass Origin parsing: without base \ No newline at end of file +Pass Origin parsing: without base \ No newline at end of file