ata-validator 1.14.1 → 1.15.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/lib/formats.js CHANGED
@@ -18,7 +18,10 @@ function date (s) {
18
18
  }
19
19
  const m = (s.charCodeAt(5) - 48) * 10 + (s.charCodeAt(6) - 48);
20
20
  const d = (s.charCodeAt(8) - 48) * 10 + (s.charCodeAt(9) - 48);
21
- return m >= 1 && m <= 12 && d >= 1 && d <= 31;
21
+ if (m < 1 || m > 12 || d < 1) return false;
22
+ const y = (s.charCodeAt(0) - 48) * 1000 + (s.charCodeAt(1) - 48) * 100 +
23
+ (s.charCodeAt(2) - 48) * 10 + (s.charCodeAt(3) - 48);
24
+ return d <= daysInMonth(y, m);
22
25
  }
23
26
 
24
27
  // RFC 3339 date-time, read once. The form this replaces ran a regular
@@ -71,7 +74,7 @@ function dateTime (s) {
71
74
  const mi0 = s.charCodeAt(14) - 48, mi1 = s.charCodeAt(15) - 48;
72
75
  if ((mi0 >>> 0) > 5 || (mi1 >>> 0) > 9) return false;
73
76
  const se0 = s.charCodeAt(17) - 48, se1 = s.charCodeAt(18) - 48;
74
- if ((se0 >>> 0) > 5 || (se1 >>> 0) > 9) return false;
77
+ if ((se0 >>> 0) > 6 || (se1 >>> 0) > 9 || se0 * 10 + se1 > 60) return false;
75
78
 
76
79
  let i = 19;
77
80
  if (s.charCodeAt(i) === 46) {
@@ -86,14 +89,27 @@ function dateTime (s) {
86
89
  }
87
90
 
88
91
  const c = s.charCodeAt(i);
89
- if (c === 90 || c === 122) return i === n - 1;
90
- if (c !== 43 && c !== 45) return false;
91
- if (n - i !== 6) return false;
92
- if (!isDigit(s.charCodeAt(i + 1)) || !isDigit(s.charCodeAt(i + 2))) return false;
93
- if (s.charCodeAt(i + 3) !== 58) return false;
94
- if (!isDigit(s.charCodeAt(i + 4)) || !isDigit(s.charCodeAt(i + 5))) return false;
95
- if (twoDigits(s, i + 1) > 23) return false;
96
- if (twoDigits(s, i + 4) > 59) return false;
92
+ const sec = se0 * 10 + se1;
93
+ const hh = h0 * 10 + h1, mi = mi0 * 10 + mi1;
94
+ let offMin = 0;
95
+ if (c === 90 || c === 122) {
96
+ if (i !== n - 1) return false;
97
+ } else {
98
+ if (c !== 43 && c !== 45) return false;
99
+ if (n - i !== 6) return false;
100
+ if (!isDigit(s.charCodeAt(i + 1)) || !isDigit(s.charCodeAt(i + 2))) return false;
101
+ if (s.charCodeAt(i + 3) !== 58) return false;
102
+ if (!isDigit(s.charCodeAt(i + 4)) || !isDigit(s.charCodeAt(i + 5))) return false;
103
+ const oh = twoDigits(s, i + 1), om = twoDigits(s, i + 4);
104
+ if (oh > 23 || om > 59) return false;
105
+ offMin = (c === 43 ? 1 : -1) * (oh * 60 + om);
106
+ }
107
+ // Same rule as time(): second 60 is the leap second and exists only at
108
+ // 23:59:60 UTC, whatever offset the string is written in.
109
+ if (sec === 60) {
110
+ const utc = ((hh * 60 + mi - offMin) % 1440 + 1440) % 1440;
111
+ if (utc !== 1439) return false;
112
+ }
97
113
  return true;
98
114
  }
99
115
 
@@ -225,7 +241,7 @@ function uri (s) {
225
241
  c === 43 || c === 45 || c === 46)) return false;
226
242
  }
227
243
  if (colon === -1) return false;
228
- return noReserved(s, colon + 1);
244
+ return uriChars(s, colon + 1) && uriAuthority(s, colon + 1);
229
245
  }
230
246
 
231
247
  // The characters a URI cannot hold: C0 controls, space, DEL, and the rest of
