diff --git a/lib/internal/bootstrap/realm.js b/lib/internal/bootstrap/realm.js index f49f0814bbc687..3172cae740348d 100644 --- a/lib/internal/bootstrap/realm.js +++ b/lib/internal/bootstrap/realm.js @@ -129,6 +129,7 @@ const schemelessBlockList = new SafeSet([ 'quic', 'test', 'test/reporters', + 'json', ]); // Modules that will only be enabled at run time. const experimentalModuleList = new SafeSet(['sqlite', 'quic']); diff --git a/lib/json.js b/lib/json.js new file mode 100644 index 00000000000000..6107210ac0cc48 --- /dev/null +++ b/lib/json.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = internalBinding('json_parser'); diff --git a/node.gyp b/node.gyp index 83351d1d82627e..e504cd961d1397 100644 --- a/node.gyp +++ b/node.gyp @@ -127,6 +127,7 @@ 'src/node_http_parser.cc', 'src/node_http2.cc', 'src/node_i18n.cc', + 'src/node_json_parser.cc', 'src/node_locks.cc', 'src/node_main_instance.cc', 'src/node_messaging.cc', @@ -261,6 +262,7 @@ 'src/node_http2_state.h', 'src/node_i18n.h', 'src/node_internals.h', + 'src/node_json_parser.h', 'src/node_locks.h', 'src/node_main_instance.h', 'src/node_mem.h', diff --git a/src/node_binding.cc b/src/node_binding.cc index 740706e917b7d2..01148574a9042c 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -57,6 +57,7 @@ V(http2) \ V(http_parser) \ V(inspector) \ + V(json_parser) \ V(internal_only_v8) \ V(js_stream) \ V(js_udp_wrap) \ diff --git a/src/node_external_reference.h b/src/node_external_reference.h index 3413bc78b9d740..7896d328b9f357 100644 --- a/src/node_external_reference.h +++ b/src/node_external_reference.h @@ -80,6 +80,7 @@ class ExternalReferenceRegistry { V(heap_utils) \ V(http_parser) \ V(internal_only_v8) \ + V(json_parser) \ V(locks) \ V(messaging) \ V(mksnapshot) \ diff --git a/src/node_json_parser.cc b/src/node_json_parser.cc new file mode 100644 index 00000000000000..823dea3328ac5b --- /dev/null +++ b/src/node_json_parser.cc @@ -0,0 +1,203 @@ +#include "node_json_parser.h" +#include "node_errors.h" +#include "node_external_reference.h" +#include "simdjson.h" +#include "env-inl.h" + +namespace node { +namespace json_parser { + +using std::string; + +using v8::Array; +using v8::Boolean; +using v8::Context; +using v8::FunctionCallbackInfo; +using v8::Isolate; +using v8::Local; +using v8::MaybeLocal; +using v8::Null; +using v8::Number; +using v8::Object; +using v8::Primitive; +using v8::String; +using v8::Undefined; +using v8::Value; + +template +inline MaybeLocal ToV8Number(Isolate* isolate, + simdjson::dom::element element) { + T value; + simdjson::error_code error = std::move(element.get()).get(value); + + if (error) { + THROW_ERR_MODULE_NOT_INSTANTIATED(isolate); + return MaybeLocal(); + } + + return MaybeLocal(Number::New(isolate, static_cast(value))); +} + +string ObjectToString(Isolate* isolate, Local value) { + Utf8Value utf8_value(isolate, value); + return string(*utf8_value); +} + +MaybeLocal ConvertSimdjsonElement(Isolate* isolate, + simdjson::dom::element element) { + simdjson::dom::element_type type = element.type(); + + switch (type) { + case simdjson::dom::element_type::INT64: { + return ToV8Number(isolate, element); + } + case simdjson::dom::element_type::UINT64: { + return ToV8Number(isolate, element); + } + case simdjson::dom::element_type::DOUBLE: { + return ToV8Number(isolate, element); + } + case simdjson::dom::element_type::BOOL: { + bool value; + simdjson::error_code error = element.get_bool().get(value); + + if (error) { + THROW_ERR_MODULE_NOT_INSTANTIATED(isolate); + return MaybeLocal(); + } + + return MaybeLocal(Boolean::New(isolate, value)); + } + case simdjson::dom::element_type::NULL_VALUE: { + Local null = Null(isolate); + + return MaybeLocal(null); + } + case simdjson::dom::element_type::STRING: { + string value; + simdjson::error_code error = element.get_string().get(value); + + if (error) { + THROW_ERR_MODULE_NOT_INSTANTIATED(isolate); + return MaybeLocal(); + } + + return String::NewFromUtf8(isolate, value.c_str()); + } + case simdjson::dom::element_type::ARRAY: { + simdjson::dom::array array; + simdjson::error_code error = element.get_array().get(array); + + if (error) { + THROW_ERR_MODULE_NOT_INSTANTIATED(isolate); + return MaybeLocal(); + } + + Local v8_array = + v8::Array::New(isolate, array.size()); + + Local context = isolate->GetCurrentContext(); + + uint32_t index = 0; + for (simdjson::dom::element child : array) { + Local converted; + + if (!ConvertSimdjsonElement(isolate, child).ToLocal(&converted)) + return MaybeLocal(); + + if (v8_array->Set(context, index, converted).IsNothing()) + return MaybeLocal(); + index++; + } + + return MaybeLocal(v8_array); + } + case simdjson::dom::element_type::OBJECT: { + simdjson::dom::object object; + simdjson::error_code error = element.get_object().get(object); + + if (error) { + THROW_ERR_MODULE_NOT_INSTANTIATED(isolate); + return MaybeLocal(); + } + + Local v8_object = Object::New(isolate); + Local context = isolate->GetCurrentContext(); + + for (const simdjson::dom::key_value_pair& kv : object) { + const std::string_view key = kv.key; + + Local v8_key; + if (!String::NewFromUtf8(isolate, + key.data(), + v8::NewStringType::kNormal, + static_cast(key.size())) + .ToLocal(&v8_key)) { + return MaybeLocal(); + } + + Local converted; + if (!ConvertSimdjsonElement(isolate, kv.value).ToLocal(&converted)) + return MaybeLocal(); + + if (v8_object->Set(context, v8_key, converted).IsNothing()) + return MaybeLocal(); + } + + return MaybeLocal(v8_object); + } + default: + THROW_ERR_MODULE_NOT_INSTANTIATED(isolate); + return MaybeLocal(); + } +} + +void Parse(const FunctionCallbackInfo& args) { + if (args.Length() < 1 || !args[0]->IsString()) { + THROW_ERR_INVALID_ARG_TYPE(args.GetIsolate(), + "The \"text\" argument must be a string."); + return; + } + + Local json_str = args[0].As(); + + // TODO(araujogui): Remove memory copy + string key = ObjectToString(args.GetIsolate(), json_str); + simdjson::padded_string padded_string(key); + + simdjson::dom::parser parser; + simdjson::dom::element doc; + + simdjson::error_code error = parser.parse(padded_string).get(doc); + + if (error) { + // TODO(araujogui): create a ERR_INVALID_JSON macro + THROW_ERR_SOURCE_PHASE_NOT_DEFINED( + args.GetIsolate(), "The \"json\" argument must be a string."); + return; + } + + Local result; + + if (!ConvertSimdjsonElement(args.GetIsolate(), doc).ToLocal(&result)) return; + + args.GetReturnValue().Set(result); +} + +void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(Parse); +} + +void Initialize(Local target, + Local unused, + Local context, + void* priv) { + SetMethodNoSideEffect(context, target, "parse", Parse); +} + +} // namespace json_parser +} // namespace node + +NODE_BINDING_CONTEXT_AWARE_INTERNAL(json_parser, node::json_parser::Initialize) +NODE_BINDING_EXTERNAL_REFERENCE(json_parser, + node::json_parser::RegisterExternalReferences) diff --git a/src/node_json_parser.h b/src/node_json_parser.h new file mode 100644 index 00000000000000..c1c80f0ca54107 --- /dev/null +++ b/src/node_json_parser.h @@ -0,0 +1,23 @@ +#ifndef SRC_NODE_JSON_PARSER_H_ +#define SRC_NODE_JSON_PARSER_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "node_external_reference.h" + +namespace node { +namespace json_parser { + +void Initialize(v8::Local target, + v8::Local unused, + v8::Local context, + void* priv); + +void RegisterExternalReferences(ExternalReferenceRegistry* registry); + +} // namespace json_parser +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_NODE_JSON_PARSER_H_ diff --git a/test/parallel/test-json-parser.js b/test/parallel/test-json-parser.js new file mode 100644 index 00000000000000..2382a6d5203790 --- /dev/null +++ b/test/parallel/test-json-parser.js @@ -0,0 +1,159 @@ +'use strict'; + +require('../common'); + +const { test, describe } = require('node:test'); +const assert = require('assert'); + +const { parse } = require('node:json'); + +describe('node:json', () => { + test('throws TypeError when called without arguments', () => { + assert.throws( + () => parse(), + { + name: 'TypeError', + } + ); + }); + + test('throws TypeError when called with number', () => { + assert.throws( + () => parse(123), + { + name: 'TypeError', + } + ); + }); + + test('throws TypeError when called with object', () => { + assert.throws( + () => parse({}), + { + name: 'TypeError', + } + ); + }); + + test('throws TypeError when called with array', () => { + assert.throws( + () => parse([]), + { + name: 'TypeError', + } + ); + }); + + test('throws TypeError when called with null', () => { + assert.throws( + () => parse(null), + { + name: 'TypeError', + } + ); + }); + + test('throws SyntaxError on invalid JSON', () => { + assert.throws( + () => parse('not valid json'), + { + name: 'SyntaxError', + } + ); + }); + + test('parses simple object', () => { + const result = parse('{"key":"value"}'); + assert.deepStrictEqual(result, { key: 'value' }); + }); + + test('parses object with multiple properties', () => { + const result = parse('{"name":"John","age":30,"active":true}'); + assert.deepStrictEqual(result, { name: 'John', age: 30, active: true }); + }); + + test('parses nested objects', () => { + const result = parse('{"user":{"name":"Jane","address":{"city":"NYC"}}}'); + assert.deepStrictEqual(result, { + user: { + name: 'Jane', + address: { city: 'NYC' }, + }, + }); + }); + + test('parses empty object', () => { + const result = parse('{}'); + assert.deepStrictEqual(result, {}); + }); + + test('parses simple array', () => { + const result = parse('[1,2,3]'); + assert.deepStrictEqual(result, [1, 2, 3]); + }); + + test('parses array of objects', () => { + const result = parse('[{"id":1},{"id":2}]'); + assert.deepStrictEqual(result, [{ id: 1 }, { id: 2 }]); + }); + + test('parses empty array', () => { + const result = parse('[]'); + assert.deepStrictEqual(result, []); + }); + + test('parses nested arrays', () => { + const result = parse('[[1,2],[3,4]]'); + assert.deepStrictEqual(result, [[1, 2], [3, 4]]); + }); + + test('parses string value', () => { + const result = parse('"hello"'); + assert.strictEqual(result, 'hello'); + }); + + test('parses integer', () => { + const result = parse('42'); + assert.strictEqual(result, 42); + }); + + test('parses negative number', () => { + const result = parse('-17'); + assert.strictEqual(result, -17); + }); + + test('parses floating point number', () => { + const result = parse('3.14159'); + assert.strictEqual(result, 3.14159); + }); + + test('parses boolean true', () => { + const result = parse('true'); + assert.strictEqual(result, true); + }); + + test('parses boolean false', () => { + const result = parse('false'); + assert.strictEqual(result, false); + }); + + test('parses null', () => { + const result = parse('null'); + assert.strictEqual(result, null); + }); + + test('parses escaped quotes', () => { + const result = parse('{"message":"Hello \\"World\\""}'); + assert.strictEqual(result.message, 'Hello "World"'); + }); + + test('parses escaped backslash', () => { + const result = parse('{"path":"C:\\\\Users"}'); + assert.strictEqual(result.path, 'C:\\Users'); + }); + + test('parses escaped newline', () => { + const result = parse('{"text":"line1\\nline2"}'); + assert.strictEqual(result.text, 'line1\nline2'); + }); +});