ata-validator 1.11.0 → 1.12.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/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  All notable changes to ata-validator are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/), and this project adheres to semantic versioning.
4
4
 
5
+ ## 1.12.1 - 2026-09-06
6
+
7
+ ### Fixed
8
+
9
+ - One format violation reports one error. The error path turns each failing statement of a format check into an error push, and it kept going after pushing, so a single bad value collected one identical error per statement it failed: twelve for `date-time` on a short string, five for `time`, two for `uuid` and `hostname`. The check now leaves the format block on the first failure. Verdicts are unchanged; only the duplicate copies of the same error are gone. `tests/test_format_single_error.js` holds every built-in format to exactly one error across 25 invalid values.
10
+
11
+ ## 1.12.0 - 2026-09-06
12
+
13
+ ### Performance
14
+
15
+ - `date-time` stopped asking every character where it sits. The scan tested each of nineteen positions against the separator indices before checking the digit; the separators are now read directly by index and each digit becomes its value in the same read that validates it, so the field numbers cost nothing extra. 40.8 to 19.5 ns on a valid value, medians across separate processes. Same answers as before on every month, day and clock boundary and under 150k random mutations.
16
+
17
+ - `uuid` reads the four hyphens by index and the 32 hex digits in five runs with fixed bounds, instead of a case-insensitive regular expression. A digit is one unsigned compare and a letter one more after folding case with a single OR. 48.4 to 35.1 ns; 300k mutated and random strings against the old expression with 0 mismatches.
18
+
19
+ - `time` reads fixed positions the same way instead of running a regular expression: 15.6 to 12.1 ns, and 200k mutations against the old pattern with 0 mismatches.
20
+
21
+ - `uri` reads the scheme from the front and scans the rest once instead of running two regular expressions: 39.3 to 32.8 ns, interleaved medians. The remaining scan then became two tiers, because the measured answer was not the expected one: a hand-written character loop beat the old `\s` expression in isolation but lost to the engine's scanner inside a compiled validator, since `\s` is what forced the Unicode machinery in. The first tier asks whether anything sits outside printable ASCII, a one-byte class the engine scans at its own speed and no ordinary URI ever trips; only a string that trips it pays for the loop that knows the exact reserved set. On a nested schema carrying six URLs, 215.5 to 189.4 ns per document, medians across separate processes. `uri-reference` shares the same scan. Fuzzed over every code point in the BMP in three positions, 0 mismatches.
22
+
23
+ - The Standard Schema bridge stopped paying for enrichment it throws away. An issue carries a message and a path and nothing else, but `~standard.validate` read the rich error list, which builds suggestions, ranks and source frames per error. It now takes the raw, schema-ordered list through the same build the rich path uses, and `parsePointerPath` walks the pointer in one pass instead of split, filter, map and a regex per segment. A 16-error rejection went from 17.6 to 4.3 microseconds; messages and order are unchanged, and `tests/test_standard_schema.js` holds issues to exact parity with `validate().errors`. Schemas using `errorMessage` keep their custom messages.
24
+
25
+ ### Fixed
26
+
27
+ - The compiled engine refused a lowercase zone letter in `time` that the interpreter accepted: `00:00:00z` answered differently depending on which engine ran the schema, and `date-time` took either case in both. One implementation now answers for every engine, and it takes both cases, per RFC 3339.
28
+
29
+ - The ReDoS integration test measured the first call, which includes compiling the schema and the pattern, against a 50 ms budget. On a loaded CI machine that reads as a failure without anything being wrong: the gate exists to separate linear matching from catastrophic backtracking, which differ by minutes, not by milliseconds. It now warms up first and allows 500 ms.
30
+
5
31
  ## 1.11.0 - 2026-08-31
6
32
 
7
33
  ### Fixed
