ata-validator 1.47.0 → 1.49.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.
@@ -3,34 +3,56 @@
3
3
  const { parse } = require('./safe-regex')
4
4
 
5
5
  // True when the platform RegExp, a backtracking engine, also runs `src` in
6
- // linear time, so it can take the pattern: every unbounded quantifier on one
7
- // character, class or `.`; at most one of them, and then anchored with `^`;
8
- // a finite repeat may cover a group that holds no unbounded quantifier; few
9
- // bounded choices in all; only escapes both engines read alike; nothing
10
- // outside the BMP, so both see the same UTF-16 units.
11
- // tests/test_regex_native_linear.js holds it.
6
+ // linear time, so it can take the pattern instead of the linear-time engine
7
+ // in safe-regex.js, which costs three to five times as much per character.
8
+ // Two rules, and a pattern passes on either:
9
+ //
10
+ // The simple rule, the original one: every unbounded quantifier is on one
11
+ // character, class or `.`; at most one of them, and then the pattern is
12
+ // anchored with `^` on every branch; a finite repeat may cover a group that
13
+ // holds no unbounded quantifier; few bounded choices in all. One loop whose
14
+ // body cannot be split two ways, exited at most once per position, with
15
+ // bounded work after it.
16
+ //
17
+ // The deterministic rule, for the patterns the simple one declines: dotted
18
+ // identifiers, `[^/]+/[^/]+`, version numbers. On the Glushkov construction
19
+ // the characters, classes and dots are positions. Around every unbounded
20
+ // loop the next character must decide the single way to continue: the
21
+ // positions that can follow a position inside a loop accept pairwise
22
+ // disjoint characters, and so do the positions a match can start at when a
23
+ // loop is among them; a position reaches a follower by one route only, since
24
+ // `(a+)+` reaches `a` from `a` through the inner loop and again through the
25
+ // outer one and that double route is what a backtracking engine explores
26
+ // exponentially; a loop body must not match the empty string, since `(a*)*`
27
+ // iterates emptily without bound. Ambiguity away from any loop, `(pwa|PWA)`,
28
+ // costs a bounded factor and is allowed, as the simple rule allows it. A
29
+ // bounded repeat is a loop only when its body holds an unbounded one, since
30
+ // `(\w+\s?){5}` splits five ways where `(?:[A-F0-9]{2}:){31}` counts. With
31
+ // an unbounded quantifier the pattern must be anchored with `^` on every
32
+ // branch, since an unanchored search retries from each position.
33
+ //
34
+ // Both rules allow only escapes both engines read alike and nothing outside
35
+ // the BMP, so both see the same UTF-16 units. tests/test_regex_native_linear.js
36
+ // holds them: every accepted pattern answers as the linear engine does and
37
+ // stays fast on inputs built to make a backtracking engine retry.
12
38
  const NATIVE_ESCAPES = new Set(['d', 'D', 'w', 'W', 's', 'S', 'n', 'r', 't', 'f', 'v', '.', '\\', '/', '^', '$', '|', '?', '*', '+', '(', ')', '[', ']', '{', '}', '-', 'x', 'u'])