@@ -238,6 +254,176 @@ function uri (s) {
238
254
  // than the engine on the common case, which is why it is not the first answer.
239
255
  const NON_PRINTABLE = /[^\u0021-\u007e]/;
240
256
 
257
+
258
+ // RFC 3986 allows these ASCII characters in a URI and nothing else: the
259
+ // unreserved set, the delimiters, and "%" when it introduces two hex digits.
260
+ // The printable characters missing from that list are the ones this rejects:
261
+ // " < > \ ^ ` { | }, plus everything below 0x21 and above 0x7e. The old check
262
+ // only looked for whitespace and non-printables, so a backslash or a lone
263
+ // percent sign passed as a URI.
264
+ function uriChar (c) {
265
+ if (c < 33 || c > 126) return false;
266
+ return !(c === 34 || c === 60 || c === 62 || c === 92 ||
267
+ c === 94 || c === 96 || c === 123 || c === 124 || c === 125);
268
+ }
269
+ function isHex (c) {
270
+ return (c >= 48 && c <= 57) || (c >= 97 && c <= 102) || (c >= 65 && c <= 70);
271
+ }
272
+ function uriChars (s, from) {
273
+ for (let i = from; i < s.length; i++) {
274
+ const c = s.charCodeAt(i);
275
+ if (!uriChar(c)) return false;
276
+ if (c === 37) { // '%'
277
+ if (!isHex(s.charCodeAt(i + 1)) || !isHex(s.charCodeAt(i + 2))) return false;
278
+ i += 2;
279
+ }
280
+ }
281
+ return true;
282
+ }
283
+ const uriCharsSource = (v, from) =>
284
+ `for(let _ri=${from};_ri<${v}.length;_ri++){const _rc=${v}.charCodeAt(_ri);` +
285
+ 'if(_rc<33||_rc>126||_rc===34||_rc===60||_rc===62||_rc===92||_rc===94||_rc===96||_rc===123||_rc===124||_rc===125)return false;' +
286
+ `if(_rc===37){const _h1=${v}.charCodeAt(_ri+1),_h2=${v}.charCodeAt(_ri+2);` +
287
+ 'if(!((_h1>=48&&_h1<=57)||(_h1>=97&&_h1<=102)||(_h1>=65&&_h1<=70))||' +
288
+ '!((_h2>=48&&_h2<=57)||(_h2>=97&&_h2<=102)||(_h2>=65&&_h2<=70)))return false;_ri+=2}}';
289
+
290
+ // The authority sits between "//" and the next "/", "?" or "#". A bracketed
291
+ // host must close, and a port is digits.
292
+ function uriAuthority (s, start) {
293
+ if (s.charCodeAt(start) !== 47 || s.charCodeAt(start + 1) !== 47) return true;
294
+ let end = s.length;
295
+ for (let i = start + 2; i < s.length; i++) {
296
+ const c = s.charCodeAt(i);
297
+ if (c === 47 || c === 63 || c === 35) { end = i; break; }
298
+ }
299
+ const auth = s.slice(start + 2, end);
300
+ const at = auth.lastIndexOf('@');
301
+ // Brackets belong to the host, so one before the "@" is not userinfo.
302
+ if (at !== -1 && /[[\]]/.test(auth.slice(0, at))) return false;
303
+ const hostPort = at === -1 ? auth : auth.slice(at + 1);
304
+ if (hostPort.charCodeAt(0) === 91) { // '['
305
+ const close = hostPort.indexOf(']');
306
+ if (close === -1) return false;
307
+ const rest = hostPort.slice(close + 1);
308
+ if (rest === '') return true;
309
+ if (rest.charCodeAt(0) !== 58) return false;
310
+ return /^[0-9]*$/.test(rest.slice(1));
311
+ }
312
+ const colon = hostPort.lastIndexOf(':');
313
+ if (colon === -1) return true;
314
+ // A host holding more than one colon is an IPv6 address, and those must be
315
+ // bracketed. Without that rule the last group reads as a port number.
316
+ if (hostPort.indexOf(':') !== colon) return false;
317
+ return /^[0-9]*$/.test(hostPort.slice(colon + 1));
318
+ }
319
+ const uriAuthoritySource = (v, start) =>
320
+ `if(${v}.charCodeAt(${start})===47&&${v}.charCodeAt(${start}+1)===47){` +
321
+ `let _ae=${v}.length;` +
322
+ `for(let _ai=${start}+2;_ai<${v}.length;_ai++){const _ac=${v}.charCodeAt(_ai);` +
323
+ 'if(_ac===47||_ac===63||_ac===35){_ae=_ai;break}}' +
324
+ `const _au=${v}.slice(${start}+2,_ae);const _aat=_au.lastIndexOf('@');` +
325
+ 'if(_aat!==-1&&/[[\\]]/.test(_au.slice(0,_aat)))return false;' +
326
+ 'const _hp=_aat===-1?_au:_au.slice(_aat+1);' +
327
+ 'if(_hp.charCodeAt(0)===91){const _cl=_hp.indexOf("]");' +
328
+ 'if(_cl===-1)return false;const _rs=_hp.slice(_cl+1);' +
329
+ 'if(_rs!==""){if(_rs.charCodeAt(0)!==58||!/^[0-9]*$/.test(_rs.slice(1)))return false}}' +
330
+ 'else{const _co=_hp.lastIndexOf(":");' +
331
+ 'if(_co!==-1){if(_hp.indexOf(":")!==_co)return false;' +
332
+ 'if(!/^[0-9]*$/.test(_hp.slice(_co+1)))return false}}}';
333
+
334
+
335
+ // An IRI is a URI that also admits non-ASCII. The ASCII rules are identical,
336
+ // so the same character test runs with everything above 0x7e allowed through.
337
+ function iriChar (c) { return c > 126 ? c !== 127 : uriChar(c); }
338
+ function iriChars (s, from) {
339
+ for (let i = from; i < s.length; i++) {
340
+ const c = s.charCodeAt(i);
341
+ if (!iriChar(c)) return false;
342
+ if (c === 37) {
343
+ if (!isHex(s.charCodeAt(i + 1)) || !isHex(s.charCodeAt(i + 2))) return false;
344
+ i += 2;
345
+ }
346
+ }
347
+ return true;
348
+ }
349
+ const iriCharsSource = (v, from) =>
350
+ `for(let _ii=${from};_ii<${v}.length;_ii++){const _ic=${v}.charCodeAt(_ii);` +
351
+ 'if(_ic===127||(_ic<127&&(_ic<33||_ic===34||_ic===60||_ic===62||_ic===92||_ic===94||_ic===96||_ic===123||_ic===124||_ic===125)))return false;' +
352
+ `if(_ic===37){const _j1=${v}.charCodeAt(_ii+1),_j2=${v}.charCodeAt(_ii+2);` +
353
+ 'if(!((_j1>=48&&_j1<=57)||(_j1>=97&&_j1<=102)||(_j1>=65&&_j1<=70))||' +
354
+ '!((_j2>=48&&_j2<=57)||(_j2>=97&&_j2<=102)||(_j2>=65&&_j2<=70)))return false;_ii+=2}}';
355
+
356
+ function iri (s) {
357
+ const n = s.length;
358
+ if (n === 0) return false;
359
+ const first = s.charCodeAt(0);
360
+ if (!((first >= 97 && first <= 122) || (first >= 65 && first <= 90))) return false;
361
+ let colon = -1;
362
+ for (let i = 1; i < n; i++) {
363
+ const c = s.charCodeAt(i);
364
+ if (c === 58) { colon = i; break; }
365
+ if (!(isDigit(c) || (c >= 97 && c <= 122) || (c >= 65 && c <= 90) ||
366
+ c === 43 || c === 45 || c === 46)) return false;
367
+ }
368
+ if (colon === -1) return false;
369
+ return iriChars(s, colon + 1) && uriAuthority(s, colon + 1);
370
+ }
371
+ function iriSource (v, isStr) {
372
+ return guard(v, isStr, `const _n=${v}.length;if(_n===0)return false;` +
373
+ `const _f=${v}.charCodeAt(0);if(!((_f>=97&&_f<=122)||(_f>=65&&_f<=90)))return false;` +
374
+ `let _co=-1;for(let _i=1;_i<_n;_i++){const _c=${v}.charCodeAt(_i);if(_c===58){_co=_i;break}` +
375
+ `if(!((_c>=48&&_c<=57)||(_c>=97&&_c<=122)||(_c>=65&&_c<=90)||_c===43||_c===45||_c===46))return false}` +
376
+ `if(_co===-1)return false;` +
377
+ iriCharsSource(v, '_co+1') +
378
+ uriAuthoritySource(v, '_co+1'));
379
+ }
380
+ function iriReference (s) { return iriChars(s, 0); }
381
+ function iriReferenceSource (v, isStr) { return guard(v, isStr, iriCharsSource(v, '0')); }
382
+
383
+ // An internationalised mailbox differs from a mailbox only in admitting
384
+ // non-ASCII on both sides. The separator stays the ASCII "@": a fullwidth one
385
+ // is a character in the local part, not a separator.
386
+ function idnEmail (s) {
387
+ const at = s.lastIndexOf('@');
388
+ if (at <= 0 || at === s.length - 1) return false;
389
+ const local = s.slice(0, at);
390
+ const domain = s.slice(at + 1);
391
+ if (local.charCodeAt(0) === 34) {
392
+ return local.length >= 2 && local.charCodeAt(local.length - 1) === 34 && domain.length > 0;
393
+ }
394
+ if (local.charCodeAt(0) === 46 || local.charCodeAt(local.length - 1) === 46) return false;
395
+ if (local.indexOf('..') !== -1) return false;
396
+ if (domain.charCodeAt(0) === 91) {
397
+ return domain.charCodeAt(domain.length - 1) === 93 && domain.length > 2;
398
+ }
399
+ if (domain.charCodeAt(0) === 46 || domain.charCodeAt(domain.length - 1) === 46) return false;
400
+ if (domain.indexOf('..') !== -1) return false;
401
+ for (let i = 0; i < domain.length; i++) {
402
+ const c = domain.charCodeAt(i);
403
+ if (c <= 32 || c === 127 || c === 64) return false;
404
+ }
405
+ return true;
406
+ }
407
+ function idnEmailSource (v, isStr) {
408
+ const inner =
409
+ `const _at=${v}.lastIndexOf('@');` +
410
+ `if(_at<=0||_at===${v}.length-1)return false;` +
411
+ `const _lp=${v}.slice(0,_at),_dm=${v}.slice(_at+1);` +
412
+ `if(_lp.charCodeAt(0)===34){if(_lp.length<2||_lp.charCodeAt(_lp.length-1)!==34||_dm.length===0)return false}` +
413
+ `else{` +
414
+ `if(_lp.charCodeAt(0)===46||_lp.charCodeAt(_lp.length-1)===46)return false;` +
415
+ `if(_lp.indexOf('..')!==-1)return false;` +
416
+ `if(_dm.charCodeAt(0)===91){if(_dm.charCodeAt(_dm.length-1)!==93||_dm.length<=2)return false}` +
417
+ `else{` +
418
+ `if(_dm.charCodeAt(0)===46||_dm.charCodeAt(_dm.length-1)===46)return false;` +
419
+ `if(_dm.indexOf('..')!==-1)return false;` +
420
+ `for(let _di=0;_di<_dm.length;_di++){const _dc=_dm.charCodeAt(_di);` +
421
+ `if(_dc<=32||_dc===127||_dc===64)return false}` +
422
+ `}` +
423
+ `}`;
424
+ return isStr ? `{${inner}}` : `if(typeof ${v}==='string'){${inner}}`;
425
+ }
426
+
241
427
  function noReserved (s, from) {
242
428
  if (!NON_PRINTABLE.test(s)) return true;
243
429
  const n = s.length;
@@ -297,8 +483,10 @@ function time (s) {
297
483
  if (s.charCodeAt(2) !== 58 || s.charCodeAt(5) !== 58) return false;
298
484
  const m1 = s.charCodeAt(3) - 48, m2 = s.charCodeAt(4) - 48;
299
485
  if ((m1 >>> 0) > 5 || (m2 >>> 0) > 9) return false;
486
+ // Seconds reach 60 for the leap second, which the offset check below pins
487
+ // to 23:59:60 UTC; anything past that is not a clock reading.
300
488
  const c1 = s.charCodeAt(6) - 48, c2 = s.charCodeAt(7) - 48;
301
- if ((c1 >>> 0) > 5 || (c2 >>> 0) > 9) return false;
489
+ if ((c1 >>> 0) > 6 || (c2 >>> 0) > 9 || c1 * 10 + c2 > 60) return false;
302
490
  let i = 8;
303
491
  if (i < n && s.charCodeAt(i) === 46) {
304
492
  i++;
@@ -310,21 +498,37 @@ function time (s) {
310
498
  }
311
499
  if (i === start) return false;
312
500
  }
313
- if (i === n) return true;
501
+ // RFC 3339 full-time requires an offset; a bare wall clock is not a time.
502
+ if (i === n) return false;
503
+ const hh = h1 * 10 + h2, mm = m1 * 10 + m2, ss = c1 * 10 + c2;
314
504
  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;
505
+ let offMin = 0;
506
+ if (z === 90 || z === 122) {
507
+ if (i !== n - 1) return false;
508
+ } else {
509
+ if (z !== 43 && z !== 45) return false;
510
+ if (n - i !== 6 || s.charCodeAt(i + 3) !== 58) return false;
511
+ for (let k = 1; k <= 5; k++) {
512
+ if (k === 3) continue;
513
+ if ((s.charCodeAt(i + k) - 48 >>> 0) > 9) return false;
514
+ }
515
+ const oh = (s.charCodeAt(i + 1) - 48) * 10 + (s.charCodeAt(i + 2) - 48);
516
+ const om = (s.charCodeAt(i + 4) - 48) * 10 + (s.charCodeAt(i + 5) - 48);
517
+ if (oh > 23 || om > 59) return false;
518
+ offMin = (z === 43 ? 1 : -1) * (oh * 60 + om);
519
+ }
520
+ // Second 60 exists only as the leap second, which is 23:59:60 in UTC. Any
521
+ // other clock reading with :60 is not a time.
522
+ if (ss === 60) {
523
+ const utc = ((hh * 60 + mm - offMin) % 1440 + 1440) % 1440;
524
+ if (utc !== 23 * 60 + 59) return false;
321
525
  }
322
526
  return true;
323
527
  }
324
528
 
325
529
  // Anything without a control character or whitespace.
326
530
  function uriReference (s) {
327
- return noReserved(s, 0);
531
+ return uriChars(s, 0);
328
532
  }
329
533
 
330
534
  // Source twins. `v` is the expression holding the string; when `isStr` is
@@ -333,7 +537,7 @@ function uriReference (s) {
333
537
  function guard (v, isStr, body) { return isStr ? `{${body}}` : `if(typeof ${v}==='string'){${body}}`; }
334
538
 
335
539
  function dateSource (v, isStr) {
336
- return guard(v, isStr, `if(${v}.length!==10)return false;for(let _i=0;_i<10;_i++){const _c=${v}.charCodeAt(_i);if(_i===4||_i===7){if(_c!==45)return false}else if(_c<48||_c>57)return false}const _m=(${v}.charCodeAt(5)-48)*10+(${v}.charCodeAt(6)-48),_d=(${v}.charCodeAt(8)-48)*10+(${v}.charCodeAt(9)-48);if(_m<1||_m>12||_d<1||_d>31)return false`);
540
+ return guard(v, isStr, `if(${v}.length!==10)return false;for(let _i=0;_i<10;_i++){const _c=${v}.charCodeAt(_i);if(_i===4||_i===7){if(_c!==45)return false}else if(_c<48||_c>57)return false}const _m=(${v}.charCodeAt(5)-48)*10+(${v}.charCodeAt(6)-48),_d=(${v}.charCodeAt(8)-48)*10+(${v}.charCodeAt(9)-48);if(_m<1||_m>12||_d<1)return false;const _y=(${v}.charCodeAt(0)-48)*1000+(${v}.charCodeAt(1)-48)*100+(${v}.charCodeAt(2)-48)*10+(${v}.charCodeAt(3)-48);const _dim=_m===2?(((_y%4===0&&_y%100!==0)||_y%400===0)?29:28):((_m===4||_m===6||_m===9||_m===11)?30:31);if(_d>_dim)return false`);
337
541
  }
338
542
 
339
543
  function dateTimeSource (v, isStr) {
@@ -354,15 +558,20 @@ function dateTimeSource (v, isStr) {
354
558
  `const _mi0=${v}.charCodeAt(14)-48,_mi1=${v}.charCodeAt(15)-48;` +
355
559
  'if((_mi0>>>0)>5||(_mi1>>>0)>9)return false;' +
356
560
  `const _se0=${v}.charCodeAt(17)-48,_se1=${v}.charCodeAt(18)-48;` +
357
- 'if((_se0>>>0)>5||(_se1>>>0)>9)return false;' +
561
+ 'if((_se0>>>0)>6||(_se1>>>0)>9||_se0*10+_se1>60)return false;' +
358
562
  `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}` +
359
- `const _tz=${v}.charCodeAt(_i2);` +
563
+ `const _tz=${v}.charCodeAt(_i2);let _dtoff=0;` +
360
564
  `if(_tz===90||_tz===122){if(_i2!==_n-1)return false}` +
361
565
  `else{if(_tz!==43&&_tz!==45)return false;if(_n-_i2!==6)return false;` +
362
566
  `const _oh=${v}.charCodeAt(_i2+1),_oh2=${v}.charCodeAt(_i2+2),_om=${v}.charCodeAt(_i2+4),_om2=${v}.charCodeAt(_i2+5);` +
363
567
  `if(_oh<48||_oh>57||_oh2<48||_oh2>57||_om<48||_om>57||_om2<48||_om2>57)return false;` +
364
568
  `if(${v}.charCodeAt(_i2+3)!==58)return false;` +
365
- `if((_oh-48)*10+(_oh2-48)>23)return false;if((_om-48)*10+(_om2-48)>59)return false}`);
569
+ `const _ohv=(_oh-48)*10+(_oh2-48),_omv=(_om-48)*10+(_om2-48);` +
570
+ `if(_ohv>23||_omv>59)return false;` +
571
+ `_dtoff=(_tz===43?1:-1)*(_ohv*60+_omv)}` +
572
+ `if(_se0*10+_se1===60){` +
573
+ `const _dtu=(((_h0*10+_h1)*60+(_mi0*10+_mi1)-_dtoff)%1440+1440)%1440;` +
574
+ `if(_dtu!==1439)return false}`);
366
575
  }
367
576
 
368
577
  function hostnameSource (v, isStr) {
@@ -393,17 +602,24 @@ function timeSource (v, isStr) {
393
602
  `const _m1=${v}.charCodeAt(3)-48,_m2=${v}.charCodeAt(4)-48;` +
394
603
  'if((_m1>>>0)>5||(_m2>>>0)>9)return false;' +
395
604
  `const _s1=${v}.charCodeAt(6)-48,_s2=${v}.charCodeAt(7)-48;` +
396
- 'if((_s1>>>0)>5||(_s2>>>0)>9)return false;' +
605
+ 'if((_s1>>>0)>6||(_s2>>>0)>9||_s1*10+_s2>60)return false;' +
397
606
  `let _ti=8;if(_ti<_n&&${v}.charCodeAt(_ti)===46){_ti++;const _tf=_ti;` +
398
607
  `while(_ti<_n){const _tc=${v}.charCodeAt(_ti)-48;if((_tc>>>0)>9)break;_ti++}` +
399
608
  'if(_ti===_tf)return false}' +
400
- 'if(_ti!==_n){' +
401
- `const _tz=${v}.charCodeAt(_ti);` +
609
+ 'if(_ti===_n)return false;' +
610
+ `const _tz=${v}.charCodeAt(_ti);let _off=0;` +
402
611
  'if(_tz===90||_tz===122){if(_ti!==_n-1)return false}' +
403
612
  'else{if(_tz!==43&&_tz!==45)return false;' +
404
613
  `if(_n-_ti!==6||${v}.charCodeAt(_ti+3)!==58)return false;` +
405
614
  `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}}`);
615
+ `(${v}.charCodeAt(_ti+4)-48>>>0)>9||(${v}.charCodeAt(_ti+5)-48>>>0)>9)return false;` +
616
+ `const _oh=(${v}.charCodeAt(_ti+1)-48)*10+(${v}.charCodeAt(_ti+2)-48),` +
617
+ `_om=(${v}.charCodeAt(_ti+4)-48)*10+(${v}.charCodeAt(_ti+5)-48);` +
618
+ 'if(_oh>23||_om>59)return false;' +
619
+ '_off=(_tz===43?1:-1)*(_oh*60+_om)}' +
620
+ 'if(_s1*10+_s2===60){' +
621
+ 'const _u=(((_h1*10+_h2)*60+(_m1*10+_m2)-_off)%1440+1440)%1440;' +
622
+ 'if(_u!==1439)return false}');
407
623
  }
408
624
 
409
625
  function uriSource (v, isStr) {
@@ -412,7 +628,8 @@ function uriSource (v, isStr) {
412
628
  `let _co=-1;for(let _i=1;_i<_n;_i++){const _c=${v}.charCodeAt(_i);if(_c===58){_co=_i;break}` +
413
629
  `if(!((_c>=48&&_c<=57)||(_c>=97&&_c<=122)||(_c>=65&&_c<=90)||_c===43||_c===45||_c===46))return false}` +
414
630
  `if(_co===-1)return false;` +
415
- noReservedSource(v, '_co+1'));
631
+ uriCharsSource(v, '_co+1') +
632
+ uriAuthoritySource(v, '_co+1'));
416
633
  }
