ata-validator 1.29.0 → 1.30.1
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/index.js +83 -9
- package/lib/aot-impl.js +13 -6
- package/lib/data-position-cache.js +46 -2
- package/lib/data-positions.js +200 -1
- package/lib/formats.js +114 -46
- package/lib/js-compiler.js +32 -7
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/index.js
CHANGED
|
@@ -500,7 +500,7 @@ Object.defineProperty(RichRejection.prototype, 'errors', {
|
|
|
500
500
|
// The position map, resolved now that an error is actually being read.
|
|
501
501
|
let positions = null;
|
|
502
502
|
if (enrich && raw.length && this._rawInput != null) {
|
|
503
|
-
positions = self._pos().
|
|
503
|
+
positions = self._pos().targeted(this._rawInput, wantedPointersFor(raw));
|
|
504
504
|
if (positions) self._posCache.reset();
|
|
505
505
|
}
|
|
506
506
|
// One options object for the whole list, not one per error.
|
|
@@ -571,7 +571,7 @@ Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
|
|
|
571
571
|
// to raw errors, and detecting on it left every collapsed oneOf/anyOf error
|
|
572
572
|
// unenriched on the text path.
|
|
573
573
|
if (!raw[0] || !raw[0].docUrl) {
|
|
574
|
-
const positions = self._pos().
|
|
574
|
+
const positions = self._pos().targeted(jsonStr, wantedPointersFor(raw));
|
|
575
575
|
// Declaration order, as validate() applies it; the text path used to
|
|
576
576
|
// enrich in emission order.
|
|
577
577
|
const ordered = raw.length > 1 ? sortErrorsBySchemaOrder(self._schemaObj, raw) : raw;
|
|
@@ -599,7 +599,7 @@ Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
|
|
|
599
599
|
// them is worth another walk of the document: resolving the map to discover
|
|
600
600
|
// there was nothing to fill cost a second full walk on every rejection.
|
|
601
601
|
if (raw.some((e) => e && !e.dataFrame)) {
|
|
602
|
-
const positions = self._pos().
|
|
602
|
+
const positions = self._pos().targeted(jsonStr, wantedPointersFor(raw.filter((e) => e && !e.dataFrame)));
|
|
603
603
|
if (positions) {
|
|
604
604
|
for (const e of raw) {
|
|
605
605
|
if (e && !e.dataFrame) {
|
|
@@ -623,6 +623,31 @@ Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
|
|
|
623
623
|
},
|
|
624
624
|
});
|
|
625
625
|
|
|
626
|
+
// The pointers a set of errors will ask the position map about. lib/enrich-error
|
|
627
|
+
// looks up the error's own path, and for an additional or unevaluated property
|
|
628
|
+
// the child pointer named in `params`, unescaped, which is the form it asks for.
|
|
629
|
+
// The escaped form goes in too: including a pointer that is never read costs
|
|
630
|
+
// nothing, and the cache answers anything outside this set from the full map
|
|
631
|
+
// rather than reporting no position.
|
|
632
|
+
function wantedPointersFor (errors) {
|
|
633
|
+
const wanted = new Set();
|
|
634
|
+
for (const e of errors) {
|
|
635
|
+
if (!e) continue;
|
|
636
|
+
const path = e.path != null ? e.path : (e.instancePath || '');
|
|
637
|
+
wanted.add(path);
|
|
638
|
+
if (e.instancePath != null && e.instancePath !== path) wanted.add(e.instancePath);
|
|
639
|
+
const params = e.params;
|
|
640
|
+
const named = params && (params.additionalProperty || params.unevaluatedProperty);
|
|
641
|
+
if (typeof named === 'string') {
|
|
642
|
+
wanted.add(path + '/' + named);
|
|
643
|
+
if (named.indexOf('~') !== -1 || named.indexOf('/') !== -1) {
|
|
644
|
+
wanted.add(path + '/' + named.replace(/~/g, '~0').replace(/\//g, '~1'));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return wanted;
|
|
649
|
+
}
|
|
650
|
+
|
|
626
651
|
// A raw error without the `_o` ordering key, for the legacy error shape.
|
|
627
652
|
function stripOrdinal(e) {
|
|
628
653
|
if (e === null || typeof e !== 'object' || e._o === undefined) return e;
|
|
@@ -1450,10 +1475,32 @@ class Validator {
|
|
|
1450
1475
|
// The factory bakes the error function in as an argument and never
|
|
1451
1476
|
// calls it for a document that passes, so a resolver here costs the
|
|
1452
1477
|
// accepted path nothing and keeps the compile off the first call.
|
|
1453
|
-
|
|
1478
|
+
// Until the first rejection this is the hybrid: the verdict function,
|
|
1479
|
+
// with the error resolver baked in and never called for a document that
|
|
1480
|
+
// passes. That keeps the combined function's compile off the first call,
|
|
1481
|
+
// which is two thirds of what a first call costs.
|
|
1482
|
+
//
|
|
1483
|
+
// From the first rejection on, the combined function answers directly.
|
|
1484
|
+
// It decides and collects in one pass, so the verdict pass in front of it
|
|
1485
|
+
// was validating the document a second time: 134.1 microseconds against
|
|
1486
|
+
// its 45.3 on a 1000-user array. Swapping rather than starting there keeps
|
|
1487
|
+
// the lazy compile, and swapping at all is only free because the combined
|
|
1488
|
+
// function now costs what the verdict function costs on accepted
|
|
1489
|
+
// documents (1.02x on that array, 0.99x on a small body) since
|
|
1490
|
+
// additionalProperties stopped materialising its key array. The
|
|
1491
|
+
// indirection this needs measured inside the noise at both sizes.
|
|
1492
|
+
let impl = null;
|
|
1493
|
+
const onReject = (data) => {
|
|
1494
|
+
const combined = combinedIfSafe();
|
|
1495
|
+
if (combined) { impl = combined; return combined(data); }
|
|
1496
|
+
return errOnly(data);
|
|
1497
|
+
};
|
|
1498
|
+
const hybridFn = jsFn._hybridFactory(VALID_RESULT, onReject);
|
|
1499
|
+
impl = hybridFn;
|
|
1500
|
+
const run = (data) => impl(data);
|
|
1454
1501
|
this.validate = preprocess
|
|
1455
|
-
? (data) => { preprocess(data); return
|
|
1456
|
-
:
|
|
1502
|
+
? (data) => { preprocess(data); return run(data); }
|
|
1503
|
+
: run;
|
|
1457
1504
|
} else {
|
|
1458
1505
|
// No hybrid factory, so the assembly needs the function itself rather
|
|
1459
1506
|
// than a reference it can call later: build it now.
|
|
@@ -1530,7 +1577,11 @@ class Validator {
|
|
|
1530
1577
|
: jsonValidateInner;
|
|
1531
1578
|
this.validateJSON = useSimdjsonForLarge && native && !preprocess
|
|
1532
1579
|
? (jsonStr) => {
|
|
1533
|
-
|
|
1580
|
+
// `_skipNativeFast` is set by the scanner short-circuit below when it
|
|
1581
|
+
// has already decided the document is invalid. The encode and the
|
|
1582
|
+
// native call would run only to return false, and the error path
|
|
1583
|
+
// underneath does not need them.
|
|
1584
|
+
if (jsonStr.length >= SIMDJSON_THRESHOLD && this._skipNativeFast !== true) {
|
|
1534
1585
|
this._ensureNative();
|
|
1535
1586
|
const buf = Buffer.from(jsonStr);
|
|
1536
1587
|
if (native.rawFastValidate(this._fastSlot, buf))
|
|
@@ -1979,8 +2030,23 @@ class Validator {
|
|
|
1979
2030
|
};
|
|
1980
2031
|
return self.isValidJSON(jsonStr);
|
|
1981
2032
|
};
|
|
1982
|
-
|
|
2033
|
+
// validateJSON gets the same short-circuit isValidJSON has. The verdict is
|
|
2034
|
+
// a property of the text, and the scanner reads the text once and
|
|
2035
|
+
// allocates nothing; above the simdjson threshold the path underneath
|
|
2036
|
+
// encoded the whole document to a Buffer and called the native validator
|
|
2037
|
+
// instead, which measured 340 microseconds against the scanner's 191 on a
|
|
2038
|
+
// 149 KB config, the same against the published addon as against a local
|
|
2039
|
+
// build.
|
|
2040
|
+
//
|
|
2041
|
+
// An accepted document stops at the scanner. A rejected one still has to
|
|
2042
|
+
// produce errors, so it goes on to the path below, which is the
|
|
2043
|
+
// rich-errors wrapper and everything under it; the flag only tells that
|
|
2044
|
+
// path to skip an encode and a native call that would return false. Doing
|
|
2045
|
+
// it the other way, returning errors from the inner function directly,
|
|
2046
|
+
// would hand back errors that never passed through enrichment.
|
|
2047
|
+
{
|
|
1983
2048
|
const validateByParsing = this.validateJSON;
|
|
2049
|
+
const abortEarly = !!options.abortEarly;
|
|
1984
2050
|
this.validateJSON = (jsonStr) => {
|
|
1985
2051
|
const scan = self._ensureScanner();
|
|
1986
2052
|
if (scan === undefined) return validateByParsing(jsonStr);
|
|
@@ -1989,7 +2055,15 @@ class Validator {
|
|
|
1989
2055
|
if (typeof text === 'string') {
|
|
1990
2056
|
const r = scan(text);
|
|
1991
2057
|
if (r === 1) return VALID_RESULT;
|
|
1992
|
-
if (r === 0)
|
|
2058
|
+
if (r === 0) {
|
|
2059
|
+
if (abortEarly) return ABORT_EARLY_RESULT;
|
|
2060
|
+
self._skipNativeFast = true;
|
|
2061
|
+
try {
|
|
2062
|
+
return validateByParsing(text);
|
|
2063
|
+
} finally {
|
|
2064
|
+
self._skipNativeFast = false;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
1993
2067
|
}
|
|
1994
2068
|
return validateByParsing(text);
|
|
1995
2069
|
};
|
package/lib/aot-impl.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// loadBundle(Validator, mods, schemas, opts)
|
|
15
15
|
|
|
16
16
|
const { compileToJSCodegenWithErrors, compileToJSCodegen, unevalContributions } = require('./js-compiler');
|
|
17
|
-
const {
|
|
17
|
+
const { buildTargetedPositionMap } = require('./data-positions');
|
|
18
18
|
const { schemaHash } = require('./schema-hash');
|
|
19
19
|
const ATA_VERSION = require('./version');
|
|
20
20
|
const SAFE_REGEX_SOURCE = require('./safe-regex-source');
|
|
@@ -595,10 +595,15 @@ function toStandaloneModule(validator, opts) {
|
|
|
595
595
|
// dataFrame (byte offset, line, col, source line) to every error by
|
|
596
596
|
// walking the original text once. Off by default for the same reason as
|
|
597
597
|
// parse(): it costs bytes in every emitted module. Ask for it with
|
|
598
|
-
// { positions: true }. The walker is the runtime's
|
|
598
|
+
// { positions: true }. The walker is the runtime's buildTargetedPositionMap,
|
|
599
599
|
// embedded verbatim via toString so the two cannot drift; that is why the
|
|
600
|
-
// function in lib/data-positions.js must stay self-contained.
|
|
601
|
-
|
|
600
|
+
// function in lib/data-positions.js must stay self-contained. It is given the
|
|
601
|
+
// pointers the errors name, which is the only thing this module looks up, so a
|
|
602
|
+
// subtree that cannot hold one is scanned to its end and never walked: framing
|
|
603
|
+
// an error went from 8.02x a JSON.parse of the document to 1.72x, and 3.11x in
|
|
604
|
+
// the worst case of a pointer at the very end. It is also 125 characters
|
|
605
|
+
// smaller than the full-map builder it replaces.
|
|
606
|
+
const positionsCore = !(opts && opts.positions) ? '' : `const _ataPosMap = ${buildTargetedPositionMap.toString()};
|
|
602
607
|
function _ataFrame(p) { return { byteOffset: p.byteOffset, length: p.length, line: p.line, col: p.col, text: p.text }; }
|
|
603
608
|
function validateJSON(text) {
|
|
604
609
|
const s = String(text);
|
|
@@ -606,13 +611,15 @@ function validateJSON(text) {
|
|
|
606
611
|
try { data = JSON.parse(s); } catch (e) {
|
|
607
612
|
const nl = s.indexOf(String.fromCharCode(10));
|
|
608
613
|
let frame = { byteOffset: 0, length: s.length, line: 1, col: 1, text: nl === -1 ? s : s.slice(0, nl) };
|
|
609
|
-
try { const m = _ataPosMap(s); if (m['']) frame = _ataFrame(m['']); } catch (_) {}
|
|
614
|
+
try { const m = _ataPosMap(s, new Set([''])); if (m['']) frame = _ataFrame(m['']); } catch (_) {}
|
|
610
615
|
return { valid: false, errors: [{ code: 'ATA9001', message: 'invalid JSON document', keyword: '__parse__', path: '', instancePath: '', dataFrame: frame }] };
|
|
611
616
|
}
|
|
612
617
|
const r = validate(data);
|
|
613
618
|
if (r.valid || !r.errors || !r.errors.length) return r;
|
|
614
619
|
let map;
|
|
615
|
-
|
|
620
|
+
const _want = new Set();
|
|
621
|
+
for (const e of r.errors) _want.add(e.instancePath != null ? e.instancePath : (e.path || ''));
|
|
622
|
+
try { map = _ataPosMap(s, _want); } catch (_) { return r; }
|
|
616
623
|
const errors = r.errors.map((e) => {
|
|
617
624
|
const ptr = e.instancePath != null ? e.instancePath : (e.path || '');
|
|
618
625
|
const p = map[ptr];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { buildDataPositionMap } = require('./data-positions');
|
|
3
|
+
const { buildDataPositionMap, buildTargetedPositionMap } = require('./data-positions');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Memoize the position map for the duration of a single validate() call.
|
|
@@ -12,7 +12,50 @@ function createCache () {
|
|
|
12
12
|
const wm = new WeakMap();
|
|
13
13
|
const sm = new Map(); // strings can't go in WeakMap; clear after each validate
|
|
14
14
|
let lastInput = null;
|
|
15
|
-
|
|
15
|
+
const cache = {
|
|
16
|
+
/**
|
|
17
|
+
* The entries for a known set of pointers, which is what a rejection needs:
|
|
18
|
+
* recording one per node in the document was the dominant cost of reading an
|
|
19
|
+
* error's frame.
|
|
20
|
+
*
|
|
21
|
+
* The caller derives `wanted` from the errors, and a derivation that misses a
|
|
22
|
+
* pointer must not cost a frame, so the result answers any pointer outside
|
|
23
|
+
* the set by building the full map once and reading from that. A wrong
|
|
24
|
+
* derivation is then slow rather than silently short of frames, which is the
|
|
25
|
+
* only acceptable direction for this trade: a missing caret is the failure
|
|
26
|
+
* nobody reports.
|
|
27
|
+
*/
|
|
28
|
+
targeted (input, wanted) {
|
|
29
|
+
if (input == null) return null;
|
|
30
|
+
if (!wanted || wanted.size === 0) return null;
|
|
31
|
+
const already = typeof input === 'string'
|
|
32
|
+
? sm.get(input)
|
|
33
|
+
: (Buffer.isBuffer(input) ? wm.get(input) : undefined);
|
|
34
|
+
if (already) return already;
|
|
35
|
+
let filtered;
|
|
36
|
+
try {
|
|
37
|
+
filtered = buildTargetedPositionMap(input, wanted);
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
let full;
|
|
42
|
+
const fallback = (key) => {
|
|
43
|
+
if (wanted.has(key)) return undefined; // asked for, absent from the document
|
|
44
|
+
if (full === undefined) full = cache.get(input) || null;
|
|
45
|
+
return full ? full[key] : undefined;
|
|
46
|
+
};
|
|
47
|
+
return new Proxy(filtered, {
|
|
48
|
+
get (target, key) {
|
|
49
|
+
if (typeof key !== 'string') return target[key];
|
|
50
|
+
if (key in target) return target[key];
|
|
51
|
+
return fallback(key);
|
|
52
|
+
},
|
|
53
|
+
has (target, key) {
|
|
54
|
+
if (typeof key !== 'string') return key in target;
|
|
55
|
+
return (key in target) || fallback(key) !== undefined;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
},
|
|
16
59
|
get (input) {
|
|
17
60
|
if (input == null) return null;
|
|
18
61
|
if (typeof input === 'string') {
|
|
@@ -38,6 +81,7 @@ function createCache () {
|
|
|
38
81
|
lastInput = null;
|
|
39
82
|
},
|
|
40
83
|
};
|
|
84
|
+
return cache;
|
|
41
85
|
}
|
|
42
86
|
|
|
43
87
|
module.exports = { createCache };
|
package/lib/data-positions.js
CHANGED
|
@@ -168,4 +168,203 @@ function buildDataPositionMap (input) {
|
|
|
168
168
|
return map;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
/**
|
|
172
|
+
* The same map, for a known set of pointers.
|
|
173
|
+
*
|
|
174
|
+
* A rejection names two or three pointers and nothing reads any other entry, so
|
|
175
|
+
* recording one per node in the document is nearly all waste: on a 149 KB config
|
|
176
|
+
* the full map was 1330 microseconds of a 2457 microsecond error read. Here a
|
|
177
|
+
* subtree that cannot contain a wanted pointer is scanned to its end and never
|
|
178
|
+
* walked, and the walk stops once every wanted pointer has been recorded.
|
|
179
|
+
* Measured against the full map: 11.5x on pointers near the start of the
|
|
180
|
+
* document, and 3.7x in the worst case for the technique, a single pointer at
|
|
181
|
+
* the very end.
|
|
182
|
+
*
|
|
183
|
+
* Deliberately a second function rather than an option on the first. That one is
|
|
184
|
+
* embedded verbatim into standalone modules via Function.prototype.toString, and
|
|
185
|
+
* adding per-node checks to it cost 8% on the full-map path for callers that want
|
|
186
|
+
* every entry. The price is two walks of the same grammar, which
|
|
187
|
+
* `tests/test_targeted_positions.js` holds to identical output.
|
|
188
|
+
*
|
|
189
|
+
* Entries are identical to the full map's for every pointer in `wanted`. A
|
|
190
|
+
* pointer absent from the document is absent here too, exactly as it would be.
|
|
191
|
+
*/
|
|
192
|
+
function buildTargetedPositionMap (input, wanted) {
|
|
193
|
+
const text = (typeof Buffer !== 'undefined' && Buffer.isBuffer(input)) ? input.toString('utf8') : String(input);
|
|
194
|
+
const map = Object.create(null);
|
|
195
|
+
const n = text.length;
|
|
196
|
+
|
|
197
|
+
// Every prefix of every wanted pointer: the containers worth descending into.
|
|
198
|
+
const interest = new Set(['']);
|
|
199
|
+
for (const w of wanted) {
|
|
200
|
+
let at = 0;
|
|
201
|
+
for (;;) {
|
|
202
|
+
const next = w.indexOf('/', at + 1);
|
|
203
|
+
if (next === -1) break;
|
|
204
|
+
interest.add(w.slice(0, next));
|
|
205
|
+
at = next;
|
|
206
|
+
}
|
|
207
|
+
interest.add(w);
|
|
208
|
+
}
|
|
209
|
+
let remaining = wanted.size;
|
|
210
|
+
|
|
211
|
+
const ptrs = [];
|
|
212
|
+
const starts = [];
|
|
213
|
+
const lens = [];
|
|
214
|
+
const keyStarts = [];
|
|
215
|
+
const keyLens = [];
|
|
216
|
+
|
|
217
|
+
let i = 0;
|
|
218
|
+
|
|
219
|
+
function skipWs () {
|
|
220
|
+
while (i < n) {
|
|
221
|
+
const ch = text.charCodeAt(i);
|
|
222
|
+
if (ch === 0x20 || ch === 0x09 || ch === 0x0a || ch === 0x0d) i++; else break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function readString (decode) {
|
|
227
|
+
const start = i;
|
|
228
|
+
let simple = text.charCodeAt(i) === 0x22;
|
|
229
|
+
i++;
|
|
230
|
+
let closed = false;
|
|
231
|
+
while (i < n) {
|
|
232
|
+
const ch = text.charCodeAt(i);
|
|
233
|
+
if (ch === 0x5c) { simple = false; i += 2; continue; }
|
|
234
|
+
if (ch === 0x22) { i++; closed = true; break; }
|
|
235
|
+
if (ch < 0x20) simple = false;
|
|
236
|
+
i++;
|
|
237
|
+
}
|
|
238
|
+
if (!closed) throw new Error('unterminated string at offset ' + start);
|
|
239
|
+
if (simple) return decode ? text.slice(start + 1, i - 1) : null;
|
|
240
|
+
const decoded = JSON.parse(text.slice(start, i));
|
|
241
|
+
return decode ? decoded : null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Past one value, recording nothing inside it. Strings are consumed through
|
|
245
|
+
// readString so a brace inside one cannot move the depth.
|
|
246
|
+
function skipValue () {
|
|
247
|
+
skipWs();
|
|
248
|
+
if (i >= n) return;
|
|
249
|
+
const ch = text.charCodeAt(i);
|
|
250
|
+
if (ch === 0x22) { readString(false); return; }
|
|
251
|
+
if (ch === 0x7b || ch === 0x5b) {
|
|
252
|
+
let depth = 0;
|
|
253
|
+
while (i < n) {
|
|
254
|
+
const c = text.charCodeAt(i);
|
|
255
|
+
if (c === 0x22) { readString(false); continue; }
|
|
256
|
+
if (c === 0x7b || c === 0x5b) { depth++; i++; continue; }
|
|
257
|
+
if (c === 0x7d || c === 0x5d) { depth--; i++; if (depth <= 0) return; continue; }
|
|
258
|
+
i++;
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
while (i < n) {
|
|
263
|
+
const c = text.charCodeAt(i);
|
|
264
|
+
if (c === 0x2c || c === 0x7d || c === 0x5d || c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) break;
|
|
265
|
+
i++;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function walk (ptr, keyStart, keyLen) {
|
|
270
|
+
skipWs();
|
|
271
|
+
if (i >= n) return;
|
|
272
|
+
const start = i;
|
|
273
|
+
const isWanted = wanted.has(ptr);
|
|
274
|
+
|
|
275
|
+
if (!interest.has(ptr)) {
|
|
276
|
+
skipValue();
|
|
277
|
+
if (isWanted) { ptrs.push(ptr); starts.push(start); lens.push(i - start); keyStarts.push(keyStart); keyLens.push(keyLen); remaining--; }
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const ch = text.charCodeAt(i);
|
|
282
|
+
if (ch === 0x7b) {
|
|
283
|
+
i++;
|
|
284
|
+
for (;;) {
|
|
285
|
+
skipWs();
|
|
286
|
+
if (i >= n) break;
|
|
287
|
+
if (text.charCodeAt(i) === 0x7d) { i++; break; }
|
|
288
|
+
if (text.charCodeAt(i) === 0x2c) { i++; continue; }
|
|
289
|
+
skipWs();
|
|
290
|
+
const memberKeyStart = i;
|
|
291
|
+
const key = readString(true);
|
|
292
|
+
const memberKeyLen = i - memberKeyStart;
|
|
293
|
+
skipWs();
|
|
294
|
+
if (text.charCodeAt(i) !== 0x3a) throw new Error('expected ":" at offset ' + i);
|
|
295
|
+
i++;
|
|
296
|
+
const seg = (key.indexOf('~') === -1 && key.indexOf('/') === -1)
|
|
297
|
+
? key
|
|
298
|
+
: key.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
299
|
+
walk(ptr + '/' + seg, memberKeyStart, memberKeyLen);
|
|
300
|
+
if (remaining === 0) return;
|
|
301
|
+
}
|
|
302
|
+
} else if (ch === 0x5b) {
|
|
303
|
+
i++;
|
|
304
|
+
let idx = 0;
|
|
305
|
+
for (;;) {
|
|
306
|
+
skipWs();
|
|
307
|
+
if (i >= n) break;
|
|
308
|
+
if (text.charCodeAt(i) === 0x5d) { i++; break; }
|
|
309
|
+
if (text.charCodeAt(i) === 0x2c) { i++; continue; }
|
|
310
|
+
const before = i;
|
|
311
|
+
walk(ptr + '/' + idx, -1, 0);
|
|
312
|
+
if (remaining === 0) return;
|
|
313
|
+
if (i === before) break;
|
|
314
|
+
idx++;
|
|
315
|
+
}
|
|
316
|
+
} else if (ch === 0x22) {
|
|
317
|
+
readString(false);
|
|
318
|
+
} else {
|
|
319
|
+
while (i < n) {
|
|
320
|
+
const c = text.charCodeAt(i);
|
|
321
|
+
if (c === 0x2c || c === 0x7d || c === 0x5d || c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) break;
|
|
322
|
+
i++;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (isWanted) { ptrs.push(ptr); starts.push(start); lens.push(i - start); keyStarts.push(keyStart); keyLens.push(keyLen); remaining--; }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (text.charCodeAt(0) === 0xfeff) i = 1;
|
|
330
|
+
walk('', -1, 0);
|
|
331
|
+
|
|
332
|
+
if (ptrs.length === 0) return map;
|
|
333
|
+
|
|
334
|
+
const lines = text.split('\n');
|
|
335
|
+
const lineStart = new Array(lines.length + 1);
|
|
336
|
+
lineStart[0] = 0;
|
|
337
|
+
for (let k = 0; k < lines.length; k++) lineStart[k + 1] = lineStart[k] + lines[k].length + 1;
|
|
338
|
+
|
|
339
|
+
function lineOf (off) {
|
|
340
|
+
let lo = 0, hi = lineStart.length - 1;
|
|
341
|
+
while (lo < hi) {
|
|
342
|
+
const mid = (lo + hi + 1) >> 1;
|
|
343
|
+
if (lineStart[mid] <= off) lo = mid; else hi = mid - 1;
|
|
344
|
+
}
|
|
345
|
+
return lo;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
for (let k = 0; k < ptrs.length; k++) {
|
|
349
|
+
const off = starts[k];
|
|
350
|
+
const line = lineOf(off);
|
|
351
|
+
const entry = {
|
|
352
|
+
byteOffset: off,
|
|
353
|
+
length: lens[k],
|
|
354
|
+
line: line + 1,
|
|
355
|
+
col: off - lineStart[line] + 1,
|
|
356
|
+
text: lines[line] || '',
|
|
357
|
+
};
|
|
358
|
+
if (keyStarts[k] >= 0) {
|
|
359
|
+
const keyLine = lineOf(keyStarts[k]);
|
|
360
|
+
entry.keyOffset = keyStarts[k];
|
|
361
|
+
entry.keyLength = keyLens[k];
|
|
362
|
+
entry.keyLine = keyLine + 1;
|
|
363
|
+
entry.keyCol = keyStarts[k] - lineStart[keyLine] + 1;
|
|
364
|
+
}
|
|
365
|
+
map[ptrs[k]] = entry;
|
|
366
|
+
}
|
|
367
|
+
return map;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
module.exports = { buildDataPositionMap, buildTargetedPositionMap };
|
package/lib/formats.js
CHANGED
|
@@ -202,12 +202,16 @@ function ipv6 (s) {
|
|
|
202
202
|
// Labels of letters, digits and hyphens, each 1 to 63 characters, none
|
|
203
203
|
// starting or ending with a hyphen, joined by single dots, 253 characters at
|
|
204
204
|
// most. Same answers as the expression it replaces, read once.
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
// `from` and `to` let a caller check a hostname inside a larger string, which is
|
|
206
|
+
// how the email check reads its domain half without slicing it out.
|
|
207
|
+
function hostname (s, from, to) {
|
|
208
|
+
const start = from === undefined ? 0 : from;
|
|
209
|
+
const end = to === undefined ? s.length : to;
|
|
210
|
+
const n = end - start;
|
|
207
211
|
if (n === 0 || n > 253) return false;
|
|
208
212
|
let labelLength = 0;
|
|
209
213
|
let previous = 46; // a dot, so a leading hyphen is refused like a leading dot
|
|
210
|
-
for (let i =
|
|
214
|
+
for (let i = start; i < end; i++) {
|
|
211
215
|
const c = s.charCodeAt(i);
|
|
212
216
|
if (c === 46) {
|
|
213
217
|
if (labelLength === 0 || previous === 45) return false;
|
|
@@ -686,15 +690,28 @@ function dateTimeSource (v, isStr) {
|
|
|
686
690
|
`if(_dtu!==1439)return false}`);
|
|
687
691
|
}
|
|
688
692
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
693
|
+
// The scan, over a range of the string given as source expressions. The email
|
|
694
|
+
// check reads its domain half through this, without slicing it out, which was an
|
|
695
|
+
// allocation on every call.
|
|
696
|
+
//
|
|
697
|
+
// A separate function rather than optional parameters on hostnameSource: the code
|
|
698
|
+
// generator calls every entry of its format table as `fn(v, isStr, ctx)`, so a
|
|
699
|
+
// third parameter here would silently receive a context object and emit
|
|
700
|
+
// `_st=[object Object]`. That declines the whole compile, which is how it was
|
|
701
|
+
// found.
|
|
702
|
+
function hostnameRangeSource (v, st, en) {
|
|
703
|
+
return `const _st=${st},_en=${en},_n=_en-_st;if(_n===0||_n>253)return false;let _ll=0,_pv=46;` +
|
|
704
|
+
`for(let _i=_st;_i<_en;_i++){const _c=${v}.charCodeAt(_i);` +
|
|
692
705
|
`if(_c===46){if(_ll===0||_pv===45)return false;_ll=0;_pv=_c;continue}` +
|
|
693
706
|
`const _an=(_c>=48&&_c<=57)||(_c>=97&&_c<=122)||(_c>=65&&_c<=90);` +
|
|
694
707
|
`if(!_an&&_c!==45)return false;` +
|
|
695
708
|
`if(_c===45&&_pv===46)return false;` +
|
|
696
709
|
`if(++_ll>63)return false;_pv=_c}` +
|
|
697
|
-
`if(_ll===0||_pv===45)return false
|
|
710
|
+
`if(_ll===0||_pv===45)return false`;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function hostnameSource (v, isStr) {
|
|
714
|
+
return guard(v, isStr, hostnameRangeSource(v, '0', `${v}.length`));
|
|
698
715
|
}
|
|
699
716
|
|
|
700
717
|
function uuidSource (v, isStr) {
|
|
@@ -856,12 +873,16 @@ function uriTemplateSource (v, isStr) {
|
|
|
856
873
|
`if(_c===125)return false;` +
|
|
857
874
|
`if(_c!==123&&(_c<=32||_c===127))return false;` +
|
|
858
875
|
`if(_c!==123){_i++;continue}` +
|
|
859
|
-
`
|
|
860
|
-
`
|
|
861
|
-
`
|
|
876
|
+
// `_ute`, not `_e`: this source is spliced into a generated function whose
|
|
877
|
+
// error accumulator is `_e`, and a `const _e` here shadowed it, so
|
|
878
|
+
// `(_e||(_e=[])).push(...)` ran against a number and threw. The verdict was
|
|
879
|
+
// right, so the suite never saw it; only a caller reading `.errors` did.
|
|
880
|
+
`const _ute=${v}.indexOf('}',_i+1);` +
|
|
881
|
+
`if(_ute===-1)return false;` +
|
|
882
|
+
`const _x=${v}.slice(_i+1,_ute);` +
|
|
862
883
|
`if(_x===''||_x.indexOf('{')!==-1)return false;` +
|
|
863
884
|
`if(!${URI_TEMPLATE_EXPR.toString()}.test(_x))return false;` +
|
|
864
|
-
`_i=
|
|
885
|
+
`_i=_ute+1` +
|
|
865
886
|
`}`;
|
|
866
887
|
return isStr ? `{${inner}}` : `if(typeof ${v}==='string'){${inner}}`;
|
|
867
888
|
}
|
|
@@ -874,35 +895,69 @@ function uriTemplateSource (v, isStr) {
|
|
|
874
895
|
// the dot that separates atoms. Written as code ranges rather than a regular
|
|
875
896
|
// expression because this lands inside every emitted module that validates an
|
|
876
897
|
// email, and the expression cost more bytes there than the whole check.
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
898
|
+
// The emitted form keeps the comparison chain: a standalone module pays for the
|
|
899
|
+
// bytes of a 128-entry table in every file that validates an email, and the
|
|
900
|
+
// runtime pays for the chain on every call. Different budgets, same answers,
|
|
901
|
+
// which `tests/test_format_engine_parity.js` holds.
|
|
902
|
+
// Stated as the complement: of the printable range, only twelve characters are
|
|
903
|
+
// not atext or the dot, so excluding them is 54 source bytes shorter than listing
|
|
904
|
+
// what is allowed, and a standalone module carries this expression. Checked
|
|
905
|
+
// against the allowed-list form over every code point from 0 to 65536 in
|
|
906
|
+
// tests/test_formats_single_pass.js.
|
|
907
|
+
const EMAIL_LOCAL_SRC = (v) => `(${v}>32&&${v}<127&&${v}!==34&&${v}!==40&&${v}!==41&&${v}!==44&&${v}!==58&&${v}!==59&&${v}!==60&&${v}!==62&&${v}!==64&&${v}!==91&&${v}!==92&&${v}!==93)`;
|
|
908
|
+
// The same table treatment the rest of this file uses for character classes, and
|
|
909
|
+
// no slicing: the two halves are read in place by offset. The version that split
|
|
910
|
+
// the string at the '@' and tested each local character against a chain of ten
|
|
911
|
+
// comparisons cost 3.2x the hostname check it contains, and that one function was
|
|
912
|
+
// the only measured place ata validated slower than the default validator.
|
|
913
|
+
const EMAIL_LOCAL = new Uint8Array(128);
|
|
914
|
+
for (let i = 48; i <= 57; i++) EMAIL_LOCAL[i] = 1; // digits
|
|
915
|
+
for (let i = 97; i <= 122; i++) EMAIL_LOCAL[i] = 1; // a-z
|
|
916
|
+
for (let i = 65; i <= 90; i++) EMAIL_LOCAL[i] = 1; // A-Z
|
|
917
|
+
for (let i = 35; i <= 39; i++) EMAIL_LOCAL[i] = 1; // # $ % & '
|
|
918
|
+
for (let i = 94; i <= 96; i++) EMAIL_LOCAL[i] = 1; // ^ _ `
|
|
919
|
+
for (let i = 123; i <= 126; i++) EMAIL_LOCAL[i] = 1; // { | } ~
|
|
920
|
+
for (const c of [46, 33, 42, 43, 45, 47, 61, 63]) EMAIL_LOCAL[c] = 1; // . ! * + - / = ?
|
|
921
|
+
|
|
922
|
+
// One forward pass over the string. '@' is not a local-part character, so in an
|
|
923
|
+
// unquoted address the first one is the separator, and the local part can be
|
|
924
|
+
// checked on the way to finding it. The version that called lastIndexOf first
|
|
925
|
+
// read the string backwards before reading it forwards twice.
|
|
884
926
|
function email(s) {
|
|
885
|
-
const
|
|
886
|
-
if (
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
if (
|
|
927
|
+
const n = s.length;
|
|
928
|
+
if (n < 3) return false;
|
|
929
|
+
let at;
|
|
930
|
+
if (s.charCodeAt(0) === 34) {
|
|
931
|
+
// A quoted local part may contain '@', so it needs the last one. Rare enough
|
|
932
|
+
// to keep the extra scan rather than complicate the common path.
|
|
933
|
+
at = s.lastIndexOf('@');
|
|
934
|
+
if (at < 2 || at === n - 1 || at > 64) return false;
|
|
935
|
+
if (s.charCodeAt(at - 1) !== 34) return false;
|
|
893
936
|
} else {
|
|
894
|
-
if (
|
|
895
|
-
|
|
896
|
-
|
|
937
|
+
if (s.charCodeAt(0) === 46) return false; // a leading dot is not a mailbox
|
|
938
|
+
at = -1;
|
|
939
|
+
let afterDot = false;
|
|
940
|
+
for (let i = 0; i < n; i++) {
|
|
941
|
+
const c = s.charCodeAt(i);
|
|
942
|
+
if (c === 64) { at = i; break; }
|
|
943
|
+
if (c > 127 || EMAIL_LOCAL[c] === 0) return false;
|
|
944
|
+
if (c === 46) {
|
|
945
|
+
if (afterDot) return false; // a doubled dot is not one either
|
|
946
|
+
afterDot = true;
|
|
947
|
+
} else {
|
|
948
|
+
afterDot = false;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
if (at <= 0 || at === n - 1 || at > 64) return false;
|
|
952
|
+
if (s.charCodeAt(at - 1) === 46) return false;
|
|
897
953
|
}
|
|
898
|
-
if (
|
|
899
|
-
if (
|
|
900
|
-
const inner = domain.slice(1, -1);
|
|
954
|
+
if (s.charCodeAt(at + 1) === 91) { // '['
|
|
955
|
+
if (s.charCodeAt(n - 1) !== 93 || n - at - 1 <= 2) return false;
|
|
901
956
|
// IPv6 literals are tagged; anything else in brackets is an IPv4 address.
|
|
902
|
-
if (
|
|
903
|
-
return ipv4(
|
|
957
|
+
if (s.startsWith('IPv6:', at + 2)) return ipv6(s.slice(at + 7, n - 1));
|
|
958
|
+
return ipv4(s.slice(at + 2, n - 1));
|
|
904
959
|
}
|
|
905
|
-
return hostname(
|
|
960
|
+
return hostname(s, at + 1, n);
|
|
906
961
|
}
|
|
907
962
|
// Emitted rather than bound as a closure: a standalone module embeds format
|
|
908
963
|
// functions by their source text, so anything reaching a free variable would
|
|
@@ -910,23 +965,36 @@ function email(s) {
|
|
|
910
965
|
// one hostname implementation for both halves of this file.
|
|
911
966
|
function emailSource (v, isStr) {
|
|
912
967
|
const inner =
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
`
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
968
|
+
// One forward pass, and neither half sliced out. '@' is not a local-part
|
|
969
|
+
// character, so in an unquoted address the first one is the separator and the
|
|
970
|
+
// local part is checked on the way to it. A quoted local part may contain
|
|
971
|
+
// '@' and keeps the backward scan, which is the rare shape.
|
|
972
|
+
// The bounds tests are merged into one condition per branch, and there is no
|
|
973
|
+
// separate length check: an empty local part fails `_at<=0` and an empty
|
|
974
|
+
// domain fails `_at===_n0-1`, so a short string is already refused. This
|
|
975
|
+
// lands in every emitted module that validates an email, and 25 bytes of it
|
|
976
|
+
// put the CLI smoke build over its --max-size once.
|
|
977
|
+
`const _n0=${v}.length;let _at;` +
|
|
978
|
+
`if(${v}.charCodeAt(0)===34){` +
|
|
979
|
+
`_at=${v}.lastIndexOf('@');` +
|
|
980
|
+
`if(_at<2||_at===_n0-1||_at>64||${v}.charCodeAt(_at-1)!==34)return false` +
|
|
981
|
+
`}else{` +
|
|
982
|
+
`if(${v}.charCodeAt(0)===46)return false;` +
|
|
983
|
+
`_at=-1;let _pd=false;` +
|
|
984
|
+
`for(let _i=0;_i<_n0;_i++){const _lc=${v}.charCodeAt(_i);if(_lc===64){_at=_i;break}if(!${EMAIL_LOCAL_SRC('_lc')})return false;if(_lc===46){if(_pd)return false;_pd=true}else _pd=false}` +
|
|
985
|
+
`if(_at<=0||_at===_n0-1||_at>64||${v}.charCodeAt(_at-1)===46)return false` +
|
|
922
986
|
`}` +
|
|
923
|
-
|
|
987
|
+
// An address literal is rare enough to pay for its own slice inside the
|
|
988
|
+
// branch that needs it; the hostname path, which is every real address,
|
|
989
|
+
// reads the domain in place.
|
|
990
|
+
`if(${v}.charCodeAt(_at+1)===91){` +
|
|
991
|
+
`const _dm=${v}.slice(_at+1);` +
|
|
924
992
|
`if(_dm.charCodeAt(_dm.length-1)!==93||_dm.length<=2)return false;` +
|
|
925
993
|
`const _in=_dm.slice(1,-1);` +
|
|
926
994
|
`if(_in.startsWith('IPv6:')){const _i6=_in.slice(5);${ipv6Source('_i6', true)}}` +
|
|
927
995
|
`else{${ipv4Source('_in', true)}}` +
|
|
928
996
|
`}` +
|
|
929
|
-
`else{${
|
|
997
|
+
`else{${hostnameRangeSource(v, '_at+1', `${v}.length`)}}`;
|
|
930
998
|
return isStr ? `{${inner}}` : `if(typeof ${v}==='string'){${inner}}`;
|
|
931
999
|
}
|
|
932
1000
|
|
package/lib/js-compiler.js
CHANGED
|
@@ -1865,7 +1865,15 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1865
1865
|
fnName = '_def' + ctx.defFns.size + '_' + defName.replace(/[^A-Za-z0-9_]/g, '_')
|
|
1866
1866
|
ctx.defFns.set(defName, fnName)
|
|
1867
1867
|
const bodyLines = []
|
|
1868
|
+
// The def is compiled as its own function over its own `d`, so the
|
|
1869
|
+
// checks _deferOrInline holds back belong at the end of this body.
|
|
1870
|
+
// The list hangs off ctx, so without the swap they are flushed into
|
|
1871
|
+
// whatever function is compiling, carrying this def's key set.
|
|
1872
|
+
const outerDeferred = ctx.deferredChecks
|
|
1873
|
+
ctx.deferredChecks = null
|
|
1868
1874
|
genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx)
|
|
1875
|
+
if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
|
|
1876
|
+
ctx.deferredChecks = outerDeferred
|
|
1869
1877
|
// Entered through the cycle guard: see emitGuardState. In the fast
|
|
1870
1878
|
// pass the wrapper only counts depth; the set is used by the second,
|
|
1871
1879
|
// guarded pass that runs when data turns out to point back at
|
|
@@ -3889,7 +3897,14 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3889
3897
|
patChecks.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ci}[_i])`)
|
|
3890
3898
|
}
|
|
3891
3899
|
const isAdditional = patChecks.length ? `!_a${ci}.has(_k${ci}[_i])&&!(${patChecks.join('||')})` : `!_a${ci}.has(_k${ci}[_i])`
|
|
3892
|
-
|
|
3900
|
+
// Same shape as the combined generator: a for-in says whether anything is
|
|
3901
|
+
// extra without allocating, and the keys are materialised only to name one.
|
|
3902
|
+
// This function walks every node of a document that failed anywhere, so on a
|
|
3903
|
+
// large array the clean items were each paying for a key array.
|
|
3904
|
+
const gateChecks = patChecks.map((c) => c.split(`_k${ci}[_i]`).join(`_f${ci}`))
|
|
3905
|
+
const gateTest = gateChecks.length ? `!_a${ci}.has(_f${ci})&&!(${gateChecks.join('||')})` : `!_a${ci}.has(_f${ci})`
|
|
3906
|
+
const detail = `const _k${ci}=Object.keys(${v});for(let _i=0;_i<_k${ci}.length;_i++){if(${isAdditional}){_e.push({code:'${apLit.codeStr}',keyword:'additionalProperties',instancePath:${pathExpr||'""'},schemaPath:'${apSp}'${ordinalField(ctx, `${apSp}`)},params:{additionalProperty:_k${ci}[_i]},message:'must NOT have additional properties',docUrl:'${apLit.docUrl}'${apLit.frame}});if(!_all)return{valid:false,errors:_e}}}`
|
|
3907
|
+
const inner = `const _a${ci}=new Set([${allowed}]);let _x${ci}=0;for(const _f${ci} in ${v}){if(${gateTest}){_x${ci}=1;break}}if(_x${ci}){${detail}}`
|
|
3893
3908
|
lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
3894
3909
|
}
|
|
3895
3910
|
|
|
@@ -4554,18 +4569,28 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4554
4569
|
if (schema.additionalProperties === false && schema.properties && !schema.patternProperties) {
|
|
4555
4570
|
const propKeys = Object.keys(schema.properties)
|
|
4556
4571
|
const ci = ctx.varCounter++
|
|
4572
|
+
// The error has to name the offending property, so the keys have to be
|
|
4573
|
+
// materialised to report one. They do not have to be materialised to find
|
|
4574
|
+
// out whether there is one: a for-in answers that with no allocation, and
|
|
4575
|
+
// an accepted document is the common case. The verdict generator pays a
|
|
4576
|
+
// bare key count here, and this kept the combined function 2.8x that on a
|
|
4577
|
+
// valid document, which is what stopped it from replacing the
|
|
4578
|
+
// verdict-then-revalidate pair on the error path.
|
|
4579
|
+
const detail = (test) =>
|
|
4580
|
+
`const _k${ci}=Object.keys(${v});for(let _i=0;_i<_k${ci}.length;_i++)if(${test}){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:_k${ci}[_i]}`, "'must NOT have additional properties'")}}`
|
|
4581
|
+
let body
|
|
4557
4582
|
if (propKeys.length <= 8) {
|
|
4558
4583
|
// Direct chain: no Set allocation for small schemas
|
|
4584
|
+
const gate = propKeys.map(k => `_f${ci}!==${JSON.stringify(k)}`).join('&&')
|
|
4559
4585
|
const checks = propKeys.map(k => `_k${ci}[_i]!==${JSON.stringify(k)}`).join('&&')
|
|
4560
|
-
|
|
4561
|
-
? `{const _k${ci}=Object.keys(${v});for(let _i=0;_i<_k${ci}.length;_i++)if(${checks}){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:_k${ci}[_i]}`, "'must NOT have additional properties'")}}}`
|
|
4562
|
-
: `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){const _k${ci}=Object.keys(${v});for(let _i=0;_i<_k${ci}.length;_i++)if(${checks}){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:_k${ci}[_i]}`, "'must NOT have additional properties'")}}}`)
|
|
4586
|
+
body = `let _x${ci}=0;for(const _f${ci} in ${v}){if(${gate}){_x${ci}=1;break}}if(_x${ci}){${detail(checks)}}`
|
|
4563
4587
|
} else {
|
|
4564
4588
|
const allowed = propKeys.map(k => JSON.stringify(k)).join(',')
|
|
4565
|
-
|
|
4566
|
-
? `{const _k${ci}=Object.keys(${v});const _a${ci}=new Set([${allowed}]);for(let _i=0;_i<_k${ci}.length;_i++)if(!_a${ci}.has(_k${ci}[_i])){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:_k${ci}[_i]}`, "'must NOT have additional properties'")}}}`
|
|
4567
|
-
: `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){const _k${ci}=Object.keys(${v});const _a${ci}=new Set([${allowed}]);for(let _i=0;_i<_k${ci}.length;_i++)if(!_a${ci}.has(_k${ci}[_i])){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:_k${ci}[_i]}`, "'must NOT have additional properties'")}}}`)
|
|
4589
|
+
body = `const _a${ci}=new Set([${allowed}]);let _x${ci}=0;for(const _f${ci} in ${v}){if(!_a${ci}.has(_f${ci})){_x${ci}=1;break}}if(_x${ci}){${detail(`!_a${ci}.has(_k${ci}[_i])`)}}`
|
|
4568
4590
|
}
|
|
4591
|
+
lines.push(isObj
|
|
4592
|
+
? `{${body}}`
|
|
4593
|
+
: `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body}}`)
|
|
4569
4594
|
}
|
|
4570
4595
|
|
|
4571
4596
|
// unevaluatedProperties: false where it is provably additionalProperties
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.1",
|
|
4
4
|
"description": "JSON Schema validation that compiles for speed and still runs where code generation is blocked. Compiled and interpreted engines answer identically at 100% of the official suite. TypeScript inference, Standard Schema V1, and a build step that emits dependency-free modules.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.mjs",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"release:check": "node scripts/regen-safe-regex-source.js && node tests/test_pack_purity.js && node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_source_sync.js && node tests/test_version_sync.js",
|
|
52
52
|
"build": "cmake-js build --target ata",
|
|
53
53
|
"rebuild": "cmake-js rebuild --target ata",
|
|
54
|
-
"test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_scanner_differential.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_output_format.js && node tests/test_retry_message.js && node tests/test_describe_schema.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_option.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_uri_helper_parity.js && node tests/test_formats_single_pass.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_unevaluated_error_shape.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_lazy_json_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_defaults_own_keys.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_position_map_cost.js && node tests/test_aot_staleness.js && node tests/test_file_positions.js && node tests/test_schema_hash.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
|
|
54
|
+
"test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_scanner_differential.js && node tests/test_validate_json_scanner_cost.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_output_format.js && node tests/test_retry_message.js && node tests/test_describe_schema.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_option.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_error_accumulator_collision.js && node tests/test_uri_helper_parity.js && node tests/test_formats_single_pass.js && node tests/test_email_format_cost.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_deferred_def_scope.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_unevaluated_error_shape.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_additional_props_combined_cost.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_single_pass_errors.js && node tests/test_lazy_json_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_defaults_own_keys.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_targeted_positions.js && node tests/test_position_map_cost.js && node tests/test_targeted_positions_cost.js && node tests/test_aot_staleness.js && node tests/test_file_positions.js && node tests/test_aot_positions_cost.js && node tests/test_schema_hash.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
|
|
55
55
|
"bench:size": "node benchmark/bench_aot_size.mjs",
|
|
56
56
|
"test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
|
|
57
57
|
"test:compat": "node tests/test_compat.js",
|
|
@@ -121,13 +121,13 @@
|
|
|
121
121
|
"LICENSE"
|
|
122
122
|
],
|
|
123
123
|
"optionalDependencies": {
|
|
124
|
-
"@ata-validator/native-darwin-arm64": "1.
|
|
125
|
-
"@ata-validator/native-darwin-x64": "1.
|
|
126
|
-
"@ata-validator/native-linux-arm64-gnu": "1.
|
|
127
|
-
"@ata-validator/native-linux-arm64-musl": "1.
|
|
128
|
-
"@ata-validator/native-linux-x64-gnu": "1.
|
|
129
|
-
"@ata-validator/native-linux-x64-musl": "1.
|
|
130
|
-
"@ata-validator/native-win32-x64": "1.
|
|
124
|
+
"@ata-validator/native-darwin-arm64": "1.30.1",
|
|
125
|
+
"@ata-validator/native-darwin-x64": "1.30.1",
|
|
126
|
+
"@ata-validator/native-linux-arm64-gnu": "1.30.1",
|
|
127
|
+
"@ata-validator/native-linux-arm64-musl": "1.30.1",
|
|
128
|
+
"@ata-validator/native-linux-x64-gnu": "1.30.1",
|
|
129
|
+
"@ata-validator/native-linux-x64-musl": "1.30.1",
|
|
130
|
+
"@ata-validator/native-win32-x64": "1.30.1"
|
|
131
131
|
},
|
|
132
132
|
"peerDependencies": {
|
|
133
133
|
"yaml": "^2.0.0"
|