13
- function nativeIsLinear (src) {
14
- if (typeof src !== 'string' || src.length > 512) return false
15
- for (let i = 0; i < src.length; i++) {
16
- const c = src.charCodeAt(i)
17
- if (c >= 0xd800 && c <= 0xdfff) return false
18
- if (src[i] === '\\') {
19
- const e = src[i + 1]
20
- if (!NATIVE_ESCAPES.has(e)) return false
21
- if (e === 'x' && !/^[0-9a-fA-F]{2}$/.test(src.slice(i + 2, i + 4))) return false
22
- if (e === 'u' && !/^[0-9a-fA-F]{4}$/.test(src.slice(i + 2, i + 6))) return false
23
- i++
24
- }
25
- }
26
- let ast
27
- try { ast = parse(src) } catch { return false }
39
+ const MAX_BOUNDED_CHOICES = 64
40
+
41
+ const atom = (n) => n.t === 'char' || n.t === 'class' || n.t === 'any'
42
+
43
+ // Anchored at the start on every top-level branch.
44
+ function anchored (ast) {
45
+ let top = ast
46
+ while (top.t === 'group') top = top.child
47
+ const branches = top.t === 'alt' ? top.opts : [top]
48
+ const first = (n) => { while (n && n.t === 'group') n = n.child; return n && n.t === 'concat' ? first(n.parts[0]) : n }
49
+ return branches.every((b) => { const f = first(b); return f && f.t === 'bol' })
50
+ }
51
+
52
+ function simpleRule (ast) {
28
53
  // Per subtree: how many unbounded quantifiers it holds (`u`) and how many
29
54
  // bounded choices a backtracking engine could try (`b`), or null where the
30
- // shape is not provably linear. A finite repeat of a group is linear when
31
- // the group holds no unbounded quantifier: `(?:[A-F0-9]{2}:){31}` has
32
- // nothing to retry, and its choices multiply through the nesting.
33
- const atom = (n) => n.t === 'char' || n.t === 'class' || n.t === 'any'
55
+ // shape is not provably linear.
34
56
  const info = (n) => {
35
57
  switch (n.t) {
36
58
  case 'star': case 'plus':
@@ -56,19 +78,154 @@ function nativeIsLinear (src) {
56
78
  }
57
79
  }
58
80
  const r = info(ast)
59
- const ok = r !== null
60
- const unbounded = ok ? r.u : 0
61
- const bounded = ok ? r.b : 0
62
- if (!ok || unbounded > 1 || bounded > 64) return false
63
- if (unbounded === 1) {
64
- // Anchored at the start on every top-level branch.
65
- let top = ast
66
- while (top.t === 'group') top = top.child
67
- const branches = top.t === 'alt' ? top.opts : [top]
68
- const first = (n) => { while (n && n.t === 'group') n = n.child; return n && n.t === 'concat' ? first(n.parts[0]) : n }
69
- if (!branches.every((b) => { const f = first(b); return f && f.t === 'bol' })) return false
81
+ if (r === null || r.u > 1 || r.b > MAX_BOUNDED_CHOICES) return false
82
+ return r.u === 0 || anchored(ast)
83
+ }
84
+
85
+ // The characters a position accepts: 128 ASCII bits and one bit for "some
86
+ // code point at 128 or above", which keeps two classes with non-ASCII
87
+ // members apart only when one of them has none. Conservative in the right
88
+ // direction: an overlap that is not there only sends a pattern to the
89
+ // linear engine.
90
+ function charSet (node) {
91
+ const bits = new Uint8Array(128)
92
+ let high = 0
93
+ if (node.t === 'any') {
94
+ bits.fill(1); bits[10] = 0; bits[13] = 0; high = 1
95
+ } else if (node.t === 'char') {
96
+ if (node.c < 128) bits[node.c] = 1; else high = 1
97
+ } else {
98
+ for (const [lo, hi] of node.ranges) {
99
+ for (let c = Math.max(0, lo), top = Math.min(127, hi); c <= top; c++) bits[c] = 1
100
+ if (hi >= 128) high = 1
101
+ }
102
+ if (node.neg) { for (let c = 0; c < 128; c++) bits[c] ^= 1; high = 1 }
70
103
  }
71
- return true
104
+ return { bits, high }
105
+ }
106
+
107
+ function overlap (a, b) {
108
+ if (a.high && b.high) return true
109
+ for (let c = 0; c < 128; c++) if (a.bits[c] & b.bits[c]) return true
110
+ return false
111
+ }
112
+
113
+ // Whether a subtree holds an unbounded quantifier.
114
+ function hasUnbounded (n) {
115
+ switch (n.t) {
116
+ case 'star': case 'plus': return true
117
+ case 'repeat': return n.max === Infinity || hasUnbounded(n.child)
118
+ case 'quest': case 'group': return hasUnbounded(n.child)
119
+ case 'concat': return n.parts.some(hasUnbounded)
120
+ case 'alt': return n.opts.some(hasUnbounded)
121
+ default: return false
122
+ }
123
+ }
124
+
125
+ function deterministicRule (ast) {
126
+ const sets = []
127
+ const follow = []
128
+ const inLoop = []
129
+ let ok = true
130
+ let unbounded = 0
131
+ let bounded = 0
132
+ let depth = 0
133
+ // Pairwise disjoint, required when a loop position is among them.
134
+ const disjoint = (ids) => {
135
+ const arr = Array.from(ids)
136
+ if (!arr.some((p) => inLoop[p])) return
137
+ for (let i = 0; i < arr.length && ok; i++) {
138
+ for (let j = i + 1; j < arr.length; j++) if (overlap(sets[arr[i]], sets[arr[j]])) { ok = false; return }
139
+ }
140
+ }
141
+ const link = (from, to) => {
142
+ for (const p of to) {
143
+ if (follow[from].has(p)) { ok = false; return }
144
+ follow[from].add(p)
145
+ }
146
+ }
147
+ const empty = () => ({ nullable: true, first: new Set(), last: new Set() })
148
+ const walk = (n) => {
149
+ if (!ok) return empty()
150
+ switch (n.t) {
151
+ case 'char': case 'class': case 'any': {
152
+ const id = sets.length
153
+ sets.push(charSet(n))
154
+ follow.push(new Set())
155
+ inLoop.push(depth > 0)
156
+ return { nullable: false, first: new Set([id]), last: new Set([id]) }
157
+ }
158
+ case 'bol': case 'eol': return empty()
159
+ case 'group': return walk(n.child)
160
+ case 'star': case 'plus': case 'quest': case 'repeat': {
161
+ const infinite = n.t === 'star' || n.t === 'plus' || (n.t === 'repeat' && n.max === Infinity)
162
+ const loops = infinite || (n.t === 'repeat' && n.max > 1 && hasUnbounded(n.child))
163
+ const optional = n.t === 'star' || n.t === 'quest' || (n.t === 'repeat' && n.min === 0)
164
+ if (loops) depth++
165
+ const c = walk(n.child)
166
+ if (loops) depth--
167
+ if (infinite) {
168
+ unbounded++
169
+ if (c.nullable) ok = false
170
+ } else if (n.t === 'quest') {
171
+ bounded += 1
172
+ } else {
173
+ bounded += (n.max - n.min)
174
+ if (!atom(n.child)) bounded += n.max
175
+ }
176
+ if (loops) for (const l of c.last) link(l, c.first)
177
+ return { nullable: c.nullable || optional, first: c.first, last: c.last }
178
+ }
179
+ case 'concat': {
180
+ let acc = empty()
181
+ for (const part of n.parts) {
182
+ const r = walk(part)
183
+ for (const l of acc.last) link(l, r.first)
184
+ const first = new Set(acc.first)
185
+ if (acc.nullable) for (const f of r.first) first.add(f)
186
+ const last = new Set(r.last)
187
+ if (r.nullable) for (const l of acc.last) last.add(l)
188
+ acc = { nullable: acc.nullable && r.nullable, first, last }
189
+ }
190
+ return acc
191
+ }
192
+ case 'alt': {
193
+ const rs = n.opts.map(walk)
194
+ const first = new Set()
195
+ const last = new Set()
196
+ for (const r of rs) { for (const f of r.first) first.add(f); for (const l of r.last) last.add(l) }
197
+ return { nullable: rs.some((r) => r.nullable), first, last }
198
+ }
199
+ default:
200
+ ok = false
201
+ return empty()
202
+ }
203
+ }
204
+ const root = walk(ast)
205
+ if (!ok) return false
206
+ disjoint(root.first)
207
+ for (let p = 0; p < sets.length && ok; p++) disjoint(follow[p])
208
+ if (!ok) return false
209
+ if (bounded > MAX_BOUNDED_CHOICES) return false
210
+ return unbounded === 0 || anchored(ast)
211
+ }
212
+
213
+ function nativeIsLinear (src) {
214
+ if (typeof src !== 'string' || src.length > 512) return false
215
+ for (let i = 0; i < src.length; i++) {
216
+ const c = src.charCodeAt(i)
217
+ if (c >= 0xd800 && c <= 0xdfff) return false
218
+ if (src[i] === '\\') {
219
+ const e = src[i + 1]
220
+ if (!NATIVE_ESCAPES.has(e)) return false
221
+ if (e === 'x' && !/^[0-9a-fA-F]{2}$/.test(src.slice(i + 2, i + 4))) return false
222
+ if (e === 'u' && !/^[0-9a-fA-F]{4}$/.test(src.slice(i + 2, i + 6))) return false
223
+ i++
224
+ }
225
+ }
226
+ let ast
227
+ try { ast = parse(src) } catch { return false }
228
+ return simpleRule(ast) || deterministicRule(ast)
72
229
  }
73
230
 
74
231
  module.exports = { nativeIsLinear }
package/lib/rejections.js CHANGED
@@ -84,17 +84,16 @@ class RichRejection {
84
84
  // legacy key set, or enriched. Shared by both rejection classes below.
85
85
  function presentErrors(raw, data, rawInput, self, root, enrich) {
86
86
  if (needsOrdering(raw)) raw = sortErrorsBySchemaOrder(root, raw);
87
- // The position map, resolved now that an error is actually being read.
88
- let positions = null;
89
- if (enrich && raw.length && rawInput != null) {
90
- positions = self._pos().targeted(rawInput, wantedPointersFor(raw));
91
- if (positions) self._posCache.reset();
92
- }
87
+ // The position map, resolved on the first read of an error's frame (see
88
+ // defineLazyFrames in lib/enrich-error.js): a walk of the document that a
89
+ // caller reading messages and paths never needs.
90
+ const lazyPositions = enrich && raw.length && rawInput != null ? lazyPositionsFor(self, rawInput, raw) : null;
93
91
  // One options object for the whole list, not one per error.
94
92
  const opts = enrich && raw.length
95
93
  ? {
96
94
  data,
97
- positions,
95
+ positions: null,
96
+ lazyPositions,
98
97
  schemaPositions: self._schemaPositions,
99
98
  schemaFile: self._source ? self._source.path : undefined,
100
99
  }
@@ -180,13 +179,16 @@ Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
180
179
  // on raw errors; taking it as the signal left those errors without
181
180
  // `received` or a suggestion on the text path while validate() had both.
182
181
  if (!raw[0] || !('received' in raw[0])) {
183
- const positions = self._pos().targeted(jsonStr, wantedPointersFor(raw));
184
182
  // Declaration order, as validate() applies it; the text path used to
185
183
  // enrich in emission order.
186
184
  const ordered = needsOrdering(raw) ? sortErrorsBySchemaOrder(self._schemaObj, raw) : raw;
185
+ // The position map waits for the first read of a frame (lib/enrich-error
186
+ // defineLazyFrames); until then reading the list costs the enrichment alone.
187
+ const lazyPositions = lazyPositionsFor(self, jsonStr, ordered);
187
188
  const enrichOpts = {
188
189
  data: parsedData,
189
- positions,
190
+ positions: null,
191
+ lazyPositions,
190
192
  schemaPositions: self._schemaPositions,
191
193
  schemaFile: self._source ? self._source.path : undefined,
192
194
  };
@@ -195,11 +197,11 @@ Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
195
197
  attachDiagnosticSource(enriched, {
196
198
  data: parsedData,
197
199
  text: jsonStr,
198
- positions,
200
+ // Read by the diagnostic renderer only, which resolves the same map.
201
+ get positions () { return lazyPositions(); },
199
202
  schema: self._schemaObj,
200
203
  mutatesInput: self._mutatesInput === true,
201
204
  });
202
- if (positions) self._posCache.reset();
203
205
  this._cached = enriched;
204
206
  return enriched;
205
207
  }
@@ -232,6 +234,20 @@ Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
232
234
  },
233
235
  });
234
236
 
237
+ // The targeted position map for a list, built on the first call and shared
238
+ // by every error's frame accessor (lib/enrich-error defineLazyFrames).
239
+ function lazyPositionsFor (self, text, errors) {
240
+ let done = false;
241
+ let positions = null;
242
+ return () => {
243
+ if (done) return positions;
244
+ done = true;
245
+ positions = self._pos().targeted(text, wantedPointersFor(errors));
246
+ if (positions) self._posCache.reset();
247
+ return positions;
248
+ };
249
+ }
250
+
235
251
  // The pointers a set of errors will ask the position map about. lib/enrich-error
236
252
  // looks up the error's own path, and for an additional or unevaluated property
237
253
  // the child pointer named in `params`, unescaped, which is the form it asks for.
package/lib/safe-regex.js CHANGED
@@ -410,7 +410,8 @@ function __ataSafeRegex () {
410
410
  id = states.length
411
411
  let isMatch = false
412
412
  for (let k = 0; k < pcs.length; k++) if (ops[pcs[k]] === OP_MATCH) { isMatch = true; break }
413
- states.push({ pcs: Int32Array.from(pcs), isMatch, next: new Int32Array(128).fill(-2), nextEnd: new Int32Array(128).fill(-2) })
413
+ // `dead`: an anchored match with no thread left cannot recover.
414
+ states.push({ pcs: Int32Array.from(pcs), isMatch, dead: anchored && pcs.length === 0, next: new Int32Array(128).fill(-2), nextEnd: new Int32Array(128).fill(-2) })
414
415
  stateIds.set(key, id)
415
416
  return id
416
417
  }
@@ -452,23 +453,36 @@ function __ataSafeRegex () {
452
453
  }
453
454
  if (id < 0) return testNFA(s)
454
455
  let state = states[id]
455
- for (let pos = 0; pos < len; pos++) {
456
+ if (len === 0) return state.isMatch
457
+ // The last character is stepped with the end-of-input flag set and is
458
+ // handled after the loop, so the loop body selects no table and tests
459
+ // no position.
460
+ const last = len - 1
461
+ for (let pos = 0; pos < last; pos++) {
456
462
  if (state.isMatch) return true
457
463
  const c = s.charCodeAt(pos)
458
- const atEnd = pos + 1 === len
459
464
  let nid
460
465
  if (c < 128) {
461
- const table = atEnd ? state.nextEnd : state.next
462
- nid = table[c]
463
- if (nid === -2) { nid = step(state, c, atEnd); table[c] = nid }
466
+ nid = state.next[c]
467
+ if (nid === -2) { nid = step(state, c, false); state.next[c] = nid }
464
468
  } else {
465
- nid = step(state, c, atEnd)
469
+ nid = step(state, c, false)
466
470
  }
467
471
  if (nid < 0) return testNFA(s)
468
472
  state = states[nid]
469
- if (anchored && state.pcs.length === 0) return false
473
+ if (state.dead) return false
470
474
  }
471
- return state.isMatch
475
+ if (state.isMatch) return true
476
+ const c = s.charCodeAt(last)
477
+ let nid
478
+ if (c < 128) {
479
+ nid = state.nextEnd[c]
480
+ if (nid === -2) { nid = step(state, c, true); state.nextEnd[c] = nid }
481
+ } else {
482
+ nid = step(state, c, true)
483
+ }
484
+ if (nid < 0) return testNFA(s)
485
+ return states[nid].isMatch
472
486
  }
473
487
 
474
488
  return function test (s) {
@@ -892,7 +892,7 @@ function emitArray(schema, ctx, out, depth) {
892
892
  // Every `return 0` inside the member becomes that record, which is also how
893
893
  // the rule reaches nested objects: an inner object's own verdict is a value
894
894
  // failure at the level above it.
895
- function emitMember(sub, ctx, out, depth, objId) {
895
+ function emitMember(sub, ctx, out, depth, objId, kn) {
896
896
  let lines = [];
897
897
  emitValue(sub, ctx, lines, depth + 1);
898
898
  // A large member value becomes a function of its own, so no one function
@@ -921,9 +921,17 @@ function emitMember(sub, ctx, out, depth, objId) {
921
921
  out.push(label + ':{');
922
922
  out.push(body.split('return 0;').join('{_f' + objId + '=2;break ' + label + ';}'));
923
923
  out.push('}');
924
- // Resync from where the value started: a scan that stopped partway through
925
- // one cannot say where it ends, and the skipper can.
926
- out.push('if(_f' + objId + '===2){_f' + objId + '=1;i=_r.skipValue(s,_vs' + label + ');if(i<0)return 0;c=s.charCodeAt(i);}');
924
+ // A failed member is the verdict unless this object repeats the key later,
925
+ // since the parser keeps the last value. Whether it can is answered by two
926
+ // native searches over the rest of the text, for the key as written and
927
+ // for any backslash, which is the only way to spell that key differently:
928
+ // when neither is there the scan ends here, and a 69 KB body with a bad
929
+ // first item is refused in the time of an indexOf rather than a full pass.
930
+ // Otherwise resync from where the value started, since a scan that stopped
931
+ // partway through one cannot say where it ends and the skipper can, and
932
+ // go on, so a repeat still bails to the parser.
933
+ out.push('if(_f' + objId + '===2){if(s.indexOf(s.slice(_p' + kn + '-1,_e' + kn + '+1),i)<0&&s.indexOf("\\\\",i)<0)return 0;');
934
+ out.push('_f' + objId + '=1;i=_r.skipValue(s,_vs' + label + ');if(i<0)return 0;c=s.charCodeAt(i);}');
927
935
  }
928
936
 
929
937
  function emitObject(schema, ctx, out, depth) {
@@ -1111,7 +1119,7 @@ function emitObject(schema, ctx, out, depth) {
1111
1119
  const cond = gi === bounds.length - 1 ? '' : 'if(_kid' + kn + '<' + g.end + ')';
1112
1120
  out.push((gi === 0 ? '' : 'else ') + cond + '{');
1113
1121
  if (g.sub === undefined) out.push(addH);
1114
- else emitMember(g.sub, ctx, out, depth, n);
1122
+ else emitMember(g.sub, ctx, out, depth, n, kn);
1115
1123
  out.push('}');
1116
1124
  prev = g.end;
1117
1125
  });
@@ -1160,7 +1168,7 @@ function emitObject(schema, ctx, out, depth) {
1160
1168
  out.push('_s' + n + '_' + slot.w + '|=' + slot.b + ';');
1161
1169
  const sub = props && Object.prototype.hasOwnProperty.call(props, name) ? props[name] : undefined;
1162
1170
  if (sub === undefined) out.push(additional.join('\n'));
1163
- else emitMember(sub, ctx, out, depth, n);
1171
+ else emitMember(sub, ctx, out, depth, n, kn);
1164
1172
  out.push('}');
1165
1173
  }
1166
1174
  out.push('else{' + additional.join('\n') + '}');
@@ -25,7 +25,7 @@ function _registerCodegen(engine) {
25
25
  _codegen = engine;
26
26
  ({ compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined } = engine.jsCompiler);
27
27
  }
28
- const { normalizeDraft7, normalizeNullable, normalizeExclusiveBounds, stripFormatAssertions } = require("./draft7");
28
+ const { draftCode, normalizeDraft2019, normalizeDraft7, normalizeNullable, normalizeExclusiveBounds, stripFormatAssertions } = require("./draft7");
29
29
  const { enabledKeywords, stripDisabledKeywords } = require("./vocabularies");
30
30
  const { needsNormalization } = require("./schema-scan");
31
31
  const { isV1Dialect } = require("./dialect");
@@ -426,21 +426,26 @@ function _deepCloneWithSymbols(v) {
426
426
  // Normalize a caller-provided schema without mutating the original.
427
427
  // Clones only when normalization would change the object (draft-07 keys
428
428
  // present or nullable fields present). Internal-only — not exported.
429
+ // `inheritDraft7` is the root's draft for a document that declares none of
430
+ // its own: 0 or absent for none, 1 for draft-07, 2 for draft-06, 3 for 2019-09.
429
431
  function _normalizeCallerSchema(s, inheritDraft7) {
430
- const declares = s && typeof s === 'object' && s.$schema !== undefined
431
- const needsDraft7 = declares
432
- ? (s.$schema === 'http://json-schema.org/draft-07/schema#' || s.$schema === 'http://json-schema.org/draft-07/schema')
433
- : !!inheritDraft7
432
+ const declared = s && typeof s === 'object' ? s.$schema : undefined
433
+ const code = declared !== undefined ? draftCode(declared) : (inheritDraft7 || 0)
434
+ const needsDraft7 = code === 1 || code === 2
435
+ const draft6 = code === 2
436
+ const draft2019 = code === 3
434
437
  // One walk answers whether there is anything to do. Almost always there is
435
438
  // not, and then the serialize, clone, normalize, serialize, compare below is
436
439
  // work spent to find that out. The walk over-reports rather than under, so a
437
440
  // schema it clears is one no normalizer would have touched;
438
441
  // `tests/test_schema_scan.js` holds that direction against the whole suite.
439
- if (!needsNormalization(s, needsDraft7)) return s
442
+ // The scan does not look for `if`/`then`/`else`, which draft-06 drops.
443
+ if (!draft6 && !draft2019 && !needsNormalization(s, needsDraft7)) return s
440
444
 
441
445
  const str = JSON.stringify(s)
442
446
  const copy = _deepCloneWithSymbols(s)
443
- if (needsDraft7) normalizeDraft7(copy, true)
447
+ if (needsDraft7) normalizeDraft7(copy, true, draft6)
448
+ if (draft2019) normalizeDraft2019(copy, true)
444
449
  normalizeNullable(copy)
445
450
  normalizeExclusiveBounds(copy)
446
451
  // Return original when normalization produced no change, copy otherwise.
@@ -467,7 +472,7 @@ function declaredId(original, normalized) {
467
472
  return undefined
468
473
  }
469
474
 
470
- // `inheritDraft7` is true when the root schema is draft-07: a retrieved
475
+ // `inheritDraft7` is non-zero when the root schema is draft-07 or draft-06: a retrieved
471
476
  // document that declares no dialect is read under the root's draft.
472
477
  // The map is derived entirely from what the caller passed, so the same
473
478
  // `schemas` gives the same map. A server building one validator per route over
@@ -485,12 +490,12 @@ function buildSchemaMap(schemas, inheritDraft7) {
485
490
  if (!schemas) return null
486
491
  const byDraft = _schemaMapCache.get(schemas)
487
492
  if (byDraft) {
488
- const hit = byDraft[inheritDraft7 ? 1 : 0]
493
+ const hit = byDraft[inheritDraft7 || 0]
489
494
  if (hit) return hit
490
495
  }
491
496
  const map = _buildSchemaMap(schemas, inheritDraft7)
492
- const slot = byDraft || [null, null]
493
- slot[inheritDraft7 ? 1 : 0] = map
497
+ const slot = byDraft || [null, null, null, null]
498
+ slot[inheritDraft7 || 0] = map
494
499
  if (!byDraft) _schemaMapCache.set(schemas, slot)
495
500
  return map
496
501
  }
@@ -676,8 +681,7 @@ class Validator {
676
681
  // uses, and a benchmark timing construction measures construction.
677
682
  // When schema is a string, JSON.parse already produces a fresh object.
678
683
  const raw = typeof schema === "string" ? JSON.parse(schema) : schema;
679
- const rootIsDraft7 = !!(raw && typeof raw === 'object' && typeof raw.$schema === 'string' &&
680
- (raw.$schema === 'http://json-schema.org/draft-07/schema#' || raw.$schema === 'http://json-schema.org/draft-07/schema'));
684
+ const rootIsDraft7 = raw && typeof raw === 'object' ? draftCode(raw.$schema) : 0;
681
685
 
682
686
  // Built here rather than below because `$vocabulary` is resolved against
683
687
  // it, and that resolution waits until compilation so a meta-schema
@@ -1561,8 +1565,7 @@ class Validator {
1561
1565
  // Normalize a copy so the caller's object is never mutated. A document
1562
1566
  // without a dialect of its own is read under the root's draft.
1563
1567
  const root = this._schemaObj
1564
- const rootIsDraft7 = !!(root && typeof root === 'object' && typeof root.$schema === 'string' &&
1565
- (root.$schema === 'http://json-schema.org/draft-07/schema#' || root.$schema === 'http://json-schema.org/draft-07/schema'))
1568
+ const rootIsDraft7 = root && typeof root === 'object' ? draftCode(root.$schema) : 0
1566
1569
  const normalized = _normalizeCallerSchema(schema, rootIsDraft7)
1567
1570
  this._ownSchemaMap()
1568
1571
  this._schemaMap.set(normalized.$id, normalized)
@@ -1802,8 +1805,12 @@ Validator.loadBundle = function (mods, schemas, opts) {
1802
1805
  // simdjson when the addon is there, JSON.parse otherwise; the addon loads on
1803
1806
  // the first call rather than when the package is required.
1804
1807
  function parseJSON(input) {
1808
+ // The addon has not carried parseJSON since the per-platform packages
1809
+ // (1.1.0); the call threw a TypeError whenever the engine was loaded and
1810
+ // returned JSON.parse's answer otherwise. A Buffer is decoded first.
1805
1811
  const native = getNative();
1806
- return native ? native.parseJSON(input) : JSON.parse(input);
1812
+ if (native && typeof native.parseJSON === 'function') return native.parseJSON(input);
1813
+ return JSON.parse(typeof input === 'string' ? input : String(input));
1807
1814
  }
1808
1815
 
1809
1816
  // Ultra-fast compile: returns validate function directly, no Validator wrapper
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.47.0';
10
+ module.exports = '1.49.0';
@@ -50,6 +50,18 @@ function keywordsByVocabulary() {
50
50
  new Set(Object.keys(meta.properties)),
51
51
  )
52
52
  }
53
+ // Draft 2019-09's vocabularies, from its own vendored meta-schemas. A
54
+ // 2019-09 dialect names these URIs in `$vocabulary`.
55
+ for (const name of ['core', 'applicator', 'validation', 'meta-data', 'format', 'content']) {
56
+ const meta = require('./metaschemas').METASCHEMAS.get(
57
+ `https://json-schema.org/draft/2019-09/meta/${name}`,
58
+ )
59
+ if (!meta || !meta.properties) continue
60
+ byVocabulary.set(
61
+ `https://json-schema.org/draft/2019-09/vocab/${name}`,
62
+ new Set(Object.keys(meta.properties)),
63
+ )
64
+ }
53
65
  // The one vocabulary with no document to read. `format-assertion` is an
54
66
  // alternative to `format-annotation` rather than part of the standard
55
67
  // dialect, so its meta-schema is not among the vendored ones. It defines
package/lite.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ // ata-validator/lite: the same Validator and one-shot functions as
2
+ // ata-validator, on the interpreted engine and without the code generator.
3
+ // Validator#parse and the static bundle methods throw a TypeError here.
4
+ export {
5
+ Validator,
6
+ validate,
7
+ validateAsync,
8
+ parseAsync,
9
+ defineSchema,
10
+ version,
11
+ } from './index.js';
12
+ export type * from './index.js';
package/lite.js ADDED
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ // ata-validator/lite: the Validator without the code generator. Every schema
4
+ // runs on the interpreted engine, which passes the same official test suite as
5
+ // the compiled path and never calls `new Function`, so it also runs under a
6
+ // strict Content-Security-Policy. The results are the ones `ata-validator`
7
+ // gives: the same verdicts, the same errors. What it leaves out is what needs
8
+ // the code generator (parse(), the ahead-of-time bundle methods) and the tools
9
+ // no Validator calls (TypeScript generation, the renderers). Made for pages
10
+ // where the size of what ships matters more than nanoseconds per call.
11
+ const core = require('./lib/validator-core.js');
12
+
13
+ module.exports = {
14
+ Validator: core.Validator,
15
+ validate: core.validate,
16
+ validateAsync: core.validateAsync,
17
+ parseAsync: core.parseAsync,
18
+ defineSchema: core.defineSchema,
19
+ version: core.version,
20
+ };
package/lite.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import mod from './lite.js';
2
+ export const { Validator, validate, validateAsync, parseAsync, defineSchema, version } = mod;
3
+ export default mod;