417
634
 
418
635
  function ipv6Source (v, isStr) {
@@ -440,4 +657,161 @@ function ipv4Source (v, isStr) {
440
657
  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`);
441
658
  }
442
659
 
443
- module.exports = { date, ipv4, dateTime, ipv6, hostname, uri, uriReference, uuid, time, noReserved, dateSource, ipv4Source, dateTimeSource, ipv6Source, hostnameSource, uriSource, uuidSource, timeSource, noReservedSource };
660
+
661
+ // --- RFC 6901 JSON pointers -------------------------------------------------
662
+ // A pointer is empty or a run of "/"-prefixed tokens, and the only escape is
663
+ // "~" followed by 0 or 1. Everything else, including "#", is not a pointer:
664
+ // the fragment form belongs to a URI, not to this format.
665
+ function jsonPointer(s) {
666
+ if (s === '') return true;
667
+ if (s.charCodeAt(0) !== 47) return false; // '/'
668
+ for (let i = 0; i < s.length; i++) {
669
+ if (s.charCodeAt(i) !== 126) continue; // '~'
670
+ const n = s.charCodeAt(i + 1);
671
+ if (n !== 48 && n !== 49) return false; // '0' | '1'
672
+ }
673
+ return true;
674
+ }
675
+ function jsonPointerSource(v, isStr) {
676
+ const body = `{let _ok=${v}==='';if(!_ok&&${v}.charCodeAt(0)===47){_ok=true;for(let _i=0;_i<${v}.length;_i++){if(${v}.charCodeAt(_i)!==126)continue;const _n=${v}.charCodeAt(_i+1);if(_n!==48&&_n!==49){_ok=false;break}}}if(!_ok)return false}`;
677
+ return isStr ? body : `if(typeof ${v}==='string')${body}`;
678
+ }
679
+
680
+ // A relative pointer is a non-negative integer without leading zeros, then
681
+ // either "#" or a JSON pointer.
682
+ function relativeJsonPointer(s) {
683
+ let i = 0;
684
+ while (i < s.length) {
685
+ const c = s.charCodeAt(i);
686
+ if (c < 48 || c > 57) break;
687
+ i++;
688
+ }
689
+ if (i === 0) return false;
690
+ if (i > 1 && s.charCodeAt(0) === 48) return false; // leading zero
691
+ const rest = s.slice(i);
692
+ if (rest === '#') return true;
693
+ return jsonPointer(rest);
694
+ }
695
+ function relativeJsonPointerSource(v, isStr) {
696
+ const body = `{let _i=0;while(_i<${v}.length){const _c=${v}.charCodeAt(_i);if(_c<48||_c>57)break;_i++}let _ok=_i>0&&!(_i>1&&${v}.charCodeAt(0)===48);if(_ok){const _r=${v}.slice(_i);if(_r!=='#'){_ok=_r==='';if(!_ok&&_r.charCodeAt(0)===47){_ok=true;for(let _j=0;_j<_r.length;_j++){if(_r.charCodeAt(_j)!==126)continue;const _n=_r.charCodeAt(_j+1);if(_n!==48&&_n!==49){_ok=false;break}}}}}if(!_ok)return false}`;
697
+ return isStr ? body : `if(typeof ${v}==='string')${body}`;
698
+ }
699
+
700
+ // --- RFC 6570 URI templates -------------------------------------------------
701
+ // Everything outside braces must be a literal, and each "{...}" must hold one
702
+ // expression: an optional operator, then a comma-separated list of varspecs,
703
+ // each a possibly dotted name of varchars with an optional ":n" prefix length
704
+ // or "*" explode.
705
+ const URI_TEMPLATE_EXPR = /^(?:[+#./;?&=,!@|]?)(?:[A-Za-z0-9_%]|%[0-9A-Fa-f]{2})+(?:\.(?:[A-Za-z0-9_%]|%[0-9A-Fa-f]{2})+)*(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[A-Za-z0-9_%]|%[0-9A-Fa-f]{2})+(?:\.(?:[A-Za-z0-9_%]|%[0-9A-Fa-f]{2})+)*(?::[1-9][0-9]{0,3}|\*)?)*$/;
706
+ function uriTemplate(s) {
707
+ let i = 0;
708
+ while (i < s.length) {
709
+ const c = s.charCodeAt(i);
710
+ if (c === 125) return false; // '}' with no opening brace
711
+ // A literal excludes the controls, the space and DEL.
712
+ if (c !== 123 && (c <= 32 || c === 127)) return false;
713
+ if (c !== 123) { i++; continue; } // not '{'
714
+ const end = s.indexOf('}', i + 1);
715
+ if (end === -1) return false;
716
+ const expr = s.slice(i + 1, end);
717
+ if (expr === '' || expr.indexOf('{') !== -1) return false;
718
+ if (!URI_TEMPLATE_EXPR.test(expr)) return false;
719
+ i = end + 1;
720
+ }
721
+ return true;
722
+ }
723
+ function uriTemplateSource (v, isStr) {
724
+ const inner =
725
+ `let _i=0;` +
726
+ `while(_i<${v}.length){` +
727
+ `const _c=${v}.charCodeAt(_i);` +
728
+ `if(_c===125)return false;` +
729
+ `if(_c!==123&&(_c<=32||_c===127))return false;` +
730
+ `if(_c!==123){_i++;continue}` +
731
+ `const _e=${v}.indexOf('}',_i+1);` +
732
+ `if(_e===-1)return false;` +
733
+ `const _x=${v}.slice(_i+1,_e);` +
734
+ `if(_x===''||_x.indexOf('{')!==-1)return false;` +
735
+ `if(!${URI_TEMPLATE_EXPR.toString()}.test(_x))return false;` +
736
+ `_i=_e+1` +
737
+ `}`;
738
+ return isStr ? `{${inner}}` : `if(typeof ${v}==='string'){${inner}}`;
739
+ }
740
+
741
+ // --- RFC 5321 mailbox -------------------------------------------------------
742
+ // Local part is either a quoted string or dot-separated atoms, so a leading,
743
+ // trailing or doubled dot is not a mailbox. Domain is a hostname or a bracketed
744
+ // address literal.
745
+ // Local-part characters: alphanumerics, the RFC 5322 atext punctuation, and
746
+ // the dot that separates atoms. Written as code ranges rather than a regular
747
+ // expression because this lands inside every emitted module that validates an
748
+ // email, and the expression cost more bytes there than the whole check.
749
+ function isEmailLocalChar (c) {
750
+ return (c >= 48 && c <= 57) || (c >= 97 && c <= 122) || (c >= 65 && c <= 90) ||
751
+ c === 46 || c === 33 || (c >= 35 && c <= 39) || c === 42 || c === 43 ||
752
+ c === 45 || c === 47 || c === 61 || c === 63 || (c >= 94 && c <= 96) ||
753
+ (c >= 123 && c <= 126);
754
+ }
755
+ 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))`;
756
+ function email(s) {
757
+ const at = s.lastIndexOf('@');
758
+ if (at <= 0 || at === s.length - 1) return false;
759
+ const local = s.slice(0, at);
760
+ const domain = s.slice(at + 1);
761
+ if (local.length > 64) return false;
762
+ if (local.charCodeAt(0) === 34) {
763
+ // quoted string: must close, and the quote may only end the local part
764
+ if (local.charCodeAt(local.length - 1) !== 34 || local.length < 2) return false;
765
+ } else {
766
+ if (local.charCodeAt(0) === 46 || local.charCodeAt(local.length - 1) === 46) return false;
767
+ if (local.indexOf('..') !== -1) return false;
768
+ for (let i = 0; i < local.length; i++) if (!isEmailLocalChar(local.charCodeAt(i))) return false;
769
+ }
770
+ if (domain.charCodeAt(0) === 91) { // '['
771
+ if (domain.charCodeAt(domain.length - 1) !== 93 || domain.length <= 2) return false;
772
+ const inner = domain.slice(1, -1);
773
+ // IPv6 literals are tagged; anything else in brackets is an IPv4 address.
774
+ if (inner.startsWith('IPv6:')) return ipv6(inner.slice(5));
775
+ return ipv4(inner);
776
+ }
777
+ return hostname(domain);
778
+ }
779
+ // Emitted rather than bound as a closure: a standalone module embeds format
780
+ // functions by their source text, so anything reaching a free variable would
781
+ // not survive the trip. The domain half reuses hostnameSource, which keeps
782
+ // one hostname implementation for both halves of this file.
783
+ function emailSource (v, isStr) {
784
+ const inner =
785
+ `const _at=${v}.lastIndexOf('@');` +
786
+ `if(_at<=0||_at===${v}.length-1)return false;` +
787
+ `const _lp=${v}.slice(0,_at);const _dm=${v}.slice(_at+1);` +
788
+ `if(_lp.length>64)return false;` +
789
+ `if(_lp.charCodeAt(0)===34){if(_lp.length<2||_lp.charCodeAt(_lp.length-1)!==34)return false}` +
790
+ `else{` +
791
+ `if(_lp.charCodeAt(0)===46||_lp.charCodeAt(_lp.length-1)===46)return false;` +
792
+ `if(_lp.indexOf('..')!==-1)return false;` +
793
+ `for(let _i=0;_i<_lp.length;_i++){const _lc=_lp.charCodeAt(_i);if(!${EMAIL_LOCAL_SRC('_lc')})return false}` +
794
+ `}` +
795
+ `if(_dm.charCodeAt(0)===91){` +
796
+ `if(_dm.charCodeAt(_dm.length-1)!==93||_dm.length<=2)return false;` +
797
+ `const _in=_dm.slice(1,-1);` +
798
+ `if(_in.startsWith('IPv6:')){const _i6=_in.slice(5);${ipv6Source('_i6', true)}}` +
799
+ `else{${ipv4Source('_in', true)}}` +
800
+ `}` +
801
+ `else{${hostnameSource('_dm', true)}}`;
802
+ return isStr ? `{${inner}}` : `if(typeof ${v}==='string'){${inner}}`;
803
+ }
804
+
805
+
806
+ // RFC 3339 appendix A duration. The units form a chain rather than a set: a
807
+ // year may be followed by a month and a month by a day, and the same on the
808
+ // time side, so "P1Y2D" and "PT1H2S" are not durations. Weeks stand alone and
809
+ // no unit takes a fraction.
810
+ const DURATION_RE = /^P(?:\d+W|(?:\d+Y(?:\d+M(?:\d+D)?)?|\d+M(?:\d+D)?|\d+D)(?:T(?:\d+H(?:\d+M(?:\d+S)?)?|\d+M(?:\d+S)?|\d+S))?|T(?:\d+H(?:\d+M(?:\d+S)?)?|\d+M(?:\d+S)?|\d+S))$/;
811
+ function duration (s) { return DURATION_RE.test(s); }
812
+ function durationSource (v, isStr) {
813
+ const inner = `if(!${DURATION_RE.toString()}.test(${v}))return false`;
814
+ return isStr ? inner : `if(typeof ${v}==='string'&&${inner.slice(3)}`;
815
+ }
816
+
817
+ module.exports = { date, ipv4, dateTime, ipv6, hostname, uri, uriReference, uuid, time, noReserved, jsonPointer, relativeJsonPointer, uriTemplate, email, duration, uriChars, uriAuthority, uriCharsSource, uriAuthoritySource, iri, iriReference, idnEmail, iriSource, iriReferenceSource, idnEmailSource, dateSource, ipv4Source, dateTimeSource, ipv6Source, hostnameSource, uriSource, uuidSource, timeSource, noReservedSource, jsonPointerSource, relativeJsonPointerSource, uriTemplateSource, emailSource, durationSource };
@@ -249,19 +249,24 @@ function multipleOfOk(d, m) {
249
249
 
250
250
  const _formats = require('./formats');
251
251
  const FORMAT_CHECKS = {
252
- email: (s) => { const at = s.indexOf('@'); return at > 0 && at < s.length - 1 && s.indexOf('.', at) > at + 1; },
252
+ email: _formats.email,
253
253
  date: _formats.date,
254
254
  'date-time': _formats.dateTime,
255
255
  time: _formats.time,
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'),
256
+ duration: _formats.duration,
257
257
  uuid: _formats.uuid,
258
258
  uri: _formats.uri,
259
259
  'uri-reference': _formats.uriReference,
260
260
  ipv4: _formats.ipv4,
261
261
  ipv6: _formats.ipv6,
262
262
  hostname: _formats.hostname,
263
- regex: (s) => { try { new RegExp(s, 'u'); return true; } catch { try { new RegExp(s); return true; } catch { return false; } } },
264
- 'json-pointer': (s) => s === '' || (/^\//.test(s) && !/~(?![01])/.test(s)),
263
+ regex: (s) => { try { new RegExp(s, 'u'); return true; } catch { return false; } },
264
+ 'json-pointer': _formats.jsonPointer,
265
+ 'relative-json-pointer': _formats.relativeJsonPointer,
266
+ 'uri-template': _formats.uriTemplate,
267
+ iri: _formats.iri,
268
+ 'iri-reference': _formats.iriReference,
269
+ 'idn-email': _formats.idnEmail,
265
270
  };
266
271
 
267
272
  // ---------- evaluation ----------
@@ -2024,7 +2024,7 @@ function genCode(schema, v, lines, ctx, knownType) {
2024
2024
  if (schema.format) {
2025
2025
  const fc = FORMAT_CODEGEN[schema.format]
2026
2026
  if (fc) {
2027
- lines.push(fc(v, isStr))
2027
+ lines.push(fc(v, isStr, ctx))
2028
2028
  } else if (ctx.userFormats && typeof ctx.userFormats[schema.format] === 'function') {
2029
2029
  // User-supplied format checker: thread the function via closure and call at runtime.
2030
2030
  const safeName = schema.format.replace(/[^a-zA-Z0-9_]/g, '_')
@@ -2959,24 +2959,37 @@ function genCode(schema, v, lines, ctx, knownType) {
2959
2959
  }
2960
2960
  }
2961
2961
 
2962
+ // Formats whose check is large enough that repeating it at every call site
2963
+ // costs more than the call. They are hoisted once per compiled function and
2964
+ // invoked by name: one real schema with four email fields grew by 20 KB when
2965
+ // the check was inlined at each of them.
2966
+ const EMAIL_HELPER = `function _em(_s){${_formats.emailSource('_s', true)}return true}`
2967
+
2962
2968
  const FORMAT_CODEGEN = {
2963
- email: (v, isStr) => {
2964
- const guard = isStr ? '' : `typeof ${v}==='string'&&`
2965
- return isStr
2966
- ? `{const _at=${v}.indexOf('@');if(_at<=0||_at>=${v}.length-1||${v}.indexOf('.',_at)<=_at+1)return false}`
2967
- : `if(typeof ${v}==='string'){const _at=${v}.indexOf('@');if(_at<=0||_at>=${v}.length-1||${v}.indexOf('.',_at)<=_at+1)return false}`
2969
+ email: (v, isStr, ctx) => {
2970
+ if (!ctx) return _formats.emailSource(v, isStr)
2971
+ hoistOnce(ctx, '_emHoisted', EMAIL_HELPER)
2972
+ return isStr ? `if(!_em(${v}))return false` : `if(typeof ${v}==='string'&&!_em(${v}))return false`
2973
+ },
2974
+ 'json-pointer': _formats.jsonPointerSource,
2975
+ 'relative-json-pointer': _formats.relativeJsonPointerSource,
2976
+ 'uri-template': _formats.uriTemplateSource,
2977
+ iri: _formats.iriSource,
2978
+ 'iri-reference': _formats.iriReferenceSource,
2979
+ 'idn-email': _formats.idnEmailSource,
2980
+ regex: (v, isStr) => {
2981
+ const inner = `try{new RegExp(${v},'u')}catch(_er){return false}`
2982
+ return isStr ? `{${inner}}` : `if(typeof ${v}==='string'){${inner}}`
2968
2983
  },
2969
2984
  date: _formats.dateSource,
2970
2985
  uuid: _formats.uuidSource,
2971
2986
  'date-time': _formats.dateTimeSource,
2972
2987
  time: _formats.timeSource,
2973
- duration: (v, isStr) => isStr
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`
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`,
2988
+ duration: _formats.durationSource,
2976
2989
  uri: _formats.uriSource,
2977
2990
  'uri-reference': (v, isStr) => isStr
2978
- ? `{${_formats.noReservedSource(v, '0')}}`
2979
- : `if(typeof ${v}==='string'){${_formats.noReservedSource(v, '0')}}`,
2991
+ ? `{${_formats.uriCharsSource(v, '0')}}`
2992
+ : `if(typeof ${v}==='string'){${_formats.uriCharsSource(v, '0')}}`,
2980
2993
  ipv4: _formats.ipv4Source,
2981
2994
  ipv6: _formats.ipv6Source,
2982
2995
  hostname: _formats.hostnameSource,
@@ -3522,7 +3535,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3522
3535
  // let every later statement of the same check push the same error again.
3523
3536
  if (fc) {
3524
3537
  const ri = ctx.varCounter++
3525
- const fmtCode = fc(v, isStr).replace(/return false/g, `{${failPush};break _fmt${ri}}`)
3538
+ const fmtCode = fc(v, isStr, ctx).replace(/return false/g, `{${failPush};break _fmt${ri}}`)
3526
3539
  lines.push(`_fmt${ri}:{${fmtCode}}`)
3527
3540
  } else if (ctx.userFormats && typeof ctx.userFormats[schema.format] === 'function') {
3528
3541
  // User-supplied format checker on the error path. Same closure plumbing
@@ -4158,7 +4171,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4158
4171
  // Same rule as the error codegen: one violation, one error. The first
4159
4172
  // failing statement pushes and leaves the labeled block.
4160
4173
  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}}`)
4174
+ const code = fc(v, isStr, ctx).replace(/return false/g, `{${fail('format', 'format', `{format:'${esc(schema.format)}'}`, `'must match format "${esc(schema.format)}"'`)};break _fmt${ri}}`)
4162
4175
  lines.push(`_fmt${ri}:{${code}}`)
4163
4176
  } else if (ctx.userFormats && typeof ctx.userFormats[schema.format] === 'function') {
4164
4177
  const safeName = schema.format.replace(/[^a-zA-Z0-9_]/g, '_')
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.14.1';
10
+ module.exports = '1.15.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.14.1",
3
+ "version": "1.15.0",
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",
@@ -122,13 +122,13 @@
122
122
  "LICENSE"
123
123
  ],
124
124
  "optionalDependencies": {
125
- "@ata-validator/native-darwin-arm64": "1.14.1",
126
- "@ata-validator/native-darwin-x64": "1.14.1",
127
- "@ata-validator/native-linux-x64-gnu": "1.14.1",
128
- "@ata-validator/native-linux-arm64-gnu": "1.14.1",
129
- "@ata-validator/native-linux-x64-musl": "1.14.1",
130
- "@ata-validator/native-linux-arm64-musl": "1.14.1",
131
- "@ata-validator/native-win32-x64": "1.14.1"
125
+ "@ata-validator/native-darwin-arm64": "1.15.0",
126
+ "@ata-validator/native-darwin-x64": "1.15.0",
127
+ "@ata-validator/native-linux-x64-gnu": "1.15.0",
128
+ "@ata-validator/native-linux-arm64-gnu": "1.15.0",
129
+ "@ata-validator/native-linux-x64-musl": "1.15.0",
130
+ "@ata-validator/native-linux-arm64-musl": "1.15.0",
131
+ "@ata-validator/native-win32-x64": "1.15.0"
132
132
  },
133
133
  "peerDependencies": {
134
134
  "yaml": "^2.0.0"