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.
- package/README.md +45 -3
- package/index.js +22 -4
- package/lib/aot-impl.js +9 -1
- package/lib/draft7.js +106 -8
- package/lib/enrich-error.js +88 -12
- package/lib/enrich-site.js +1 -1
- package/lib/js-compiler-errors.js +103 -8
- package/lib/js-compiler.js +119 -7
- package/lib/metaschemas.js +21 -2
- package/lib/regex-linear.js +194 -37
- package/lib/rejections.js +27 -11
- package/lib/safe-regex.js +23 -9
- package/lib/scan-compiler.js +14 -6
- package/lib/validator-core.js +23 -16
- package/lib/version.js +1 -1
- package/lib/vocabularies.js +12 -0
- package/lite.d.ts +12 -0
- package/lite.js +20 -0
- package/lite.mjs +3 -0
- package/package.json +18 -9
package/lib/regex-linear.js
CHANGED
|
@@ -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
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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.
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
462
|
-
nid =
|
|
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,
|
|
469
|
+
nid = step(state, c, false)
|
|
466
470
|
}
|
|
467
471
|
if (nid < 0) return testNFA(s)
|
|
468
472
|
state = states[nid]
|
|
469
|
-
if (
|
|
473
|
+
if (state.dead) return false
|
|
470
474
|
}
|
|
471
|
-
|
|
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) {
|
package/lib/scan-compiler.js
CHANGED
|
@@ -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
|
-
//
|
|
925
|
-
//
|
|
926
|
-
|
|
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') + '}');
|
package/lib/validator-core.js
CHANGED
|
@@ -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
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
package/lib/vocabularies.js
CHANGED
|
@@ -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