@elaraai/east-node-std 1.0.70 → 1.0.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/json.d.ts +191 -0
- package/dist/src/json.d.ts.map +1 -0
- package/dist/src/json.js +265 -0
- package/dist/src/json.js.map +1 -0
- package/dist/src/json_reader.d.ts +153 -0
- package/dist/src/json_reader.d.ts.map +1 -0
- package/dist/src/json_reader.js +1271 -0
- package/dist/src/json_reader.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Elara AI Pty Ltd
|
|
3
|
+
* Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
|
|
4
|
+
*/
|
|
5
|
+
import { compareFor, EAST_JSON_PATTERNS, SortedMap, SortedSet, matrix, ref, variant, } from "@elaraai/east";
|
|
6
|
+
import { closeSync, fstatSync, openSync, readSync } from "node:fs";
|
|
7
|
+
/** How many bytes each refill pulls from the source. */
|
|
8
|
+
const CHUNK_BYTES = 64 * 1024;
|
|
9
|
+
/**
|
|
10
|
+
* How deeply a document may nest before it is refused.
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* JSON is an untrusted-input boundary, and skipping past a value recurses per
|
|
14
|
+
* level, so a document of nothing but brackets would otherwise exhaust the
|
|
15
|
+
* stack. The same limit east-c's reader applies (`JSON_MAX_DEPTH`), counted
|
|
16
|
+
* the same way — every value, a scalar included, is one level — so every
|
|
17
|
+
* runtime refuses the same documents.
|
|
18
|
+
*/
|
|
19
|
+
const MAX_DEPTH = 2048;
|
|
20
|
+
/**
|
|
21
|
+
* How many code points of an offending value a message quotes before an
|
|
22
|
+
* ellipsis.
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* The value under the cursor can be arbitrarily long; the message is for a
|
|
26
|
+
* person. The same bound, counted in code points, applies on east-c, which is
|
|
27
|
+
* what keeps the text identical across runtimes.
|
|
28
|
+
*/
|
|
29
|
+
const QUOTE_MAX = 200;
|
|
30
|
+
const INTEGER_RE = new RegExp(EAST_JSON_PATTERNS.integer);
|
|
31
|
+
const DATETIME_RE = new RegExp(EAST_JSON_PATTERNS.datetime);
|
|
32
|
+
const BLOB_RE = new RegExp(EAST_JSON_PATTERNS.blob);
|
|
33
|
+
const TAB = 0x09, LF = 0x0a, CR = 0x0d, SPACE = 0x20;
|
|
34
|
+
const QUOTE = 0x22, COMMA = 0x2c, MINUS = 0x2d, DOT = 0x2e, COLON = 0x3a;
|
|
35
|
+
const LBRACKET = 0x5b, BACKSLASH = 0x5c, RBRACKET = 0x5d, LBRACE = 0x7b, RBRACE = 0x7d;
|
|
36
|
+
const ZERO = 0x30, NINE = 0x39, PLUS = 0x2b;
|
|
37
|
+
const LOWER_E = 0x65, UPPER_E = 0x45;
|
|
38
|
+
/** End of input, where a byte would be. */
|
|
39
|
+
const EOF = -1;
|
|
40
|
+
/** A JSON document that does not satisfy the contract, located by pointer. */
|
|
41
|
+
export class JsonReadError extends Error {
|
|
42
|
+
/** RFC 6901 pointer to the offending node. */
|
|
43
|
+
pointer;
|
|
44
|
+
constructor(message, pointer) {
|
|
45
|
+
super(pointer === "" ? message : `${pointer}: ${message}`);
|
|
46
|
+
this.name = "JsonReadError";
|
|
47
|
+
this.pointer = pointer;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function stringChunks(text) {
|
|
51
|
+
// A payload already in hand is encoded once, so the reader sees the bytes
|
|
52
|
+
// a file would hold. A lone surrogate has no UTF-8 form and encodes as
|
|
53
|
+
// U+FFFD, as it would on any write.
|
|
54
|
+
let bytes = new TextEncoder().encode(text);
|
|
55
|
+
return {
|
|
56
|
+
next: () => { const chunk = bytes; bytes = null; return chunk; },
|
|
57
|
+
close: () => { },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function fileChunks(path) {
|
|
61
|
+
const fd = openSync(path, "r");
|
|
62
|
+
let size;
|
|
63
|
+
try {
|
|
64
|
+
size = fstatSync(fd).size;
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
closeSync(fd);
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
if (size === 0) {
|
|
71
|
+
closeSync(fd);
|
|
72
|
+
throw new JsonReadError("the document is empty", "");
|
|
73
|
+
}
|
|
74
|
+
let done = false;
|
|
75
|
+
return {
|
|
76
|
+
next: () => {
|
|
77
|
+
if (done)
|
|
78
|
+
return null;
|
|
79
|
+
// A fresh buffer per chunk: the reader keeps a reference to the
|
|
80
|
+
// unconsumed tail, so a reused one would be overwritten under it.
|
|
81
|
+
const buffer = Buffer.allocUnsafe(CHUNK_BYTES);
|
|
82
|
+
const read = readSync(fd, buffer, 0, CHUNK_BYTES, null);
|
|
83
|
+
if (read === 0) {
|
|
84
|
+
done = true;
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return buffer.subarray(0, read);
|
|
88
|
+
},
|
|
89
|
+
close: () => closeSync(fd),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Streaming UTF-8 validity, per RFC 3629.
|
|
94
|
+
*
|
|
95
|
+
* @remarks
|
|
96
|
+
* Overlong forms, encoded surrogates and code points past U+10FFFF are
|
|
97
|
+
* refused, byte by byte, so a document can be checked without being decoded —
|
|
98
|
+
* which is what the skip path needs — and east-c runs the identical table.
|
|
99
|
+
*/
|
|
100
|
+
class Utf8 {
|
|
101
|
+
need = 0;
|
|
102
|
+
lo = 0x80;
|
|
103
|
+
hi = 0xbf;
|
|
104
|
+
/** Feeds one byte; false when it cannot belong to well-formed UTF-8. */
|
|
105
|
+
feed(b) {
|
|
106
|
+
if (this.need === 0) {
|
|
107
|
+
if (b < 0x80)
|
|
108
|
+
return true;
|
|
109
|
+
if (b >= 0xc2 && b <= 0xdf) {
|
|
110
|
+
this.need = 1;
|
|
111
|
+
this.lo = 0x80;
|
|
112
|
+
this.hi = 0xbf;
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if (b === 0xe0) {
|
|
116
|
+
this.need = 2;
|
|
117
|
+
this.lo = 0xa0;
|
|
118
|
+
this.hi = 0xbf;
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if ((b >= 0xe1 && b <= 0xec) || b === 0xee || b === 0xef) {
|
|
122
|
+
this.need = 2;
|
|
123
|
+
this.lo = 0x80;
|
|
124
|
+
this.hi = 0xbf;
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
if (b === 0xed) {
|
|
128
|
+
this.need = 2;
|
|
129
|
+
this.lo = 0x80;
|
|
130
|
+
this.hi = 0x9f;
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (b === 0xf0) {
|
|
134
|
+
this.need = 3;
|
|
135
|
+
this.lo = 0x90;
|
|
136
|
+
this.hi = 0xbf;
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (b >= 0xf1 && b <= 0xf3) {
|
|
140
|
+
this.need = 3;
|
|
141
|
+
this.lo = 0x80;
|
|
142
|
+
this.hi = 0xbf;
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
if (b === 0xf4) {
|
|
146
|
+
this.need = 3;
|
|
147
|
+
this.lo = 0x80;
|
|
148
|
+
this.hi = 0x8f;
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
if (b < this.lo || b > this.hi)
|
|
154
|
+
return false;
|
|
155
|
+
this.need--;
|
|
156
|
+
this.lo = 0x80;
|
|
157
|
+
this.hi = 0xbf;
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
/** Whether a multi-byte sequence is still open — true when the input ended mid-character. */
|
|
161
|
+
get open() {
|
|
162
|
+
return this.need > 0;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** The byte length a UTF-8 lead byte announces, or 1 when it announces nothing valid. */
|
|
166
|
+
function sequenceLength(lead) {
|
|
167
|
+
if (lead >= 0xc2 && lead <= 0xdf)
|
|
168
|
+
return 2;
|
|
169
|
+
if (lead >= 0xe0 && lead <= 0xef)
|
|
170
|
+
return 3;
|
|
171
|
+
if (lead >= 0xf0 && lead <= 0xf4)
|
|
172
|
+
return 4;
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* A value quoted for a message, as `JSON.stringify` would, clipped to
|
|
177
|
+
* {@link QUOTE_MAX} code points with an ellipsis.
|
|
178
|
+
*/
|
|
179
|
+
function quote(text) {
|
|
180
|
+
let count = 0;
|
|
181
|
+
let end = text.length;
|
|
182
|
+
for (let i = 0; i < text.length; i++) {
|
|
183
|
+
if (++count > QUOTE_MAX) {
|
|
184
|
+
end = i;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
const c = text.charCodeAt(i);
|
|
188
|
+
// A surrogate pair is one code point.
|
|
189
|
+
if (c >= 0xd800 && c <= 0xdbff && i + 1 < text.length) {
|
|
190
|
+
const d = text.charCodeAt(i + 1);
|
|
191
|
+
if (d >= 0xdc00 && d <= 0xdfff)
|
|
192
|
+
i++;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return JSON.stringify(end === text.length ? text : text.slice(0, end) + "…");
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* A pull reader over a JSON document.
|
|
199
|
+
*
|
|
200
|
+
* @remarks
|
|
201
|
+
* Holds one chunk of bytes and the path it has descended, never the document
|
|
202
|
+
* and never a batch. Values are constructed one at a time, directly against
|
|
203
|
+
* the East type, so nothing intermediate is materialised either — a string is
|
|
204
|
+
* decoded from its bytes in runs, never grown a character at a time, and a
|
|
205
|
+
* skipped value is validated without being built at all.
|
|
206
|
+
*/
|
|
207
|
+
export class JsonReader {
|
|
208
|
+
chunks;
|
|
209
|
+
buf = new Uint8Array(0);
|
|
210
|
+
pos = 0;
|
|
211
|
+
eof = false;
|
|
212
|
+
/** The path descended so far, for error pointers. */
|
|
213
|
+
path = [];
|
|
214
|
+
/** Index within the array currently being iterated. */
|
|
215
|
+
index = 0;
|
|
216
|
+
/** Whether iteration has begun, so a separator is expected next. */
|
|
217
|
+
started = false;
|
|
218
|
+
/** The container kind at the cursor: `[` for an array, `{` for an object. */
|
|
219
|
+
container = null;
|
|
220
|
+
/** Nesting depth of the value currently being read or skipped. */
|
|
221
|
+
depth = 0;
|
|
222
|
+
closed = false;
|
|
223
|
+
decoder = new TextDecoder("utf-8");
|
|
224
|
+
constructor(chunks) {
|
|
225
|
+
this.chunks = chunks;
|
|
226
|
+
}
|
|
227
|
+
/** Opens a document and descends to the container the pointer names. */
|
|
228
|
+
static open(chunks, pointer) {
|
|
229
|
+
const reader = new JsonReader(chunks);
|
|
230
|
+
try {
|
|
231
|
+
reader.descend(pointer, true);
|
|
232
|
+
return reader;
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
reader.close();
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Opens a document and positions on the value the pointer names, without
|
|
241
|
+
* entering it — for reading one whole value rather than iterating.
|
|
242
|
+
*/
|
|
243
|
+
static openValue(chunks, pointer) {
|
|
244
|
+
const reader = new JsonReader(chunks);
|
|
245
|
+
try {
|
|
246
|
+
reader.descend(pointer, false);
|
|
247
|
+
return reader;
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
reader.close();
|
|
251
|
+
throw err;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
static openFile(path, pointer) {
|
|
255
|
+
return JsonReader.open(fileChunks(path), pointer);
|
|
256
|
+
}
|
|
257
|
+
static openText(text, pointer) {
|
|
258
|
+
return JsonReader.open(stringChunks(text), pointer);
|
|
259
|
+
}
|
|
260
|
+
static openValueFile(path, pointer) {
|
|
261
|
+
return JsonReader.openValue(fileChunks(path), pointer);
|
|
262
|
+
}
|
|
263
|
+
static openValueText(text, pointer) {
|
|
264
|
+
return JsonReader.openValue(stringChunks(text), pointer);
|
|
265
|
+
}
|
|
266
|
+
close() {
|
|
267
|
+
if (this.closed)
|
|
268
|
+
return;
|
|
269
|
+
this.closed = true;
|
|
270
|
+
this.chunks.close();
|
|
271
|
+
}
|
|
272
|
+
// ── byte access ─────────────────────────────────────────────────────
|
|
273
|
+
/** Ensures at least `n` bytes are buffered; false once the input cannot supply them. */
|
|
274
|
+
ensure(n) {
|
|
275
|
+
while (this.buf.length - this.pos < n) {
|
|
276
|
+
if (this.eof)
|
|
277
|
+
return false;
|
|
278
|
+
const chunk = this.chunks.next();
|
|
279
|
+
if (chunk === null) {
|
|
280
|
+
this.eof = true;
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
// Keep only the unconsumed tail, so the buffer stays bounded.
|
|
284
|
+
const rest = this.buf.subarray(this.pos);
|
|
285
|
+
const next = new Uint8Array(rest.length + chunk.length);
|
|
286
|
+
next.set(rest);
|
|
287
|
+
next.set(chunk, rest.length);
|
|
288
|
+
this.buf = next;
|
|
289
|
+
this.pos = 0;
|
|
290
|
+
}
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
/** The byte at the cursor, or {@link EOF}. */
|
|
294
|
+
peek() {
|
|
295
|
+
return this.ensure(1) ? this.buf[this.pos] : EOF;
|
|
296
|
+
}
|
|
297
|
+
take() {
|
|
298
|
+
if (!this.ensure(1))
|
|
299
|
+
this.fail("unexpected end of document");
|
|
300
|
+
return this.buf[this.pos++];
|
|
301
|
+
}
|
|
302
|
+
skipSpace() {
|
|
303
|
+
for (;;) {
|
|
304
|
+
const b = this.peek();
|
|
305
|
+
if (b === SPACE || b === TAB || b === LF || b === CR) {
|
|
306
|
+
this.pos++;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/** Whether the bytes at the cursor spell `word`, without consuming them. */
|
|
313
|
+
startsWith(word) {
|
|
314
|
+
if (!this.ensure(word.length))
|
|
315
|
+
return false;
|
|
316
|
+
for (let i = 0; i < word.length; i++) {
|
|
317
|
+
if (this.buf[this.pos + i] !== word.charCodeAt(i))
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* The code point at the cursor, for a message — U+FFFD when the bytes
|
|
324
|
+
* there are not a well-formed sequence.
|
|
325
|
+
*/
|
|
326
|
+
charAtCursor() {
|
|
327
|
+
const lead = this.peek();
|
|
328
|
+
if (lead === EOF)
|
|
329
|
+
return "";
|
|
330
|
+
const length = sequenceLength(lead);
|
|
331
|
+
this.ensure(length);
|
|
332
|
+
const available = Math.min(length, this.buf.length - this.pos);
|
|
333
|
+
const check = new Utf8();
|
|
334
|
+
for (let i = 0; i < available; i++) {
|
|
335
|
+
if (!check.feed(this.buf[this.pos + i]))
|
|
336
|
+
return "�";
|
|
337
|
+
}
|
|
338
|
+
if (check.open)
|
|
339
|
+
return "�";
|
|
340
|
+
return this.decoder.decode(this.buf.subarray(this.pos, this.pos + available));
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* What sits at the cursor, for a message: `a string`, `a number`, `an
|
|
344
|
+
* array`, `an object`, `a boolean`, `null`, `end of document`, or the
|
|
345
|
+
* quoted character when it starts no JSON value.
|
|
346
|
+
*/
|
|
347
|
+
describe() {
|
|
348
|
+
this.skipSpace();
|
|
349
|
+
const b = this.peek();
|
|
350
|
+
if (b === EOF)
|
|
351
|
+
return "end of document";
|
|
352
|
+
if (b === QUOTE)
|
|
353
|
+
return "a string";
|
|
354
|
+
if (b === LBRACKET)
|
|
355
|
+
return "an array";
|
|
356
|
+
if (b === LBRACE)
|
|
357
|
+
return "an object";
|
|
358
|
+
if (b === MINUS || isDigit(b))
|
|
359
|
+
return "a number";
|
|
360
|
+
if (this.startsWith("true") || this.startsWith("false"))
|
|
361
|
+
return "a boolean";
|
|
362
|
+
if (this.startsWith("null"))
|
|
363
|
+
return "null";
|
|
364
|
+
return JSON.stringify(this.charAtCursor());
|
|
365
|
+
}
|
|
366
|
+
fail(message) {
|
|
367
|
+
throw new JsonReadError(message, pointerOf(this.path));
|
|
368
|
+
}
|
|
369
|
+
// ── tokens ──────────────────────────────────────────────────────────
|
|
370
|
+
/**
|
|
371
|
+
* Reads the string whose opening quote is at the cursor.
|
|
372
|
+
*
|
|
373
|
+
* @remarks
|
|
374
|
+
* The bytes between escapes are validated as UTF-8 and decoded in runs,
|
|
375
|
+
* one chunk at a time, so a long string costs its own size and no more —
|
|
376
|
+
* appending a character at a time would build a rope of thirty-odd bytes
|
|
377
|
+
* per character.
|
|
378
|
+
*/
|
|
379
|
+
readString() {
|
|
380
|
+
this.pos++; // the opening quote, checked by the caller
|
|
381
|
+
const parts = [];
|
|
382
|
+
const check = new Utf8();
|
|
383
|
+
let pendingRun = false;
|
|
384
|
+
for (;;) {
|
|
385
|
+
if (!this.ensure(1))
|
|
386
|
+
this.fail("unexpected end of document");
|
|
387
|
+
const buf = this.buf;
|
|
388
|
+
const n = buf.length;
|
|
389
|
+
let i = this.pos;
|
|
390
|
+
while (i < n) {
|
|
391
|
+
const b = buf[i];
|
|
392
|
+
if (b === QUOTE || b === BACKSLASH || b < 0x20)
|
|
393
|
+
break;
|
|
394
|
+
if (b >= 0x80 && !check.feed(b))
|
|
395
|
+
this.fail("invalid UTF-8 in string");
|
|
396
|
+
i++;
|
|
397
|
+
}
|
|
398
|
+
if (i > this.pos) {
|
|
399
|
+
parts.push(this.decoder.decode(buf.subarray(this.pos, i), { stream: true }));
|
|
400
|
+
pendingRun = true;
|
|
401
|
+
this.pos = i;
|
|
402
|
+
}
|
|
403
|
+
if (i === n)
|
|
404
|
+
continue; // the run continues in the next chunk
|
|
405
|
+
const b = buf[i];
|
|
406
|
+
if (b < 0x20)
|
|
407
|
+
this.fail(`unescaped control character U+${b.toString(16).padStart(4, "0")} in string`);
|
|
408
|
+
// A quote or an escape ends the run; a character cannot straddle it.
|
|
409
|
+
if (check.open)
|
|
410
|
+
this.fail("invalid UTF-8 in string");
|
|
411
|
+
if (pendingRun) {
|
|
412
|
+
parts.push(this.decoder.decode());
|
|
413
|
+
pendingRun = false;
|
|
414
|
+
}
|
|
415
|
+
if (b === QUOTE) {
|
|
416
|
+
this.pos = i + 1;
|
|
417
|
+
return parts.length === 1 ? parts[0] : parts.join("");
|
|
418
|
+
}
|
|
419
|
+
this.pos = i + 1;
|
|
420
|
+
parts.push(this.readEscape());
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/** One escape, the backslash already consumed. */
|
|
424
|
+
readEscape() {
|
|
425
|
+
const esc = this.take();
|
|
426
|
+
switch (esc) {
|
|
427
|
+
case QUOTE: return "\"";
|
|
428
|
+
case BACKSLASH: return "\\";
|
|
429
|
+
case 0x2f: return "/";
|
|
430
|
+
case 0x62: return "\b";
|
|
431
|
+
case 0x66: return "\f";
|
|
432
|
+
case 0x6e: return "\n";
|
|
433
|
+
case 0x72: return "\r";
|
|
434
|
+
case 0x74: return "\t";
|
|
435
|
+
case 0x75: {
|
|
436
|
+
let hex = "";
|
|
437
|
+
for (let i = 0; i < 4; i++)
|
|
438
|
+
hex += String.fromCharCode(this.take());
|
|
439
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex))
|
|
440
|
+
this.fail(`invalid \\u escape "\\u${hex}"`);
|
|
441
|
+
// A surrogate pair written as two escapes joins into one code
|
|
442
|
+
// point by concatenation; a lone surrogate stays one code unit,
|
|
443
|
+
// as JSON.parse leaves it.
|
|
444
|
+
return String.fromCharCode(parseInt(hex, 16));
|
|
445
|
+
}
|
|
446
|
+
default:
|
|
447
|
+
this.fail(`invalid escape "\\${String.fromCharCode(esc)}"`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/** Validates the string at the cursor without building it. */
|
|
451
|
+
skipString() {
|
|
452
|
+
this.pos++;
|
|
453
|
+
const check = new Utf8();
|
|
454
|
+
for (;;) {
|
|
455
|
+
if (!this.ensure(1))
|
|
456
|
+
this.fail("unexpected end of document");
|
|
457
|
+
const buf = this.buf;
|
|
458
|
+
const n = buf.length;
|
|
459
|
+
let i = this.pos;
|
|
460
|
+
while (i < n) {
|
|
461
|
+
const b = buf[i];
|
|
462
|
+
if (b === QUOTE || b === BACKSLASH || b < 0x20)
|
|
463
|
+
break;
|
|
464
|
+
if (b >= 0x80 && !check.feed(b))
|
|
465
|
+
this.fail("invalid UTF-8 in string");
|
|
466
|
+
i++;
|
|
467
|
+
}
|
|
468
|
+
this.pos = i;
|
|
469
|
+
if (i === n)
|
|
470
|
+
continue;
|
|
471
|
+
const b = buf[i];
|
|
472
|
+
if (b < 0x20)
|
|
473
|
+
this.fail(`unescaped control character U+${b.toString(16).padStart(4, "0")} in string`);
|
|
474
|
+
if (check.open)
|
|
475
|
+
this.fail("invalid UTF-8 in string");
|
|
476
|
+
this.pos = i + 1;
|
|
477
|
+
if (b === QUOTE)
|
|
478
|
+
return;
|
|
479
|
+
this.readEscape();
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/** The JSON number at the cursor, exactly as written, checked against the grammar. */
|
|
483
|
+
readNumber() {
|
|
484
|
+
let out = "";
|
|
485
|
+
if (this.peek() === MINUS) {
|
|
486
|
+
this.pos++;
|
|
487
|
+
out += "-";
|
|
488
|
+
if (!isDigit(this.peek()))
|
|
489
|
+
this.fail("expected a digit after \"-\"");
|
|
490
|
+
}
|
|
491
|
+
if (this.peek() === ZERO) {
|
|
492
|
+
this.pos++;
|
|
493
|
+
out += "0"; // a leading zero stands alone
|
|
494
|
+
}
|
|
495
|
+
else if (isDigit(this.peek())) {
|
|
496
|
+
while (isDigit(this.peek()))
|
|
497
|
+
out += String.fromCharCode(this.take());
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
this.fail(`expected a number, got ${this.describe()}`);
|
|
501
|
+
}
|
|
502
|
+
if (this.peek() === DOT) {
|
|
503
|
+
this.pos++;
|
|
504
|
+
out += ".";
|
|
505
|
+
if (!isDigit(this.peek()))
|
|
506
|
+
this.fail("expected a digit after the decimal point");
|
|
507
|
+
while (isDigit(this.peek()))
|
|
508
|
+
out += String.fromCharCode(this.take());
|
|
509
|
+
}
|
|
510
|
+
const e = this.peek();
|
|
511
|
+
if (e === LOWER_E || e === UPPER_E) {
|
|
512
|
+
this.pos++;
|
|
513
|
+
out += "e";
|
|
514
|
+
const sign = this.peek();
|
|
515
|
+
if (sign === PLUS || sign === MINUS)
|
|
516
|
+
out += String.fromCharCode(this.take());
|
|
517
|
+
if (!isDigit(this.peek()))
|
|
518
|
+
this.fail("expected a digit in the exponent");
|
|
519
|
+
while (isDigit(this.peek()))
|
|
520
|
+
out += String.fromCharCode(this.take());
|
|
521
|
+
}
|
|
522
|
+
return out;
|
|
523
|
+
}
|
|
524
|
+
readLiteral(word) {
|
|
525
|
+
for (let i = 0; i < word.length; i++) {
|
|
526
|
+
if (this.take() !== word.charCodeAt(i))
|
|
527
|
+
this.fail(`expected ${word}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/** Consumes one value without constructing anything, checking it against the grammar. */
|
|
531
|
+
skipValue() {
|
|
532
|
+
if (++this.depth > MAX_DEPTH) {
|
|
533
|
+
this.depth--;
|
|
534
|
+
this.fail(`document nests deeper than ${MAX_DEPTH}`);
|
|
535
|
+
}
|
|
536
|
+
try {
|
|
537
|
+
this.skipValueInner();
|
|
538
|
+
}
|
|
539
|
+
finally {
|
|
540
|
+
this.depth--;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
skipValueInner() {
|
|
544
|
+
this.skipSpace();
|
|
545
|
+
const b = this.peek();
|
|
546
|
+
if (b === EOF)
|
|
547
|
+
this.fail("unexpected end of document");
|
|
548
|
+
if (b === QUOTE) {
|
|
549
|
+
this.skipString();
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (b === MINUS || isDigit(b)) {
|
|
553
|
+
this.readNumber();
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (b === 0x74) {
|
|
557
|
+
this.readLiteral("true");
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (b === 0x66) {
|
|
561
|
+
this.readLiteral("false");
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (b === 0x6e) {
|
|
565
|
+
this.readLiteral("null");
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (b === LBRACKET) {
|
|
569
|
+
this.pos++;
|
|
570
|
+
this.skipSpace();
|
|
571
|
+
if (this.peek() === RBRACKET) {
|
|
572
|
+
this.pos++;
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
for (;;) {
|
|
576
|
+
this.skipValue();
|
|
577
|
+
this.skipSpace();
|
|
578
|
+
const sep = this.take();
|
|
579
|
+
if (sep === RBRACKET)
|
|
580
|
+
return;
|
|
581
|
+
if (sep !== COMMA)
|
|
582
|
+
this.fail("expected \",\" or \"]\" in array");
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (b === LBRACE) {
|
|
586
|
+
this.pos++;
|
|
587
|
+
this.skipSpace();
|
|
588
|
+
if (this.peek() === RBRACE) {
|
|
589
|
+
this.pos++;
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
for (;;) {
|
|
593
|
+
this.readName("field");
|
|
594
|
+
this.skipSpace();
|
|
595
|
+
if (this.peek() !== COLON)
|
|
596
|
+
this.fail(`expected ":" after a field name, got ${this.describe()}`);
|
|
597
|
+
this.pos++;
|
|
598
|
+
this.skipValue();
|
|
599
|
+
this.skipSpace();
|
|
600
|
+
const sep = this.take();
|
|
601
|
+
if (sep === RBRACE)
|
|
602
|
+
return;
|
|
603
|
+
if (sep !== COMMA)
|
|
604
|
+
this.fail("expected \",\" or \"}\" in object");
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
this.fail(`unexpected character ${JSON.stringify(this.charAtCursor())}`);
|
|
608
|
+
}
|
|
609
|
+
/** A quoted name — a struct field, an object member, a variant case — at the cursor. */
|
|
610
|
+
readName(what) {
|
|
611
|
+
this.skipSpace();
|
|
612
|
+
if (this.peek() !== QUOTE)
|
|
613
|
+
this.fail(`expected a ${what} name, got ${this.describe()}`);
|
|
614
|
+
return this.readString();
|
|
615
|
+
}
|
|
616
|
+
/** The `:` after a name. */
|
|
617
|
+
expectColon(what) {
|
|
618
|
+
this.skipSpace();
|
|
619
|
+
if (this.peek() !== COLON)
|
|
620
|
+
this.fail(`expected ":" after a ${what} name, got ${this.describe()}`);
|
|
621
|
+
this.pos++;
|
|
622
|
+
}
|
|
623
|
+
// ── navigation ──────────────────────────────────────────────────────
|
|
624
|
+
/**
|
|
625
|
+
* Descends to the node the RFC 6901 pointer names.
|
|
626
|
+
*
|
|
627
|
+
* @param pointer - The pointer to follow
|
|
628
|
+
* @param enter - Whether to step inside the node and iterate it, rather
|
|
629
|
+
* than stop in front of it to read it whole
|
|
630
|
+
*/
|
|
631
|
+
descend(pointer, enter) {
|
|
632
|
+
for (const segment of parsePointer(pointer)) {
|
|
633
|
+
this.skipSpace();
|
|
634
|
+
const b = this.peek();
|
|
635
|
+
if (b === LBRACE) {
|
|
636
|
+
this.pos++;
|
|
637
|
+
this.enterObjectMember(segment);
|
|
638
|
+
}
|
|
639
|
+
else if (b === LBRACKET) {
|
|
640
|
+
this.pos++;
|
|
641
|
+
this.enterArrayIndex(segment);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
this.fail(`cannot descend into ${this.describe()} looking for ${JSON.stringify(segment)}`);
|
|
645
|
+
}
|
|
646
|
+
this.path.push(segment);
|
|
647
|
+
}
|
|
648
|
+
if (!enter)
|
|
649
|
+
return;
|
|
650
|
+
this.skipSpace();
|
|
651
|
+
const b = this.peek();
|
|
652
|
+
if (b !== LBRACKET && b !== LBRACE) {
|
|
653
|
+
this.fail(`expected an array or object to iterate, got ${this.describe()}`);
|
|
654
|
+
}
|
|
655
|
+
this.pos++;
|
|
656
|
+
this.container = b === LBRACKET ? "[" : "{";
|
|
657
|
+
}
|
|
658
|
+
/** Positions the cursor on the value of `key`, skipping the members before it. */
|
|
659
|
+
enterObjectMember(key) {
|
|
660
|
+
this.skipSpace();
|
|
661
|
+
if (this.peek() === RBRACE)
|
|
662
|
+
this.fail(`no member ${JSON.stringify(key)}`);
|
|
663
|
+
for (;;) {
|
|
664
|
+
const name = this.readName("field");
|
|
665
|
+
this.expectColon("field");
|
|
666
|
+
if (name === key)
|
|
667
|
+
return;
|
|
668
|
+
this.skipValue();
|
|
669
|
+
this.skipSpace();
|
|
670
|
+
const sep = this.take();
|
|
671
|
+
if (sep === RBRACE)
|
|
672
|
+
this.fail(`no member ${JSON.stringify(key)}`);
|
|
673
|
+
if (sep !== COMMA)
|
|
674
|
+
this.fail("expected \",\" or \"}\" in object");
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
/** Positions the cursor on element `index`, skipping the elements before it. */
|
|
678
|
+
enterArrayIndex(segment) {
|
|
679
|
+
if (!/^(?:0|[1-9][0-9]*)$/.test(segment)) {
|
|
680
|
+
this.fail(`expected an array index, got ${JSON.stringify(segment)}`);
|
|
681
|
+
}
|
|
682
|
+
const target = Number(segment);
|
|
683
|
+
this.skipSpace();
|
|
684
|
+
if (this.peek() === RBRACKET)
|
|
685
|
+
this.fail(`no element ${segment}`);
|
|
686
|
+
for (let i = 0;; i++) {
|
|
687
|
+
if (i === target)
|
|
688
|
+
return;
|
|
689
|
+
this.skipValue();
|
|
690
|
+
this.skipSpace();
|
|
691
|
+
const sep = this.take();
|
|
692
|
+
if (sep === RBRACKET)
|
|
693
|
+
this.fail(`no element ${segment}`);
|
|
694
|
+
if (sep !== COMMA)
|
|
695
|
+
this.fail("expected \",\" or \"]\" in array");
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// ── iteration ───────────────────────────────────────────────────────
|
|
699
|
+
/**
|
|
700
|
+
* Whether another element remains in the container being iterated.
|
|
701
|
+
*
|
|
702
|
+
* @remarks
|
|
703
|
+
* A predicate: it consumes the container's closing bracket once there is
|
|
704
|
+
* nothing left, and otherwise leaves the cursor where it was. Advancing is
|
|
705
|
+
* {@link next}'s job, so the two do not have to alternate.
|
|
706
|
+
*/
|
|
707
|
+
more() {
|
|
708
|
+
if (this.container === null)
|
|
709
|
+
return false;
|
|
710
|
+
const close = this.container === "[" ? RBRACKET : RBRACE;
|
|
711
|
+
this.skipSpace();
|
|
712
|
+
if (this.peek() === close) {
|
|
713
|
+
this.pos++;
|
|
714
|
+
this.container = null;
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Reads the next element as `type`.
|
|
721
|
+
*
|
|
722
|
+
* @remarks
|
|
723
|
+
* For an object container, `type` must be a two-field struct named `key`
|
|
724
|
+
* and `value`, in either order, and each member arrives as one of those.
|
|
725
|
+
* An error inside an element is located by its index in an array and by
|
|
726
|
+
* its member name in an object, as RFC 6901 addresses them.
|
|
727
|
+
*/
|
|
728
|
+
next(type) {
|
|
729
|
+
if (this.container === null)
|
|
730
|
+
this.fail("the reader is exhausted");
|
|
731
|
+
// An object's entry type is checked before anything is consumed, the
|
|
732
|
+
// separator included, at the container's own pointer: no member has
|
|
733
|
+
// been named yet, and a refused call leaves the reader where it was.
|
|
734
|
+
const valueType = this.container === "{" ? this.entryValueType(type) : type;
|
|
735
|
+
// The separator belongs to the advance, not to the predicate, so
|
|
736
|
+
// reading two elements in a row does not need a `more` between them.
|
|
737
|
+
if (this.started) {
|
|
738
|
+
this.skipSpace();
|
|
739
|
+
const b = this.peek();
|
|
740
|
+
const close = this.container === "[" ? RBRACKET : RBRACE;
|
|
741
|
+
if (b === close) {
|
|
742
|
+
this.pos++;
|
|
743
|
+
this.container = null;
|
|
744
|
+
this.fail("the reader is exhausted");
|
|
745
|
+
}
|
|
746
|
+
if (b !== COMMA)
|
|
747
|
+
this.fail(`expected "," or ${JSON.stringify(String.fromCharCode(close))}`);
|
|
748
|
+
this.pos++;
|
|
749
|
+
}
|
|
750
|
+
this.started = true;
|
|
751
|
+
if (this.container === "{")
|
|
752
|
+
return this.readMember(valueType);
|
|
753
|
+
this.path.push(String(this.index));
|
|
754
|
+
this.index++;
|
|
755
|
+
try {
|
|
756
|
+
return this.readValue(type);
|
|
757
|
+
}
|
|
758
|
+
finally {
|
|
759
|
+
this.path.pop();
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* The value type of an object entry: `type` must be a struct of exactly
|
|
764
|
+
* the fields `key` and `value`, in either order, with a String key.
|
|
765
|
+
*/
|
|
766
|
+
entryValueType(type) {
|
|
767
|
+
if (type.type !== "Struct") {
|
|
768
|
+
this.fail("iterating an object needs a Struct with exactly the fields key and value");
|
|
769
|
+
}
|
|
770
|
+
const fields = type.value;
|
|
771
|
+
const keyField = fields.find(f => f.name === "key");
|
|
772
|
+
const valueField = fields.find(f => f.name === "value");
|
|
773
|
+
if (fields.length !== 2 || keyField === undefined || valueField === undefined) {
|
|
774
|
+
this.fail("iterating an object needs a Struct with exactly the fields key and value");
|
|
775
|
+
}
|
|
776
|
+
if (keyField.type.type !== "String") {
|
|
777
|
+
this.fail("iterating an object needs a String key");
|
|
778
|
+
}
|
|
779
|
+
return valueField.type;
|
|
780
|
+
}
|
|
781
|
+
/** One object member, as a `{ key, value }` struct. */
|
|
782
|
+
readMember(valueType) {
|
|
783
|
+
const name = this.readName("member");
|
|
784
|
+
this.expectColon("member");
|
|
785
|
+
this.index++;
|
|
786
|
+
this.path.push(name);
|
|
787
|
+
try {
|
|
788
|
+
const value = this.readValue(valueType);
|
|
789
|
+
return { key: name, value };
|
|
790
|
+
}
|
|
791
|
+
finally {
|
|
792
|
+
this.path.pop();
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/** Reads one whole value, strictly, as `type`. */
|
|
796
|
+
readValue(type) {
|
|
797
|
+
if (++this.depth > MAX_DEPTH) {
|
|
798
|
+
this.depth--;
|
|
799
|
+
this.fail(`document nests deeper than ${MAX_DEPTH}`);
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
return this.readValueInner(type);
|
|
803
|
+
}
|
|
804
|
+
catch (e) {
|
|
805
|
+
// V8's stack can give out before MAX_DEPTH on the typed read, so
|
|
806
|
+
// the depth guard above never fires there. The document is still
|
|
807
|
+
// refused for nesting, with the contract's error rather than a bare
|
|
808
|
+
// RangeError escaping json_next.
|
|
809
|
+
if (e instanceof RangeError)
|
|
810
|
+
this.fail("document nests deeper than this runtime can read");
|
|
811
|
+
throw e;
|
|
812
|
+
}
|
|
813
|
+
finally {
|
|
814
|
+
this.depth--;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
readValueInner(type) {
|
|
818
|
+
switch (type.type) {
|
|
819
|
+
case "Null": {
|
|
820
|
+
this.skipSpace();
|
|
821
|
+
if (this.startsWith("null")) {
|
|
822
|
+
this.pos += 4;
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
this.fail(`expected null, got ${this.describe()}`);
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
case "Boolean": {
|
|
829
|
+
this.skipSpace();
|
|
830
|
+
if (this.startsWith("true")) {
|
|
831
|
+
this.pos += 4;
|
|
832
|
+
return true;
|
|
833
|
+
}
|
|
834
|
+
if (this.startsWith("false")) {
|
|
835
|
+
this.pos += 5;
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
this.fail(`expected a boolean, got ${this.describe()}`);
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
case "String": {
|
|
842
|
+
this.skipSpace();
|
|
843
|
+
if (this.peek() !== QUOTE)
|
|
844
|
+
this.fail(`expected a String, got ${this.describe()}`);
|
|
845
|
+
return this.readString();
|
|
846
|
+
}
|
|
847
|
+
case "Integer": {
|
|
848
|
+
// East JSON writes Integer as a decimal string, so no value
|
|
849
|
+
// ever passes through a double.
|
|
850
|
+
this.skipSpace();
|
|
851
|
+
if (this.peek() !== QUOTE)
|
|
852
|
+
this.fail(`expected Integer as a quoted decimal string, got ${this.describe()}`);
|
|
853
|
+
const text = this.readString();
|
|
854
|
+
if (!INTEGER_RE.test(text)) {
|
|
855
|
+
this.fail(`${quote(text)} is not a 64-bit integer in East JSON's form`);
|
|
856
|
+
}
|
|
857
|
+
return BigInt(text);
|
|
858
|
+
}
|
|
859
|
+
case "Float": {
|
|
860
|
+
this.skipSpace();
|
|
861
|
+
const b = this.peek();
|
|
862
|
+
if (b === QUOTE) {
|
|
863
|
+
const text = this.readString();
|
|
864
|
+
if (!EAST_JSON_PATTERNS.floatSpecials.includes(text)) {
|
|
865
|
+
this.fail(`${quote(text)} is not one of the non-finite float spellings`);
|
|
866
|
+
}
|
|
867
|
+
return text === "-0.0" ? -0 : Number(text);
|
|
868
|
+
}
|
|
869
|
+
if (b !== MINUS && !isDigit(b))
|
|
870
|
+
this.fail(`expected a Float, got ${this.describe()}`);
|
|
871
|
+
return Number(this.readNumber());
|
|
872
|
+
}
|
|
873
|
+
case "DateTime": {
|
|
874
|
+
this.skipSpace();
|
|
875
|
+
if (this.peek() !== QUOTE)
|
|
876
|
+
this.fail(`expected DateTime as a string, got ${this.describe()}`);
|
|
877
|
+
const text = this.readString();
|
|
878
|
+
if (!DATETIME_RE.test(text)) {
|
|
879
|
+
this.fail(`${quote(text)} is not East JSON's UTC date-time form`);
|
|
880
|
+
}
|
|
881
|
+
const date = parseUtcDateTime(text);
|
|
882
|
+
// The pattern bounds each field independently but cannot rule
|
|
883
|
+
// out a day the month does not have. `new Date` will not either
|
|
884
|
+
// — it rolls 30 February into 2 March rather than failing — so
|
|
885
|
+
// the fields are checked against what came back.
|
|
886
|
+
if (date === null)
|
|
887
|
+
this.fail(`${quote(text)} is not a real date`);
|
|
888
|
+
return date;
|
|
889
|
+
}
|
|
890
|
+
case "Blob": {
|
|
891
|
+
this.skipSpace();
|
|
892
|
+
if (this.peek() !== QUOTE)
|
|
893
|
+
this.fail(`expected Blob as a string, got ${this.describe()}`);
|
|
894
|
+
const text = this.readString();
|
|
895
|
+
if (!BLOB_RE.test(text)) {
|
|
896
|
+
this.fail(`${quote(text)} is not East JSON's 0x-prefixed lowercase hex form`);
|
|
897
|
+
}
|
|
898
|
+
const hex = text.slice(2);
|
|
899
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
900
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
901
|
+
bytes[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
|
|
902
|
+
}
|
|
903
|
+
return bytes;
|
|
904
|
+
}
|
|
905
|
+
case "Array": {
|
|
906
|
+
const out = [];
|
|
907
|
+
this.eachElement(i => { this.path.push(String(i)); try {
|
|
908
|
+
out.push(this.readValue(type.value));
|
|
909
|
+
}
|
|
910
|
+
finally {
|
|
911
|
+
this.path.pop();
|
|
912
|
+
} });
|
|
913
|
+
return out;
|
|
914
|
+
}
|
|
915
|
+
case "Set": {
|
|
916
|
+
const element = type.value;
|
|
917
|
+
const set = new SortedSet([], compareFor(element));
|
|
918
|
+
this.eachElement(i => {
|
|
919
|
+
this.path.push(String(i));
|
|
920
|
+
try {
|
|
921
|
+
const v = this.readValue(element);
|
|
922
|
+
if (set.has(v))
|
|
923
|
+
this.fail("duplicate element in Set");
|
|
924
|
+
set.add(v);
|
|
925
|
+
}
|
|
926
|
+
finally {
|
|
927
|
+
this.path.pop();
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
return set;
|
|
931
|
+
}
|
|
932
|
+
case "Vector": {
|
|
933
|
+
const element = type.value;
|
|
934
|
+
const values = [];
|
|
935
|
+
this.eachElement(i => { this.path.push(String(i)); try {
|
|
936
|
+
values.push(this.readValue(element));
|
|
937
|
+
}
|
|
938
|
+
finally {
|
|
939
|
+
this.path.pop();
|
|
940
|
+
} });
|
|
941
|
+
return typedArrayFor(element, values, (m) => this.fail(m));
|
|
942
|
+
}
|
|
943
|
+
case "Matrix": {
|
|
944
|
+
const element = type.value;
|
|
945
|
+
const rows = [];
|
|
946
|
+
this.eachElement(i => {
|
|
947
|
+
this.path.push(String(i));
|
|
948
|
+
try {
|
|
949
|
+
const row = [];
|
|
950
|
+
this.eachElement(j => { this.path.push(String(j)); try {
|
|
951
|
+
row.push(this.readValue(element));
|
|
952
|
+
}
|
|
953
|
+
finally {
|
|
954
|
+
this.path.pop();
|
|
955
|
+
} });
|
|
956
|
+
rows.push(row);
|
|
957
|
+
}
|
|
958
|
+
finally {
|
|
959
|
+
this.path.pop();
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
const cols = rows.length === 0 ? 0 : rows[0].length;
|
|
963
|
+
const flat = [];
|
|
964
|
+
for (let r = 0; r < rows.length; r++) {
|
|
965
|
+
if (rows[r].length !== cols) {
|
|
966
|
+
this.fail(`Matrix row ${r} has ${rows[r].length} columns, expected ${cols}`);
|
|
967
|
+
}
|
|
968
|
+
for (const v of rows[r])
|
|
969
|
+
flat.push(v);
|
|
970
|
+
}
|
|
971
|
+
return matrix(typedArrayFor(element, flat, (m) => this.fail(m)), rows.length, cols);
|
|
972
|
+
}
|
|
973
|
+
case "Dict": {
|
|
974
|
+
const d = type.value;
|
|
975
|
+
const map = new SortedMap([], compareFor(d.key));
|
|
976
|
+
this.eachElement(i => {
|
|
977
|
+
this.path.push(String(i));
|
|
978
|
+
try {
|
|
979
|
+
const entry = this.readEntry(d);
|
|
980
|
+
if (map.has(entry.key))
|
|
981
|
+
this.fail("duplicate key in Dict");
|
|
982
|
+
map.set(entry.key, entry.value);
|
|
983
|
+
}
|
|
984
|
+
finally {
|
|
985
|
+
this.path.pop();
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
return map;
|
|
989
|
+
}
|
|
990
|
+
case "Ref":
|
|
991
|
+
return ref(this.readOneElementArray(type.value));
|
|
992
|
+
case "Struct":
|
|
993
|
+
return this.readStruct(type.value);
|
|
994
|
+
case "Variant":
|
|
995
|
+
return this.readVariant(type.value);
|
|
996
|
+
case "Recursive": {
|
|
997
|
+
const rec = type.value;
|
|
998
|
+
if (rec.type === "wrapper") {
|
|
999
|
+
const w = rec.value;
|
|
1000
|
+
this.recursive.set(w.id, w.inner);
|
|
1001
|
+
return this.readValue(w.inner);
|
|
1002
|
+
}
|
|
1003
|
+
const inner = this.recursive.get(rec.value);
|
|
1004
|
+
if (inner === undefined)
|
|
1005
|
+
this.fail("unresolved recursive type");
|
|
1006
|
+
return this.readValue(inner);
|
|
1007
|
+
}
|
|
1008
|
+
case "Never":
|
|
1009
|
+
this.fail("Never has no values, so no document satisfies it");
|
|
1010
|
+
break;
|
|
1011
|
+
default:
|
|
1012
|
+
this.fail("a function has no JSON form");
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
/** Recursive type bodies, keyed by the id their wrapper carries. */
|
|
1016
|
+
recursive = new Map();
|
|
1017
|
+
/** Runs `body` for each element of a JSON array. */
|
|
1018
|
+
eachElement(body) {
|
|
1019
|
+
this.skipSpace();
|
|
1020
|
+
if (this.peek() !== LBRACKET)
|
|
1021
|
+
this.fail(`expected an array, got ${this.describe()}`);
|
|
1022
|
+
this.pos++;
|
|
1023
|
+
this.skipSpace();
|
|
1024
|
+
if (this.peek() === RBRACKET) {
|
|
1025
|
+
this.pos++;
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
for (let i = 0;; i++) {
|
|
1029
|
+
body(i);
|
|
1030
|
+
this.skipSpace();
|
|
1031
|
+
const sep = this.take();
|
|
1032
|
+
if (sep === RBRACKET)
|
|
1033
|
+
return;
|
|
1034
|
+
if (sep !== COMMA)
|
|
1035
|
+
this.fail("expected \",\" or \"]\" in array");
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
readOneElementArray(inner) {
|
|
1039
|
+
this.skipSpace();
|
|
1040
|
+
if (this.peek() !== LBRACKET)
|
|
1041
|
+
this.fail(`expected a Ref as a one-element array, got ${this.describe()}`);
|
|
1042
|
+
this.pos++;
|
|
1043
|
+
this.skipSpace();
|
|
1044
|
+
if (this.peek() === RBRACKET)
|
|
1045
|
+
this.fail("expected a Ref to hold exactly one element");
|
|
1046
|
+
const value = this.readValue(inner);
|
|
1047
|
+
this.skipSpace();
|
|
1048
|
+
if (this.take() !== RBRACKET)
|
|
1049
|
+
this.fail("expected a Ref to hold exactly one element");
|
|
1050
|
+
return value;
|
|
1051
|
+
}
|
|
1052
|
+
readEntry(d) {
|
|
1053
|
+
this.skipSpace();
|
|
1054
|
+
if (this.peek() !== LBRACE)
|
|
1055
|
+
this.fail(`expected an object, got ${this.describe()}`);
|
|
1056
|
+
this.pos++;
|
|
1057
|
+
let key;
|
|
1058
|
+
let value;
|
|
1059
|
+
let haveKey = false;
|
|
1060
|
+
let haveValue = false;
|
|
1061
|
+
this.skipSpace();
|
|
1062
|
+
if (this.peek() === RBRACE)
|
|
1063
|
+
this.fail("a Dict entry needs key and value");
|
|
1064
|
+
for (;;) {
|
|
1065
|
+
const name = this.readName("field");
|
|
1066
|
+
this.expectColon("field");
|
|
1067
|
+
if (name === "key") {
|
|
1068
|
+
if (haveKey)
|
|
1069
|
+
this.fail("duplicate \"key\" in Dict entry");
|
|
1070
|
+
this.path.push("key");
|
|
1071
|
+
try {
|
|
1072
|
+
key = this.readValue(d.key);
|
|
1073
|
+
}
|
|
1074
|
+
finally {
|
|
1075
|
+
this.path.pop();
|
|
1076
|
+
}
|
|
1077
|
+
haveKey = true;
|
|
1078
|
+
}
|
|
1079
|
+
else if (name === "value") {
|
|
1080
|
+
if (haveValue)
|
|
1081
|
+
this.fail("duplicate \"value\" in Dict entry");
|
|
1082
|
+
this.path.push("value");
|
|
1083
|
+
try {
|
|
1084
|
+
value = this.readValue(d.value);
|
|
1085
|
+
}
|
|
1086
|
+
finally {
|
|
1087
|
+
this.path.pop();
|
|
1088
|
+
}
|
|
1089
|
+
haveValue = true;
|
|
1090
|
+
}
|
|
1091
|
+
else {
|
|
1092
|
+
this.fail(`unexpected field ${quote(name)} in Dict entry`);
|
|
1093
|
+
}
|
|
1094
|
+
this.skipSpace();
|
|
1095
|
+
const sep = this.take();
|
|
1096
|
+
if (sep === RBRACE)
|
|
1097
|
+
break;
|
|
1098
|
+
if (sep !== COMMA)
|
|
1099
|
+
this.fail("expected \",\" or \"}\" in Dict entry");
|
|
1100
|
+
}
|
|
1101
|
+
if (!haveKey || !haveValue)
|
|
1102
|
+
this.fail("a Dict entry needs both key and value");
|
|
1103
|
+
return { key, value };
|
|
1104
|
+
}
|
|
1105
|
+
readStruct(fields) {
|
|
1106
|
+
this.skipSpace();
|
|
1107
|
+
if (this.peek() !== LBRACE)
|
|
1108
|
+
this.fail(`expected an object, got ${this.describe()}`);
|
|
1109
|
+
this.pos++;
|
|
1110
|
+
const seen = new Map();
|
|
1111
|
+
this.skipSpace();
|
|
1112
|
+
if (this.peek() !== RBRACE) {
|
|
1113
|
+
for (;;) {
|
|
1114
|
+
const name = this.readName("field");
|
|
1115
|
+
this.expectColon("field");
|
|
1116
|
+
const field = fields.find(f => f.name === name);
|
|
1117
|
+
if (field === undefined)
|
|
1118
|
+
this.fail(`unexpected field ${quote(name)}`);
|
|
1119
|
+
if (seen.has(name))
|
|
1120
|
+
this.fail(`duplicate field ${quote(name)}`);
|
|
1121
|
+
this.path.push(name);
|
|
1122
|
+
try {
|
|
1123
|
+
seen.set(name, this.readValue(field.type));
|
|
1124
|
+
}
|
|
1125
|
+
finally {
|
|
1126
|
+
this.path.pop();
|
|
1127
|
+
}
|
|
1128
|
+
this.skipSpace();
|
|
1129
|
+
const sep = this.take();
|
|
1130
|
+
if (sep === RBRACE)
|
|
1131
|
+
break;
|
|
1132
|
+
if (sep !== COMMA)
|
|
1133
|
+
this.fail("expected \",\" or \"}\" in object");
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
this.pos++;
|
|
1138
|
+
}
|
|
1139
|
+
// Field order is the type's, not the document's — JSON objects are
|
|
1140
|
+
// unordered, so the encoder's order is not something to require.
|
|
1141
|
+
const out = {};
|
|
1142
|
+
for (const f of fields) {
|
|
1143
|
+
if (!seen.has(f.name))
|
|
1144
|
+
this.fail(`missing field ${quote(f.name)}`);
|
|
1145
|
+
out[f.name] = seen.get(f.name);
|
|
1146
|
+
}
|
|
1147
|
+
return out;
|
|
1148
|
+
}
|
|
1149
|
+
readVariant(cases) {
|
|
1150
|
+
this.skipSpace();
|
|
1151
|
+
if (this.peek() !== LBRACE)
|
|
1152
|
+
this.fail(`expected an object, got ${this.describe()}`);
|
|
1153
|
+
this.pos++;
|
|
1154
|
+
let tag = null;
|
|
1155
|
+
let value;
|
|
1156
|
+
let haveValue = false;
|
|
1157
|
+
this.skipSpace();
|
|
1158
|
+
if (this.peek() === RBRACE)
|
|
1159
|
+
this.fail("a Variant needs type and value");
|
|
1160
|
+
for (;;) {
|
|
1161
|
+
const name = this.readName("field");
|
|
1162
|
+
this.expectColon("field");
|
|
1163
|
+
if (name === "type") {
|
|
1164
|
+
if (tag !== null)
|
|
1165
|
+
this.fail("duplicate \"type\" in Variant");
|
|
1166
|
+
this.skipSpace();
|
|
1167
|
+
if (this.peek() !== QUOTE)
|
|
1168
|
+
this.fail(`expected a variant case name, got ${this.describe()}`);
|
|
1169
|
+
tag = this.readString();
|
|
1170
|
+
if (!cases.some(c => c.name === tag))
|
|
1171
|
+
this.fail(`unknown variant case ${quote(tag)}`);
|
|
1172
|
+
if (haveValue) {
|
|
1173
|
+
// The payload arrived first; it could not be typed then.
|
|
1174
|
+
this.fail("a Variant must carry \"type\" before \"value\"");
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
else if (name === "value") {
|
|
1178
|
+
if (haveValue)
|
|
1179
|
+
this.fail("duplicate \"value\" in Variant");
|
|
1180
|
+
if (tag === null)
|
|
1181
|
+
this.fail("a Variant must carry \"type\" before \"value\"");
|
|
1182
|
+
const chosen = cases.find(c => c.name === tag);
|
|
1183
|
+
this.path.push(tag);
|
|
1184
|
+
try {
|
|
1185
|
+
value = this.readValue(chosen.type);
|
|
1186
|
+
}
|
|
1187
|
+
finally {
|
|
1188
|
+
this.path.pop();
|
|
1189
|
+
}
|
|
1190
|
+
haveValue = true;
|
|
1191
|
+
}
|
|
1192
|
+
else {
|
|
1193
|
+
this.fail(`unexpected field ${quote(name)} in Variant`);
|
|
1194
|
+
}
|
|
1195
|
+
this.skipSpace();
|
|
1196
|
+
const sep = this.take();
|
|
1197
|
+
if (sep === RBRACE)
|
|
1198
|
+
break;
|
|
1199
|
+
if (sep !== COMMA)
|
|
1200
|
+
this.fail("expected \",\" or \"}\" in Variant");
|
|
1201
|
+
}
|
|
1202
|
+
if (tag === null || !haveValue)
|
|
1203
|
+
this.fail("a Variant needs both type and value");
|
|
1204
|
+
return variant(tag, value);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
function isDigit(b) {
|
|
1208
|
+
return b >= ZERO && b <= NINE;
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* East JSON's UTC date-time text as a `Date`, or null when the calendar has no
|
|
1212
|
+
* such day.
|
|
1213
|
+
*
|
|
1214
|
+
* @param text - Text already known to match the date-time pattern
|
|
1215
|
+
* @returns The date, or null for a day the month does not have
|
|
1216
|
+
*
|
|
1217
|
+
* @remarks
|
|
1218
|
+
* `new Date` cannot be trusted to reject an impossible date: it rolls
|
|
1219
|
+
* `2026-02-30` into `2026-03-02` and `2025-02-29` into `2025-03-01`, so a
|
|
1220
|
+
* NaN check lets a payload through and stores a different day than it sent.
|
|
1221
|
+
* The fields are read back off the constructed date instead.
|
|
1222
|
+
*/
|
|
1223
|
+
function parseUtcDateTime(text) {
|
|
1224
|
+
const year = Number(text.slice(0, 4));
|
|
1225
|
+
const month = Number(text.slice(5, 7));
|
|
1226
|
+
const day = Number(text.slice(8, 10));
|
|
1227
|
+
const hour = Number(text.slice(11, 13));
|
|
1228
|
+
const minute = Number(text.slice(14, 16));
|
|
1229
|
+
const second = Number(text.slice(17, 19));
|
|
1230
|
+
const millis = Number(text.slice(20, 23));
|
|
1231
|
+
// The pattern already pins the year to 0001..9999, the range every runtime
|
|
1232
|
+
// shares (python's datetime starts at year 1); this guards the arithmetic.
|
|
1233
|
+
if (year < 1)
|
|
1234
|
+
return null;
|
|
1235
|
+
// Date.UTC maps years 0-99 into the 1900s, so a one- or two-digit year has
|
|
1236
|
+
// to be set explicitly. The base year is a leap year so that 02-29 always
|
|
1237
|
+
// constructs, and the read-back below is what then rejects it in a common
|
|
1238
|
+
// year — `new Date` rolls 2025-02-29 into 2025-03-01 rather than failing.
|
|
1239
|
+
const date = new Date(Date.UTC(2000, month - 1, day, hour, minute, second, millis));
|
|
1240
|
+
date.setUTCFullYear(year);
|
|
1241
|
+
if (date.getUTCFullYear() !== year
|
|
1242
|
+
|| date.getUTCMonth() !== month - 1
|
|
1243
|
+
|| date.getUTCDate() !== day) {
|
|
1244
|
+
return null;
|
|
1245
|
+
}
|
|
1246
|
+
return date;
|
|
1247
|
+
}
|
|
1248
|
+
function pointerOf(path) {
|
|
1249
|
+
if (path.length === 0)
|
|
1250
|
+
return "";
|
|
1251
|
+
return "/" + path.map(s => s.replace(/~/g, "~0").replace(/\//g, "~1")).join("/");
|
|
1252
|
+
}
|
|
1253
|
+
/** RFC 6901: `""` is the whole document; every other pointer starts with `/`. */
|
|
1254
|
+
function parsePointer(pointer) {
|
|
1255
|
+
if (pointer === "")
|
|
1256
|
+
return [];
|
|
1257
|
+
if (!pointer.startsWith("/")) {
|
|
1258
|
+
throw new JsonReadError(`a JSON Pointer must be empty or start with "/", got ${JSON.stringify(pointer)}`, "");
|
|
1259
|
+
}
|
|
1260
|
+
return pointer.slice(1).split("/").map(s => s.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
1261
|
+
}
|
|
1262
|
+
function typedArrayFor(element, values, fail) {
|
|
1263
|
+
if (element.type === "Float")
|
|
1264
|
+
return new Float64Array(values);
|
|
1265
|
+
if (element.type === "Integer")
|
|
1266
|
+
return new BigInt64Array(values);
|
|
1267
|
+
if (element.type === "Boolean")
|
|
1268
|
+
return new Uint8ClampedArray(values.map(v => (v ? 1 : 0)));
|
|
1269
|
+
return fail("a Vector or Matrix element must be Float, Integer or Boolean");
|
|
1270
|
+
}
|
|
1271
|
+
//# sourceMappingURL=json_reader.js.map
|