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.
@@ -168,4 +168,203 @@ function buildDataPositionMap (input) {
168
168
  return map;
169
169
  }
170
170
 
171
- module.exports = { buildDataPositionMap };
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
- function hostname (s) {
206
- const n = s.length;
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 = 0; i < n; 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
- function hostnameSource (v, isStr) {
690
- return guard(v, isStr, `const _n=${v}.length;if(_n===0||_n>253)return false;let _ll=0,_pv=46;` +
691
- `for(let _i=0;_i<_n;_i++){const _c=${v}.charCodeAt(_i);` +
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
- `const _e=${v}.indexOf('}',_i+1);` +
860
- `if(_e===-1)return false;` +
861
- `const _x=${v}.slice(_i+1,_e);` +
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=_e+1` +
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
- function isEmailLocalChar (c) {
878
- return (c >= 48 && c <= 57) || (c >= 97 && c <= 122) || (c >= 65 && c <= 90) ||
879
- c === 46 || c === 33 || (c >= 35 && c <= 39) || c === 42 || c === 43 ||
880
- c === 45 || c === 47 || c === 61 || c === 63 || (c >= 94 && c <= 96) ||
881
- (c >= 123 && c <= 126);
882
- }
883
- const EMAIL_LOCAL_SRC = (v) => `((${v}>=48&&${v}<=57)||(${v}>=97&&${v}<=122)||(${v}>=65&&${v}<=90)||${v}===46||${v}===33||(${v}>=35&&${v}<=39)||${v}===42||${v}===43||${v}===45||${v}===47||${v}===61||${v}===63||(${v}>=94&&${v}<=96)||(${v}>=123&&${v}<=126))`;
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 at = s.lastIndexOf('@');
886
- if (at <= 0 || at === s.length - 1) return false;
887
- const local = s.slice(0, at);
888
- const domain = s.slice(at + 1);
889
- if (local.length > 64) return false;
890
- if (local.charCodeAt(0) === 34) {
891
- // quoted string: must close, and the quote may only end the local part
892
- if (local.charCodeAt(local.length - 1) !== 34 || local.length < 2) return false;
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 (local.charCodeAt(0) === 46 || local.charCodeAt(local.length - 1) === 46) return false;
895
- if (local.indexOf('..') !== -1) return false;
896
- for (let i = 0; i < local.length; i++) if (!isEmailLocalChar(local.charCodeAt(i))) return false;
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 (domain.charCodeAt(0) === 91) { // '['
899
- if (domain.charCodeAt(domain.length - 1) !== 93 || domain.length <= 2) return false;
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 (inner.startsWith('IPv6:')) return ipv6(inner.slice(5));
903
- return ipv4(inner);
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(domain);
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
- `const _at=${v}.lastIndexOf('@');` +
914
- `if(_at<=0||_at===${v}.length-1)return false;` +
915
- `const _lp=${v}.slice(0,_at);const _dm=${v}.slice(_at+1);` +
916
- `if(_lp.length>64)return false;` +
917
- `if(_lp.charCodeAt(0)===34){if(_lp.length<2||_lp.charCodeAt(_lp.length-1)!==34)return false}` +
918
- `else{` +
919
- `if(_lp.charCodeAt(0)===46||_lp.charCodeAt(_lp.length-1)===46)return false;` +
920
- `if(_lp.indexOf('..')!==-1)return false;` +
921
- `for(let _i=0;_i<_lp.length;_i++){const _lc=_lp.charCodeAt(_i);if(!${EMAIL_LOCAL_SRC('_lc')})return false}` +
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
- `if(_dm.charCodeAt(0)===91){` +
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{${hostnameSource('_dm', true)}}`;
997
+ `else{${hostnameRangeSource(v, '_at+1', `${v}.length`)}}`;
930
998
  return isStr ? `{${inner}}` : `if(typeof ${v}==='string'){${inner}}`;
931
999
  }
932
1000
 
@@ -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
- const inner = `const _k${ci}=Object.keys(${v});const _a${ci}=new Set([${allowed}]);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}}}`
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
- lines.push(isObj
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
- lines.push(isObj
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 — do not edit.
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 _default: { validate: typeof validate; isValid: typeof isValid; schemaHash: typeof schemaHash };
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
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '1.28.0';
10
+ module.exports = '1.30.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.28.0",
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.28.0",
126
- "@ata-validator/native-darwin-x64": "1.28.0",
127
- "@ata-validator/native-linux-arm64-gnu": "1.28.0",
128
- "@ata-validator/native-linux-arm64-musl": "1.28.0",
129
- "@ata-validator/native-linux-x64-gnu": "1.28.0",
130
- "@ata-validator/native-linux-x64-musl": "1.28.0",
131
- "@ata-validator/native-win32-x64": "1.28.0"
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"