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.
@@ -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 };