ata-validator 1.62.0 → 1.63.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.
Files changed (56) hide show
  1. package/README.md +68 -13
  2. package/build.mjs +1 -0
  3. package/index.browser.mjs +23 -4
  4. package/index.bundle.mjs +14 -4
  5. package/index.edge.mjs +14 -4
  6. package/index.js +7 -2
  7. package/lib/aot-build.js +18 -0
  8. package/lib/aot-impl.js +20 -0
  9. package/lib/codegen-engine.js +72 -3
  10. package/lib/compiled-common.js +2 -1
  11. package/lib/email-literal.js +4 -1
  12. package/lib/error-runtime-features.js +41 -0
  13. package/lib/error-runtime.browser.js +15 -0
  14. package/lib/error-runtime.js +1 -32
  15. package/lib/formats-source.js +39 -40
  16. package/lib/interpreter-core.js +17 -9
  17. package/lib/ir/emit-deps.mjs +131 -0
  18. package/lib/ir/emit.mjs +560 -0
  19. package/lib/ir/front.mjs +834 -0
  20. package/lib/ir/index.mjs +10 -0
  21. package/lib/ir/refs.mjs +108 -0
  22. package/lib/ir/run.mjs +520 -0
  23. package/lib/ir/table.mjs +233 -0
  24. package/lib/js-compiler-errors.js +25 -7
  25. package/lib/js-compiler.js +6 -2
  26. package/lib/kernels/collapse.mjs +114 -0
  27. package/lib/kernels/custom.mjs +70 -0
  28. package/lib/kernels/fill.mjs +6 -0
  29. package/lib/kernels/formats-small.mjs +40 -0
  30. package/lib/kernels/formats.mjs +29 -0
  31. package/lib/kernels/index.mjs +11 -0
  32. package/lib/kernels/kernels.mjs +285 -0
  33. package/lib/kernels/leaves.mjs +28 -0
  34. package/lib/kernels/order.mjs +25 -0
  35. package/lib/kernels/plan-rich.mjs +17 -0
  36. package/lib/kernels/plan.d.ts +9 -0
  37. package/lib/kernels/plan.mjs +128 -0
  38. package/lib/kernels/preprocess.mjs +223 -0
  39. package/lib/kernels/rank.mjs +97 -0
  40. package/lib/kernels/rt.mjs +249 -0
  41. package/lib/kernels/table.mjs +182 -0
  42. package/lib/kernels/unique.mjs +157 -0
  43. package/lib/native-positions.js +1 -1
  44. package/lib/openapi.d.ts +2 -0
  45. package/lib/openapi.js +128 -0
  46. package/lib/plan-compiler.js +84 -7
  47. package/lib/public-api.js +7 -2
  48. package/lib/rejections.js +3 -1
  49. package/lib/schema-helpers.js +12 -0
  50. package/lib/schema-helpers.mjs +20 -0
  51. package/lib/schema-order.js +4 -2
  52. package/lib/standard-schema.js +1 -1
  53. package/lib/validator-core.js +35 -30
  54. package/lib/version.js +1 -1
  55. package/lite.js +7 -2
  56. package/package.json +32 -13
