ata-validator 1.22.1 → 1.24.0
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/CHANGELOG.md +30 -0
- package/bin/ata.js +27 -1
- package/compat.d.ts +5 -0
- package/compat.js +11 -2
- package/index.js +256 -70
- package/lib/aot-build.js +24 -2
- package/lib/aot-impl.js +14 -1
- package/lib/compat-errors.js +49 -22
- package/lib/js-compiler.js +15 -4
- package/lib/scan-compiler.js +1007 -0
- package/lib/scan-runtime.js +158 -0
- package/lib/strict-check.js +195 -0
- package/lib/version.js +1 -1
- package/package.json +9 -9
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// A schema-directed scanner: it answers the verdict from the JSON text without
|
|
4
|
+
// building the document. `JSON.parse` is three quarters of the cost of a real
|
|
5
|
+
// request, so a validator that only has to say yes or no should not pay it.
|
|
6
|
+
//
|
|
7
|
+
// The scanner is a second JSON reader, which is the dangerous part. Two rules
|
|
8
|
+
// keep it honest:
|
|
9
|
+
//
|
|
10
|
+
// 1. Anything it is not sure about it declines. `compileScanner` returns null
|
|
11
|
+
// for a schema outside the supported set, and a compiled scanner returns
|
|
12
|
+
// BAIL at runtime for a document shape it cannot answer (a duplicate key,
|
|
13
|
+
// an escaped property name, a subtree deeper than it will walk). The
|
|
14
|
+
// caller then parses and validates as before. Declining costs a little
|
|
15
|
+
// speed; guessing costs a silently accepted document.
|
|
16
|
+
// 2. Every check that needs the decoded value gets the decoded value. The
|
|
17
|
+
// scanner materialises that one scalar, never the document around it, and
|
|
18
|
+
// hands it to the same compiled check the validator itself runs. So
|
|
19
|
+
// `format`, `pattern`, `const` and `enum` cannot drift from the validator:
|
|
20
|
+
// they are the validator.
|
|
21
|
+
//
|
|
22
|
+
// tests/test_scanner_differential.js holds the scanner and `validate()` to the
|
|
23
|
+
// same verdict on the whole official suite plus a malformed-JSON corpus, and
|
|
24
|
+
// counts disagreements, not bails.
|
|
25
|
+
|
|
26
|
+
const { compileToJSCodegen } = require('./js-compiler');
|
|
27
|
+
|
|
28
|
+
const VALID = 1;
|
|
29
|
+
const INVALID = 0;
|
|
30
|
+
const BAIL = -1;
|
|
31
|
+
|
|
32
|
+
// A nested schema emits nested code, so depth is source size. Past this the
|
|
33
|
+
// scanner is not worth the function it would build.
|
|
34
|
+
const MAX_SCHEMA_DEPTH = 12;
|
|
35
|
+
// `seen` is a set of 31-bit words, one emitted variable per word, so the cap
|
|
36
|
+
// is source size rather than bit count. A generated config schema carries a
|
|
37
|
+
// property per environment variable, which is how a real document gets past a
|
|
38
|
+
// thousand names on one node.
|
|
39
|
+
const MAX_PROPS = 4096;
|
|
40
|
+
|
|
41
|
+
// How two layers of an intersection combine, per keyword. The bounds take the
|
|
42
|
+
// tighter of the two, which is what an intersection means for a range.
|
|
43
|
+
const TIGHTEN_MAX = ['minLength', 'minItems', 'minProperties', 'minimum', 'exclusiveMinimum'];
|
|
44
|
+
const TIGHTEN_MIN = ['maxLength', 'maxItems', 'maxProperties', 'maximum', 'exclusiveMaximum'];
|
|
45
|
+
// Answered by a compiled check over one materialised scalar, so two layers can
|
|
46
|
+
// only be combined when they ask the same thing. Two different patterns would
|
|
47
|
+
// need two checks, which this does not emit yet.
|
|
48
|
+
const SAME_OR_NOTHING = ['pattern', 'format', 'const', 'enum', 'multipleOf'];
|
|
49
|
+
|
|
50
|
+
// Read structurally, straight out of the text.
|
|
51
|
+
const STRUCTURAL = new Set([
|
|
52
|
+
'type', 'properties', 'required', 'additionalProperties',
|
|
53
|
+
'minProperties', 'maxProperties', 'items', 'prefixItems', 'minItems', 'maxItems',
|
|
54
|
+
// In a schema the scanner accepts, these two are exact synonyms for
|
|
55
|
+
// additionalProperties and items. The unevaluated keywords differ from them
|
|
56
|
+
// only through annotations contributed by in-place applicators and by
|
|
57
|
+
// patternProperties, and every construct that could contribute one is on
|
|
58
|
+
// this compiler's decline list ($ref with verdict-bearing siblings, anyOf,
|
|
59
|
+
// oneOf, if/then/else, dependentSchemas, patternProperties, contains), or is
|
|
60
|
+
// resolved away before emission (a lone $ref is inlined, allOf is merged so
|
|
61
|
+
// the union of branch properties IS the node's properties). What remains
|
|
62
|
+
// evaluated is exactly what properties and prefixItems match, which is the
|
|
63
|
+
// set additionalProperties and items are defined against.
|
|
64
|
+
'unevaluatedProperties', 'unevaluatedItems',
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
// Constrain one scalar. Answered by the validator's own compiled check over
|
|
68
|
+
// the materialised scalar, or inline where the scan already has the number.
|
|
69
|
+
const LEAF = new Set([
|
|
70
|
+
'minLength', 'maxLength', 'pattern', 'format', 'const', 'enum',
|
|
71
|
+
'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf',
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
// No bearing on the verdict. `default` is deliberately not here: defaults are
|
|
75
|
+
// filled in before validation, so a missing property with a default is a
|
|
76
|
+
// property the scanner never sees and `validate()` does. A schema carrying one
|
|
77
|
+
// is declined.
|
|
78
|
+
const IGNORED = new Set([
|
|
79
|
+
'title', 'description', '$comment', 'examples', 'deprecated',
|
|
80
|
+
'readOnly', 'writeOnly', '$schema', '$defs', 'definitions', '$vocabulary',
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
// `$id` and `$anchor` move the base a reference resolves against, and the
|
|
84
|
+
// scanner resolves references itself. Rather than reimplement base-URI
|
|
85
|
+
// resolution, it declines any document that carries one below the root, which
|
|
86
|
+
// is where every historical silent acceptance in `$ref` handling has lived.
|
|
87
|
+
const BASE_CHANGING = new Set(['$id', '$anchor', '$dynamicAnchor', '$dynamicRef']);
|
|
88
|
+
|
|
89
|
+
// A reference is inlined, so a fan-out of them is emitted source. This caps
|
|
90
|
+
// it. Sized for a node per declared property of a large generated schema,
|
|
91
|
+
// not just for hand-written ones.
|
|
92
|
+
const MAX_NODES = 8192;
|
|
93
|
+
|
|
94
|
+
// Each position in a tuple emits its own branch inside the element loop, so a
|
|
95
|
+
// long one is both a lot of source and a linear test per element.
|
|
96
|
+
const MAX_PREFIX_ITEMS = 24;
|
|
97
|
+
|
|
98
|
+
const KINDS = ['object', 'array', 'string', 'number', 'integer', 'boolean', 'null'];
|
|
99
|
+
|
|
100
|
+
// Declining is the safe answer, and it is also the thing worth measuring: the
|
|
101
|
+
// reason travels with the throw so a caller can ask what the scanner cannot do
|
|
102
|
+
// yet, rather than guessing at coverage.
|
|
103
|
+
const DECLINE = Symbol('decline');
|
|
104
|
+
let declineReason = null;
|
|
105
|
+
function decline(reason) { declineReason = reason || 'unknown'; throw DECLINE; }
|
|
106
|
+
|
|
107
|
+
// The outer guard is one compare for the char that is not whitespace, which
|
|
108
|
+
// is nearly every char in machine-written JSON; the inner loop stays the
|
|
109
|
+
// exact four, because other characters at or below 32 are not whitespace and
|
|
110
|
+
// skipping one would accept text the parser refuses. NaN fails the guard the
|
|
111
|
+
// same way it failed the loop.
|
|
112
|
+
const WS = 'if(c<=32){while(c===32||c===10||c===9||c===13){c=s.charCodeAt(++i);}}';
|
|
113
|
+
|
|
114
|
+
function isPlainObject(v) { return v !== null && typeof v === 'object' && !Array.isArray(v); }
|
|
115
|
+
|
|
116
|
+
// The set of JSON kinds this node can accept, from `type` alone.
|
|
117
|
+
function typeSet(schema) {
|
|
118
|
+
const t = schema.type;
|
|
119
|
+
if (t === undefined) return null; // any
|
|
120
|
+
const list = Array.isArray(t) ? t : [t];
|
|
121
|
+
const out = new Set();
|
|
122
|
+
for (const name of list) {
|
|
123
|
+
if (typeof name !== 'string' || !KINDS.includes(name)) decline('type:' + String(name));
|
|
124
|
+
out.add(name);
|
|
125
|
+
}
|
|
126
|
+
if (out.size === 0) decline('type:empty');
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function allows(types, kind) {
|
|
131
|
+
if (types === null) return true;
|
|
132
|
+
if (kind === 'number') return types.has('number') || types.has('integer');
|
|
133
|
+
return types.has(kind);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Only `integer` and not `number`: the scan has to reject 1.5.
|
|
137
|
+
function integerOnly(types) {
|
|
138
|
+
return types !== null && types.has('integer') && !types.has('number');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function compileLeaf(keys, schema, ctx) {
|
|
142
|
+
const sub = {};
|
|
143
|
+
for (const k of keys) sub[k] = schema[k];
|
|
144
|
+
const fn = compileToJSCodegen(sub, null, ctx.userFormats);
|
|
145
|
+
if (typeof fn !== 'function') decline('leaf:' + keys.join('+'));
|
|
146
|
+
ctx.helpers.push(fn);
|
|
147
|
+
return '_h[' + (ctx.helpers.length - 1) + ']';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Only a pointer into the document being compiled. Anything with a scheme, a
|
|
151
|
+
// path or an anchor is somebody else's document or somebody else's base, and
|
|
152
|
+
// the scanner has no registry and no resolver.
|
|
153
|
+
function resolveLocalRef(root, ref) {
|
|
154
|
+
if (typeof ref !== 'string') decline('$ref:not-string');
|
|
155
|
+
if (ref === '#') return root;
|
|
156
|
+
if (!ref.startsWith('#/')) decline('$ref:not-local');
|
|
157
|
+
let node = root;
|
|
158
|
+
for (const rawToken of ref.slice(2).split('/')) {
|
|
159
|
+
const token = decodeURIComponent(rawToken).split('~1').join('/').split('~0').join('~');
|
|
160
|
+
if (node === null || typeof node !== 'object') decline('$ref:unresolved');
|
|
161
|
+
if (Array.isArray(node)) {
|
|
162
|
+
if (!/^\d+$/.test(token)) decline('$ref:unresolved');
|
|
163
|
+
node = node[Number(token)];
|
|
164
|
+
} else {
|
|
165
|
+
if (!Object.prototype.hasOwnProperty.call(node, token)) decline('$ref:unresolved');
|
|
166
|
+
node = node[token];
|
|
167
|
+
}
|
|
168
|
+
if (node === undefined) decline('$ref:unresolved');
|
|
169
|
+
}
|
|
170
|
+
return node;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// `allOf` is an intersection, and the scanner reads the document once, so the
|
|
174
|
+
// branches have to become one schema before anything is emitted. Merging is
|
|
175
|
+
// where an intersection can be got quietly wrong, so the rule is written out in
|
|
176
|
+
// full rather than approximated.
|
|
177
|
+
//
|
|
178
|
+
// The subtle keyword is `additionalProperties`. A branch applies its own to the
|
|
179
|
+
// names ITS OWN `properties` does not list, which is not the same set as the
|
|
180
|
+
// names the merged `properties` does not list. Merging by taking the union of
|
|
181
|
+
// the properties and keeping one `additionalProperties` accepts documents every
|
|
182
|
+
// branch rejects: `allOf: [{properties: {a}, additionalProperties: false}, {properties: {b}}]`
|
|
183
|
+
// must reject `{"b": 1}`, and a union would not. So a name in the merged set
|
|
184
|
+
// takes, from every branch, either that branch's subschema for it or that
|
|
185
|
+
// branch's `additionalProperties`.
|
|
186
|
+
|
|
187
|
+
// Flattens a node and its branches into layers, resolving references as it goes.
|
|
188
|
+
function collectLayers(node, ctx, out, depth) {
|
|
189
|
+
if (depth > MAX_SCHEMA_DEPTH) decline('allOf:too-deep');
|
|
190
|
+
if (node === true) return;
|
|
191
|
+
// Nothing satisfies a false branch, so nothing satisfies the intersection.
|
|
192
|
+
if (node === false) { out.unsatisfiable = true; return; }
|
|
193
|
+
if (!isPlainObject(node)) decline('allOf:branch-not-object');
|
|
194
|
+
for (const k of Object.keys(node)) {
|
|
195
|
+
if (BASE_CHANGING.has(k)) decline('allOf:base-changing:' + k);
|
|
196
|
+
}
|
|
197
|
+
if (node.$ref !== undefined) {
|
|
198
|
+
for (const k of Object.keys(node)) {
|
|
199
|
+
if (k !== '$ref' && !IGNORED.has(k)) decline('allOf:$ref-sibling:' + k);
|
|
200
|
+
}
|
|
201
|
+
if (ctx.refStack.includes(node.$ref)) decline('$ref:recursive');
|
|
202
|
+
const target = resolveLocalRef(ctx.root, node.$ref);
|
|
203
|
+
ctx.refStack.push(node.$ref);
|
|
204
|
+
collectLayers(target, ctx, out, depth + 1);
|
|
205
|
+
ctx.refStack.pop();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const own = {};
|
|
209
|
+
for (const k of Object.keys(node)) {
|
|
210
|
+
if (k === 'allOf' || IGNORED.has(k)) continue;
|
|
211
|
+
own[k] = node[k];
|
|
212
|
+
}
|
|
213
|
+
if (Object.keys(own).length > 0) out.push(own);
|
|
214
|
+
if (node.allOf !== undefined) {
|
|
215
|
+
if (!Array.isArray(node.allOf)) decline('allOf:not-array');
|
|
216
|
+
for (const branch of node.allOf) collectLayers(branch, ctx, out, depth + 1);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// The schema a value must satisfy to satisfy all of `parts`.
|
|
221
|
+
function combine(parts) {
|
|
222
|
+
const real = parts.filter((p) => p !== true && p !== undefined);
|
|
223
|
+
if (real.some((p) => p === false)) return false;
|
|
224
|
+
if (real.length === 0) return true;
|
|
225
|
+
if (real.length === 1) return real[0];
|
|
226
|
+
return { allOf: real };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function mergeAllOf(schema, ctx) {
|
|
230
|
+
const layers = [];
|
|
231
|
+
collectLayers(schema, ctx, layers, 0);
|
|
232
|
+
if (layers.unsatisfiable) return false;
|
|
233
|
+
if (layers.length === 0) return true;
|
|
234
|
+
if (layers.length === 1) return layers[0];
|
|
235
|
+
|
|
236
|
+
// unevaluated* must not survive a merge. The keyword sees annotations from
|
|
237
|
+
// its OWN schema object's adjacent keywords and applicators, and nothing
|
|
238
|
+
// from cousin branches: `allOf: [{unevaluatedProperties: false},
|
|
239
|
+
// {properties: {foo}}]` rejects every object with a property, because the
|
|
240
|
+
// branch holding the keyword evaluates nothing itself. Flattening the
|
|
241
|
+
// branches into one node erases exactly that positional fact, and the suite
|
|
242
|
+
// holds three cases that turn the flattened reading into a silent accept.
|
|
243
|
+
// On a node the merge never touched the keyword is safe, and emitObject
|
|
244
|
+
// reads it as additionalProperties there.
|
|
245
|
+
for (const L of layers) {
|
|
246
|
+
if (L.unevaluatedProperties !== undefined || L.unevaluatedItems !== undefined) decline('allOf:unevaluated');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const out = {};
|
|
250
|
+
|
|
251
|
+
// type: the intersection of the named sets, with `integer` inside `number`,
|
|
252
|
+
// so number and integer together are integer rather than nothing. Empty means
|
|
253
|
+
// no value satisfies every branch.
|
|
254
|
+
let types = null;
|
|
255
|
+
for (const L of layers) {
|
|
256
|
+
const t = typeSet(L);
|
|
257
|
+
if (t === null) continue;
|
|
258
|
+
if (types === null) { types = t; continue; }
|
|
259
|
+
const both = new Set();
|
|
260
|
+
for (const name of types) {
|
|
261
|
+
if (t.has(name)) both.add(name);
|
|
262
|
+
else if ((name === 'number' && t.has('integer')) || (name === 'integer' && t.has('number'))) both.add('integer');
|
|
263
|
+
}
|
|
264
|
+
types = both;
|
|
265
|
+
}
|
|
266
|
+
if (types !== null) {
|
|
267
|
+
if (types.size === 0) return false;
|
|
268
|
+
out.type = [...types];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
for (const key of TIGHTEN_MAX) {
|
|
272
|
+
let v;
|
|
273
|
+
for (const L of layers) if (typeof L[key] === 'number') v = v === undefined ? L[key] : Math.max(v, L[key]);
|
|
274
|
+
if (v !== undefined) out[key] = v;
|
|
275
|
+
}
|
|
276
|
+
for (const key of TIGHTEN_MIN) {
|
|
277
|
+
let v;
|
|
278
|
+
for (const L of layers) if (typeof L[key] === 'number') v = v === undefined ? L[key] : Math.min(v, L[key]);
|
|
279
|
+
if (v !== undefined) out[key] = v;
|
|
280
|
+
}
|
|
281
|
+
for (const key of TIGHTEN_MAX.concat(TIGHTEN_MIN)) {
|
|
282
|
+
for (const L of layers) if (L[key] !== undefined && typeof L[key] !== 'number') decline('allOf:' + key + ':not-number');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
for (const key of SAME_OR_NOTHING) {
|
|
286
|
+
let seen, has = false;
|
|
287
|
+
for (const L of layers) {
|
|
288
|
+
if (L[key] === undefined) continue;
|
|
289
|
+
const rendered = JSON.stringify(L[key]);
|
|
290
|
+
if (!has) { seen = rendered; out[key] = L[key]; has = true; continue; }
|
|
291
|
+
if (rendered !== seen) decline('allOf:conflicting:' + key);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const required = [];
|
|
296
|
+
for (const L of layers) {
|
|
297
|
+
if (L.required === undefined) continue;
|
|
298
|
+
if (!Array.isArray(L.required)) decline('required:not-array');
|
|
299
|
+
for (const name of L.required) if (!required.includes(name)) required.push(name);
|
|
300
|
+
}
|
|
301
|
+
if (required.length > 0) out.required = required;
|
|
302
|
+
|
|
303
|
+
const names = [];
|
|
304
|
+
for (const L of layers) {
|
|
305
|
+
if (L.properties === undefined) continue;
|
|
306
|
+
if (!isPlainObject(L.properties)) decline('properties:not-object');
|
|
307
|
+
for (const k of Object.keys(L.properties)) if (!names.includes(k)) names.push(k);
|
|
308
|
+
}
|
|
309
|
+
if (names.length > 0) {
|
|
310
|
+
out.properties = {};
|
|
311
|
+
for (const name of names) {
|
|
312
|
+
const parts = [];
|
|
313
|
+
for (const L of layers) {
|
|
314
|
+
if (L.properties && Object.prototype.hasOwnProperty.call(L.properties, name)) parts.push(L.properties[name]);
|
|
315
|
+
else if (L.additionalProperties !== undefined) parts.push(L.additionalProperties);
|
|
316
|
+
}
|
|
317
|
+
out.properties[name] = combine(parts);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
{
|
|
321
|
+
const parts = [];
|
|
322
|
+
for (const L of layers) if (L.additionalProperties !== undefined) parts.push(L.additionalProperties);
|
|
323
|
+
if (parts.length > 0) {
|
|
324
|
+
const merged = combine(parts);
|
|
325
|
+
if (merged !== true) out.additionalProperties = merged;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
{
|
|
329
|
+
const parts = [];
|
|
330
|
+
for (const L of layers) if (L.items !== undefined) parts.push(L.items);
|
|
331
|
+
if (parts.length > 0) {
|
|
332
|
+
const merged = combine(parts);
|
|
333
|
+
if (merged !== true) out.items = merged;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
{
|
|
337
|
+
// Position by position, and a tuple that runs out falls back to whatever
|
|
338
|
+
// that branch says about the rest.
|
|
339
|
+
let longest = -1;
|
|
340
|
+
for (const L of layers) {
|
|
341
|
+
if (L.prefixItems === undefined) continue;
|
|
342
|
+
if (!Array.isArray(L.prefixItems)) decline('prefixItems:not-array');
|
|
343
|
+
if (L.prefixItems.length > longest) longest = L.prefixItems.length;
|
|
344
|
+
}
|
|
345
|
+
if (longest >= 0) {
|
|
346
|
+
const tuple = [];
|
|
347
|
+
for (let k = 0; k < longest; k++) {
|
|
348
|
+
const parts = [];
|
|
349
|
+
for (const L of layers) {
|
|
350
|
+
if (L.prefixItems !== undefined && k < L.prefixItems.length) parts.push(L.prefixItems[k]);
|
|
351
|
+
else if (L.items !== undefined) parts.push(L.items);
|
|
352
|
+
}
|
|
353
|
+
tuple.push(combine(parts));
|
|
354
|
+
}
|
|
355
|
+
out.prefixItems = tuple;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Anything not merged above is carried through untouched, so an unsupported
|
|
360
|
+
// keyword still reaches the check that declines it rather than disappearing.
|
|
361
|
+
const HANDLED = new Set(['type', 'required', 'properties', 'additionalProperties', 'items', 'prefixItems']
|
|
362
|
+
.concat(TIGHTEN_MAX, TIGHTEN_MIN, SAME_OR_NOTHING));
|
|
363
|
+
for (const L of layers) {
|
|
364
|
+
for (const k of Object.keys(L)) {
|
|
365
|
+
if (HANDLED.has(k)) continue;
|
|
366
|
+
if (out[k] === undefined) out[k] = L[k];
|
|
367
|
+
else if (JSON.stringify(out[k]) !== JSON.stringify(L[k])) decline('allOf:conflicting:' + k);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
// The compile-time twin of the rolling hash the key scan accumulates. A key
|
|
375
|
+
// that carries an escape never reaches the dispatch (escaped keys bail), so
|
|
376
|
+
// hashing the raw span is hashing the decoded name.
|
|
377
|
+
function spanHash(name) {
|
|
378
|
+
let h = 5381;
|
|
379
|
+
for (let i = 0; i < name.length; i++) h = (Math.imul(h, 33) + name.charCodeAt(i)) | 0;
|
|
380
|
+
return h;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Above this many known names, a chain inside each key-length bucket is the
|
|
384
|
+
// membership quadratic again, this time in the scanner's own dispatch. The
|
|
385
|
+
// hash lands in a real lookup object passed through the helpers array, so a
|
|
386
|
+
// thousand names cost no generated source, and the dense id switch below it
|
|
387
|
+
// is a jump table.
|
|
388
|
+
const HASH_DISPATCH_MIN = 48;
|
|
389
|
+
|
|
390
|
+
function compileScanner(schema, options) {
|
|
391
|
+
const opts = options || {};
|
|
392
|
+
const ctx = {
|
|
393
|
+
uid: 0, helpers: [], userFormats: opts.userFormats || null,
|
|
394
|
+
root: schema, refStack: [], nodes: 0,
|
|
395
|
+
};
|
|
396
|
+
declineReason = null;
|
|
397
|
+
let body;
|
|
398
|
+
try {
|
|
399
|
+
const out = [];
|
|
400
|
+
out.push('var len=s.length,i=0,c=s.charCodeAt(0);');
|
|
401
|
+
out.push(WS);
|
|
402
|
+
emitValue(schema, ctx, out, 0);
|
|
403
|
+
out.push(WS);
|
|
404
|
+
out.push('if(i<len)return 0;');
|
|
405
|
+
out.push('return 1;');
|
|
406
|
+
body = out.join('\n');
|
|
407
|
+
} catch (e) {
|
|
408
|
+
if (e === DECLINE) {
|
|
409
|
+
if (opts.onDecline) opts.onDecline(declineReason);
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
throw e;
|
|
413
|
+
}
|
|
414
|
+
let fn;
|
|
415
|
+
try {
|
|
416
|
+
// eslint-disable-next-line no-new-func
|
|
417
|
+
fn = new Function('_h', '_r', 'return function scan(s){\n' + body + '\n}')(ctx.helpers, require('./scan-runtime'));
|
|
418
|
+
} catch {
|
|
419
|
+
return null; // a runtime without code generation has no scanner, by design
|
|
420
|
+
}
|
|
421
|
+
return { scan: fn, source: body, helpers: ctx.helpers.length };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function emitValue(schema, ctx, out, depth) {
|
|
425
|
+
if (depth > MAX_SCHEMA_DEPTH) decline('depth');
|
|
426
|
+
if (schema === true) { emitSkip(out); return; }
|
|
427
|
+
if (schema === false) { out.push('return 0;'); return; }
|
|
428
|
+
if (!isPlainObject(schema)) decline('schema-not-object');
|
|
429
|
+
if (++ctx.nodes > MAX_NODES) decline('too-many-nodes');
|
|
430
|
+
for (const k of Object.keys(schema)) {
|
|
431
|
+
if (BASE_CHANGING.has(k) && !(k === '$id' && depth === 0)) decline('base-changing:' + k);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// A reference is inlined. Draft 7 lets `$ref` override its siblings and 2020-12
|
|
435
|
+
// applies both, so a reference with any sibling that bears on the verdict is
|
|
436
|
+
// declined rather than read under one dialect's rule and compiled under the
|
|
437
|
+
// other's. A reference that re-enters one already being expanded describes a
|
|
438
|
+
// recursive document, which does not fit in a finite emitted function.
|
|
439
|
+
if (schema.$ref !== undefined) {
|
|
440
|
+
for (const k of Object.keys(schema)) {
|
|
441
|
+
if (k !== '$ref' && !IGNORED.has(k)) decline('$ref:sibling:' + k);
|
|
442
|
+
}
|
|
443
|
+
const ref = schema.$ref;
|
|
444
|
+
if (ctx.refStack.includes(ref)) decline('$ref:recursive');
|
|
445
|
+
const target = resolveLocalRef(ctx.root, ref);
|
|
446
|
+
ctx.refStack.push(ref);
|
|
447
|
+
emitValue(target, ctx, out, depth);
|
|
448
|
+
ctx.refStack.pop();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// An intersection becomes one schema, and then the ordinary emitter runs. A
|
|
453
|
+
// branch that is itself an intersection merges when its own turn comes.
|
|
454
|
+
if (schema.allOf !== undefined) {
|
|
455
|
+
emitValue(mergeAllOf(schema, ctx), ctx, out, depth);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const present = [];
|
|
460
|
+
for (const k of Object.keys(schema)) {
|
|
461
|
+
// A root `$id` names the document. It does not move the base a
|
|
462
|
+
// fragment-only pointer resolves against, which is the only kind of
|
|
463
|
+
// reference the scanner follows, so it carries no meaning here.
|
|
464
|
+
if (k === '$id' && depth === 0) continue;
|
|
465
|
+
if (IGNORED.has(k)) continue;
|
|
466
|
+
if (STRUCTURAL.has(k) || LEAF.has(k)) { present.push(k); continue; }
|
|
467
|
+
decline('keyword:' + k);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const types = typeSet(schema);
|
|
471
|
+
const has = (k) => present.includes(k);
|
|
472
|
+
|
|
473
|
+
// const and enum over a composite would need the whole subtree as a value,
|
|
474
|
+
// which is the materialisation the scanner exists to avoid.
|
|
475
|
+
// A composite value would have to be compared as a whole, which is the
|
|
476
|
+
// materialisation the scanner exists to avoid. An empty `enum` admits
|
|
477
|
+
// nothing, so it bars composites too: the test is that the keyword is
|
|
478
|
+
// present, not that it lists anything.
|
|
479
|
+
let compositeAllowed = true;
|
|
480
|
+
let compositeDelegate = null;
|
|
481
|
+
if (has('const') || has('enum')) {
|
|
482
|
+
compositeAllowed = false;
|
|
483
|
+
const listed = [];
|
|
484
|
+
if (has('const')) listed.push(schema.const);
|
|
485
|
+
if (has('enum')) {
|
|
486
|
+
if (!Array.isArray(schema.enum)) decline('enum:not-array');
|
|
487
|
+
for (const v of schema.enum) listed.push(v);
|
|
488
|
+
}
|
|
489
|
+
// An object or an array can only be compared as a whole. The scan steps
|
|
490
|
+
// over that one subtree, parses its own text, and hands it to the check
|
|
491
|
+
// the validator itself runs, so the document around it is still never
|
|
492
|
+
// built and the comparison is the validator's, not a copy of it.
|
|
493
|
+
if (listed.some((v) => v !== null && typeof v === 'object')) {
|
|
494
|
+
const fn = compileToJSCodegen(schema, null, ctx.userFormats);
|
|
495
|
+
if (typeof fn !== 'function') decline('const/enum:composite');
|
|
496
|
+
ctx.helpers.push(fn);
|
|
497
|
+
compositeDelegate = '_h[' + (ctx.helpers.length - 1) + ']';
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const branches = [];
|
|
502
|
+
|
|
503
|
+
const compositeBranch = () => {
|
|
504
|
+
const k = ctx.uid++;
|
|
505
|
+
return ['var _cs' + k + '=i;var _ce' + k + '=_r.skipValue(s,i);'
|
|
506
|
+
+ 'if(_ce' + k + '===-2)return -1;if(_ce' + k + '<0)return 0;'
|
|
507
|
+
+ 'var _cv' + k + ';try{_cv' + k + '=JSON.parse(s.slice(_cs' + k + ',_ce' + k + '))}catch(_ce_){return 0}'
|
|
508
|
+
+ 'if(!' + compositeDelegate + '(_cv' + k + '))return 0;'
|
|
509
|
+
+ 'i=_ce' + k + ';c=s.charCodeAt(i);'];
|
|
510
|
+
};
|
|
511
|
+
if (allows(types, 'object')) {
|
|
512
|
+
if (compositeDelegate) branches.push(['c===123', compositeBranch()]);
|
|
513
|
+
else if (!compositeAllowed) branches.push(['c===123', ['return 0;']]);
|
|
514
|
+
else {
|
|
515
|
+
const b = [];
|
|
516
|
+
emitObject(schema, ctx, b, depth);
|
|
517
|
+
branches.push(['c===123', b]);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (allows(types, 'array')) {
|
|
521
|
+
if (compositeDelegate) branches.push(['c===91', compositeBranch()]);
|
|
522
|
+
else if (!compositeAllowed) branches.push(['c===91', ['return 0;']]);
|
|
523
|
+
else {
|
|
524
|
+
const b = [];
|
|
525
|
+
emitArray(schema, ctx, b, depth);
|
|
526
|
+
branches.push(['c===91', b]);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (allows(types, 'string')) {
|
|
530
|
+
const b = [];
|
|
531
|
+
emitString(schema, ctx, b);
|
|
532
|
+
branches.push(['c===34', b]);
|
|
533
|
+
}
|
|
534
|
+
if (allows(types, 'number')) {
|
|
535
|
+
const b = [];
|
|
536
|
+
emitNumber(schema, ctx, b, types);
|
|
537
|
+
branches.push(['c===45||(c>=48&&c<=57)', b]);
|
|
538
|
+
}
|
|
539
|
+
if (allows(types, 'boolean')) {
|
|
540
|
+
const b = [];
|
|
541
|
+
emitLiteral(schema, ctx, b, 'boolean');
|
|
542
|
+
branches.push(['c===116||c===102', b]);
|
|
543
|
+
}
|
|
544
|
+
if (allows(types, 'null')) {
|
|
545
|
+
const b = [];
|
|
546
|
+
emitLiteral(schema, ctx, b, 'null');
|
|
547
|
+
branches.push(['c===110', b]);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (branches.length === 0) { out.push('return 0;'); return; }
|
|
551
|
+
const parts = [];
|
|
552
|
+
for (let k = 0; k < branches.length; k++) {
|
|
553
|
+
parts.push((k === 0 ? 'if(' : 'else if(') + branches[k][0] + '){');
|
|
554
|
+
parts.push(branches[k][1].join('\n'));
|
|
555
|
+
parts.push('}');
|
|
556
|
+
}
|
|
557
|
+
parts.push('else return 0;');
|
|
558
|
+
out.push(parts.join('\n'));
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Any JSON value, validated as JSON and stepped over.
|
|
562
|
+
function emitSkip(out) {
|
|
563
|
+
out.push('{var _j=_r.skipValue(s,i);if(_j===-2)return -1;if(_j<0)return 0;i=_j;c=s.charCodeAt(i);}');
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Leaves `_p<n>` just past the opening quote, `_e<n>` at the closing one,
|
|
567
|
+
// `_x<n>` set when the span carries an escape, `_u<n>` the UTF-16 length when
|
|
568
|
+
// one was asked for. `i` ends just past the string and `c` is in step.
|
|
569
|
+
function emitStringSpan(ctx, out, n, wantLen, wantHash) {
|
|
570
|
+
out.push('var _p' + n + '=++i,_x' + n + '=0' + (wantLen ? ',_u' + n + '=0,_hs' + n + '=0' : '') + (wantHash ? ',_h' + n + '=5381' : '') + ';');
|
|
571
|
+
out.push('for(;;){c=s.charCodeAt(i);');
|
|
572
|
+
out.push('if(c===34)break;');
|
|
573
|
+
out.push('if(c===92){var _q' + n + '=s.charCodeAt(i+1);');
|
|
574
|
+
out.push('if(_q' + n + '===117){if(!_r.hex4(s,i+2))return 0;i+=6;_x' + n + '=1;' + (wantLen ? '_u' + n + '++;' : '') + 'continue;}');
|
|
575
|
+
out.push('if(_q' + n + '===34||_q' + n + '===92||_q' + n + '===47||_q' + n + '===98||_q' + n + '===102||_q' + n + '===110||_q' + n + '===114||_q' + n + '===116){i+=2;_x' + n + '=1;' + (wantLen ? '_u' + n + '++;' : '') + 'continue;}');
|
|
576
|
+
out.push('return 0;}');
|
|
577
|
+
out.push('if(!(c>=32))return 0;');
|
|
578
|
+
// One wraparound compare says whether this unit is a high surrogate, which is
|
|
579
|
+
// the only case where counting units is not counting characters.
|
|
580
|
+
out.push('i++;' + (wantLen ? '_u' + n + '++;if(((c-55296)>>>0)<1024)_hs' + n + '=1;' : '') + (wantHash ? '_h' + n + '=(Math.imul(_h' + n + ',33)+c)|0;' : ''));
|
|
581
|
+
out.push('}');
|
|
582
|
+
out.push('var _e' + n + '=i;c=s.charCodeAt(++i);');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function emitString(schema, ctx, out) {
|
|
586
|
+
const n = ctx.uid++;
|
|
587
|
+
const inlineLen = schema.minLength !== undefined || schema.maxLength !== undefined;
|
|
588
|
+
if (schema.minLength !== undefined && typeof schema.minLength !== 'number') decline('minLength:not-number');
|
|
589
|
+
if (schema.maxLength !== undefined && typeof schema.maxLength !== 'number') decline('maxLength:not-number');
|
|
590
|
+
const delegated = [];
|
|
591
|
+
for (const k of ['pattern', 'format', 'const', 'enum']) if (schema[k] !== undefined) delegated.push(k);
|
|
592
|
+
|
|
593
|
+
emitStringSpan(ctx, out, n, inlineLen);
|
|
594
|
+
if (inlineLen) {
|
|
595
|
+
// minLength and maxLength count Unicode code points. Counting code units
|
|
596
|
+
// while scanning is right for every span that carries neither an escape
|
|
597
|
+
// nor a surrogate, which is almost all of them; for the rest the string is
|
|
598
|
+
// materialised and the validator's own length check answers, so there is
|
|
599
|
+
// one definition of "characters" and the scanner does not hold a copy.
|
|
600
|
+
const lenKeys = [];
|
|
601
|
+
if (schema.minLength !== undefined) lenKeys.push('minLength');
|
|
602
|
+
if (schema.maxLength !== undefined) lenKeys.push('maxLength');
|
|
603
|
+
const lf = compileLeaf(lenKeys, schema, ctx);
|
|
604
|
+
out.push('if(_x' + n + '||_hs' + n + '){if(!' + lf + '(_r.span(s,_p' + n + ',_e' + n + ',_x' + n + ')))return 0;}');
|
|
605
|
+
out.push('else{');
|
|
606
|
+
if (schema.minLength !== undefined) out.push('if(_u' + n + '<' + schema.minLength + ')return 0;');
|
|
607
|
+
if (schema.maxLength !== undefined) out.push('if(_u' + n + '>' + schema.maxLength + ')return 0;');
|
|
608
|
+
out.push('}');
|
|
609
|
+
}
|
|
610
|
+
if (delegated.length > 0) {
|
|
611
|
+
// `const` and `enum` may list non-strings; the compiled check answers that
|
|
612
|
+
// for the materialised string exactly as it would for a parsed one.
|
|
613
|
+
const f = compileLeaf(delegated, schema, ctx);
|
|
614
|
+
out.push('if(!' + f + '(_r.span(s,_p' + n + ',_e' + n + ',_x' + n + ')))return 0;');
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function emitNumber(schema, ctx, out, types) {
|
|
619
|
+
const n = ctx.uid++;
|
|
620
|
+
for (const k of ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum']) {
|
|
621
|
+
if (schema[k] !== undefined && typeof schema[k] !== 'number') decline('bound:' + k + ':not-number');
|
|
622
|
+
}
|
|
623
|
+
const delegated = [];
|
|
624
|
+
for (const k of ['multipleOf', 'const', 'enum']) if (schema[k] !== undefined) delegated.push(k);
|
|
625
|
+
const intOnly = integerOnly(types);
|
|
626
|
+
// `type: number` rejects a literal that overflows to Infinity, so the value
|
|
627
|
+
// is needed whenever a type was named at all.
|
|
628
|
+
const wantValue = delegated.length > 0
|
|
629
|
+
|| schema.minimum !== undefined || schema.maximum !== undefined
|
|
630
|
+
|| schema.exclusiveMinimum !== undefined || schema.exclusiveMaximum !== undefined
|
|
631
|
+
|| types !== null;
|
|
632
|
+
|
|
633
|
+
out.push('var _np' + n + '=i,_v' + n + '=0,_ng' + n + '=0,_nd' + n + '=0,_pl' + n + '=1;');
|
|
634
|
+
out.push('if(c===45){_ng' + n + '=1;c=s.charCodeAt(++i);}');
|
|
635
|
+
out.push('if(c===48){_nd' + n + '=1;c=s.charCodeAt(++i);}');
|
|
636
|
+
out.push('else if(c>=49&&c<=57){do{_v' + n + '=_v' + n + '*10+(c-48);_nd' + n + '++;c=s.charCodeAt(++i);}while(c>=48&&c<=57);}');
|
|
637
|
+
out.push('else return 0;');
|
|
638
|
+
out.push('if(c===46){_pl' + n + '=0;c=s.charCodeAt(++i);if(!(c>=48&&c<=57))return 0;do{c=s.charCodeAt(++i);}while(c>=48&&c<=57);}');
|
|
639
|
+
out.push('if(c===101||c===69){_pl' + n + '=0;c=s.charCodeAt(++i);if(c===43||c===45)c=s.charCodeAt(++i);if(!(c>=48&&c<=57))return 0;do{c=s.charCodeAt(++i);}while(c>=48&&c<=57);}');
|
|
640
|
+
if (wantValue) {
|
|
641
|
+
// Digits accumulated during the scan are exact below 2^53; past that, and
|
|
642
|
+
// for anything with a fraction or an exponent, the parser's own conversion
|
|
643
|
+
// is the one that has to agree.
|
|
644
|
+
out.push('if(_pl' + n + '===0||_nd' + n + '>15){_v' + n + '=+s.slice(_np' + n + ',i);}else if(_ng' + n + '){_v' + n + '=-_v' + n + ';}');
|
|
645
|
+
}
|
|
646
|
+
if (intOnly) out.push('if(_pl' + n + '===0&&!Number.isInteger(_v' + n + '))return 0;');
|
|
647
|
+
if (types !== null) out.push('if(!isFinite(_v' + n + '))return 0;');
|
|
648
|
+
if (schema.minimum !== undefined) out.push('if(!(_v' + n + '>=' + schema.minimum + '))return 0;');
|
|
649
|
+
if (schema.maximum !== undefined) out.push('if(!(_v' + n + '<=' + schema.maximum + '))return 0;');
|
|
650
|
+
if (schema.exclusiveMinimum !== undefined) out.push('if(!(_v' + n + '>' + schema.exclusiveMinimum + '))return 0;');
|
|
651
|
+
if (schema.exclusiveMaximum !== undefined) out.push('if(!(_v' + n + '<' + schema.exclusiveMaximum + '))return 0;');
|
|
652
|
+
if (delegated.length > 0) {
|
|
653
|
+
const f = compileLeaf(delegated, schema, ctx);
|
|
654
|
+
out.push('if(!' + f + '(_v' + n + '))return 0;');
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// true, false and null carry no information the scan has to read, so whether
|
|
659
|
+
// const or enum admits them is settled while the scanner is built.
|
|
660
|
+
function emitLiteral(schema, ctx, out, kind) {
|
|
661
|
+
const delegated = [];
|
|
662
|
+
for (const k of ['const', 'enum']) if (schema[k] !== undefined) delegated.push(k);
|
|
663
|
+
let allowTrue = true, allowFalse = true, allowNull = true;
|
|
664
|
+
if (delegated.length > 0) {
|
|
665
|
+
const sub = {};
|
|
666
|
+
for (const k of delegated) sub[k] = schema[k];
|
|
667
|
+
const fn = compileToJSCodegen(sub, null, ctx.userFormats);
|
|
668
|
+
if (typeof fn !== 'function') decline('literal-leaf');
|
|
669
|
+
allowTrue = fn(true) === true;
|
|
670
|
+
allowFalse = fn(false) === true;
|
|
671
|
+
allowNull = fn(null) === true;
|
|
672
|
+
}
|
|
673
|
+
if (kind === 'null') {
|
|
674
|
+
if (!allowNull) { out.push('return 0;'); return; }
|
|
675
|
+
out.push('if(s.charCodeAt(i+1)!==117||s.charCodeAt(i+2)!==108||s.charCodeAt(i+3)!==108)return 0;');
|
|
676
|
+
out.push('i+=4;c=s.charCodeAt(i);');
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
out.push('if(c===116){' + (allowTrue ? '' : 'return 0;') + 'if(s.charCodeAt(i+1)!==114||s.charCodeAt(i+2)!==117||s.charCodeAt(i+3)!==101)return 0;i+=4;}');
|
|
680
|
+
out.push('else{' + (allowFalse ? '' : 'return 0;') + 'if(s.charCodeAt(i+1)!==97||s.charCodeAt(i+2)!==108||s.charCodeAt(i+3)!==115||s.charCodeAt(i+4)!==101)return 0;i+=5;}');
|
|
681
|
+
out.push('c=s.charCodeAt(i);');
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function emitArray(schema, ctx, out, depth) {
|
|
685
|
+
const n = ctx.uid++;
|
|
686
|
+
for (const k of ['minItems', 'maxItems']) {
|
|
687
|
+
if (schema[k] !== undefined && typeof schema[k] !== 'number') decline('itemsCount:' + k + ':not-number');
|
|
688
|
+
}
|
|
689
|
+
// Same synonym argument as the object side: with items present every
|
|
690
|
+
// position past prefixItems is evaluated and unevaluatedItems never fires;
|
|
691
|
+
// without it, unevaluatedItems covers exactly those positions.
|
|
692
|
+
const items = schema.items !== undefined ? schema.items : schema.unevaluatedItems;
|
|
693
|
+
// The draft-7 array form of `items`, with `additionalItems` for the rest, is
|
|
694
|
+
// deliberately not read here: whether an array `items` is a tuple depends on
|
|
695
|
+
// the dialect, and the scanner is not told which one it is compiling.
|
|
696
|
+
if (items !== undefined && !isPlainObject(items) && typeof items !== 'boolean') decline('items:tuple');
|
|
697
|
+
const prefix = schema.prefixItems;
|
|
698
|
+
if (prefix !== undefined) {
|
|
699
|
+
if (!Array.isArray(prefix)) decline('prefixItems:not-array');
|
|
700
|
+
if (prefix.length > MAX_PREFIX_ITEMS) decline('prefixItems:too-long');
|
|
701
|
+
}
|
|
702
|
+
// The position has to be counted to know which tuple entry applies, whether
|
|
703
|
+
// or not anyone asked for a size.
|
|
704
|
+
const wantCount = schema.minItems !== undefined || schema.maxItems !== undefined || prefix !== undefined;
|
|
705
|
+
|
|
706
|
+
const emitRest = (into) => {
|
|
707
|
+
if (items === undefined || items === true) emitSkip(into);
|
|
708
|
+
else emitValue(items, ctx, into, depth + 1);
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
out.push('c=s.charCodeAt(++i);');
|
|
712
|
+
if (wantCount) out.push('var _n' + n + '=0;');
|
|
713
|
+
out.push(WS);
|
|
714
|
+
out.push('if(c!==93){for(;;){');
|
|
715
|
+
if (prefix === undefined) {
|
|
716
|
+
emitRest(out);
|
|
717
|
+
} else {
|
|
718
|
+
for (let k = 0; k < prefix.length; k++) {
|
|
719
|
+
out.push((k === 0 ? 'if(' : 'else if(') + '_n' + n + '===' + k + '){');
|
|
720
|
+
emitValue(prefix[k], ctx, out, depth + 1);
|
|
721
|
+
out.push('}');
|
|
722
|
+
}
|
|
723
|
+
out.push('else{');
|
|
724
|
+
emitRest(out);
|
|
725
|
+
out.push('}');
|
|
726
|
+
}
|
|
727
|
+
if (wantCount) {
|
|
728
|
+
out.push('_n' + n + '++;');
|
|
729
|
+
if (schema.maxItems !== undefined) out.push('if(_n' + n + '>' + schema.maxItems + ')return 0;');
|
|
730
|
+
}
|
|
731
|
+
out.push(WS);
|
|
732
|
+
out.push('if(c===44){c=s.charCodeAt(++i);' + WS + 'continue;}');
|
|
733
|
+
out.push('if(c===93)break;');
|
|
734
|
+
out.push('return 0;}}');
|
|
735
|
+
out.push('c=s.charCodeAt(++i);');
|
|
736
|
+
if (schema.minItems !== undefined) {
|
|
737
|
+
if (wantCount) out.push('if(_n' + n + '<' + schema.minItems + ')return 0;');
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// One object member's value. A value that fails does not end the scan: JSON
|
|
742
|
+
// lets a name repeat and the parser keeps the last one, so the member the
|
|
743
|
+
// parser would have judged may still be ahead. The failure is recorded, the
|
|
744
|
+
// value is stepped over, and scanning continues; a repeat of any name already
|
|
745
|
+
// seen bails, and an object that reaches its closing brace with a recorded
|
|
746
|
+
// failure and no repeat is invalid.
|
|
747
|
+
//
|
|
748
|
+
// Every `return 0` inside the member becomes that record, which is also how
|
|
749
|
+
// the rule reaches nested objects: an inner object's own verdict is a value
|
|
750
|
+
// failure at the level above it.
|
|
751
|
+
function emitMember(sub, ctx, out, depth, objId) {
|
|
752
|
+
const lines = [];
|
|
753
|
+
emitValue(sub, ctx, lines, depth + 1);
|
|
754
|
+
const body = lines.join('\n');
|
|
755
|
+
if (!body.includes('return 0;')) { out.push(body); return; }
|
|
756
|
+
const label = '_mb' + (ctx.uid++);
|
|
757
|
+
out.push('var _vs' + label + '=i;');
|
|
758
|
+
out.push(label + ':{');
|
|
759
|
+
out.push(body.split('return 0;').join('{_f' + objId + '=2;break ' + label + ';}'));
|
|
760
|
+
out.push('}');
|
|
761
|
+
// Resync from where the value started: a scan that stopped partway through
|
|
762
|
+
// one cannot say where it ends, and the skipper can.
|
|
763
|
+
out.push('if(_f' + objId + '===2){_f' + objId + '=1;i=_r.skipValue(s,_vs' + label + ');if(i<0)return 0;c=s.charCodeAt(i);}');
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function emitObject(schema, ctx, out, depth) {
|
|
767
|
+
const n = ctx.uid++;
|
|
768
|
+
const props = schema.properties;
|
|
769
|
+
if (props !== undefined && !isPlainObject(props)) decline('properties:not-object');
|
|
770
|
+
const required = schema.required;
|
|
771
|
+
if (required !== undefined && !Array.isArray(required)) decline('required:not-array');
|
|
772
|
+
// additionalProperties wins when both are present, because with it present
|
|
773
|
+
// every property is evaluated and unevaluatedProperties never fires.
|
|
774
|
+
const ap = schema.additionalProperties !== undefined ? schema.additionalProperties : schema.unevaluatedProperties;
|
|
775
|
+
if (ap !== undefined && !isPlainObject(ap) && typeof ap !== 'boolean') decline('additionalProperties:shape');
|
|
776
|
+
for (const k of ['minProperties', 'maxProperties']) {
|
|
777
|
+
if (schema[k] !== undefined && typeof schema[k] !== 'number') decline('propCount:' + k + ':not-number');
|
|
778
|
+
}
|
|
779
|
+
const counting = schema.minProperties !== undefined || schema.maxProperties !== undefined;
|
|
780
|
+
// A duplicate key is last-wins for the parser but two members for the scan,
|
|
781
|
+
// so a count over a document that may repeat an unknown name is not the same
|
|
782
|
+
// count. With `additionalProperties: false` every name is a known name and a
|
|
783
|
+
// repeat bails, so the count is safe there and only there.
|
|
784
|
+
if (counting && ap !== false) decline('min/maxProperties:open-object');
|
|
785
|
+
|
|
786
|
+
const names = [];
|
|
787
|
+
if (props) for (const k of Object.keys(props)) names.push(k);
|
|
788
|
+
if (required) for (const k of required) {
|
|
789
|
+
if (typeof k !== 'string') decline('required:not-string');
|
|
790
|
+
if (!names.includes(k)) names.push(k);
|
|
791
|
+
}
|
|
792
|
+
if (names.length > MAX_PROPS) decline('too-many-properties');
|
|
793
|
+
// Each name owns one bit in a row of 31-bit words: word idx/31, bit
|
|
794
|
+
// 1 << (idx % 31). 31 rather than 32 keeps every mask a positive SMI.
|
|
795
|
+
const words = Math.max(1, Math.ceil(names.length / 31));
|
|
796
|
+
const bit = new Map();
|
|
797
|
+
names.forEach((k, idx) => bit.set(k, { w: (idx / 31) | 0, b: 1 << (idx % 31) }));
|
|
798
|
+
const reqMasks = new Array(words).fill(0);
|
|
799
|
+
if (required) for (const k of required) { const x = bit.get(k); reqMasks[x.w] |= x.b; }
|
|
800
|
+
|
|
801
|
+
out.push('c=s.charCodeAt(++i);');
|
|
802
|
+
const useHash = names.length >= HASH_DISPATCH_MIN;
|
|
803
|
+
if (useHash) {
|
|
804
|
+
out.push('var _sa' + n + '=new Int32Array(' + (Math.ceil(names.length / 32) || 1) + '),_nx' + n + '=0,_f' + n + '=0' + (counting ? ',_c' + n + '=0' : '') + ';');
|
|
805
|
+
} else {
|
|
806
|
+
const seenDecl = [];
|
|
807
|
+
for (let w = 0; w < words; w++) seenDecl.push('_s' + n + '_' + w + '=0');
|
|
808
|
+
out.push('var ' + seenDecl.join(',') + ',_f' + n + '=0' + (counting ? ',_c' + n + '=0' : '') + ';');
|
|
809
|
+
}
|
|
810
|
+
out.push(WS);
|
|
811
|
+
out.push('if(c!==125){for(;;){');
|
|
812
|
+
out.push(WS);
|
|
813
|
+
out.push('if(c!==34)return 0;');
|
|
814
|
+
const kn = ctx.uid++;
|
|
815
|
+
// Hash dispatch pays off only past the threshold, and only when no two
|
|
816
|
+
// known names collide, which the chain fallback below covers for free.
|
|
817
|
+
let hashById = null;
|
|
818
|
+
if (names.length >= HASH_DISPATCH_MIN) {
|
|
819
|
+
const table = new Map();
|
|
820
|
+
let collision = false;
|
|
821
|
+
names.forEach((nm, id) => {
|
|
822
|
+
const h = spanHash(nm);
|
|
823
|
+
if (table.has(h)) collision = true;
|
|
824
|
+
table.set(h, id);
|
|
825
|
+
});
|
|
826
|
+
if (!collision) {
|
|
827
|
+
ctx.helpers.push(table);
|
|
828
|
+
hashById = '_h[' + (ctx.helpers.length - 1) + ']';
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
emitStringSpan(ctx, out, kn, false, hashById !== null);
|
|
832
|
+
// A key written with an escape decodes to a name the raw span does not show,
|
|
833
|
+
// so the dispatch below would read it as a different property.
|
|
834
|
+
out.push('if(_x' + kn + ')return -1;');
|
|
835
|
+
out.push(WS);
|
|
836
|
+
out.push('if(c!==58)return 0;');
|
|
837
|
+
out.push('c=s.charCodeAt(++i);');
|
|
838
|
+
out.push(WS);
|
|
839
|
+
if (counting) out.push('_c' + n + '++;');
|
|
840
|
+
out.push('var _kl' + kn + '=_e' + kn + '-_p' + kn + ';');
|
|
841
|
+
|
|
842
|
+
if (hashById !== null) {
|
|
843
|
+
// Three ideas carry this branch, all for the same reason: a thousand
|
|
844
|
+
// declared names must not become a thousand of anything at runtime or in
|
|
845
|
+
// the generated source.
|
|
846
|
+
// - Dispatch is the hash the key scan already accumulated, one lookup in
|
|
847
|
+
// a real object passed through the helpers, one startsWith against the
|
|
848
|
+
// name table to confirm. An unknown key whose hash happens to match
|
|
849
|
+
// fails the confirmation and is a stranger like any other.
|
|
850
|
+
// - Names sharing a subschema share one emitted body: ids are assigned
|
|
851
|
+
// group-contiguous, so the body choice is a couple of range tests and
|
|
852
|
+
// the source stays proportional to the number of DISTINCT subschemas.
|
|
853
|
+
// A megabyte-sized function never reaches V8's optimizer, which costs
|
|
854
|
+
// more than any dispatch wins.
|
|
855
|
+
// - `seen` is an Int32Array indexed by id, so the duplicate bail and the
|
|
856
|
+
// required check need no per-name code at all.
|
|
857
|
+
const addH = (() => {
|
|
858
|
+
const a = [];
|
|
859
|
+
if (ap === false) a.push('return 0;');
|
|
860
|
+
else if (ap === undefined || ap === true) emitSkip(a);
|
|
861
|
+
else {
|
|
862
|
+
const l = [];
|
|
863
|
+
emitValue(ap, ctx, l, depth + 1);
|
|
864
|
+
a.push(l.join('\n').split('return 0;').join('return -1;'));
|
|
865
|
+
}
|
|
866
|
+
return a.join('\n');
|
|
867
|
+
})();
|
|
868
|
+
|
|
869
|
+
// Group by identical subschema; required-only names share the additional
|
|
870
|
+
// body and still get a bit for the required mask.
|
|
871
|
+
const groups = new Map();
|
|
872
|
+
for (const name of names) {
|
|
873
|
+
const sub = props && Object.prototype.hasOwnProperty.call(props, name) ? props[name] : undefined;
|
|
874
|
+
const key = sub === undefined ? '\u0000additional' : JSON.stringify(sub);
|
|
875
|
+
if (!groups.has(key)) groups.set(key, { sub, members: [] });
|
|
876
|
+
groups.get(key).members.push(name);
|
|
877
|
+
}
|
|
878
|
+
const ordered = [];
|
|
879
|
+
const bounds = [];
|
|
880
|
+
for (const g of groups.values()) {
|
|
881
|
+
for (const nm of g.members) ordered.push(nm);
|
|
882
|
+
bounds.push({ end: ordered.length, sub: g.sub });
|
|
883
|
+
}
|
|
884
|
+
const table = new Map();
|
|
885
|
+
ordered.forEach((nm, id) => { table.set(spanHash(nm), id); });
|
|
886
|
+
ctx.helpers.push(table);
|
|
887
|
+
const tbl = '_h[' + (ctx.helpers.length - 1) + ']';
|
|
888
|
+
ctx.helpers.push(ordered.slice());
|
|
889
|
+
const nmArr = '_h[' + (ctx.helpers.length - 1) + ']';
|
|
890
|
+
const wordCount = Math.ceil(ordered.length / 32) || 1;
|
|
891
|
+
const rq = new Int32Array(wordCount);
|
|
892
|
+
if (required) for (const k of required) { const id = ordered.indexOf(k); rq[id >> 5] |= 1 << (id & 31); }
|
|
893
|
+
ctx.helpers.push(rq);
|
|
894
|
+
const rqArr = '_h[' + (ctx.helpers.length - 1) + ']';
|
|
895
|
+
|
|
896
|
+
// Machine-written JSON keeps its keys in schema order, so the key that
|
|
897
|
+
// arrives is usually the one expected next. One length test and one
|
|
898
|
+
// startsWith settle that case with the hash, the Map and the name-table
|
|
899
|
+
// verification all skipped; a document in any other order just fails one
|
|
900
|
+
// comparison per key and takes the exact path below.
|
|
901
|
+
const eids = new Int32Array(names.length);
|
|
902
|
+
names.forEach((nm, si) => { eids[si] = ordered.indexOf(nm); });
|
|
903
|
+
ctx.helpers.push(eids);
|
|
904
|
+
const eidArr = '_h[' + (ctx.helpers.length - 1) + ']';
|
|
905
|
+
// The reverse map, so a document that omits optional keys resynchronises
|
|
906
|
+
// the expected pointer on the first hash hit instead of losing the fast
|
|
907
|
+
// path for the rest of the object.
|
|
908
|
+
const sids = new Int32Array(ordered.length);
|
|
909
|
+
eids.forEach((id, si) => { sids[id] = si; });
|
|
910
|
+
ctx.helpers.push(sids);
|
|
911
|
+
const sidArr = '_h[' + (ctx.helpers.length - 1) + ']';
|
|
912
|
+
out.push('var _kid' + kn + '=-1;');
|
|
913
|
+
out.push('if(_nx' + n + '<' + names.length + '){var _ec' + kn + '=' + eidArr + '[_nx' + n + '],_en' + kn + '=' + nmArr + '[_ec' + kn + '];');
|
|
914
|
+
out.push('if(_kl' + kn + '===_en' + kn + '.length&&s.startsWith(_en' + kn + ',_p' + kn + ')){_kid' + kn + '=_ec' + kn + ';_nx' + n + '++;}}');
|
|
915
|
+
out.push('if(_kid' + kn + '<0){var _tg' + kn + '=' + tbl + '.get(_h' + kn + ');');
|
|
916
|
+
out.push('if(_tg' + kn + '===undefined){' + addH + '}');
|
|
917
|
+
out.push('else{var _nv' + kn + '=' + nmArr + '[_tg' + kn + '];');
|
|
918
|
+
out.push('if(_kl' + kn + '!==_nv' + kn + '.length||!s.startsWith(_nv' + kn + ',_p' + kn + ')){' + addH + '}');
|
|
919
|
+
out.push('else{_kid' + kn + '=_tg' + kn + ';_nx' + n + '=' + sidArr + '[_kid' + kn + ']+1;}}}');
|
|
920
|
+
out.push('if(_kid' + kn + '>=0){');
|
|
921
|
+
out.push('var _wd' + kn + '=_kid' + kn + '>>5,_bt' + kn + '=1<<(_kid' + kn + '&31);');
|
|
922
|
+
out.push('if(_sa' + n + '[_wd' + kn + ']&_bt' + kn + ')return -1;');
|
|
923
|
+
out.push('_sa' + n + '[_wd' + kn + ']|=_bt' + kn + ';');
|
|
924
|
+
let prev = 0;
|
|
925
|
+
bounds.forEach((g, gi) => {
|
|
926
|
+
const cond = gi === bounds.length - 1 ? '' : 'if(_kid' + kn + '<' + g.end + ')';
|
|
927
|
+
out.push((gi === 0 ? '' : 'else ') + cond + '{');
|
|
928
|
+
if (g.sub === undefined) out.push(addH);
|
|
929
|
+
else emitMember(g.sub, ctx, out, depth, n);
|
|
930
|
+
out.push('}');
|
|
931
|
+
prev = g.end;
|
|
932
|
+
});
|
|
933
|
+
out.push('}');
|
|
934
|
+
ctx._saState = ctx._saState || {};
|
|
935
|
+
ctx._saState[n] = { words: wordCount, rqArr, hasReq: rq.some ? Array.from(rq).some((x) => x !== 0) : false };
|
|
936
|
+
} else {
|
|
937
|
+
// Dispatch on the raw key span: switch on its length, then compare
|
|
938
|
+
// characters. No substring is built, so a known name costs no allocation.
|
|
939
|
+
const byLen = new Map();
|
|
940
|
+
for (const name of names) {
|
|
941
|
+
const len = name.length;
|
|
942
|
+
if (!byLen.has(len)) byLen.set(len, []);
|
|
943
|
+
byLen.get(len).push(name);
|
|
944
|
+
}
|
|
945
|
+
const additional = [];
|
|
946
|
+
if (ap === false) {
|
|
947
|
+
// An unknown name makes the object invalid however many times it appears,
|
|
948
|
+
// so no later member can change this answer and the scan can stop here.
|
|
949
|
+
additional.push('return 0;');
|
|
950
|
+
} else if (ap === undefined || ap === true) {
|
|
951
|
+
emitSkip(additional);
|
|
952
|
+
} else {
|
|
953
|
+
// A repeated unknown name is not tracked by the seen mask, and the parser
|
|
954
|
+
// keeps the last one, so a failure here may be a failure the parser never
|
|
955
|
+
// sees. Bail rather than guess.
|
|
956
|
+
const lines = [];
|
|
957
|
+
emitValue(ap, ctx, lines, depth + 1);
|
|
958
|
+
additional.push(lines.join('\n').split('return 0;').join('return -1;'));
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
out.push('switch(_kl' + kn + '){');
|
|
962
|
+
for (const [len, group] of byLen) {
|
|
963
|
+
out.push('case ' + len + ':{');
|
|
964
|
+
let first = true;
|
|
965
|
+
for (const name of group) {
|
|
966
|
+
const tests = [];
|
|
967
|
+
for (let k = 0; k < name.length; k++) {
|
|
968
|
+
tests.push('s.charCodeAt(_p' + kn + (k === 0 ? '' : '+' + k) + ')===' + name.charCodeAt(k));
|
|
969
|
+
}
|
|
970
|
+
out.push((first ? 'if(' : 'else if(') + (tests.length ? tests.join('&&') : 'true') + '){');
|
|
971
|
+
first = false;
|
|
972
|
+
const slot = bit.get(name);
|
|
973
|
+
out.push('if(_s' + n + '_' + slot.w + '&' + slot.b + ')return -1;');
|
|
974
|
+
out.push('_s' + n + '_' + slot.w + '|=' + slot.b + ';');
|
|
975
|
+
const sub = props && Object.prototype.hasOwnProperty.call(props, name) ? props[name] : undefined;
|
|
976
|
+
if (sub === undefined) out.push(additional.join('\n'));
|
|
977
|
+
else emitMember(sub, ctx, out, depth, n);
|
|
978
|
+
out.push('}');
|
|
979
|
+
}
|
|
980
|
+
out.push('else{' + additional.join('\n') + '}');
|
|
981
|
+
out.push('break;}');
|
|
982
|
+
}
|
|
983
|
+
out.push('default:{' + additional.join('\n') + '}}');
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
out.push(WS);
|
|
987
|
+
out.push('if(c===44){c=s.charCodeAt(++i);' + WS + 'continue;}');
|
|
988
|
+
out.push('if(c===125)break;');
|
|
989
|
+
out.push('return 0;}}');
|
|
990
|
+
out.push('c=s.charCodeAt(++i);');
|
|
991
|
+
out.push('if(_f' + n + ')return 0;');
|
|
992
|
+
if (ctx._saState && ctx._saState[n]) {
|
|
993
|
+
const st = ctx._saState[n];
|
|
994
|
+
if (st.hasReq) {
|
|
995
|
+
const wv = '_rw' + n;
|
|
996
|
+
out.push('for(var ' + wv + '=0;' + wv + '<' + st.words + ';' + wv + '++)if((_sa' + n + '[' + wv + ']&' + st.rqArr + '[' + wv + '])!==' + st.rqArr + '[' + wv + '])return 0;');
|
|
997
|
+
}
|
|
998
|
+
} else {
|
|
999
|
+
for (let w = 0; w < words; w++) {
|
|
1000
|
+
if (reqMasks[w] !== 0) out.push('if((_s' + n + '_' + w + '&' + reqMasks[w] + ')!==' + reqMasks[w] + ')return 0;');
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
if (schema.minProperties !== undefined) out.push('if(_c' + n + '<' + schema.minProperties + ')return 0;');
|
|
1004
|
+
if (schema.maxProperties !== undefined) out.push('if(_c' + n + '>' + schema.maxProperties + ')return 0;');
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
module.exports = { compileScanner, VALID, INVALID, BAIL };
|