diff options
-rw-r--r-- | Libraries/LibJS/Runtime/ErrorTypes.h | 1 | ||||
-rw-r--r-- | Libraries/LibJS/Runtime/JSONObject.cpp | 119 | ||||
-rw-r--r-- | Libraries/LibJS/Runtime/JSONObject.h | 8 | ||||
-rw-r--r-- | Libraries/LibJS/Tests/JSON.parse-reviver.js | 15 | ||||
-rw-r--r-- | Libraries/LibJS/Tests/JSON.parse.js | 43 | ||||
-rw-r--r-- | Libraries/LibJS/Tests/test-common.js | 37 |
6 files changed, 218 insertions, 5 deletions
diff --git a/Libraries/LibJS/Runtime/ErrorTypes.h b/Libraries/LibJS/Runtime/ErrorTypes.h index 75835195a9..34353de0f8 100644 --- a/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Libraries/LibJS/Runtime/ErrorTypes.h @@ -49,6 +49,7 @@ M(IsNotAEvaluatedFrom, "%s is not a %s (evaluated from '%s')") \ M(JsonBigInt, "Cannot serialize BigInt value to JSON") \ M(JsonCircular, "Cannot stringify circular object") \ + M(JsonMalformed, "Malformed JSON string") \ M(NotA, "Not a %s object") \ M(NotACtor, "%s is not a constructor") \ M(NotAFunction, "%s is not a function") \ diff --git a/Libraries/LibJS/Runtime/JSONObject.cpp b/Libraries/LibJS/Runtime/JSONObject.cpp index dae2b5af5f..f5747085c2 100644 --- a/Libraries/LibJS/Runtime/JSONObject.cpp +++ b/Libraries/LibJS/Runtime/JSONObject.cpp @@ -24,6 +24,9 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +#include <AK/JsonParser.h> +#include <AK/JsonObject.h> +#include <AK/JsonArray.h> #include <AK/StringBuilder.h> #include <LibJS/Interpreter.h> #include <LibJS/Runtime/Array.h> @@ -39,7 +42,7 @@ JSONObject::JSONObject() { u8 attr = Attribute::Writable | Attribute::Configurable; define_native_function("stringify", stringify, 3, attr); - define_native_function("parse", parse, 1, attr); + define_native_function("parse", parse, 2, attr); } JSONObject::~JSONObject() @@ -365,9 +368,119 @@ String JSONObject::quote_json_string(String string) return builder.to_string(); } -Value JSONObject::parse(Interpreter&) +Value JSONObject::parse(Interpreter& interpreter) { - return js_undefined(); + if (!interpreter.argument_count()) + return js_undefined(); + auto string = interpreter.argument(0).to_string(interpreter); + if (interpreter.exception()) + return {}; + auto reviver = interpreter.argument(1); + + auto json = JsonValue::from_string(string); + if (!json.has_value()) { + interpreter.throw_exception<SyntaxError>(ErrorType::JsonMalformed); + return {}; + } + Value result = parse_json_value(interpreter, json.value()); + if (reviver.is_function()) { + auto* holder_object = Object::create_empty(interpreter, interpreter.global_object()); + holder_object->define_property(String::empty(), result); + if (interpreter.exception()) + return {}; + return internalize_json_property(interpreter, holder_object, String::empty(), reviver.as_function()); + } + return result; +} + +Value JSONObject::parse_json_value(Interpreter& interpreter, const JsonValue& value) +{ + if (value.is_object()) + return Value(parse_json_object(interpreter, value.as_object())); + if (value.is_array()) + return Value(parse_json_array(interpreter, value.as_array())); + if (value.is_null()) + return js_null(); +#if !defined(KERNEL) + if (value.is_double()) + return Value(value.as_double()); +#endif + if (value.is_number()) + return Value(value.to_i32(0)); + if (value.is_string()) + return js_string(interpreter, value.to_string()); + if (value.is_bool()) + return Value(static_cast<bool>(value.as_bool())); + ASSERT_NOT_REACHED(); +} + +Object* JSONObject::parse_json_object(Interpreter& interpreter, const JsonObject& json_object) +{ + auto* object = Object::create_empty(interpreter, interpreter.global_object()); + json_object.for_each_member([&object, &interpreter](String key, JsonValue value) { + object->put(key, parse_json_value(interpreter, value)); + }); + return object; +} + +Array* JSONObject::parse_json_array(Interpreter& interpreter, const JsonArray& json_array) +{ + auto* array = Array::create(interpreter.global_object()); + size_t index = 0; + json_array.for_each([&array, &interpreter, &index](JsonValue value) { + array->put(index++, parse_json_value(interpreter, value)); + }); + return array; +} + +Value JSONObject::internalize_json_property(Interpreter& interpreter, Object* holder, const PropertyName& name, Function& reviver) +{ + auto value = holder->get(name); + if (interpreter.exception()) + return {}; + if (value.is_object()) { + auto& value_object = value.as_object(); + + auto process_property = [&](const PropertyName& key) { + auto element = internalize_json_property(interpreter, &value_object, key, reviver); + if (interpreter.exception()) + return; + if (element.is_undefined()) { + value_object.delete_property(key); + } else { + value_object.define_property(key, element, default_attributes, false); + } + }; + + if (value_object.is_array()) { + auto length = length_of_array_like(interpreter, value); + for (size_t i = 0; i < length; ++i) { + process_property(i); + if (interpreter.exception()) + return {}; + } + } else { + for (auto& entry : value_object.indexed_properties()) { + auto value_and_attributes = entry.value_and_attributes(&value_object); + if (!value_and_attributes.attributes.is_enumerable()) + continue; + process_property(entry.index()); + if (interpreter.exception()) + return {}; + } + for (auto& [key, metadata] : value_object.shape().property_table_ordered()) { + if (!metadata.attributes.is_enumerable()) + continue; + process_property(key); + if (interpreter.exception()) + return {}; + } + } + } + MarkedValueList arguments(interpreter.heap()); + arguments.values().append(js_string(interpreter, name.to_string())); + arguments.values().append(value); + return interpreter.call(reviver, Value(holder), move(arguments)); } } diff --git a/Libraries/LibJS/Runtime/JSONObject.h b/Libraries/LibJS/Runtime/JSONObject.h index fa5491b210..c9b8a488ef 100644 --- a/Libraries/LibJS/Runtime/JSONObject.h +++ b/Libraries/LibJS/Runtime/JSONObject.h @@ -26,8 +26,6 @@ #pragma once -#include <LibJS/Runtime/Object.h> - namespace JS { class JSONObject final : public Object { @@ -52,6 +50,12 @@ private: static String serialize_json_array(Interpreter&, StringifyState&, Object&); static String quote_json_string(String); + // Parse helpers + static Object* parse_json_object(Interpreter&, const JsonObject&); + static Array* parse_json_array(Interpreter&, const JsonArray&); + static Value parse_json_value(Interpreter&, const JsonValue&); + static Value internalize_json_property(Interpreter&, Object* holder, const PropertyName& name, Function& reviver); + static Value stringify(Interpreter&); static Value parse(Interpreter&); }; diff --git a/Libraries/LibJS/Tests/JSON.parse-reviver.js b/Libraries/LibJS/Tests/JSON.parse-reviver.js new file mode 100644 index 0000000000..aa2d92b24d --- /dev/null +++ b/Libraries/LibJS/Tests/JSON.parse-reviver.js @@ -0,0 +1,15 @@ +load("test-common.js"); + +try { + let string = `{"var1":10,"var2":"hello","var3":{"nested":5}}`; + + let object = JSON.parse(string, (key, value) => typeof value === "number" ? value * 2 : value); + assertDeepEquals(object, { var1: 20, var2: "hello", var3: { nested: 10 } }); + + object = JSON.parse(string, (key, value) => typeof value === "number" ? undefined : value); + assertDeepEquals(object, { var2: "hello", var3: {} }); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Tests/JSON.parse.js b/Libraries/LibJS/Tests/JSON.parse.js new file mode 100644 index 0000000000..40f79cee60 --- /dev/null +++ b/Libraries/LibJS/Tests/JSON.parse.js @@ -0,0 +1,43 @@ +load("test-common.js"); + +try { + assert(JSON.parse.length === 2); + + const properties = [ + ["5", 5], + ["null", null], + ["true", true], + ["false", false], + ['"test"', "test"], + ['[1,2,"foo"]', [1, 2, "foo"]], + ['{"foo":1,"bar":"baz"}', { foo: 1, bar: "baz" }], + ]; + + properties.forEach(testCase => { + assertDeepEquals(JSON.parse(testCase[0]), testCase[1]); + }); + + let syntaxErrors = [ + undefined, + NaN, + -NaN, + Infinity, + -Infinity, + '{ "foo" }', + '{ foo: "bar" }', + "[1,2,3,]", + "[1,2,3, ]", + '{ "foo": "bar",}', + '{ "foo": "bar", }', + ]; + + syntaxErrors.forEach(error => assertThrowsError(() => { + JSON.parse(error); + }, { + error: SyntaxError, + })); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Tests/test-common.js b/Libraries/LibJS/Tests/test-common.js index d0c59f35f7..4c7e23ac8b 100644 --- a/Libraries/LibJS/Tests/test-common.js +++ b/Libraries/LibJS/Tests/test-common.js @@ -92,3 +92,40 @@ const assertVisitsAll = (testFunction, expectedOutput) => { function isClose(a, b) { return Math.abs(a - b) < 0.000001; } + +/** + * Quick and dirty deep equals method. + * @param {*} a First value + * @param {*} b Second value + */ +function assertDeepEquals(a, b) { + assert(deepEquals(a, b)); +} + +function deepEquals(a, b) { + if (Array.isArray(a)) + return Array.isArray(b) && deepArrayEquals(a, b); + if (typeof a === "object") + return typeof b === "object" && deepObjectEquals(a, b); + return a === b; +} + +function deepArrayEquals(a, b) { + if (a.length !== b.length) + return false; + for (let i = 0; i < a.length; ++i) { + if (!deepEquals(a[i], b[i])) + return false; + } + return true; +} + +function deepObjectEquals(a, b) { + if (a === null) + return b === null; + for (let key of Reflect.ownKeys(a)) { + if (!deepEquals(a[key], b[key])) + return false; + } + return true; +} |