From 5e0ee26e8bef854553dfe3a6cdf3ce508a926acd Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sun, 11 Jan 2026 21:09:41 +0100 Subject: [PATCH] LibJS: Use simdjson for JSON.parse Replace the custom AK JSON parser with simdjson for parsing JSON in LibJS. This eliminates the intermediate AK::JsonValue object graph, going directly from JSON text to JS::Value. simdjson's on-demand API parses at ~4GB/s and only materializes values as they are accessed, making this both faster and more memory efficient than the previous approach. The AK JSON parser is still used elsewhere (WebDriver protocol, config files, etc.) but LibJS now uses simdjson exclusively for JSON.parse() and JSON.rawJSON(). --- Libraries/LibJS/CMakeLists.txt | 4 + Libraries/LibJS/Runtime/JSONObject.cpp | 401 +++++++++++++++--- Libraries/LibJS/Runtime/JSONObject.h | 3 - .../LibJS/Tests/builtins/JSON/JSON.parse.js | 70 +++ Libraries/LibWeb/WebDriver/JSON.cpp | 47 +- Meta/CMake/flatpak/org.ladybird.Ladybird.json | 19 + Utilities/js.cpp | 7 +- vcpkg.json | 5 + 8 files changed, 497 insertions(+), 59 deletions(-) diff --git a/Libraries/LibJS/CMakeLists.txt b/Libraries/LibJS/CMakeLists.txt index d8eb468ad9..1473304b2f 100644 --- a/Libraries/LibJS/CMakeLists.txt +++ b/Libraries/LibJS/CMakeLists.txt @@ -276,6 +276,10 @@ generate_bytecode_def_derived() set(GENERATED_SOURCES Bytecode/Op.cpp) ladybird_lib(LibJS js EXPLICIT_SYMBOL_EXPORT) + +find_package(simdjson CONFIG REQUIRED) +target_link_libraries(LibJS PRIVATE simdjson::simdjson) + target_link_libraries(LibJS PRIVATE LibCore LibCrypto LibFileSystem LibRegex LibSyntax LibGC) # Link LibUnicode publicly to ensure ICU data (which is in libicudata.a) is available in any process using LibJS. diff --git a/Libraries/LibJS/Runtime/JSONObject.cpp b/Libraries/LibJS/Runtime/JSONObject.cpp index b63d09f205..5e9eb68288 100644 --- a/Libraries/LibJS/Runtime/JSONObject.cpp +++ b/Libraries/LibJS/Runtime/JSONObject.cpp @@ -5,10 +5,9 @@ */ #include -#include -#include -#include +#include #include +#include #include #include #include @@ -26,6 +25,8 @@ #include #include +#include + namespace JS { GC_DEFINE_ALLOCATOR(JSONObject); @@ -447,13 +448,327 @@ JS_DEFINE_NATIVE_FUNCTION(JSONObject::parse) return unfiltered; } +// Unescape a JSON string, properly handling \uXXXX escape sequences including lone surrogates. +// simdjson validates UTF-8 strictly and rejects lone surrogates, but JSON allows them. +// Returns {} on malformed escape sequences. +static Optional unescape_json_string(StringView raw) +{ + StringBuilder builder(StringBuilder::Mode::UTF16, raw.length()); + + GenericLexer lexer { raw }; + + auto consume_hex4 = [&]() -> Optional { + if (lexer.tell_remaining() < 4) + return {}; + u16 value = 0; + for (int i = 0; i < 4; ++i) { + auto ch = lexer.consume(); + value <<= 4; + if (ch >= '0' && ch <= '9') + value |= ch - '0'; + else if (ch >= 'a' && ch <= 'f') + value |= ch - 'a' + 10; + else if (ch >= 'A' && ch <= 'F') + value |= ch - 'A' + 10; + else + return {}; + } + return value; + }; + + while (!lexer.is_eof()) { + if (lexer.consume_specific('\\')) { + if (lexer.is_eof()) + return {}; + auto escaped = lexer.consume(); + switch (escaped) { + case '"': + builder.append_code_unit('"'); + break; + case '\\': + builder.append_code_unit('\\'); + break; + case '/': + builder.append_code_unit('/'); + break; + case 'b': + builder.append_code_unit('\b'); + break; + case 'f': + builder.append_code_unit('\f'); + break; + case 'n': + builder.append_code_unit('\n'); + break; + case 'r': + builder.append_code_unit('\r'); + break; + case 't': + builder.append_code_unit('\t'); + break; + case 'u': { + auto code_unit = consume_hex4(); + if (!code_unit.has_value()) + return {}; + builder.append_code_unit(*code_unit); + break; + } + default: + return {}; + } + } else { + // Non-escaped character - copy UTF-8 code point to UTF-16 + auto ch = lexer.consume(); + if ((ch & 0x80) == 0) { + // ASCII + builder.append_code_unit(ch); + } else if ((ch & 0xE0) == 0xC0) { + // 2-byte UTF-8 + if (lexer.is_eof()) + return {}; + auto ch2 = lexer.consume(); + u32 code_point = ((ch & 0x1F) << 6) | (ch2 & 0x3F); + builder.append_code_unit(code_point); + } else if ((ch & 0xF0) == 0xE0) { + // 3-byte UTF-8 + if (lexer.tell_remaining() < 2) + return {}; + auto ch2 = lexer.consume(); + auto ch3 = lexer.consume(); + u32 code_point = ((ch & 0x0F) << 12) | ((ch2 & 0x3F) << 6) | (ch3 & 0x3F); + builder.append_code_unit(code_point); + } else if ((ch & 0xF8) == 0xF0) { + // 4-byte UTF-8 (needs surrogate pair) + if (lexer.tell_remaining() < 3) + return {}; + auto ch2 = lexer.consume(); + auto ch3 = lexer.consume(); + auto ch4 = lexer.consume(); + u32 code_point = ((ch & 0x07) << 18) | ((ch2 & 0x3F) << 12) | ((ch3 & 0x3F) << 6) | (ch4 & 0x3F); + builder.append_code_point(code_point); + } else { + return {}; + } + } + } + + return builder.to_utf16_string(); +} + +template +static ALWAYS_INLINE ThrowCompletionOr ensure_simdjson_fully_parsed(VM& vm, T& value) +{ + if constexpr (IsSame) { + if (!value.at_end()) + return vm.throw_completion(ErrorType::JsonMalformed); + } + return {}; +} + +static ThrowCompletionOr parse_simdjson_value(VM&, simdjson::ondemand::value); + +template +static ThrowCompletionOr parse_simdjson_number(VM& vm, T& value, StringView raw_sv) +{ + // Validate JSON number format (simdjson is more lenient than spec) + // - No leading zeros (except "0" or "0.xxx") + // - No trailing decimal point (e.g., "1." is invalid) + size_t i = 0; + if (i < raw_sv.length() && raw_sv[i] == '-') + ++i; + if (i < raw_sv.length() && raw_sv[i] == '0' && i + 1 < raw_sv.length() && is_ascii_digit(raw_sv[i + 1])) + return vm.throw_completion(ErrorType::JsonMalformed); // Leading zero + while (i < raw_sv.length() && is_ascii_digit(raw_sv[i])) + ++i; + if (i < raw_sv.length() && raw_sv[i] == '.') { + ++i; + if (i >= raw_sv.length() || !is_ascii_digit(raw_sv[i])) + return vm.throw_completion(ErrorType::JsonMalformed); // Trailing decimal + } + + double double_value; + auto error = value.get_double().get(double_value); + if (!error) { + TRY(ensure_simdjson_fully_parsed(vm, value)); + return Value(double_value); + } + + // Handle overflow to infinity (e.g., 1e309) + // simdjson returns NUMBER_ERROR for numbers that overflow double + // Use parse_first_number as fallback - it handles overflow correctly + if (error == simdjson::NUMBER_ERROR) { + auto result = parse_first_number(raw_sv, TrimWhitespace::No); + if (result.has_value() && result->characters_parsed == raw_sv.length()) + return Value(result->value); + } + + return vm.throw_completion(ErrorType::JsonMalformed); +} + +template +static ThrowCompletionOr parse_simdjson_string(VM& vm, T& value) +{ + // Use get_raw_json_string() to get the raw JSON string content (without quotes, with escapes), + // then unescape ourselves to properly handle lone surrogates like \uD800 which simdjson rejects. + simdjson::ondemand::raw_json_string raw_string; + if (value.get_raw_json_string().get(raw_string)) + return vm.throw_completion(ErrorType::JsonMalformed); + char const* raw = raw_string.raw(); + // Find the length by looking for the closing quote (simdjson validated the structure) + size_t length = 0; + while (raw[length] != '"') { + if (raw[length] == '\\') + ++length; // Skip escaped character + ++length; + } + auto unescaped = unescape_json_string({ raw, length }); + if (!unescaped.has_value()) + return vm.throw_completion(ErrorType::JsonMalformed); + return PrimitiveString::create(vm, unescaped.release_value()); +} + +template +static ThrowCompletionOr parse_simdjson_array(VM& vm, T& value) +{ + auto& realm = *vm.current_realm(); + + simdjson::ondemand::array simdjson_array; + if (value.get_array().get(simdjson_array)) + return vm.throw_completion(ErrorType::JsonMalformed); + + auto array = MUST(Array::create(realm, 0)); + size_t index = 0; + + for (auto element : simdjson_array) { + simdjson::ondemand::value element_value; + if (element.get(element_value)) + return vm.throw_completion(ErrorType::JsonMalformed); + auto parsed = TRY(parse_simdjson_value(vm, element_value)); + array->define_direct_property(index++, parsed, default_attributes); + } + + TRY(ensure_simdjson_fully_parsed(vm, value)); + return array; +} + +template +static ThrowCompletionOr parse_simdjson_object(VM& vm, T& value) +{ + auto& realm = *vm.current_realm(); + + simdjson::ondemand::object simdjson_object; + if (value.get_object().get(simdjson_object)) + return vm.throw_completion(ErrorType::JsonMalformed); + + auto object = Object::create(realm, realm.intrinsics().object_prototype()); + + for (auto field : simdjson_object) { + // Use escaped_key() to get the raw JSON key (with escapes), then unescape ourselves + std::string_view raw_key; + if (field.escaped_key().get(raw_key)) + return vm.throw_completion(ErrorType::JsonMalformed); + auto unescaped_key = unescape_json_string({ raw_key.data(), raw_key.size() }); + if (!unescaped_key.has_value()) + return vm.throw_completion(ErrorType::JsonMalformed); + simdjson::ondemand::value field_value; + if (field.value().get(field_value)) + return vm.throw_completion(ErrorType::JsonMalformed); + auto parsed = TRY(parse_simdjson_value(vm, field_value)); + object->define_direct_property(unescaped_key.release_value(), parsed, default_attributes); + } + + TRY(ensure_simdjson_fully_parsed(vm, value)); + return object; +} + +static ThrowCompletionOr parse_simdjson_value(VM& vm, simdjson::ondemand::value value) +{ + simdjson::ondemand::json_type type; + if (value.type().get(type)) + return vm.throw_completion(ErrorType::JsonMalformed); + + switch (type) { + case simdjson::ondemand::json_type::null: + return js_null(); + case simdjson::ondemand::json_type::boolean: { + bool boolean_value; + if (value.get_bool().get(boolean_value)) + return vm.throw_completion(ErrorType::JsonMalformed); + return Value(boolean_value); + } + case simdjson::ondemand::json_type::number: { + auto raw = value.raw_json_token(); + StringView raw_sv { raw.data(), raw.size() }; + return parse_simdjson_number(vm, value, raw_sv); + } + case simdjson::ondemand::json_type::string: + return parse_simdjson_string(vm, value); + case simdjson::ondemand::json_type::array: + return parse_simdjson_array(vm, value); + case simdjson::ondemand::json_type::object: + return parse_simdjson_object(vm, value); + } + VERIFY_NOT_REACHED(); +} + +static ThrowCompletionOr parse_simdjson_document(VM& vm, simdjson::ondemand::document& document) +{ + simdjson::ondemand::json_type type; + if (document.type().get(type)) + return vm.throw_completion(ErrorType::JsonMalformed); + + switch (type) { + case simdjson::ondemand::json_type::null: { + if (document.is_null().error()) + return vm.throw_completion(ErrorType::JsonMalformed); + if (!document.at_end()) + return vm.throw_completion(ErrorType::JsonMalformed); + return js_null(); + } + case simdjson::ondemand::json_type::boolean: { + bool boolean_value; + if (document.get_bool().get(boolean_value)) + return vm.throw_completion(ErrorType::JsonMalformed); + if (!document.at_end()) + return vm.throw_completion(ErrorType::JsonMalformed); + return Value(boolean_value); + } + case simdjson::ondemand::json_type::number: { + // Get raw token first in case get_double fails (e.g., overflow) + std::string_view raw; + if (document.raw_json_token().get(raw)) + return vm.throw_completion(ErrorType::JsonMalformed); + StringView raw_sv { raw.data(), raw.size() }; + auto trimmed = raw_sv.trim_whitespace(); + return parse_simdjson_number(vm, document, trimmed); + } + case simdjson::ondemand::json_type::string: + return parse_simdjson_string(vm, document); + case simdjson::ondemand::json_type::array: + return parse_simdjson_array(vm, document); + case simdjson::ondemand::json_type::object: + return parse_simdjson_object(vm, document); + } + VERIFY_NOT_REACHED(); +} + // 25.5.1.1 ParseJSON ( text ), https://tc39.es/ecma262/#sec-ParseJSON ThrowCompletionOr JSONObject::parse_json(VM& vm, StringView text) { - auto json = JsonValue::from_string(text); - // 1. If StringToCodePoints(text) is not a valid JSON text as specified in ECMA-404, throw a SyntaxError exception. - if (json.is_error()) + // NB: Per ECMA-404, the BOM is not valid JSON whitespace. simdjson silently skips it, so we must reject it explicitly. + if (text.length() >= 3 + && static_cast(text[0]) == 0xEF + && static_cast(text[1]) == 0xBB + && static_cast(text[2]) == 0xBF) { + return vm.throw_completion(ErrorType::JsonMalformed); + } + + simdjson::ondemand::parser parser; + simdjson::padded_string padded(text.characters_without_null_termination(), text.length()); + + simdjson::ondemand::document document; + if (parser.iterate(padded).get(document)) return vm.throw_completion(ErrorType::JsonMalformed); // 2. Let scriptString be the string-concatenation of "(", text, and ");". @@ -461,7 +776,7 @@ ThrowCompletionOr JSONObject::parse_json(VM& vm, StringView text) // 4. NOTE: The early error rules defined in 13.2.5.1 have special handling for the above invocation of ParseText. // 5. Assert: script is a Parse Node. // 6. Let result be ! Evaluation of script. - auto result = JSONObject::parse_json_value(vm, json.value()); + auto result = TRY(parse_simdjson_document(vm, document)); // 7. NOTE: The PropertyDefinitionEvaluation semantics defined in 13.2.5.5 have special handling for the above evaluation. // 8. Assert: result is either a String, a Number, a Boolean, an Object that is defined by either an ArrayLiteral or an ObjectLiteral, or null. @@ -470,44 +785,6 @@ ThrowCompletionOr JSONObject::parse_json(VM& vm, StringView text) return result; } -Value JSONObject::parse_json_value(VM& vm, JsonValue const& value) -{ - if (value.is_object()) - return Value(parse_json_object(vm, value.as_object())); - if (value.is_array()) - return Value(parse_json_array(vm, value.as_array())); - if (value.is_null()) - return js_null(); - if (auto double_value = value.get_double_with_precision_loss(); double_value.has_value()) - return Value(double_value.value()); - if (value.is_string()) - return PrimitiveString::create(vm, value.as_string()); - if (value.is_bool()) - return Value(value.as_bool()); - VERIFY_NOT_REACHED(); -} - -Object* JSONObject::parse_json_object(VM& vm, JsonObject const& json_object) -{ - auto& realm = *vm.current_realm(); - auto object = Object::create(realm, realm.intrinsics().object_prototype()); - json_object.for_each_member([&](auto& key, auto& value) { - object->define_direct_property(Utf16String::from_utf8(key), parse_json_value(vm, value), default_attributes); - }); - return object; -} - -Array* JSONObject::parse_json_array(VM& vm, JsonArray const& json_array) -{ - auto& realm = *vm.current_realm(); - auto array = MUST(Array::create(realm, 0)); - size_t index = 0; - json_array.for_each([&](auto& value) { - array->define_direct_property(index++, parse_json_value(vm, value), default_attributes); - }); - return array; -} - // 25.5.1.1 InternalizeJSONProperty ( holder, name, reviver ), https://tc39.es/ecma262/#sec-internalizejsonproperty ThrowCompletionOr JSONObject::internalize_json_property(VM& vm, Object* holder, PropertyKey const& name, FunctionObject& reviver) { @@ -564,13 +841,41 @@ JS_DEFINE_NATIVE_FUNCTION(JSONObject::raw_json) // 3. Parse StringToCodePoints(jsonString) as a JSON text as specified in ECMA-404. Throw a SyntaxError exception // if it is not a valid JSON text as defined in that specification, or if its outermost value is an object or // array as defined in that specification. - auto json = JsonValue::from_string(json_string); - if (json.is_error()) + simdjson::ondemand::parser parser; + simdjson::padded_string padded(json_string.bytes_as_string_view().characters_without_null_termination(), json_string.bytes_as_string_view().length()); + + simdjson::ondemand::document doc; + if (parser.iterate(padded).get(doc)) return vm.throw_completion(ErrorType::JsonMalformed); - if (json.value().is_object() || json.value().is_array()) + simdjson::ondemand::json_type type; + if (doc.type().get(type)) + return vm.throw_completion(ErrorType::JsonMalformed); + + if (type == simdjson::ondemand::json_type::object || type == simdjson::ondemand::json_type::array) return vm.throw_completion(ErrorType::JsonRawJSONNonPrimitive); + // Consume the value to advance past it, then check for trailing content + switch (type) { + case simdjson::ondemand::json_type::null: + (void)doc.is_null(); + break; + case simdjson::ondemand::json_type::boolean: + (void)doc.get_bool(); + break; + case simdjson::ondemand::json_type::number: + (void)doc.get_double(); + break; + case simdjson::ondemand::json_type::string: + (void)doc.get_string(); + break; + default: + VERIFY_NOT_REACHED(); + } + + if (!doc.at_end()) + return vm.throw_completion(ErrorType::JsonMalformed); + // 4. Let internalSlotsList be « [[IsRawJSON]] ». // 5. Let obj be OrdinaryObjectCreate(null, internalSlotsList). auto object = RawJSONObject::create(realm, nullptr); diff --git a/Libraries/LibJS/Runtime/JSONObject.h b/Libraries/LibJS/Runtime/JSONObject.h index e1615c9690..69212cce4a 100644 --- a/Libraries/LibJS/Runtime/JSONObject.h +++ b/Libraries/LibJS/Runtime/JSONObject.h @@ -24,7 +24,6 @@ public: static ThrowCompletionOr> stringify_impl(VM&, Value value, Value replacer, Value space); static ThrowCompletionOr parse_json(VM&, StringView text); - static Value parse_json_value(VM&, JsonValue const&); private: explicit JSONObject(Realm&); @@ -44,8 +43,6 @@ private: static String quote_json_string(Utf16View const&); // Parse helpers - static Object* parse_json_object(VM&, JsonObject const&); - static Array* parse_json_array(VM&, JsonArray const&); static ThrowCompletionOr internalize_json_property(VM&, Object* holder, PropertyKey const& name, FunctionObject& reviver); JS_DECLARE_NATIVE_FUNCTION(stringify); diff --git a/Libraries/LibJS/Tests/builtins/JSON/JSON.parse.js b/Libraries/LibJS/Tests/builtins/JSON/JSON.parse.js index c42dbafa57..0bbde1aa06 100644 --- a/Libraries/LibJS/Tests/builtins/JSON/JSON.parse.js +++ b/Libraries/LibJS/Tests/builtins/JSON/JSON.parse.js @@ -74,3 +74,73 @@ test("does not truncate large integers", () => { expect(JSON.parse("18446744073709551616")).toEqual(18446744073709551616); expect(JSON.parse("18446744073709551617")).toEqual(18446744073709551617); }); + +test("number overflow to infinity", () => { + expect(JSON.parse("1e309")).toBe(Infinity); + expect(JSON.parse("-1e309")).toBe(-Infinity); + expect(JSON.parse("1e-400")).toBe(0); +}); + +test("rejects invalid number formats", () => { + // Leading zeros not allowed + expect(() => JSON.parse("01")).toThrow(SyntaxError); + expect(() => JSON.parse("-01")).toThrow(SyntaxError); + expect(() => JSON.parse("00")).toThrow(SyntaxError); + expect(() => JSON.parse("007")).toThrow(SyntaxError); + + // Trailing decimal point not allowed + expect(() => JSON.parse("1.")).toThrow(SyntaxError); + expect(() => JSON.parse("0.")).toThrow(SyntaxError); + expect(() => JSON.parse("-1.")).toThrow(SyntaxError); + + // Other invalid formats + expect(() => JSON.parse("+1")).toThrow(SyntaxError); + expect(() => JSON.parse(".1")).toThrow(SyntaxError); + expect(() => JSON.parse("1e")).toThrow(SyntaxError); + expect(() => JSON.parse("1e+")).toThrow(SyntaxError); + expect(() => JSON.parse("1e-")).toThrow(SyntaxError); +}); + +test("rejects trailing content", () => { + expect(() => JSON.parse("123 garbage")).toThrow(SyntaxError); + expect(() => JSON.parse("null garbage")).toThrow(SyntaxError); + expect(() => JSON.parse("true garbage")).toThrow(SyntaxError); + expect(() => JSON.parse('"string" garbage')).toThrow(SyntaxError); + expect(() => JSON.parse("[] garbage")).toThrow(SyntaxError); + expect(() => JSON.parse("{} garbage")).toThrow(SyntaxError); +}); + +test("string escape sequences", () => { + expect(JSON.parse('"\\""')).toBe('"'); + expect(JSON.parse('"\\\\"')).toBe("\\"); + expect(JSON.parse('"\\/"')).toBe("/"); + expect(JSON.parse('"\\b"')).toBe("\b"); + expect(JSON.parse('"\\f"')).toBe("\f"); + expect(JSON.parse('"\\n"')).toBe("\n"); + expect(JSON.parse('"\\r"')).toBe("\r"); + expect(JSON.parse('"\\t"')).toBe("\t"); + expect(JSON.parse('"\\u0041"')).toBe("A"); + expect(JSON.parse('"\\u0000"')).toBe("\0"); +}); + +test("unicode and surrogate pairs", () => { + expect(JSON.parse('"café"')).toBe("café"); + expect(JSON.parse('"日本語"')).toBe("日本語"); + expect(JSON.parse('"\\uD83D\\uDE00"')).toBe("😀"); + expect(JSON.parse('"\\u4e2d\\u6587"')).toBe("中文"); + + // Lone surrogates (valid JSON) + expect(JSON.parse('"\\uD800"')).toBe("\uD800"); + expect(JSON.parse('"\\uDFFF"')).toBe("\uDFFF"); +}); + +test("whitespace handling", () => { + expect(JSON.parse(" null")).toBe(null); + expect(JSON.parse("null ")).toBe(null); + expect(JSON.parse(" null ")).toBe(null); + expect(JSON.parse("\t123")).toBe(123); + expect(JSON.parse("123\n")).toBe(123); + expect(JSON.parse("\r\n123\r\n")).toBe(123); + expect(JSON.parse(" { } ")).toEqual({}); + expect(JSON.parse(" [ ] ")).toEqual([]); +}); diff --git a/Libraries/LibWeb/WebDriver/JSON.cpp b/Libraries/LibWeb/WebDriver/JSON.cpp index 06911f21b3..c1b4dd6917 100644 --- a/Libraries/LibWeb/WebDriver/JSON.cpp +++ b/Libraries/LibWeb/WebDriver/JSON.cpp @@ -10,9 +10,11 @@ #include #include #include +#include #include #include -#include +#include +#include #include #include #include @@ -67,6 +69,47 @@ static bool is_collection(JS::Object const& value) || is(value)); } +// Helper to convert AK::JsonValue to JS::Value (for WebDriver protocol) +static JS::Value json_value_to_js_value(JS::VM& vm, JsonValue const& value); + +static JS::Object* json_object_to_js_object(JS::VM& vm, JsonObject const& json_object) +{ + auto& realm = *vm.current_realm(); + auto object = JS::Object::create(realm, realm.intrinsics().object_prototype()); + json_object.for_each_member([&](auto& key, auto& value) { + object->define_direct_property(Utf16String::from_utf8(key), json_value_to_js_value(vm, value), JS::default_attributes); + }); + return object; +} + +static JS::Array* json_array_to_js_array(JS::VM& vm, JsonArray const& json_array) +{ + auto& realm = *vm.current_realm(); + auto array = MUST(JS::Array::create(realm, 0)); + size_t index = 0; + json_array.for_each([&](auto& value) { + array->define_direct_property(index++, json_value_to_js_value(vm, value), JS::default_attributes); + }); + return array; +} + +static JS::Value json_value_to_js_value(JS::VM& vm, JsonValue const& value) +{ + if (value.is_object()) + return JS::Value(json_object_to_js_object(vm, value.as_object())); + if (value.is_array()) + return JS::Value(json_array_to_js_array(vm, value.as_array())); + if (value.is_null()) + return JS::js_null(); + if (auto double_value = value.get_double_with_precision_loss(); double_value.has_value()) + return JS::Value(double_value.value()); + if (value.is_string()) + return JS::PrimitiveString::create(vm, value.as_string()); + if (value.is_bool()) + return JS::Value(value.as_bool()); + VERIFY_NOT_REACHED(); +} + // https://w3c.github.io/webdriver/#dfn-clone-an-object template static ErrorOr clone_an_object(HTML::BrowsingContext const& browsing_context, JS::Object const& value, SeenMap& seen, CloneAlgorithm const& clone_algorithm) @@ -334,7 +377,7 @@ ErrorOr json_deserialize(HTML::BrowsingContext cons auto& vm = browsing_context.vm(); SeenMap seen; - return internal_json_deserialize(browsing_context, JS::JSONObject::parse_json_value(vm, value), seen); + return internal_json_deserialize(browsing_context, json_value_to_js_value(vm, value), seen); } } diff --git a/Meta/CMake/flatpak/org.ladybird.Ladybird.json b/Meta/CMake/flatpak/org.ladybird.Ladybird.json index 1e1684a7eb..72515176f1 100644 --- a/Meta/CMake/flatpak/org.ladybird.Ladybird.json +++ b/Meta/CMake/flatpak/org.ladybird.Ladybird.json @@ -29,6 +29,25 @@ "/share/man" ], "modules": [ + { + "name": "simdjson", + "buildsystem": "cmake-ninja", + "sources": [ + { + "type": "git", + "url": "https://github.com/simdjson/simdjson.git", + "tag": "v3.12.2" + } + ], + "config-opts": [ + "-DCMAKE_BUILD_TYPE=Release", + "-DCMAKE_PREFIX_PATH=/app", + "-DCMAKE_INSTALL_LIBDIR=lib", + "-DCMAKE_POSITION_INDEPENDENT_CODE=ON", + "-DSIMDJSON_BUILD_STATIC_LIB=OFF", + "-DSIMDJSON_DEVELOPER_MODE=OFF" + ] + }, { "name": "simdutf", "buildsystem": "cmake-ninja", diff --git a/Utilities/js.cpp b/Utilities/js.cpp index 49bbae2987..cfe2998101 100644 --- a/Utilities/js.cpp +++ b/Utilities/js.cpp @@ -7,7 +7,6 @@ * SPDX-License-Identifier: BSD-2-Clause */ -#include #include #include #include @@ -290,11 +289,7 @@ static JS::ThrowCompletionOr load_json_impl(JS::VM& vm) if (file_contents_or_error.is_error()) return vm.throw_completion(TRY_OR_THROW_OOM(vm, String::formatted("Failed to read '{}': {}", filename, file_contents_or_error.error()))); - auto json = JsonValue::from_string(file_contents_or_error.value()); - if (json.is_error()) - return vm.throw_completion(JS::ErrorType::JsonMalformed); - - return JS::JSONObject::parse_json_value(vm, json.value()); + return JS::JSONObject::parse_json(vm, file_contents_or_error.value()); } void ReplObject::initialize(JS::Realm& realm) diff --git a/vcpkg.json b/vcpkg.json index ffbb406aa1..50aa1068da 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -163,6 +163,7 @@ "name": "sdl3", "default-features": false }, + "simdjson", "simdutf", { "name": "skia", @@ -307,6 +308,10 @@ "name": "sdl3", "version": "3.2.22#0" }, + { + "name": "simdjson", + "version": "3.13.0#0" + }, { "name": "simdutf", "version": "7.4.0#0"