package/index.js CHANGED
@@ -326,15 +326,22 @@ const ABORT_EARLY_RESULT = Object.freeze({
326
326
  // the eager shape. Note for tests: deepStrictEqual against a plain object
327
327
  // compares prototypes; read `.errors` and compare that.
328
328
  class LazyRejection {
329
- constructor(build, data) {
329
+ constructor(build, data, buildRaw) {
330
330
  this.valid = false;
331
331
  this._build = build;
332
332
  this._data = data;
333
333
  this._errors = null;
334
+ this._buildRaw = buildRaw;
334
335
  }
335
336
  toJSON() {
336
337
  return { valid: false, errors: this.errors };
337
338
  }
339
+ // Raw shape for consumers that carry only message and path, such as the
340
+ // Standard Schema bridge: schema order, no enrichment. Reading `errors`
341
+ // afterwards still enriches through its own build.
342
+ _ataRaw() {
343
+ return this._buildRaw ? this._buildRaw(this._data) : this.errors;
344
+ }
338
345
  }
339
346
  Object.defineProperty(LazyRejection.prototype, 'errors', {
340
347
  enumerable: true,
@@ -463,18 +470,30 @@ function sortErrorsBySchemaOrder(rootSchema, errors) {
463
470
 
464
471
  function parsePointerPath(path) {
465
472
  if (!path) return [];
466
- return path
467
- .split("/")
468
- .filter(Boolean)
469
- .map((seg) => {
470
- const decoded = seg.replace(/~1/g, "/").replace(/~0/g, "~");
471
- // Per Standard Schema V1: array indices should be emitted as numbers,
472
- // object keys as strings. Treat all-digit segments as numeric indices.
473
- if (/^(0|[1-9][0-9]*)$/.test(decoded)) {
474
- return { key: Number(decoded) };
473
+ // One pass, no intermediate arrays. Per Standard Schema V1 an array index
474
+ // is emitted as a number and an object key as a string; a segment is an
475
+ // index when it is all digits with no leading zero.
476
+ const out = [];
477
+ const n = path.length;
478
+ let start = 1;
479
+ for (let i = 1; i <= n; i++) {
480
+ if (i !== n && path.charCodeAt(i) !== 47) continue;
481
+ if (i > start) {
482
+ let seg = path.slice(start, i);
483
+ if (seg.indexOf('~') >= 0) seg = seg.replace(/~1/g, '/').replace(/~0/g, '~');
484
+ const c0 = seg.charCodeAt(0);
485
+ let numeric = c0 >= 48 && c0 <= 57 && (seg.length === 1 || c0 !== 48);
486
+ if (numeric) {
487
+ for (let k = 1; k < seg.length; k++) {
488
+ const c = seg.charCodeAt(k);
489
+ if (c < 48 || c > 57) { numeric = false; break; }
490
+ }
475
491
  }
476
- return { key: decoded };
477
- });
492
+ out.push({ key: numeric ? Number(seg) : seg });
493
+ }
494
+ start = i + 1;
495
+ }
496
+ return out;
478
497
  }
479
498
 
480
499
  function createPaddedBuffer(jsonStr) {
@@ -1379,6 +1398,14 @@ class Validator {
1379
1398
  let cached = null;
1380
1399
  return {
1381
1400
  valid: false,
1401
+ // Raw shape for consumers that carry only message and path, such
1402
+ // as the Standard Schema bridge: schema order, no enrichment.
1403
+ // Reading `errors` afterwards still enriches from the same build.
1404
+ _ataRaw() {
1405
+ let raw = result.errors || [];
1406
+ if (raw.length > 1) raw = sortErrorsBySchemaOrder(root, raw);
1407
+ return raw;
1408
+ },
1382
1409
  get errors() {
1383
1410
  if (cached === null) {
1384
1411
  let raw = result.errors || [];
@@ -1550,17 +1577,24 @@ class Validator {
1550
1577
  const _full = this.validate;
1551
1578
  const _fast = this._fastVerdict;
1552
1579
  const EMPTY_ERRORS = Object.freeze([]);
1580
+ const _verdictFallback = [{ keyword: 'validation', instancePath: '', schemaPath: '#', params: {}, message: 'schema validation failed' }];
1553
1581
  const _buildErrors = (data) => {
1554
1582
  const r = _full(data);
1555
1583
  return (r && r.valid === false && r.errors && r.errors.length)
1556
1584
  ? r.errors
1557
1585
  // The data changed between the verdict and this read; keep the
1558
1586
  // verdict and say so rather than inventing a specific error.
1559
- : [{ keyword: 'validation', instancePath: '', schemaPath: '#', params: {}, message: 'schema validation failed' }];
1587
+ : _verdictFallback;
1588
+ };
1589
+ const _buildRawErrors = (data) => {
1590
+ const r = _full(data);
1591
+ if (!r || r.valid !== false) return _verdictFallback;
1592
+ const raw = typeof r._ataRaw === 'function' ? r._ataRaw() : r.errors;
1593
+ return raw && raw.length ? raw : _verdictFallback;
1560
1594
  };
1561
1595
  this.validate = (data) => {
1562
1596
  if (_fast(data)) return { valid: true, data, errors: EMPTY_ERRORS };
1563
- return new LazyRejection(_buildErrors, data);
1597
+ return new LazyRejection(_buildErrors, data, _buildRawErrors);
1564
1598
  };
1565
1599
  }
1566
1600
 
@@ -2018,12 +2052,24 @@ Object.defineProperty(Validator.prototype, "~standard", {
2018
2052
  if (result.valid) {
2019
2053
  return { value };
2020
2054
  }
2021
- return {
2022
- issues: result.errors.map((err) => ({
2023
- message: err.message,
2024
- path: parsePointerPath(err.instancePath),
2025
- })),
2026
- };
2055
+ // An issue carries a message and a path and nothing else, so the
2056
+ // suggestion and source-frame work the rich error path does would be
2057
+ // thrown away here. Take the raw, schema-ordered list when the result
2058
+ // offers one; fall back to the public list otherwise.
2059
+ const raw = typeof result._ataRaw === 'function' ? result._ataRaw() : result.errors;
2060
+ const issues = new Array(raw.length);
2061
+ for (let i = 0; i < raw.length; i++) {
2062
+ const err = raw[i];
2063
+ const path = err.instancePath != null ? err.instancePath : (err.path || '');
2064
+ let message = err.message;
2065
+ if (!message) {
2066
+ // The native engine reports numeric codes without a message; the
2067
+ // enrich pass knows how to word those. Rare, so required lazily.
2068
+ message = require('./lib/enrich-error').enrich(err, {}).message;
2069
+ }
2070
+ issues[i] = { message, path: parsePointerPath(path) };
2071
+ }
2072
+ return { issues };
2027
2073
  },
2028
2074
  });
2029
2075
  Object.defineProperty(this, "~standard", {
package/lib/formats.js CHANGED
@@ -42,23 +42,36 @@ function dateTime (s) {
42
42
  const n = s.length;
43
43
  // The shortest accepted form is 2026-08-31T12:00:00Z
44
44
  if (n < 20) return false;
45
- for (let i = 0; i < 19; i++) {
46
- const c = s.charCodeAt(i);
47
- if (i === 4 || i === 7) { if (c !== 45) return false; continue; }
48
- if (i === 10) { if (c !== 84 && c !== 116) return false; continue; }
49
- if (i === 13 || i === 16) { if (c !== 58) return false; continue; }
50
- if (c < 48 || c > 57) return false;
51
- }
45
+ // Separators sit at known indices, so they are read directly rather than
46
+ // asked for at every step of a scan. Each remaining digit is turned into
47
+ // its value once and checked as one unsigned compare, which also gives the
48
+ // field its number without a second read.
49
+ if (s.charCodeAt(4) !== 45 || s.charCodeAt(7) !== 45) return false;
50
+ const sep = s.charCodeAt(10);
51
+ if (sep !== 84 && sep !== 116) return false;
52
+ if (s.charCodeAt(13) !== 58 || s.charCodeAt(16) !== 58) return false;
53
+
54
+ const y0 = s.charCodeAt(0) - 48, y1 = s.charCodeAt(1) - 48;
55
+ const y2 = s.charCodeAt(2) - 48, y3 = s.charCodeAt(3) - 48;
56
+ if ((y0 >>> 0) > 9 || (y1 >>> 0) > 9 || (y2 >>> 0) > 9 || (y3 >>> 0) > 9) return false;
57
+ const year = y0 * 1000 + y1 * 100 + y2 * 10 + y3;
52
58
 
53
- const year = (s.charCodeAt(0) - 48) * 1000 + (s.charCodeAt(1) - 48) * 100 +
54
- (s.charCodeAt(2) - 48) * 10 + (s.charCodeAt(3) - 48);
55
- const month = twoDigits(s, 5);
59
+ const mo0 = s.charCodeAt(5) - 48, mo1 = s.charCodeAt(6) - 48;
60
+ if ((mo0 >>> 0) > 9 || (mo1 >>> 0) > 9) return false;
61
+ const month = mo0 * 10 + mo1;
56
62
  if (month < 1 || month > 12) return false;
57
- const day = twoDigits(s, 8);
63
+
64
+ const d0 = s.charCodeAt(8) - 48, d1 = s.charCodeAt(9) - 48;
65
+ if ((d0 >>> 0) > 9 || (d1 >>> 0) > 9) return false;
66
+ const day = d0 * 10 + d1;
58
67
  if (day < 1 || day > daysInMonth(year, month)) return false;
59
- if (twoDigits(s, 11) > 23) return false;
60
- if (twoDigits(s, 14) > 59) return false;
61
- if (twoDigits(s, 17) > 59) return false;
68
+
69
+ const h0 = s.charCodeAt(11) - 48, h1 = s.charCodeAt(12) - 48;
70
+ if ((h0 >>> 0) > 9 || (h1 >>> 0) > 9 || h0 * 10 + h1 > 23) return false;
71
+ const mi0 = s.charCodeAt(14) - 48, mi1 = s.charCodeAt(15) - 48;
72
+ if ((mi0 >>> 0) > 5 || (mi1 >>> 0) > 9) return false;
73
+ const se0 = s.charCodeAt(17) - 48, se1 = s.charCodeAt(18) - 48;
74
+ if ((se0 >>> 0) > 5 || (se1 >>> 0) > 9) return false;
62
75
 
63
76
  let i = 19;
64
77
  if (s.charCodeAt(i) === 46) {
@@ -195,6 +208,125 @@ function hostname (s) {
195
208
  return labelLength !== 0 && previous !== 45;
196
209
  }
197
210
 
211
+ // A scheme followed by a colon, and no control characters or spaces after it.
212
+ // Two expressions became one pass: the scheme is read from the front and the
213
+ // rest of the string is scanned once for characters a URI cannot hold.
214
+ function uri (s) {
215
+ const n = s.length;
216
+ if (n === 0) return false;
217
+ const first = s.charCodeAt(0);
218
+ if (!((first >= 97 && first <= 122) || (first >= 65 && first <= 90))) return false;
219
+ let colon = -1;
220
+ for (let i = 1; i < n; i++) {
221
+ const c = s.charCodeAt(i);
222
+ if (c === 58) { colon = i; break; }
223
+ // scheme characters: letters, digits, "+", "-", "."
224
+ if (!(isDigit(c) || (c >= 97 && c <= 122) || (c >= 65 && c <= 90) ||
225
+ c === 43 || c === 45 || c === 46)) return false;
226
+ }
227
+ if (colon === -1) return false;
228
+ return noReserved(s, colon + 1);
229
+ }
230
+
231
+ // The characters a URI cannot hold: C0 controls, space, DEL, and the rest of
232
+ // Unicode whitespace. Asking for that set directly costs more than it looks:
233
+ // `\s` pulls in the engine's Unicode machinery for eleven code points spread
234
+ // across the BMP. Asking instead whether anything at all sits outside printable
235
+ // ASCII is a one-byte character class, which the engine scans at its own speed
236
+ // and which no ordinary URI ever trips. Only a string that does trip it pays
237
+ // for the loop that decides the exact set, and that loop was measured slower
238
+ // than the engine on the common case, which is why it is not the first answer.
239
+ const NON_PRINTABLE = /[^\u0021-\u007e]/;
240
+
241
+ function noReserved (s, from) {
242
+ if (!NON_PRINTABLE.test(s)) return true;
243
+ const n = s.length;
244
+ for (let i = from; i < n; i++) {
245
+ const c = s.charCodeAt(i);
246
+ if (c > 32 && c < 127) continue;
247
+ if (c <= 32 || c === 127) return false;
248
+ if (c === 160 || c === 5760 || (c >= 8192 && c <= 8202) || c === 8232 ||
249
+ c === 8233 || c === 8239 || c === 8287 || c === 12288 || c === 65279) return false;
250
+ }
251
+ return true;
252
+ }
253
+
254
+ // Source twin of noReserved, same two tiers. `from` is an expression for the
255
+ // first index the loop reads; the variable names stay clear of the ones the
256
+ // other format emitters use.
257
+ function noReservedSource (v, from) {
258
+ return `if(/[^\\u0021-\\u007e]/.test(${v})){` +
259
+ `for(let _ri=${from};_ri<${v}.length;_ri++){const _rc=${v}.charCodeAt(_ri);` +
260
+ 'if(_rc>32&&_rc<127)continue;' +
261
+ 'if(_rc<=32||_rc===127)return false;' +
262
+ 'if(_rc===160||_rc===5760||(_rc>=8192&&_rc<=8202)||_rc===8232||' +
263
+ '_rc===8233||_rc===8239||_rc===8287||_rc===12288||_rc===65279)return false}}';
264
+ }
265
+
266
+ // 8-4-4-4-12 hexadecimal digits. The four hyphens are read by index, then
267
+ // each run of hex digits is read with fixed bounds, so no character pays for a
268
+ // test of where it sits. A digit is one unsigned compare and a letter is one
269
+ // more after folding case with a single OR, which is what a case-insensitive
270
+ // regex spends a state machine on.
271
+ function hexRun (s, from, to) {
272
+ for (let i = from; i < to; i++) {
273
+ const c = s.charCodeAt(i);
274
+ if (!((c - 48 >>> 0) < 10 || ((c | 32) - 97 >>> 0) < 6)) return false;
275
+ }
276
+ return true;
277
+ }
278
+
279
+ function uuid (s) {
280
+ if (s.length !== 36) return false;
281
+ if (s.charCodeAt(8) !== 45 || s.charCodeAt(13) !== 45 ||
282
+ s.charCodeAt(18) !== 45 || s.charCodeAt(23) !== 45) return false;
283
+ return hexRun(s, 0, 8) && hexRun(s, 9, 13) && hexRun(s, 14, 18) &&
284
+ hexRun(s, 19, 23) && hexRun(s, 24, 36);
285
+ }
286
+
287
+ // RFC 3339 full-time: HH:MM:SS, an optional fraction, an optional offset.
288
+ // Every position is known in advance, so the check reads fixed indices and
289
+ // compares digits as numbers instead of running a state machine over the
290
+ // string. The zone letter is accepted in either case, which is what the
291
+ // interpreter and the date-time check already did.
292
+ function time (s) {
293
+ const n = s.length;
294
+ if (n < 8) return false;
295
+ const h1 = s.charCodeAt(0) - 48, h2 = s.charCodeAt(1) - 48;
296
+ if ((h1 >>> 0) > 9 || (h2 >>> 0) > 9 || h1 * 10 + h2 > 23) return false;
297
+ if (s.charCodeAt(2) !== 58 || s.charCodeAt(5) !== 58) return false;
298
+ const m1 = s.charCodeAt(3) - 48, m2 = s.charCodeAt(4) - 48;
299
+ if ((m1 >>> 0) > 5 || (m2 >>> 0) > 9) return false;
300
+ const c1 = s.charCodeAt(6) - 48, c2 = s.charCodeAt(7) - 48;
301
+ if ((c1 >>> 0) > 5 || (c2 >>> 0) > 9) return false;
302
+ let i = 8;
303
+ if (i < n && s.charCodeAt(i) === 46) {
304
+ i++;
305
+ const start = i;
306
+ while (i < n) {
307
+ const c = s.charCodeAt(i) - 48;
308
+ if ((c >>> 0) > 9) break;
309
+ i++;
310
+ }
311
+ if (i === start) return false;
312
+ }
313
+ if (i === n) return true;
314
+ const z = s.charCodeAt(i);
315
+ if (z === 90 || z === 122) return i === n - 1;
316
+ if (z !== 43 && z !== 45) return false;
317
+ if (n - i !== 6 || s.charCodeAt(i + 3) !== 58) return false;
318
+ for (let k = 1; k <= 5; k++) {
319
+ if (k === 3) continue;
320
+ if ((s.charCodeAt(i + k) - 48 >>> 0) > 9) return false;
321
+ }
322
+ return true;
323
+ }
324
+
325
+ // Anything without a control character or whitespace.
326
+ function uriReference (s) {
327
+ return noReserved(s, 0);
328
+ }
329
+
198
330
  // Source twins. `v` is the expression holding the string; when `isStr` is
199
331
  // false the check is wrapped in a typeof guard, matching FORMAT_CODEGEN's
200
332
  // convention. Each returns a statement that `return false`s on mismatch.
@@ -205,14 +337,24 @@ function dateSource (v, isStr) {
205
337
  }
206
338
 
207
339
  function dateTimeSource (v, isStr) {
208
- return guard(v, isStr, `const _n=${v}.length;if(_n<20)return false;for(let _i=0;_i<19;_i++){const _c=${v}.charCodeAt(_i);if(_i===4||_i===7){if(_c!==45)return false;continue}if(_i===10){if(_c!==84&&_c!==116)return false;continue}if(_i===13||_i===16){if(_c!==58)return false;continue}if(_c<48||_c>57)return false}` +
209
- `const _y=(${v}.charCodeAt(0)-48)*1000+(${v}.charCodeAt(1)-48)*100+(${v}.charCodeAt(2)-48)*10+(${v}.charCodeAt(3)-48);` +
210
- `const _mo=(${v}.charCodeAt(5)-48)*10+(${v}.charCodeAt(6)-48);if(_mo<1||_mo>12)return false;` +
340
+ return guard(v, isStr, `const _n=${v}.length;if(_n<20)return false;` +
341
+ `if(${v}.charCodeAt(4)!==45||${v}.charCodeAt(7)!==45)return false;` +
342
+ `const _sep=${v}.charCodeAt(10);if(_sep!==84&&_sep!==116)return false;` +
343
+ `if(${v}.charCodeAt(13)!==58||${v}.charCodeAt(16)!==58)return false;` +
344
+ `const _y0=${v}.charCodeAt(0)-48,_y1=${v}.charCodeAt(1)-48,_y2=${v}.charCodeAt(2)-48,_y3=${v}.charCodeAt(3)-48;` +
345
+ 'if((_y0>>>0)>9||(_y1>>>0)>9||(_y2>>>0)>9||(_y3>>>0)>9)return false;' +
346
+ 'const _y=_y0*1000+_y1*100+_y2*10+_y3;' +
347
+ `const _mo0=${v}.charCodeAt(5)-48,_mo1=${v}.charCodeAt(6)-48;` +
348
+ 'if((_mo0>>>0)>9||(_mo1>>>0)>9)return false;const _mo=_mo0*10+_mo1;if(_mo<1||_mo>12)return false;' +
211
349
  `const _dm=_mo===2?(((_y%4===0&&_y%100!==0)||_y%400===0)?29:28):(_mo===4||_mo===6||_mo===9||_mo===11?30:31);` +
212
- `const _d=(${v}.charCodeAt(8)-48)*10+(${v}.charCodeAt(9)-48);if(_d<1||_d>_dm)return false;` +
213
- `if((${v}.charCodeAt(11)-48)*10+(${v}.charCodeAt(12)-48)>23)return false;` +
214
- `if((${v}.charCodeAt(14)-48)*10+(${v}.charCodeAt(15)-48)>59)return false;` +
215
- `if((${v}.charCodeAt(17)-48)*10+(${v}.charCodeAt(18)-48)>59)return false;` +
350
+ `const _d0=${v}.charCodeAt(8)-48,_d1=${v}.charCodeAt(9)-48;` +
351
+ 'if((_d0>>>0)>9||(_d1>>>0)>9)return false;const _d=_d0*10+_d1;if(_d<1||_d>_dm)return false;' +
352
+ `const _h0=${v}.charCodeAt(11)-48,_h1=${v}.charCodeAt(12)-48;` +
353
+ 'if((_h0>>>0)>9||(_h1>>>0)>9||_h0*10+_h1>23)return false;' +
354
+ `const _mi0=${v}.charCodeAt(14)-48,_mi1=${v}.charCodeAt(15)-48;` +
355
+ 'if((_mi0>>>0)>5||(_mi1>>>0)>9)return false;' +
356
+ `const _se0=${v}.charCodeAt(17)-48,_se1=${v}.charCodeAt(18)-48;` +
357
+ 'if((_se0>>>0)>5||(_se1>>>0)>9)return false;' +
216
358
  `let _i2=19;if(${v}.charCodeAt(19)===46){_i2=20;const _st=_i2;while(_i2<_n){const _c2=${v}.charCodeAt(_i2);if(_c2<48||_c2>57)break;_i2++}if(_i2===_st)return false}` +
217
359
  `const _tz=${v}.charCodeAt(_i2);` +
218
360
  `if(_tz===90||_tz===122){if(_i2!==_n-1)return false}` +
@@ -234,6 +376,45 @@ function hostnameSource (v, isStr) {
234
376
  `if(_ll===0||_pv===45)return false`);
235
377
  }
236
378
 
379
+ function uuidSource (v, isStr) {
380
+ const run = (from, to) => `for(let _ui=${from};_ui<${to};_ui++){const _uc=${v}.charCodeAt(_ui);` +
381
+ 'if(!((_uc-48>>>0)<10||((_uc|32)-97>>>0)<6))return false}';
382
+ return guard(v, isStr, `if(${v}.length!==36)return false;` +
383
+ `if(${v}.charCodeAt(8)!==45||${v}.charCodeAt(13)!==45||` +
384
+ `${v}.charCodeAt(18)!==45||${v}.charCodeAt(23)!==45)return false;` +
385
+ run(0, 8) + run(9, 13) + run(14, 18) + run(19, 23) + run(24, 36));
386
+ }
387
+
388
+ function timeSource (v, isStr) {
389
+ return guard(v, isStr, `const _n=${v}.length;if(_n<8)return false;` +
390
+ `const _h1=${v}.charCodeAt(0)-48,_h2=${v}.charCodeAt(1)-48;` +
391
+ 'if((_h1>>>0)>9||(_h2>>>0)>9||_h1*10+_h2>23)return false;' +
392
+ `if(${v}.charCodeAt(2)!==58||${v}.charCodeAt(5)!==58)return false;` +
393
+ `const _m1=${v}.charCodeAt(3)-48,_m2=${v}.charCodeAt(4)-48;` +
394
+ 'if((_m1>>>0)>5||(_m2>>>0)>9)return false;' +
395
+ `const _s1=${v}.charCodeAt(6)-48,_s2=${v}.charCodeAt(7)-48;` +
396
+ 'if((_s1>>>0)>5||(_s2>>>0)>9)return false;' +
397
+ `let _ti=8;if(_ti<_n&&${v}.charCodeAt(_ti)===46){_ti++;const _tf=_ti;` +
398
+ `while(_ti<_n){const _tc=${v}.charCodeAt(_ti)-48;if((_tc>>>0)>9)break;_ti++}` +
399
+ 'if(_ti===_tf)return false}' +
400
+ 'if(_ti!==_n){' +
401
+ `const _tz=${v}.charCodeAt(_ti);` +
402
+ 'if(_tz===90||_tz===122){if(_ti!==_n-1)return false}' +
403
+ 'else{if(_tz!==43&&_tz!==45)return false;' +
404
+ `if(_n-_ti!==6||${v}.charCodeAt(_ti+3)!==58)return false;` +
405
+ `if((${v}.charCodeAt(_ti+1)-48>>>0)>9||(${v}.charCodeAt(_ti+2)-48>>>0)>9||` +
406
+ `(${v}.charCodeAt(_ti+4)-48>>>0)>9||(${v}.charCodeAt(_ti+5)-48>>>0)>9)return false}}`);
407
+ }
408
+
409
+ function uriSource (v, isStr) {
410
+ return guard(v, isStr, `const _n=${v}.length;if(_n===0)return false;` +
411
+ `const _f=${v}.charCodeAt(0);if(!((_f>=97&&_f<=122)||(_f>=65&&_f<=90)))return false;` +
412
+ `let _co=-1;for(let _i=1;_i<_n;_i++){const _c=${v}.charCodeAt(_i);if(_c===58){_co=_i;break}` +
413
+ `if(!((_c>=48&&_c<=57)||(_c>=97&&_c<=122)||(_c>=65&&_c<=90)||_c===43||_c===45||_c===46))return false}` +
414
+ `if(_co===-1)return false;` +
415
+ noReservedSource(v, '_co+1'));
416
+ }
417
+
237
418
  function ipv6Source (v, isStr) {
238
419
  return guard(v, isStr, `const _n=${v}.length;if(_n<2||_n>45)return false;let _end=_n,_g=0;` +
239
420
  `const _dot=${v}.indexOf('.');` +
@@ -259,4 +440,4 @@ function ipv4Source (v, isStr) {
259
440
  return guard(v, isStr, `const _n=${v}.length;if(_n<7||_n>15)return false;let _o=0,_val=0,_dg=0;for(let _i=0;_i<=_n;_i++){const _c=_i<_n?${v}.charCodeAt(_i):46;if(_c===46){if(_dg===0||_val>255)return false;_o++;_val=0;_dg=0;if(_o>4)return false}else if(_c>=48&&_c<=57){if(_dg===1&&_val===0)return false;_val=_val*10+(_c-48);_dg++;if(_dg>3)return false}else return false}if(_o!==4)return false`);
260
441
  }
261
442
 
262
- module.exports = { date, ipv4, dateTime, ipv6, hostname, dateSource, ipv4Source, dateTimeSource, ipv6Source, hostnameSource };
443
+ module.exports = { date, ipv4, dateTime, ipv6, hostname, uri, uriReference, uuid, time, noReserved, dateSource, ipv4Source, dateTimeSource, ipv6Source, hostnameSource, uriSource, uuidSource, timeSource, noReservedSource };
@@ -252,11 +252,11 @@ const FORMAT_CHECKS = {
252
252
  email: (s) => { const at = s.indexOf('@'); return at > 0 && at < s.length - 1 && s.indexOf('.', at) > at + 1; },
253
253
  date: _formats.date,
254
254
  'date-time': _formats.dateTime,
255
- time: (s) => /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d+)?([Zz]|[+-]\d{2}:\d{2})?$/.test(s),
255
+ time: _formats.time,
256
256
  duration: (s) => /^P(?:\d+Y)?(?:\d+M)?(?:\d+W)?(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/.test(s) && s !== 'P' && !s.endsWith('T'),
257
- uuid: (s) => s.length === 36 && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s),
258
- uri: (s) => /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(s) && !/[\s\u0000-\u001f\u007f]/.test(s),
259
- 'uri-reference': (s) => !/[\s\u0000-\u001f\u007f]/.test(s),
257
+ uuid: _formats.uuid,
258
+ uri: _formats.uri,
259
+ 'uri-reference': _formats.uriReference,
260
260
  ipv4: _formats.ipv4,
261
261
  ipv6: _formats.ipv6,
262
262
  hostname: _formats.hostname,
@@ -762,7 +762,7 @@ const _formats = require('./formats')
762
762
  const FORMAT_CHECKS = {
763
763
  email: (s) => { const at = s.indexOf('@'); return at > 0 && at < s.length - 1 && s.indexOf('.', at) > at + 1 },
764
764
  date: _formats.date,
765
- uuid: (s) => s.length === 36 && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s),
765
+ uuid: _formats.uuid,
766
766
  ipv4: _formats.ipv4,
767
767
  hostname: _formats.hostname,
768
768
  }
@@ -2967,22 +2967,16 @@ const FORMAT_CODEGEN = {
2967
2967
  : `if(typeof ${v}==='string'){const _at=${v}.indexOf('@');if(_at<=0||_at>=${v}.length-1||${v}.indexOf('.',_at)<=_at+1)return false}`
2968
2968
  },
2969
2969
  date: _formats.dateSource,
2970
- uuid: (v, isStr) => isStr
2971
- ? `if(${v}.length!==36||!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(${v}))return false`
2972
- : `if(typeof ${v}==='string'&&(${v}.length!==36||!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(${v})))return false`,
2970
+ uuid: _formats.uuidSource,
2973
2971
  'date-time': _formats.dateTimeSource,
2974
- time: (v, isStr) => isStr
2975
- ? `if(!/^([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})?$/.test(${v}))return false`
2976
- : `if(typeof ${v}==='string'&&!/^([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})?$/.test(${v}))return false`,
2972
+ time: _formats.timeSource,
2977
2973
  duration: (v, isStr) => isStr
2978
2974
  ? `if(!/^P(?:\\d+Y)?(?:\\d+M)?(?:\\d+W)?(?:\\d+D)?(?:T(?:\\d+H)?(?:\\d+M)?(?:\\d+(?:\\.\\d+)?S)?)?$/.test(${v})||${v}==='P'||${v}.endsWith('T'))return false`
2979
2975
  : `if(typeof ${v}==='string'&&(!/^P(?:\\d+Y)?(?:\\d+M)?(?:\\d+W)?(?:\\d+D)?(?:T(?:\\d+H)?(?:\\d+M)?(?:\\d+(?:\\.\\d+)?S)?)?$/.test(${v})||${v}==='P'||${v}.endsWith('T')))return false`,
2980
- uri: (v, isStr) => isStr
2981
- ? `if(!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(${v})||/[\\s\\u0000-\\u001f\\u007f]/.test(${v}))return false`
2982
- : `if(typeof ${v}==='string'&&(!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(${v})||/[\\s\\u0000-\\u001f\\u007f]/.test(${v})))return false`,
2976
+ uri: _formats.uriSource,
2983
2977
  'uri-reference': (v, isStr) => isStr
2984
- ? `if(/[\\s\\u0000-\\u001f\\u007f]/.test(${v}))return false`
2985
- : `if(typeof ${v}==='string'&&/[\\s\\u0000-\\u001f\\u007f]/.test(${v}))return false`,
2978
+ ? `{${_formats.noReservedSource(v, '0')}}`
2979
+ : `if(typeof ${v}==='string'){${_formats.noReservedSource(v, '0')}}`,
2986
2980
  ipv4: _formats.ipv4Source,
2987
2981
  ipv6: _formats.ipv6Source,
2988
2982
  hostname: _formats.hostnameSource,
@@ -3522,14 +3516,14 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3522
3516
  const fmtSp = `${schemaPrefix}/format`
3523
3517
  const fmtLit = buildErrorLiteral({ keyword: 'format', format: schema.format, schemaPath: fmtSp, sourceMap: ctx.sourceMap })
3524
3518
  const failPush = `_e.push({code:'${fmtLit.codeStr}',keyword:'format',instancePath:${pathExpr||'""'},schemaPath:'${fmtSp}',params:{format:'${esc(schema.format)}'},message:'must match format "${esc(schema.format)}"',docUrl:'${fmtLit.docUrl}'${fmtLit.frame}});if(!_all)return{valid:false,errors:_e}`
3525
- // Format errors use the boolean codegen — just wrap with error push
3519
+ // Format errors use the boolean codegen, each `return false` becoming an
3520
+ // error push. One violation reports once: the first failing statement
3521
+ // pushes and leaves the labeled block, where it used to fall through and
3522
+ // let every later statement of the same check push the same error again.
3526
3523
  if (fc) {
3527
3524
  const ri = ctx.varCounter++
3528
- const boolLines = []
3529
- boolLines.push(fc(v, isStr))
3530
- // Replace `return false` with error push in the format check
3531
- const fmtCode = boolLines.join(';').replace(/return false/g, `{${failPush}}`)
3532
- lines.push(fmtCode)
3525
+ const fmtCode = fc(v, isStr).replace(/return false/g, `{${failPush};break _fmt${ri}}`)
3526
+ lines.push(`_fmt${ri}:{${fmtCode}}`)
3533
3527
  } else if (ctx.userFormats && typeof ctx.userFormats[schema.format] === 'function') {
3534
3528
  // User-supplied format checker on the error path. Same closure plumbing
3535
3529
  // as the boolean codegen — but error codegen has no closure factory, so
@@ -4161,8 +4155,11 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4161
4155
  if (schema.format) {
4162
4156
  const fc = FORMAT_CODEGEN[schema.format]
4163
4157
  if (fc) {
4164
- const code = fc(v, isStr).replace(/return false/g, `{${fail('format', 'format', `{format:'${esc(schema.format)}'}`, `'must match format "${esc(schema.format)}"'`)}}`)
4165
- lines.push(code)
4158
+ // Same rule as the error codegen: one violation, one error. The first
4159
+ // failing statement pushes and leaves the labeled block.
4160
+ const ri = ctx.varCounter++
4161
+ const code = fc(v, isStr).replace(/return false/g, `{${fail('format', 'format', `{format:'${esc(schema.format)}'}`, `'must match format "${esc(schema.format)}"'`)};break _fmt${ri}}`)
4162
+ lines.push(`_fmt${ri}:{${code}}`)
4166
4163
  } else if (ctx.userFormats && typeof ctx.userFormats[schema.format] === 'function') {
4167
4164
  const safeName = schema.format.replace(/[^a-zA-Z0-9_]/g, '_')
4168
4165
  const closureName = `_uf_${safeName}`
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.11.0';
10
+ module.exports = '1.12.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.11.0",
3
+ "version": "1.12.1",
4
4
  "description": "JSON Schema validation with first-class TypeScript and zero runtime cost. AOT compile to per-schema ESM modules with zero validator dependency. Generic Validator<T> for TypeBox/Zod/Valibot composition. Optional runtime API. Standard Schema V1 compatible.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -44,7 +44,7 @@
44
44
  "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",
45
45
  "build": "cmake-js build --target ata",
46
46
  "rebuild": "cmake-js rebuild --target ata",
47
- "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_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_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_formats_single_pass.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_pattern_properties_errors.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_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_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_data_positions.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",
47
+ "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_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_diagnostic.js && node tests/test_format_engine_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_pattern_properties_errors.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_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_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_data_positions.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",
48
48
  "bench:size": "node benchmark/bench_aot_size.mjs",
49
49
  "test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
50
50
  "test:compat": "node tests/test_compat.js",
@@ -112,13 +112,13 @@
112
112
  "LICENSE"
113
113
  ],
114
114
  "optionalDependencies": {
115
- "@ata-validator/native-darwin-arm64": "1.11.0",
116
- "@ata-validator/native-darwin-x64": "1.11.0",
117
- "@ata-validator/native-linux-x64-gnu": "1.11.0",
118
- "@ata-validator/native-linux-arm64-gnu": "1.11.0",
119
- "@ata-validator/native-linux-x64-musl": "1.11.0",
120
- "@ata-validator/native-linux-arm64-musl": "1.11.0",
121
- "@ata-validator/native-win32-x64": "1.11.0"
115
+ "@ata-validator/native-darwin-arm64": "1.12.1",
116
+ "@ata-validator/native-darwin-x64": "1.12.1",
117
+ "@ata-validator/native-linux-x64-gnu": "1.12.1",
118
+ "@ata-validator/native-linux-arm64-gnu": "1.12.1",
119
+ "@ata-validator/native-linux-x64-musl": "1.12.1",
120
+ "@ata-validator/native-linux-arm64-musl": "1.12.1",
121
+ "@ata-validator/native-win32-x64": "1.12.1"
122
122
  },
123
123
  "peerDependencies": {
124
124
  "yaml": "^2.0.0"