Skip to content

Commit c3e4ac2

Browse files
committed
src: add JSON parse from buffer
1 parent 8968aa6 commit c3e4ac2

File tree

3 files changed

+218
-12
lines changed

3 files changed

+218
-12
lines changed

src/node_json_parser.cc

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ namespace json_parser {
1919
using std::string;
2020

2121
using v8::Array;
22+
using v8::ArrayBuffer;
23+
using v8::ArrayBufferView;
2224
using v8::Boolean;
2325
using v8::Context;
2426
using v8::FunctionCallbackInfo;
@@ -148,6 +150,26 @@ MaybeLocal<Value> ConvertSimdjsonElement(Isolate* isolate,
148150
}
149151
}
150152

153+
MaybeLocal<Value> ParseInternal(Isolate* isolate,
154+
const char* data,
155+
size_t length) {
156+
simdjson::padded_string padded_string(data, length);
157+
158+
simdjson::dom::parser parser;
159+
simdjson::dom::element doc;
160+
161+
simdjson::error_code error = parser.parse(padded_string).get(doc);
162+
163+
if (error) {
164+
// TODO(araujogui): create a ERR_INVALID_JSON macro
165+
THROW_ERR_SOURCE_PHASE_NOT_DEFINED(
166+
isolate, "The \"json\" argument must be valid JSON.");
167+
return MaybeLocal<Value>();
168+
}
169+
170+
return ConvertSimdjsonElement(isolate, doc);
171+
}
172+
151173
void Parse(const FunctionCallbackInfo<Value>& args) {
152174
if (args.Length() < 1 || !args[0]->IsString()) {
153175
THROW_ERR_INVALID_ARG_TYPE(args.GetIsolate(),
@@ -157,38 +179,59 @@ void Parse(const FunctionCallbackInfo<Value>& args) {
157179

158180
Local<String> json_str = args[0].As<String>();
159181

160-
// TODO(araujogui): Remove memory copy
161-
string key = ObjectToString(args.GetIsolate(), json_str);
162-
simdjson::padded_string padded_string(key);
182+
string str = ObjectToString(args.GetIsolate(), json_str);
163183

164-
simdjson::dom::parser parser;
165-
simdjson::dom::element doc;
184+
Local<Value> result;
185+
if (!ParseInternal(args.GetIsolate(), str.data(), str.size()).ToLocal(&result))
186+
return;
166187

167-
simdjson::error_code error = parser.parse(padded_string).get(doc);
188+
args.GetReturnValue().Set(result);
189+
}
168190

169-
if (error) {
170-
// TODO(araujogui): create a ERR_INVALID_JSON macro
171-
THROW_ERR_SOURCE_PHASE_NOT_DEFINED(
172-
args.GetIsolate(), "The \"json\" argument must be a string.");
191+
void ParseFromBuffer(const FunctionCallbackInfo<Value>& args) {
192+
Isolate* isolate = args.GetIsolate();
193+
194+
if (args.Length() < 1) {
195+
THROW_ERR_INVALID_ARG_TYPE(
196+
isolate, "The \"buffer\" argument must be an ArrayBuffer or a view.");
173197
return;
174198
}
175199

176-
Local<Value> result;
200+
const char* data;
201+
size_t length;
202+
203+
if (args[0]->IsArrayBufferView()) {
204+
Local<ArrayBufferView> view = args[0].As<ArrayBufferView>();
205+
Local<ArrayBuffer> buffer = view->Buffer();
206+
data = static_cast<const char*>(buffer->Data()) + view->ByteOffset();
207+
length = view->ByteLength();
208+
} else if (args[0]->IsArrayBuffer()) {
209+
Local<ArrayBuffer> buffer = args[0].As<ArrayBuffer>();
210+
data = static_cast<const char*>(buffer->Data());
211+
length = buffer->ByteLength();
212+
} else {
213+
THROW_ERR_INVALID_ARG_TYPE(
214+
isolate, "The \"buffer\" argument must be an ArrayBuffer or a view.");
215+
return;
216+
}
177217

178-
if (!ConvertSimdjsonElement(args.GetIsolate(), doc).ToLocal(&result)) return;
218+
Local<Value> result;
219+
if (!ParseInternal(isolate, data, length).ToLocal(&result)) return;
179220

180221
args.GetReturnValue().Set(result);
181222
}
182223

183224
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
184225
registry->Register(Parse);
226+
registry->Register(ParseFromBuffer);
185227
}
186228

187229
void Initialize(Local<Object> target,
188230
Local<Value> unused,
189231
Local<Context> context,
190232
void* priv) {
191233
SetMethodNoSideEffect(context, target, "parse", Parse);
234+
SetMethodNoSideEffect(context, target, "parseFromBuffer", ParseFromBuffer);
192235
}
193236

194237
} // namespace json_parser
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
'use strict';
2+
3+
require('../common');
4+
5+
const { test, describe } = require('node:test');
6+
const assert = require('assert');
7+
const { TextEncoder } = require('node:util');
8+
9+
const { parseFromBuffer } = require('node:json');
10+
11+
// TODO(araujogui): should I use Buffer.from instead?
12+
const encoder = new TextEncoder();
13+
14+
const toArrayBuffer = (str) => encoder.encode(str).buffer;
15+
16+
describe('node:json', () => {
17+
test('throws TypeError when called without arguments', () => {
18+
assert.throws(() => parseFromBuffer(), {
19+
name: 'TypeError',
20+
});
21+
});
22+
23+
test('throws TypeError when called with number', () => {
24+
assert.throws(() => parseFromBuffer(123), {
25+
name: 'TypeError',
26+
});
27+
});
28+
29+
test('throws TypeError when called with object', () => {
30+
assert.throws(() => parseFromBuffer({}), {
31+
name: 'TypeError',
32+
});
33+
});
34+
35+
test('throws TypeError when called with array', () => {
36+
assert.throws(() => parseFromBuffer([]), {
37+
name: 'TypeError',
38+
});
39+
});
40+
41+
test('throws TypeError when called with null', () => {
42+
assert.throws(() => parseFromBuffer(null), {
43+
name: 'TypeError',
44+
});
45+
});
46+
47+
test('throws SyntaxError on invalid string JSON', () => {
48+
assert.throws(() => parseFromBuffer(toArrayBuffer('not valid json')), {
49+
name: 'SyntaxError',
50+
});
51+
});
52+
53+
test('throws SyntaxError on invalid object JSON', () => {
54+
assert.throws(() => parseFromBuffer(toArrayBuffer('{"key": "property}')), {
55+
name: 'SyntaxError',
56+
});
57+
});
58+
59+
test('throws SyntaxError on invalid array JSON', () => {
60+
assert.throws(() => parseFromBuffer(toArrayBuffer('[1,2,3,]')), {
61+
name: 'SyntaxError',
62+
});
63+
});
64+
65+
test('parses simple object', () => {
66+
const result = parseFromBuffer(toArrayBuffer('{"key":"value"}'));
67+
assert.deepStrictEqual(result, { key: 'value' });
68+
});
69+
70+
test('parses object with multiple properties', () => {
71+
const result = parseFromBuffer(
72+
toArrayBuffer('{"name":"John","age":30,"active":true}')
73+
);
74+
assert.deepStrictEqual(result, { name: 'John', age: 30, active: true });
75+
});
76+
77+
test('parses nested objects', () => {
78+
const result = parseFromBuffer(
79+
toArrayBuffer('{"user":{"name":"Jane","address":{"city":"NYC"}}}')
80+
);
81+
assert.deepStrictEqual(result, {
82+
user: {
83+
name: 'Jane',
84+
address: { city: 'NYC' },
85+
},
86+
});
87+
});
88+
89+
test('parses empty object', () => {
90+
const result = parseFromBuffer(toArrayBuffer('{}'));
91+
assert.deepStrictEqual(result, {});
92+
});
93+
94+
test('parses simple array', () => {
95+
const result = parseFromBuffer(toArrayBuffer('[1,2,3]'));
96+
assert.deepStrictEqual(result, [1, 2, 3]);
97+
});
98+
99+
test('parses array of objects', () => {
100+
const result = parseFromBuffer(toArrayBuffer('[{"id":1},{"id":2}]'));
101+
assert.deepStrictEqual(result, [{ id: 1 }, { id: 2 }]);
102+
});
103+
104+
test('parses empty array', () => {
105+
const result = parseFromBuffer(toArrayBuffer('[]'));
106+
assert.deepStrictEqual(result, []);
107+
});
108+
109+
test('parses nested arrays', () => {
110+
const result = parseFromBuffer(toArrayBuffer('[[1,2],[3,4]]'));
111+
assert.deepStrictEqual(result, [[1, 2], [3, 4]]);
112+
});
113+
114+
test('parses string value', () => {
115+
const result = parseFromBuffer(toArrayBuffer('"hello"'));
116+
assert.strictEqual(result, 'hello');
117+
});
118+
119+
test('parses integer', () => {
120+
const result = parseFromBuffer(toArrayBuffer('42'));
121+
assert.strictEqual(result, 42);
122+
});
123+
124+
test('parses negative number', () => {
125+
const result = parseFromBuffer(toArrayBuffer('-17'));
126+
assert.strictEqual(result, -17);
127+
});
128+
129+
test('parses floating point number', () => {
130+
const result = parseFromBuffer(toArrayBuffer('3.14159'));
131+
assert.strictEqual(result, 3.14159);
132+
});
133+
134+
test('parses boolean true', () => {
135+
const result = parseFromBuffer(toArrayBuffer('true'));
136+
assert.strictEqual(result, true);
137+
});
138+
139+
test('parses boolean false', () => {
140+
const result = parseFromBuffer(toArrayBuffer('false'));
141+
assert.strictEqual(result, false);
142+
});
143+
144+
test('parses null', () => {
145+
const result = parseFromBuffer(toArrayBuffer('null'));
146+
assert.strictEqual(result, null);
147+
});
148+
149+
test('parses escaped quotes', () => {
150+
const result = parseFromBuffer(toArrayBuffer('{"message":"Hello \\"World\\""}'));
151+
assert.strictEqual(result.message, 'Hello "World"');
152+
});
153+
154+
test('parses escaped backslash', () => {
155+
const result = parseFromBuffer(toArrayBuffer('{"path":"C:\\\\Users"}'));
156+
assert.strictEqual(result.path, 'C:\\Users');
157+
});
158+
159+
test('parses escaped newline', () => {
160+
const result = parseFromBuffer(toArrayBuffer('{"text":"line1\\nline2"}'));
161+
assert.strictEqual(result.text, 'line1\nline2');
162+
});
163+
});

0 commit comments

Comments
 (0)