@@ -96,14 +96,23 @@ function dateTimeSource (v, isStr) {
96
96
  // third parameter here would silently receive a context object and emit
97
97
  // `_st=[object Object]`. That declines the whole compile, which is how it was
98
98
  // found.
99
+ // Membership in a set of ASCII characters as one shift: the 32-bit word for
100
+ // c's quarter of the range is picked by comparison and `>>>` reads bit c&31.
101
+ // Four numbers rather than a 128-entry table, so a module carries a few bytes,
102
+ // and one test per character where a chain of comparisons ran up to twelve.
103
+ // tests/test_format_char_masks.js checks every code point against the rule.
104
+ const maskSource = (v, w) => `((${v}<64?${v}<32?${w[0]}:${w[1]}:${v}<96?${w[2]}:${v}<128?${w[3]}:0)>>>${v}&1)`;
105
+ // A hostname label character: a letter, a digit or the hyphen.
106
+ const HOST_CHAR_WORDS = [0, 67051520, 134217726, 134217726];
107
+ const HOST_CHAR_SRC = (v) => maskSource(v, HOST_CHAR_WORDS);
108
+
99
109
  function hostnameRangeSource (v, st, en) {
100
- return `const _st=${st},_en=${en},_n=_en-_st;if(_n===0||_n>253)return false;let _ll=0,_pv=46;` +
101
- `for(let _i=_st;_i<_en;_i++){const _c=${v}.charCodeAt(_i);` +
102
- `if(_c===46){if(_ll===0||_pv===45)return false;_ll=0;_pv=_c;continue}` +
103
- `const _an=(_c>=48&&_c<=57)||(_c>=97&&_c<=122)||(_c>=65&&_c<=90);` +
104
- `if(!_an&&_c!==45)return false;` +
105
- `if(_c===45&&_pv===46)return false;` +
106
- `if(++_ll>63)return false;_pv=_c}` +
110
+ // An empty name never counts a label, so the `_ll===0` test at the end
111
+ // refuses it without a length test of its own.
112
+ return `if(${en}-(${st})>253)return false;let _ll=0,_pv=46;` +
113
+ `for(let _i=${st};_i<${en};_i++){const _c=${v}.charCodeAt(_i);` +
114
+ `if(_c===46){if(_ll===0||_pv===45)return false;_ll=0}` +
115
+ `else if(!${HOST_CHAR_SRC('_c')}||_c===45&&_pv===46||++_ll>63)return false;_pv=_c}` +
107
116
  `if(_ll===0||_pv===45)return false`;
108
117
  }
109
118
 
@@ -257,19 +266,14 @@ function uriTemplateSource (v, isStr) {
257
266
  // trailing or doubled dot is not a mailbox. Domain is a hostname or a bracketed
258
267
  // address literal.
259
268
  // Local-part characters: alphanumerics, the RFC 5322 atext punctuation, and
260
- // the dot that separates atoms. Written as code ranges rather than a regular
261
- // expression because this lands inside every emitted module that validates an
262
- // email, and the expression cost more bytes there than the whole check.
263
- // The emitted form keeps the comparison chain: a standalone module pays for the
264
- // bytes of a 128-entry table in every file that validates an email, and the
265
- // runtime pays for the chain on every call. Different budgets, same answers,
266
- // which `tests/test_format_engine_parity.js` holds.
267
- // Stated as the complement: of the printable range, only twelve characters are
268
- // not atext or the dot, so excluding them is 54 source bytes shorter than listing
269
- // what is allowed, and a standalone module carries this expression. Checked
270
- // against the allowed-list form over every code point from 0 to 65536 in
271
- // tests/test_formats_single_pass.js.
272
- 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)`;
269
+ // the dot that separates atoms, as a mask (maskSource above): every printable
270
+ // character except the twelve that are neither atext nor the dot. It replaced a
271
+ // chain of twelve comparisons, which was more bytes in every module that checks
272
+ // an email and up to twelve tests per character. Checked against the allowed-list
273
+ // form over every code point from 0 to 65536 in tests/test_format_char_masks.js,
274
+ // and against the runtime's check by `tests/test_format_engine_parity.js`.
275
+ const EMAIL_LOCAL_WORDS = [0, 2751458554, 3355443198, 2147483647];
276
+ const EMAIL_LOCAL_SRC = (v) => maskSource(v, EMAIL_LOCAL_WORDS);
273
277
  // Emitted rather than bound as a closure: a standalone module embeds format
274
278
  // functions by their source text, so anything reaching a free variable would
275
279
  // not survive the trip. The domain half reuses hostnameSource, which keeps
@@ -295,31 +299,26 @@ function emailSource (v, isStr) {
295
299
  // character, so in an unquoted address the first one is the separator and the
296
300
  // local part is checked on the way to it. A quoted local part may contain
297
301
  // '@' and keeps the backward scan, which is the rare shape.
298
- // The bounds tests are merged into one condition per branch, and there is no
299
- // separate length check: an empty local part fails `_at<=0` and an empty
300
- // domain fails `_at===_n0-1`, so a short string is already refused. This
301
- // lands in every emitted module that validates an email, and 25 bytes of it
302
- // put the CLI smoke build over its --max-size once.
303
- `const _n0=${v}.length;let _at;` +
302
+ // Both shapes share their variables and one bounds test after the local
303
+ // part: an empty local part fails `_at<1`, an empty domain `_at===_n0-1`,
304
+ // and `_pd` holds the character before the '@' (46 when it is a dot, which
305
+ // a quoted part sets aside). This lands in every emitted module that
306
+ // validates an email, so its bytes are counted (tests/test_email_bytes.js).
307
+ `const _n0=${v}.length;let _at=-1,_qi,_qc,_pd=46;` +
304
308
  `if(${v}.charCodeAt(0)===34){` +
305
- `_at=${v}.lastIndexOf('@');` +
306
- `if(_at<2||_at===_n0-1||_at>64||${v}.charCodeAt(_at-1)!==34)return false;` +
307
- `for(let _qi=1;_qi<_at-1;_qi++){const _qc=${v}.charCodeAt(_qi);` +
308
- `if(_qc===92){const _qp=${v}.charCodeAt(_qi+1);if(_qp<32||_qp>126||_qi+1>=_at-1)return false;_qi++;continue}` +
309
- `if(_qc<32||_qc>126||_qc===34)return false}` +
310
- `}else{` +
311
- `if(${v}.charCodeAt(0)===46)return false;` +
312
- `_at=-1;let _pd=false;` +
313
- `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}` +
314
- `if(_at<=0||_at===_n0-1||_at>64||${v}.charCodeAt(_at-1)===46)return false` +
315
- `}` +
309
+ `_at=${v}.lastIndexOf('@');if(_at<2||${v}.charCodeAt(_at-1)!==34)return false;` +
310
+ `for(_qi=1;_qi<_at-1;_qi++){_qc=${v}.charCodeAt(_qi);` +
311
+ `if(_qc===92){_qc=${v}.charCodeAt(++_qi);if(_qc<32||_qc>126||_qi>=_at-1)return false}` +
312
+ `else if(_qc<32||_qc>126||_qc===34)return false}_pd=0` +
313
+ `}else for(_qi=0;_qi<_n0;_qi++){_qc=${v}.charCodeAt(_qi);if(_qc===64){_at=_qi;break}` +
314
+ `if(_qc===46?_pd===46:!${EMAIL_LOCAL_SRC('_qc')})return false;_pd=_qc}` +
315
+ `if(_at<1||_at>64||_at===_n0-1||_pd===46)return false;` +
316
316
  // An address literal is rare enough to pay for its own slice inside the
317
317
  // branch that needs it; the hostname path, which is every real address,
318
318
  // reads the domain in place.
319
319
  `if(${v}.charCodeAt(_at+1)===91){` +
320
- `const _dm=${v}.slice(_at+1);` +
321
- `if(_dm.charCodeAt(_dm.length-1)!==93||_dm.length<=2)return false;` +
322
- `const _in=_dm.slice(1,-1);` +
320
+ `if(${v}.charCodeAt(_n0-1)!==93||_n0-_at<4)return false;` +
321
+ `const _in=${v}.slice(_at+2,-1);` +
323
322
  // The literal is rare, and checked by expression rather than by the
324
323
  // IPv6 and IPv4 parsers, which made it 72% of this check's source:
325
324
  // the same answers (tests/test_ipv6_expression.js), a fraction of the bytes.
@@ -89,8 +89,6 @@ function indexSchemas(rootSchema, schemaMap, flat) {
89
89
  state.rootBase = rootBase;
90
90
  if (flat === true && !schemaMap && typeof rootSchema === 'object' && rootSchema !== null) {
91
91
  state.resources.set(rootBase, rootSchema);
92
- state.anchors.set(rootBase, new Map());
93
- state.dynamicAnchors.set(rootBase, new Map());
94
92
  state.nodeBase.set(rootSchema, rootBase);
95
93
  return state;
96
94
  }
@@ -107,11 +105,15 @@ function indexSchemas(rootSchema, schemaMap, flat) {
107
105
  return state;
108
106
  }
109
107
 
108
+ function anchorMap(maps, baseUri) {
109
+ let m = maps.get(baseUri);
110
+ if (m === undefined) maps.set(baseUri, (m = new Map()));
111
+ return m;
112
+ }
113
+
110
114
  function indexResource(node, baseUri, state) {
111
115
  if (typeof node !== 'object' || node === null) return;
112
116
  if (!state.resources.has(baseUri)) state.resources.set(baseUri, node);
113
- if (!state.anchors.has(baseUri)) state.anchors.set(baseUri, new Map());
114
- if (!state.dynamicAnchors.has(baseUri)) state.dynamicAnchors.set(baseUri, new Map());
115
117
  walkSchema(node, baseUri, state, true);
116
118
  }
117
119
 
@@ -128,11 +130,14 @@ function walkSchema(node, baseUri, state, isResourceRoot) {
128
130
  return;
129
131
  }
130
132
 
131
- if (typeof node.$anchor === 'string') state.anchors.get(baseUri).set(node.$anchor, node);
133
+ // A resource's anchor maps are made at its first anchor: most schemas have
134
+ // none, and two empty maps per resource were a sixth of a small schema's
135
+ // interpreter. Every reader takes a missing map as no anchor.
136
+ if (typeof node.$anchor === 'string') anchorMap(state.anchors, baseUri).set(node.$anchor, node);
132
137
  if (typeof node.$dynamicAnchor === 'string') {
133
- state.dynamicAnchors.get(baseUri).set(node.$dynamicAnchor, node);
138
+ anchorMap(state.dynamicAnchors, baseUri).set(node.$dynamicAnchor, node);
134
139
  // A $dynamicAnchor is also addressable as a plain anchor.
135
- state.anchors.get(baseUri).set(node.$dynamicAnchor, node);
140
+ anchorMap(state.anchors, baseUri).set(node.$dynamicAnchor, node);
136
141
  }
137
142
 
138
143
  for (const kw of SCHEMA_KEYWORDS.single) {
@@ -847,7 +852,8 @@ class Interpreter {
847
852
  // 2020-12 requires bookending for `$dynamicRef`; v1 removes the
848
853
  // requirement. See the `$dynamicRef` branch in eval().
849
854
  this.bookending = !opts.v1;
850
- this.patternCache = new Map();
855
+ // Made at the first pattern.
856
+ this.patternCache = null;
851
857
  // Plans are built on first visit; a schema node is never re-read after.
852
858
  this.plans = new Map();
853
859
  this.rootNode = this.node(rootSchema);
@@ -919,6 +925,7 @@ class Interpreter {
919
925
  }
920
926
 
921
927
  pattern(src) {
928
+ if (this.patternCache === null) this.patternCache = new Map();
922
929
  let re = this.patternCache.get(src);
923
930
  if (!re) {
924
931
  if (/\\[pP]\{/.test(src)) {
@@ -1549,7 +1556,8 @@ function planCompiler() {
1549
1556
  let installCompat = null;
1550
1557
  let compatInstalled = null;
1551
1558
  function setCompat(install) {
1552
- installCompat = install;
1559
+ // esm.sh hands over a module of one function as { default: fn }.
1560
+ installCompat = typeof install === 'function' ? install : install && install.default;
1553
1561
  compatInstalled = null;
1554
1562
  }
1555
1563
  function compatExt() {
@@ -0,0 +1,131 @@
1
+ // Generated by scripts/gen-ir-emit-deps.js from lib/js-compiler.js and lib/suggestions.js. Do not edit.
2
+ const FIXED_LITERAL = /[A-Za-z0-9 _\-:@#%&=,;'"<>!~`\/]/
3
+ function compilePatternInline(pattern, varName) {
4
+ let m = pattern.match(/^\^(\[[\w\-]+\])\{(\d+)\}\$$/)
5
+ if (m) {
6
+ const len = parseInt(m[2])
7
+ if (len <= 16) {
8
+ const checks = []
9
+ for (let i = 0; i < len; i++) {
10
+ const ck = charClassToCheck(m[1], `${varName}.charCodeAt(${i})`)
11
+ if (!ck) return null
12
+ checks.push(ck)
13
+ }
14
+ return `${varName}.length===${len}&&${checks.join('&&')}`
15
+ }
16
+ const rangeCheck = charClassToCheck(m[1], `${varName}.charCodeAt(_pi)`)
17
+ if (!rangeCheck) return null
18
+ return `${varName}.length===${len}&&(()=>{for(let _pi=0;_pi<${len};_pi++){if(!(${rangeCheck}))return false}return true})()`
19
+ }
20
+ m = pattern.match(/^\^(\[[\w\-]+\])\+\$$/)
21
+ if (m) {
22
+ const rangeCheck = charClassToCheck(m[1], `${varName}.charCodeAt(_pi)`)
23
+ if (!rangeCheck) return null
24
+ return `${varName}.length>0&&(()=>{for(let _pi=0;_pi<${varName}.length;_pi++){if(!(${rangeCheck}))return false}return true})()`
25
+ }
26
+ m = pattern.match(/^\^(\[[\w\-]+\])\{(\d+),(\d+)\}\$$/)
27
+ if (m) {
28
+ const rangeCheck = charClassToCheck(m[1], `${varName}.charCodeAt(_pi)`)
29
+ if (!rangeCheck) return null
30
+ const min = parseInt(m[2]), max = parseInt(m[3])
31
+ return `${varName}.length>=${min}&&${varName}.length<=${max}&&(()=>{for(let _pi=0;_pi<${varName}.length;_pi++){if(!(${rangeCheck}))return false}return true})()`
32
+ }
33
+ return fixedSequenceInline(pattern, varName)
34
+ }
35
+ function fixedSequenceInline(pattern, varName) {
36
+ if (pattern.length < 3 || pattern[0] !== '^' || pattern[pattern.length - 1] !== '$' || pattern[pattern.length - 2] === '\\') return null
37
+ const end = pattern.length - 1
38
+ const pieces = []
39
+ let i = 1
40
+ while (i < end) {
41
+ let piece
42
+ const c = pattern[i]
43
+ if (c === '[') {
44
+ const j = pattern.indexOf(']', i)
45
+ if (j < 0 || j >= end) return null
46
+ const cls = pattern.slice(i, j + 1)
47
+ if (!/^\[[\w-]+\]$/.test(cls)) return null
48
+ piece = { cls }
49
+ i = j + 1
50
+ } else if (c === '\\') {
51
+ const e = pattern[i + 1]
52
+ if (e === 'd') piece = { cls: '[0-9]' }
53
+ else if (e !== undefined && './()[]{}+*?^$|\\'.includes(e)) piece = { lit: e }
54
+ else return null
55
+ i += 2
56
+ } else if (FIXED_LITERAL.test(c)) {
57
+ piece = { lit: c }
58
+ i += 1
59
+ } else {
60
+ return null
61
+ }
62
+ let count = 1
63
+ if (pattern[i] === '{') {
64
+ const q = /^\{(\d+)\}/.exec(pattern.slice(i))
65
+ if (!q) return null
66
+ count = Number(q[1])
67
+ if (!Number.isSafeInteger(count) || count > 32 - pieces.length) return null
68
+ i += q[0].length
69
+ } else if (i < end && '*+?'.includes(pattern[i])) {
70
+ return null
71
+ }
72
+ for (let k = 0; k < count; k++) pieces.push(piece)
73
+ if (pieces.length > 32) return null
74
+ }
75
+ if (pieces.length === 0) return null
76
+ const checks = []
77
+ for (let k = 0; k < pieces.length; k++) {
78
+ const at = `${varName}.charCodeAt(${k})`
79
+ if (pieces[k].lit !== undefined) checks.push(`${at}===${pieces[k].lit.charCodeAt(0)}`)
80
+ else {
81
+ const ck = charClassToCheck(pieces[k].cls, at)
82
+ if (!ck) return null
83
+ checks.push(`(${ck})`)
84
+ }
85
+ }
86
+ return `${varName}.length===${pieces.length}&&${checks.join('&&')}`
87
+ }
88
+ function charClassToCheck(charClass, codeExpr) {
89
+ const inner = charClass.slice(1, -1)
90
+ const ranges = []
91
+ let i = 0
92
+ while (i < inner.length) {
93
+ if (i + 2 < inner.length && inner[i + 1] === '-') {
94
+ ranges.push([inner.charCodeAt(i), inner.charCodeAt(i + 2)])
95
+ i += 3
96
+ } else {
97
+ ranges.push([inner.charCodeAt(i), inner.charCodeAt(i)])
98
+ i++
99
+ }
100
+ }
101
+ if (ranges.length === 0) return null
102
+ const checks = ranges.map(([lo, hi]) =>
103
+ lo === hi ? `${codeExpr}===${lo}` : `(${codeExpr}>=${lo}&&${codeExpr}<=${hi})`
104
+ )
105
+ return `(${checks.join('||')})`
106
+ }
107
+ const FORMAT_HINTS = {
108
+ email: (val) => {
109
+ if (typeof val !== 'string') return null;
110
+ if (!val.includes('@')) return "missing '@' and domain part";
111
+ if (val.split('@').length > 2) return "multiple '@' characters";
112
+ const [, dom] = val.split('@');
113
+ if (!dom || !dom.includes('.')) return 'domain part missing dot';
114
+ return null;
115
+ },
116
+ date: (val) => {
117
+ if (typeof val !== 'string') return null;
118
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(val);
119
+ if (!m) return 'expected YYYY-MM-DD layout';
120
+ const mo = +m[2]; if (mo < 1 || mo > 12) return 'month must be 01-12';
121
+ const d = +m[3]; if (d < 1 || d > 31) return 'day must be 01-31';
122
+ return null;
123
+ },
124
+ uuid: (val) => {
125
+ if (typeof val !== 'string') return null;
126
+ if (!/^[0-9a-fA-F-]+$/.test(val)) return 'expected hex digits and dashes';
127
+ return 'expected 8-4-4-4-12 hex layout';
128
+ },
129
+ ipv4: (val) => typeof val === 'string' ? 'expected four 0-255 octets separated by dots' : null,
130
+ };
131
+ export { compilePatternInline, FORMAT_HINTS };