ata-validator 1.28.0 → 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/build.d.ts +4 -2
- package/index.js +203 -75
- package/lib/aot-impl.js +28 -14
- 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 +24 -7
- package/lib/ts-gen.js +5 -2
- package/lib/version.js +1 -1
- package/package.json +9 -10
- package/CHANGELOG.md +0 -750
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
|
@@ -3889,7 +3889,14 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3889
3889
|
patChecks.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ci}[_i])`)
|
|
3890
3890
|
}
|
|
3891
3891
|
const isAdditional = patChecks.length ? `!_a${ci}.has(_k${ci}[_i])&&!(${patChecks.join('||')})` : `!_a${ci}.has(_k${ci}[_i])`
|
|
3892
|
-
|
|
3892
|
+
// Same shape as the combined generator: a for-in says whether anything is
|
|
3893
|
+
// extra without allocating, and the keys are materialised only to name one.
|
|
3894
|
+
// This function walks every node of a document that failed anywhere, so on a
|
|
3895
|
+
// large array the clean items were each paying for a key array.
|
|
3896
|
+
const gateChecks = patChecks.map((c) => c.split(`_k${ci}[_i]`).join(`_f${ci}`))
|
|
3897
|
+
const gateTest = gateChecks.length ? `!_a${ci}.has(_f${ci})&&!(${gateChecks.join('||')})` : `!_a${ci}.has(_f${ci})`
|
|
3898
|
+
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}}}`
|
|
3899
|
+
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
3900
|
lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
3894
3901
|
}
|
|
3895
3902
|
|
|
@@ -4554,18 +4561,28 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4554
4561
|
if (schema.additionalProperties === false && schema.properties && !schema.patternProperties) {
|
|
4555
4562
|
const propKeys = Object.keys(schema.properties)
|
|
4556
4563
|
const ci = ctx.varCounter++
|
|
4564
|
+
// The error has to name the offending property, so the keys have to be
|
|
4565
|
+
// materialised to report one. They do not have to be materialised to find
|
|
4566
|
+
// out whether there is one: a for-in answers that with no allocation, and
|
|
4567
|
+
// an accepted document is the common case. The verdict generator pays a
|
|
4568
|
+
// bare key count here, and this kept the combined function 2.8x that on a
|
|
4569
|
+
// valid document, which is what stopped it from replacing the
|
|
4570
|
+
// verdict-then-revalidate pair on the error path.
|
|
4571
|
+
const detail = (test) =>
|
|
4572
|
+
`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'")}}`
|
|
4573
|
+
let body
|
|
4557
4574
|
if (propKeys.length <= 8) {
|
|
4558
4575
|
// Direct chain: no Set allocation for small schemas
|
|
4576
|
+
const gate = propKeys.map(k => `_f${ci}!==${JSON.stringify(k)}`).join('&&')
|
|
4559
4577
|
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'")}}}`)
|
|
4578
|
+
body = `let _x${ci}=0;for(const _f${ci} in ${v}){if(${gate}){_x${ci}=1;break}}if(_x${ci}){${detail(checks)}}`
|
|
4563
4579
|
} else {
|
|
4564
4580
|
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'")}}}`)
|
|
4581
|
+
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
4582
|
}
|
|
4583
|
+
lines.push(isObj
|
|
4584
|
+
? `{${body}}`
|
|
4585
|
+
: `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body}}`)
|
|
4569
4586
|
}
|
|
4570
4587
|
|
|
4571
4588
|
// unevaluatedProperties: false where it is provably additionalProperties
|
package/lib/ts-gen.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const ATA_VERSION = require('./version');
|
|
4
|
+
|
|
3
5
|
// Schema -> TypeScript type declaration.
|
|
4
6
|
// Emits an interface or type alias for each top-level schema, plus
|
|
5
7
|
// `isValid` (type predicate) and `validate` signatures.
|
|
@@ -215,7 +217,7 @@ function toTypeScript(schema, opts) {
|
|
|
215
217
|
? `${rootDoc}export interface ${rootName} ${rootType}`
|
|
216
218
|
: `${rootDoc}export type ${rootName} = ${rootType};`;
|
|
217
219
|
|
|
218
|
-
return `// Auto-generated by ata-validator
|
|
220
|
+
return `// Auto-generated by ata-validator ${ATA_VERSION}, do not edit.
|
|
219
221
|
${defLines.length ? defLines.join('\n\n') + '\n\n' : ''}${rootDecl}
|
|
220
222
|
|
|
221
223
|
export interface ValidationError {
|
|
@@ -239,7 +241,8 @@ export type Result = ValidResult | InvalidResult;
|
|
|
239
241
|
export declare function isValid(data: unknown): data is ${rootName};
|
|
240
242
|
export declare function validate(data: unknown): Result;
|
|
241
243
|
export declare const schemaHash: string;
|
|
242
|
-
declare const
|
|
244
|
+
export declare const ataVersion: string;
|
|
245
|
+
declare const _default: { validate: typeof validate; isValid: typeof isValid; schemaHash: typeof schemaHash; ataVersion: typeof ataVersion };
|
|
243
246
|
export default _default;
|
|
244
247
|
`;
|
|
245
248
|
}
|
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.0",
|
|
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_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_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_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",
|
|
@@ -118,17 +118,16 @@
|
|
|
118
118
|
"t.d.ts",
|
|
119
119
|
"bin/",
|
|
120
120
|
"README.md",
|
|
121
|
-
"CHANGELOG.md",
|
|
122
121
|
"LICENSE"
|
|
123
122
|
],
|
|
124
123
|
"optionalDependencies": {
|
|
125
|
-
"@ata-validator/native-darwin-arm64": "1.
|
|
126
|
-
"@ata-validator/native-darwin-x64": "1.
|
|
127
|
-
"@ata-validator/native-linux-arm64-gnu": "1.
|
|
128
|
-
"@ata-validator/native-linux-arm64-musl": "1.
|
|
129
|
-
"@ata-validator/native-linux-x64-gnu": "1.
|
|
130
|
-
"@ata-validator/native-linux-x64-musl": "1.
|
|
131
|
-
"@ata-validator/native-win32-x64": "1.
|
|
124
|
+
"@ata-validator/native-darwin-arm64": "1.30.0",
|
|
125
|
+
"@ata-validator/native-darwin-x64": "1.30.0",
|
|
126
|
+
"@ata-validator/native-linux-arm64-gnu": "1.30.0",
|
|
127
|
+
"@ata-validator/native-linux-arm64-musl": "1.30.0",
|
|
128
|
+
"@ata-validator/native-linux-x64-gnu": "1.30.0",
|
|
129
|
+
"@ata-validator/native-linux-x64-musl": "1.30.0",
|
|
130
|
+
"@ata-validator/native-win32-x64": "1.30.0"
|
|
132
131
|
},
|
|
133
132
|
"peerDependencies": {
|
|
134
133
|
"yaml": "^2.0.0"
|