ata-validator 1.6.2 → 1.7.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/CHANGELOG.md +26 -0
- package/README.md +6 -5
- package/build.d.ts +12 -0
- package/index.d.ts +7 -0
- package/index.js +58 -13
- package/lib/aot-build.js +1 -0
- package/lib/aot.js +69 -19
- package/lib/buffer-gate.js +132 -0
- package/lib/draft7.js +24 -2
- package/lib/interpreter.js +432 -200
- package/lib/js-compiler.js +184 -53
- package/lib/metaschemas.js +21 -0
- package/lib/safe-regex-source.js +1 -1
- package/lib/safe-regex.js +209 -29
- package/lib/version.js +1 -1
- package/package.json +8 -8
package/lib/js-compiler.js
CHANGED
|
@@ -108,6 +108,14 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
if (!defs && needsBaseTracking(schema, schemaMap, new Set())) return null
|
|
111
|
+
if (!defs && externalDocsNeedInterpreter(schema, schemaMap)) return null
|
|
112
|
+
|
|
113
|
+
// unevaluatedProperties / unevaluatedItems need annotation tracking this
|
|
114
|
+
// path does not do. Decline so the schema reaches an engine that does.
|
|
115
|
+
if (!defs) {
|
|
116
|
+
const str = JSON.stringify(schema)
|
|
117
|
+
if (str.includes('"unevaluatedProperties"') || str.includes('"unevaluatedItems"')) return null
|
|
118
|
+
}
|
|
111
119
|
|
|
112
120
|
// Collect $defs early so sub-schemas can resolve $ref
|
|
113
121
|
const rootDefs = defs || collectDefs(schema)
|
|
@@ -158,13 +166,13 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
158
166
|
const primitives = vals.filter(v => v === null || typeof v !== 'object')
|
|
159
167
|
const objects = vals.filter(v => v !== null && typeof v === 'object')
|
|
160
168
|
const primSet = new Set(primitives.map(v => v === null ? 'null' : typeof v === 'string' ? 's:' + v : 'n:' + v))
|
|
161
|
-
const objStrs = objects.map(v =>
|
|
169
|
+
const objStrs = objects.map(v => _canonical(v))
|
|
162
170
|
checks.push((d) => {
|
|
163
171
|
// Fast primitive check
|
|
164
172
|
const key = d === null ? 'null' : typeof d === 'string' ? 's:' + d : typeof d === 'number' || typeof d === 'boolean' ? 'n:' + d : null
|
|
165
173
|
if (key !== null && primSet.has(key)) return true
|
|
166
|
-
// Slow object check
|
|
167
|
-
const ds =
|
|
174
|
+
// Slow object check, key order independent
|
|
175
|
+
const ds = _canonical(d)
|
|
168
176
|
for (let i = 0; i < objStrs.length; i++) {
|
|
169
177
|
if (ds === objStrs[i]) return true
|
|
170
178
|
}
|
|
@@ -182,15 +190,15 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
182
190
|
if (cv === null || typeof cv !== 'object') {
|
|
183
191
|
checks.push((d) => d === cv)
|
|
184
192
|
} else {
|
|
185
|
-
const cs =
|
|
186
|
-
checks.push((d) =>
|
|
193
|
+
const cs = _canonical(cv)
|
|
194
|
+
checks.push((d) => _canonical(d) === cs)
|
|
187
195
|
}
|
|
188
196
|
}
|
|
189
197
|
|
|
190
|
-
// required
|
|
198
|
+
// required applies to objects only; other types are ignored.
|
|
191
199
|
if (schema.required && Array.isArray(schema.required)) {
|
|
192
200
|
for (const key of schema.required) {
|
|
193
|
-
checks.push((d) => typeof d
|
|
201
|
+
checks.push((d) => typeof d !== 'object' || d === null || Array.isArray(d) || d[key] !== undefined)
|
|
194
202
|
}
|
|
195
203
|
}
|
|
196
204
|
|
|
@@ -206,10 +214,10 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
206
214
|
}
|
|
207
215
|
}
|
|
208
216
|
|
|
209
|
-
// additionalProperties
|
|
210
|
-
if (schema.additionalProperties !== undefined
|
|
217
|
+
// additionalProperties (with or without a properties map)
|
|
218
|
+
if (schema.additionalProperties !== undefined) {
|
|
211
219
|
if (schema.additionalProperties === false) {
|
|
212
|
-
const allowed = new Set(Object.keys(schema.properties))
|
|
220
|
+
const allowed = new Set(Object.keys(schema.properties || {}))
|
|
213
221
|
checks.push((d) => {
|
|
214
222
|
if (typeof d !== 'object' || d === null || Array.isArray(d)) return true
|
|
215
223
|
const keys = Object.keys(d)
|
|
@@ -246,13 +254,14 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
246
254
|
}
|
|
247
255
|
}
|
|
248
256
|
|
|
249
|
-
// items
|
|
257
|
+
// items, applied after the prefixItems positions
|
|
250
258
|
if (schema.items) {
|
|
251
259
|
const itemCheck = compileToJS(schema.items, rootDefs)
|
|
252
260
|
if (!itemCheck) return null
|
|
261
|
+
const start = Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0
|
|
253
262
|
checks.push((d) => {
|
|
254
263
|
if (!Array.isArray(d)) return true
|
|
255
|
-
for (let i =
|
|
264
|
+
for (let i = start; i < d.length; i++) {
|
|
256
265
|
if (!itemCheck(d[i])) return false
|
|
257
266
|
}
|
|
258
267
|
return true
|
|
@@ -330,7 +339,11 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
330
339
|
}
|
|
331
340
|
if (schema.multipleOf !== undefined) {
|
|
332
341
|
const div = schema.multipleOf
|
|
333
|
-
checks.push((d) =>
|
|
342
|
+
checks.push((d) => {
|
|
343
|
+
if (typeof d !== 'number') return true
|
|
344
|
+
const r = d % div
|
|
345
|
+
return Math.abs(r) <= 1e-8 || Math.abs(r - div) <= 1e-8
|
|
346
|
+
})
|
|
334
347
|
}
|
|
335
348
|
|
|
336
349
|
// string
|
|
@@ -352,10 +365,12 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
352
365
|
}
|
|
353
366
|
}
|
|
354
367
|
|
|
355
|
-
// format — hand-written fast checks
|
|
368
|
+
// format — hand-written fast checks. A format the code generator asserts
|
|
369
|
+
// but this path cannot is a decline, not a pass.
|
|
356
370
|
if (schema.format) {
|
|
357
371
|
const fc = FORMAT_CHECKS[schema.format]
|
|
358
372
|
if (fc) checks.push((d) => typeof d !== 'string' || fc(d))
|
|
373
|
+
else if (FORMAT_CODEGEN[schema.format]) return null
|
|
359
374
|
}
|
|
360
375
|
|
|
361
376
|
// array size
|
|
@@ -371,11 +386,11 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
371
386
|
// object size
|
|
372
387
|
if (schema.minProperties !== undefined) {
|
|
373
388
|
const min = schema.minProperties
|
|
374
|
-
checks.push((d) => typeof d !== 'object' || d === null || Object.keys(d).length >= min)
|
|
389
|
+
checks.push((d) => typeof d !== 'object' || d === null || Array.isArray(d) || Object.keys(d).length >= min)
|
|
375
390
|
}
|
|
376
391
|
if (schema.maxProperties !== undefined) {
|
|
377
392
|
const max = schema.maxProperties
|
|
378
|
-
checks.push((d) => typeof d !== 'object' || d === null || Object.keys(d).length <= max)
|
|
393
|
+
checks.push((d) => typeof d !== 'object' || d === null || Array.isArray(d) || Object.keys(d).length <= max)
|
|
379
394
|
}
|
|
380
395
|
|
|
381
396
|
// allOf
|
|
@@ -558,7 +573,7 @@ function walkJsonPointer(root, fragment) {
|
|
|
558
573
|
for (const p of parts) {
|
|
559
574
|
if (target == null || typeof target !== 'object') return null
|
|
560
575
|
if (!(p in target)) {
|
|
561
|
-
const alt = p === 'definitions' ? '$defs' : p === '$defs' ? 'definitions' : null
|
|
576
|
+
const alt = p === 'definitions' ? '$defs' : p === '$defs' ? 'definitions' : p === 'items' && Array.isArray(target.prefixItems) ? 'prefixItems' : null
|
|
562
577
|
if (alt !== null && alt in target) { target = target[alt]; continue }
|
|
563
578
|
return null
|
|
564
579
|
}
|
|
@@ -597,8 +612,9 @@ function resolveCrossSchemaRef(ref, schemaMap) {
|
|
|
597
612
|
}
|
|
598
613
|
|
|
599
614
|
function resolveRef(ref, defs, schemaMap) {
|
|
600
|
-
// Self-reference: "#"
|
|
601
|
-
|
|
615
|
+
// Self-reference: "#" cannot be expressed as a closure without a recursion
|
|
616
|
+
// guard. Decline rather than substitute a vacuous check.
|
|
617
|
+
if (ref === '#') return null
|
|
602
618
|
|
|
603
619
|
// 1. Local ref
|
|
604
620
|
if (defs) {
|
|
@@ -660,6 +676,13 @@ const TYPE_CHECKS = {
|
|
|
660
676
|
object: (d) => typeof d === 'object' && d !== null && !Array.isArray(d),
|
|
661
677
|
}
|
|
662
678
|
|
|
679
|
+
// Key-order independent JSON rendering for const/enum comparison.
|
|
680
|
+
function _canonical(x) {
|
|
681
|
+
if (x === null || typeof x !== 'object') return JSON.stringify(x)
|
|
682
|
+
if (Array.isArray(x)) return '[' + x.map(_canonical).join(',') + ']'
|
|
683
|
+
return '{' + Object.keys(x).sort().map((k) => JSON.stringify(k) + ':' + _canonical(x[k])).join(',') + '}'
|
|
684
|
+
}
|
|
685
|
+
|
|
663
686
|
const FORMAT_CHECKS = {
|
|
664
687
|
email: (s) => { const at = s.indexOf('@'); return at > 0 && at < s.length - 1 && s.indexOf('.', at) > at + 1 },
|
|
665
688
|
date: (s) => { if (s.length !== 10 || !/^\d{4}-\d{2}-\d{2}$/.test(s)) return false; const m = +s.slice(5, 7), d = +s.slice(8, 10); return m >= 1 && m <= 12 && d >= 1 && d <= 31 },
|
|
@@ -1147,14 +1170,85 @@ function hasUnresolvableRef(node, rootDefs, anchors, schemaMap, seen) {
|
|
|
1147
1170
|
return false
|
|
1148
1171
|
}
|
|
1149
1172
|
|
|
1173
|
+
|
|
1174
|
+
// --- The one gate every code generation entry point runs first ---
|
|
1175
|
+
//
|
|
1176
|
+
// Four entry points build four contexts, and three times a bail present in
|
|
1177
|
+
// one was missing from another; each time the generator emitted nothing for
|
|
1178
|
+
// a construct it could not represent and returned the empty program as
|
|
1179
|
+
// always-valid. The checks that do not depend on an entry point's own anchor
|
|
1180
|
+
// map live here, so adding a bail is one edit. Entry-point-specific checks
|
|
1181
|
+
// (hasUnresolvableRef against each path's anchors) still run after this.
|
|
1182
|
+
// tests/test_codegen_entrypoint_agreement.js holds the four to one verdict.
|
|
1183
|
+
|
|
1184
|
+
function collectExternalRefKeys(node, out, seen) {
|
|
1185
|
+
if (typeof node !== 'object' || node === null) return
|
|
1186
|
+
if (seen.has(node)) return
|
|
1187
|
+
seen.add(node)
|
|
1188
|
+
if (Array.isArray(node)) { for (const n of node) collectExternalRefKeys(n, out, seen); return }
|
|
1189
|
+
for (const key of Object.keys(node)) {
|
|
1190
|
+
const v = node[key]
|
|
1191
|
+
if ((key === '$ref' || key === '$dynamicRef') && typeof v === 'string' && !v.startsWith('#')) out.add(v.split('#')[0])
|
|
1192
|
+
else if (typeof v === 'object' && v !== null && key !== 'enum' && key !== 'const' && key !== 'default' && key !== 'examples') collectExternalRefKeys(v, out, seen)
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function lookupExternal(key, schemaMap) {
|
|
1197
|
+
if (schemaMap.has(key)) return schemaMap.get(key)
|
|
1198
|
+
if (!key.includes('://')) {
|
|
1199
|
+
for (const [id, doc] of schemaMap) if (id.endsWith('/' + key)) return doc
|
|
1200
|
+
}
|
|
1201
|
+
return null
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// Documents reachable from `schema` through cross-document references.
|
|
1205
|
+
function reachableExternalDocs(schema, schemaMap) {
|
|
1206
|
+
const docs = new Set()
|
|
1207
|
+
if (!schemaMap || schemaMap.size === 0) return docs
|
|
1208
|
+
const queue = [schema]
|
|
1209
|
+
const seenDocs = new Set([schema])
|
|
1210
|
+
while (queue.length) {
|
|
1211
|
+
const doc = queue.shift()
|
|
1212
|
+
const keys = new Set()
|
|
1213
|
+
collectExternalRefKeys(doc, keys, new Set())
|
|
1214
|
+
for (const key of keys) {
|
|
1215
|
+
const target = lookupExternal(key, schemaMap)
|
|
1216
|
+
if (target && !seenDocs.has(target)) { seenDocs.add(target); docs.add(target); queue.push(target) }
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
return docs
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// Dynamic scope and annotation tracking across documents is interpreter
|
|
1223
|
+
// work. A referenced document that uses either must decline the whole
|
|
1224
|
+
// schema, or the generator emits a vacuous check for the reference.
|
|
1225
|
+
function externalDocsNeedInterpreter(schema, schemaMap) {
|
|
1226
|
+
for (const doc of reachableExternalDocs(schema, schemaMap)) {
|
|
1227
|
+
const str = JSON.stringify(doc)
|
|
1228
|
+
if (str.includes('"$dynamicRef"') || str.includes('"$dynamicAnchor"') ||
|
|
1229
|
+
str.includes('"unevaluatedProperties"') || str.includes('"unevaluatedItems"')) return true
|
|
1230
|
+
// An embedded $id inside a referenced document opens a resource the
|
|
1231
|
+
// generator never registers, so references to it would be vacuous.
|
|
1232
|
+
if (typeof doc === 'object' && doc !== null && hasNestedIdScope(doc)) return true
|
|
1233
|
+
}
|
|
1234
|
+
return false
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function sharedCodegenGate(schema, schemaMap) {
|
|
1238
|
+
if (typeof schema !== 'object' || schema === null) return true
|
|
1239
|
+
if (!codegenSafe(schema, schemaMap)) return false
|
|
1240
|
+
if (needsBaseTracking(schema, schemaMap, new Set())) return false
|
|
1241
|
+
if (externalDocsNeedInterpreter(schema, schemaMap)) return false
|
|
1242
|
+
return true
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1150
1245
|
// --- Codegen mode: generates a single Function (NOT CSP-safe) ---
|
|
1151
1246
|
// This matches ajv's approach: one monolithic function, V8 JIT fully inlines it
|
|
1152
1247
|
function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
1153
1248
|
if (typeof schema === 'boolean') return schema ? () => true : () => false
|
|
1154
1249
|
if (typeof schema !== 'object' || schema === null) return null
|
|
1155
1250
|
|
|
1156
|
-
|
|
1157
|
-
if (!codegenSafe(schema, schemaMap)) return null
|
|
1251
|
+
if (!sharedCodegenGate(schema, schemaMap)) return null
|
|
1158
1252
|
|
|
1159
1253
|
// Collect defs for $ref resolution
|
|
1160
1254
|
const rootDefs = schema.$defs || schema.definitions || null
|
|
@@ -1290,7 +1384,13 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
|
1290
1384
|
const fmtEntries = []
|
|
1291
1385
|
for (let i = 0; i < closureNames.length; i++) {
|
|
1292
1386
|
if (closureNames[i].startsWith('_uf_')) {
|
|
1293
|
-
|
|
1387
|
+
// The format's own name, so an emitter can look it up at runtime
|
|
1388
|
+
// instead of embedding the function.
|
|
1389
|
+
let format = null
|
|
1390
|
+
for (const key of Object.keys(ctx.userFormats)) {
|
|
1391
|
+
if (ctx.userFormats[key] === closureValues[i]) { format = key; break }
|
|
1392
|
+
}
|
|
1393
|
+
fmtEntries.push({ name: closureNames[i], fn: closureValues[i], format })
|
|
1294
1394
|
}
|
|
1295
1395
|
}
|
|
1296
1396
|
if (fmtEntries.length) boolFn._formatClosures = fmtEntries
|
|
@@ -1434,7 +1534,7 @@ function tryGenCombined(schema, access, ctx) {
|
|
|
1434
1534
|
if (schema.maximum !== undefined) conds.push(`_v>${schema.maximum}`)
|
|
1435
1535
|
if (schema.exclusiveMinimum !== undefined) conds.push(`_v<=${schema.exclusiveMinimum}`)
|
|
1436
1536
|
if (schema.exclusiveMaximum !== undefined) conds.push(`_v>=${schema.exclusiveMaximum}`)
|
|
1437
|
-
if (schema.multipleOf !== undefined) conds.push(`_v%${schema.multipleOf}
|
|
1537
|
+
if (schema.multipleOf !== undefined) conds.push(`(Math.abs(_v%${schema.multipleOf})>1e-8&&Math.abs(_v%${schema.multipleOf}-${schema.multipleOf})>1e-8)`)
|
|
1438
1538
|
if (conds.length < 2) return null
|
|
1439
1539
|
return bind(conds)
|
|
1440
1540
|
}
|
|
@@ -1445,7 +1545,7 @@ function tryGenCombined(schema, access, ctx) {
|
|
|
1445
1545
|
if (schema.maximum !== undefined) conds.push(`_v>${schema.maximum}`)
|
|
1446
1546
|
if (schema.exclusiveMinimum !== undefined) conds.push(`_v<=${schema.exclusiveMinimum}`)
|
|
1447
1547
|
if (schema.exclusiveMaximum !== undefined) conds.push(`_v>=${schema.exclusiveMaximum}`)
|
|
1448
|
-
if (schema.multipleOf !== undefined) conds.push(`_v%${schema.multipleOf}
|
|
1548
|
+
if (schema.multipleOf !== undefined) conds.push(`(Math.abs(_v%${schema.multipleOf})>1e-8&&Math.abs(_v%${schema.multipleOf}-${schema.multipleOf})>1e-8)`)
|
|
1449
1549
|
if (conds.length < 2) return null
|
|
1450
1550
|
return bind(conds)
|
|
1451
1551
|
}
|
|
@@ -1458,7 +1558,9 @@ function tryGenCombined(schema, access, ctx) {
|
|
|
1458
1558
|
// function is only safe when we're at the root (`v === 'd'`). For nested
|
|
1459
1559
|
// nodes, emit inline so block-scoped variables like `_o0` stay in scope.
|
|
1460
1560
|
function _deferOrInline(ctx, lines, v, check) {
|
|
1461
|
-
|
|
1561
|
+
// Deferring is only sound at the top level of the root function. Inside a
|
|
1562
|
+
// conditional applicator (dependentSchemas) the check must stay in its block.
|
|
1563
|
+
if (v === 'd' && !ctx.condDepth) {
|
|
1462
1564
|
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1463
1565
|
ctx.deferredChecks.push(check)
|
|
1464
1566
|
} else {
|
|
@@ -1599,7 +1701,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1599
1701
|
const isArr = effectiveType === 'array'
|
|
1600
1702
|
const isStr = effectiveType === 'string'
|
|
1601
1703
|
const isNum = effectiveType === 'number' || effectiveType === 'integer'
|
|
1602
|
-
const objGuard = isObj ? '' : `typeof ${v}==='object'&&${v}!==null&&`
|
|
1704
|
+
const objGuard = isObj ? '' : `typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&`
|
|
1603
1705
|
const objCheck = isObj ? '' : `if(typeof ${v}!=='object'||${v}===null)return false;`
|
|
1604
1706
|
|
|
1605
1707
|
// enum
|
|
@@ -1619,7 +1721,12 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1619
1721
|
if (cv === null || typeof cv !== 'object') {
|
|
1620
1722
|
lines.push(`if(${v}!==${JSON.stringify(cv)})return false`)
|
|
1621
1723
|
} else {
|
|
1622
|
-
|
|
1724
|
+
// Key-order independent comparison, defined inline so every emitter
|
|
1725
|
+
// (runtime, hybrid, standalone) carries it.
|
|
1726
|
+
const ci = ctx.varCounter++
|
|
1727
|
+
const canonFn = `_cnB${ci}`
|
|
1728
|
+
const expected = _canonical(cv)
|
|
1729
|
+
lines.push(`{const ${canonFn}=function(x){if(x===null||typeof x!=='object')return JSON.stringify(x);if(Array.isArray(x))return'['+x.map(${canonFn}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+${canonFn}(x[k])}).join(',')+'}'};if(${canonFn}(${v})!==${JSON.stringify(expected)})return false}`)
|
|
1623
1730
|
}
|
|
1624
1731
|
}
|
|
1625
1732
|
|
|
@@ -1647,9 +1754,9 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1647
1754
|
const checks = schema.required.map(key => `${v}[${JSON.stringify(key)}]===undefined`)
|
|
1648
1755
|
lines.push(`if(${checks.join('||')})return false`)
|
|
1649
1756
|
} else {
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
}
|
|
1757
|
+
// required applies to objects only; other types are ignored.
|
|
1758
|
+
const checks = schema.required.map(key => `${v}[${JSON.stringify(key)}]===undefined`)
|
|
1759
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&(${checks.join('||')}))return false`)
|
|
1653
1760
|
}
|
|
1654
1761
|
}
|
|
1655
1762
|
|
|
@@ -1679,7 +1786,11 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1679
1786
|
if (schema.maximum !== undefined) lines.push(isNum ? `if(${v}>${schema.maximum})return false` : `if(typeof ${v}==='number'&&${v}>${schema.maximum})return false`)
|
|
1680
1787
|
if (schema.exclusiveMinimum !== undefined) lines.push(isNum ? `if(${v}<=${schema.exclusiveMinimum})return false` : `if(typeof ${v}==='number'&&${v}<=${schema.exclusiveMinimum})return false`)
|
|
1681
1788
|
if (schema.exclusiveMaximum !== undefined) lines.push(isNum ? `if(${v}>=${schema.exclusiveMaximum})return false` : `if(typeof ${v}==='number'&&${v}>=${schema.exclusiveMaximum})return false`)
|
|
1682
|
-
if (schema.multipleOf !== undefined)
|
|
1789
|
+
if (schema.multipleOf !== undefined) {
|
|
1790
|
+
const m = schema.multipleOf
|
|
1791
|
+
const bad = `(Math.abs(${v}%${m})>1e-8&&Math.abs(${v}%${m}-${m})>1e-8)`
|
|
1792
|
+
lines.push(isNum ? `if${bad}return false` : `if(typeof ${v}==='number'&&${bad})return false`)
|
|
1793
|
+
}
|
|
1683
1794
|
|
|
1684
1795
|
// string length — skip type guard if known string.
|
|
1685
1796
|
// s.length (UTF-16 code units) is an upper bound on cpLen, and at least cpLen
|
|
@@ -1938,7 +2049,9 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1938
2049
|
for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
|
|
1939
2050
|
const guard = isObj ? '' : `typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&`
|
|
1940
2051
|
lines.push(`if(${guard}${JSON.stringify(key)} in ${v}){`)
|
|
2052
|
+
ctx.condDepth = (ctx.condDepth || 0) + 1
|
|
1941
2053
|
genCode(depSchema, v, lines, ctx, effectiveType)
|
|
2054
|
+
ctx.condDepth--
|
|
1942
2055
|
lines.push(`}`)
|
|
1943
2056
|
}
|
|
1944
2057
|
}
|
|
@@ -2015,9 +2128,11 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2015
2128
|
const idx = `_j${ctx.varCounter}`
|
|
2016
2129
|
const elem = `_e${ctx.varCounter}`
|
|
2017
2130
|
ctx.varCounter++
|
|
2131
|
+
// items applies after the prefixItems positions (Draft 2020-12).
|
|
2132
|
+
const start = Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0
|
|
2018
2133
|
lines.push(isArr
|
|
2019
|
-
? `for(let ${idx}
|
|
2020
|
-
: `if(Array.isArray(${v})){for(let ${idx}
|
|
2134
|
+
? `for(let ${idx}=${start};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`
|
|
2135
|
+
: `if(Array.isArray(${v})){for(let ${idx}=${start};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
|
|
2021
2136
|
genCode(schema.items, elem, lines, ctx)
|
|
2022
2137
|
lines.push(isArr ? `}` : `}}`)
|
|
2023
2138
|
}
|
|
@@ -2682,6 +2797,9 @@ const FORMAT_CODEGEN = {
|
|
|
2682
2797
|
|
|
2683
2798
|
// Safe key escaping: use JSON.stringify to handle all special chars (newlines, null bytes, etc.)
|
|
2684
2799
|
function esc(s) { return JSON.stringify(s).slice(1, -1) }
|
|
2800
|
+
// A schema pointer segment for a pattern: JSON Pointer escaping, then made
|
|
2801
|
+
// safe for the single-quoted literal the error emitters use.
|
|
2802
|
+
function ptrSeg(s) { return s.replace(/~/g, '~0').replace(/\//g, '~1').replace(/\\/g, '\\\\').replace(/'/g, "\\'") }
|
|
2685
2803
|
|
|
2686
2804
|
// Resolve child path at codegen time when parent is a static string literal.
|
|
2687
2805
|
// This enables frozen pre-allocation for ALL nested error objects.
|
|
@@ -2828,7 +2946,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
2828
2946
|
: () => ({ valid: false, errors: [{ keyword: 'false schema', instancePath: '', schemaPath: '#', params: {}, message: 'boolean schema is false' }] })
|
|
2829
2947
|
}
|
|
2830
2948
|
if (typeof schema !== 'object' || schema === null) return null
|
|
2831
|
-
if (!
|
|
2949
|
+
if (!sharedCodegenGate(schema, schemaMap)) return null
|
|
2832
2950
|
if (schema.patternProperties) {
|
|
2833
2951
|
for (const [pat, sub] of Object.entries(schema.patternProperties)) {
|
|
2834
2952
|
if (typeof sub === 'boolean') return null
|
|
@@ -3226,13 +3344,30 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3226
3344
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length>${schema.maxProperties}){${fail('maxProperties', 'maxProperties', `{limit:${schema.maxProperties}}`, `'must NOT have more than ${schema.maxProperties} properties'`)}}`)
|
|
3227
3345
|
}
|
|
3228
3346
|
|
|
3229
|
-
// additionalProperties: false
|
|
3230
|
-
|
|
3231
|
-
|
|
3347
|
+
// additionalProperties: false. A key matched by any patternProperties
|
|
3348
|
+
// entry is not additional, so those patterns are consulted here as well.
|
|
3349
|
+
if (schema.additionalProperties === false && (schema.properties || schema.patternProperties)) {
|
|
3350
|
+
const allowed = Object.keys(schema.properties || {}).map(k => `${JSON.stringify(k)}`).join(',')
|
|
3232
3351
|
const ci = ctx.varCounter++
|
|
3233
3352
|
const apSp = `${schemaPrefix}/additionalProperties`
|
|
3234
3353
|
const apLit = buildErrorLiteral({ keyword: 'additionalProperties', schemaPath: apSp, sourceMap: ctx.sourceMap })
|
|
3235
|
-
const
|
|
3354
|
+
const patChecks = []
|
|
3355
|
+
for (const pat of Object.keys(schema.patternProperties || {})) {
|
|
3356
|
+
const pattern = JSON.stringify(pat)
|
|
3357
|
+
if (!ctx.regExpMap.has(pattern)) {
|
|
3358
|
+
const ri = ctx.varCounter++
|
|
3359
|
+
ctx.regExpMap.set(pattern, ri)
|
|
3360
|
+
if (patternIsSafe(pat)) {
|
|
3361
|
+
ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
|
|
3362
|
+
ctx.usesSafeRe = true
|
|
3363
|
+
} else {
|
|
3364
|
+
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
patChecks.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ci}[_i])`)
|
|
3368
|
+
}
|
|
3369
|
+
const isAdditional = patChecks.length ? `!_a${ci}.has(_k${ci}[_i])&&!(${patChecks.join('||')})` : `!_a${ci}.has(_k${ci}[_i])`
|
|
3370
|
+
const inner = `const _k${ci}=Object.keys(${v});const _a${ci}=new Set([${allowed}]);for(let _i=0;_i<_k${ci}.length;_i++){if(${isAdditional}){_e.push({code:'${apLit.codeStr}',keyword:'additionalProperties',instancePath:${pathExpr||'""'},schemaPath:'${apSp}',params:{additionalProperty:_k${ci}[_i]},message:'must NOT have additional properties',docUrl:'${apLit.docUrl}'${apLit.frame}});if(!_all)return{valid:false,errors:_e}}}`
|
|
3236
3371
|
lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
3237
3372
|
}
|
|
3238
3373
|
|
|
@@ -3275,7 +3410,8 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3275
3410
|
const ki = ctx.varCounter++
|
|
3276
3411
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){if(_re${ri}.test(_k${ki})){`)
|
|
3277
3412
|
const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
|
|
3278
|
-
|
|
3413
|
+
// The real pointer, so source maps can locate the failing keyword.
|
|
3414
|
+
genCodeE(sub, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix + '/patternProperties/' + pat.replace(/~/g, '~0').replace(/\//g, '~1'))
|
|
3279
3415
|
lines.push(`}}}`)
|
|
3280
3416
|
}
|
|
3281
3417
|
}
|
|
@@ -3454,7 +3590,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
|
|
|
3454
3590
|
: () => ({ valid: false, errors: [{ keyword: 'false schema', instancePath: '', schemaPath: '#', params: {}, message: 'boolean schema is false' }] })
|
|
3455
3591
|
}
|
|
3456
3592
|
if (typeof schema !== 'object' || schema === null) return null
|
|
3457
|
-
if (!
|
|
3593
|
+
if (!sharedCodegenGate(schema, schemaMap)) return null
|
|
3458
3594
|
if (schema.patternProperties) {
|
|
3459
3595
|
for (const [pat, sub] of Object.entries(schema.patternProperties)) {
|
|
3460
3596
|
if (typeof sub === 'boolean') return null
|
|
@@ -3888,17 +4024,6 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3888
4024
|
}
|
|
3889
4025
|
}
|
|
3890
4026
|
|
|
3891
|
-
// Build sub-schema validators as closure vars
|
|
3892
|
-
for (let i = 0; i < ppEntries.length; i++) {
|
|
3893
|
-
const [, sub] = ppEntries[i]
|
|
3894
|
-
const subLines = []
|
|
3895
|
-
genCode(sub, `_ppv`, subLines, ctx)
|
|
3896
|
-
const fnBody = subLines.length === 0 ? `return true` : `${subLines.join(';')};return true`
|
|
3897
|
-
const fnVar = `_ppf${pi}_${i}`
|
|
3898
|
-
ctx.closureVars.push(fnVar)
|
|
3899
|
-
ctx.closureVals.push(new Function('_ppv', fnBody))
|
|
3900
|
-
}
|
|
3901
|
-
|
|
3902
4027
|
const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
|
|
3903
4028
|
const kVar = `_k${pi}`
|
|
3904
4029
|
|
|
@@ -3942,7 +4067,11 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3942
4067
|
const matchExpr = keyCheck || `_as${pi}.has(${kVar})`
|
|
3943
4068
|
lines.push(`let _m${pi}=${matchExpr}`)
|
|
3944
4069
|
for (let i = 0; i < ppEntries.length; i++) {
|
|
3945
|
-
|
|
4070
|
+
// The subschema is generated in place so its own keywords report
|
|
4071
|
+
// the real instance path and schema pointer.
|
|
4072
|
+
lines.push(`if(${matchers[i].check}){_m${pi}=true;{const _ppv${pi}_${i}=${v}[${kVar}]`)
|
|
4073
|
+
genCodeC(ppEntries[i][1], `_ppv${pi}_${i}`, childPathDynExpr(pathExpr, kVar), lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(ppEntries[i][0]))
|
|
4074
|
+
lines.push(`}}`)
|
|
3946
4075
|
}
|
|
3947
4076
|
lines.push(`if(!_m${pi}){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:${kVar}}`, "'must NOT have additional properties'")}}`)
|
|
3948
4077
|
lines.push(`}}`)
|
|
@@ -3972,7 +4101,9 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3972
4101
|
}
|
|
3973
4102
|
}
|
|
3974
4103
|
for (let i = 0; i < ppEntries.length; i++) {
|
|
3975
|
-
lines.push(`if(${matchers[i].check}
|
|
4104
|
+
lines.push(`if(${matchers[i].check}){const _ppv${pi}_${i}=${v}[${kVar}]`)
|
|
4105
|
+
genCodeC(ppEntries[i][1], `_ppv${pi}_${i}`, childPathDynExpr(pathExpr, kVar), lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(ppEntries[i][0]))
|
|
4106
|
+
lines.push(`}`)
|
|
3976
4107
|
}
|
|
3977
4108
|
lines.push(`}}`)
|
|
3978
4109
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The official meta-schemas, vendored so a `$ref` to one of them resolves
|
|
4
|
+
// without a network. Draft 2020-12 is eight documents joined by $dynamicRef;
|
|
5
|
+
// draft-07 is one. Sources: https://json-schema.org/draft/2020-12/schema and
|
|
6
|
+
// http://json-schema.org/draft-07/schema, fetched 2026-08-23, unmodified.
|
|
7
|
+
// Keyed by $id; index.js also registers the http/https and trailing-# spellings.
|
|
8
|
+
|
|
9
|
+
const METASCHEMAS = new Map([
|
|
10
|
+
["https://json-schema.org/draft/2020-12/schema", {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/schema","$vocabulary":{"https://json-schema.org/draft/2020-12/vocab/core":true,"https://json-schema.org/draft/2020-12/vocab/applicator":true,"https://json-schema.org/draft/2020-12/vocab/unevaluated":true,"https://json-schema.org/draft/2020-12/vocab/validation":true,"https://json-schema.org/draft/2020-12/vocab/meta-data":true,"https://json-schema.org/draft/2020-12/vocab/format-annotation":true,"https://json-schema.org/draft/2020-12/vocab/content":true},"$dynamicAnchor":"meta","title":"Core and Validation specifications meta-schema","allOf":[{"$ref":"meta/core"},{"$ref":"meta/applicator"},{"$ref":"meta/unevaluated"},{"$ref":"meta/validation"},{"$ref":"meta/meta-data"},{"$ref":"meta/format-annotation"},{"$ref":"meta/content"}],"type":["object","boolean"],"$comment":"This meta-schema also defines keywords that have appeared in previous drafts in order to prevent incompatible extensions as they remain in common use.","properties":{"definitions":{"$comment":"\"definitions\" has been replaced by \"$defs\".","type":"object","additionalProperties":{"$dynamicRef":"#meta"},"deprecated":true,"default":{}},"dependencies":{"$comment":"\"dependencies\" has been split and replaced by \"dependentSchemas\" and \"dependentRequired\" in order to serve their differing semantics.","type":"object","additionalProperties":{"anyOf":[{"$dynamicRef":"#meta"},{"$ref":"meta/validation#/$defs/stringArray"}]},"deprecated":true,"default":{}},"$recursiveAnchor":{"$comment":"\"$recursiveAnchor\" has been replaced by \"$dynamicAnchor\".","$ref":"meta/core#/$defs/anchorString","deprecated":true},"$recursiveRef":{"$comment":"\"$recursiveRef\" has been replaced by \"$dynamicRef\".","$ref":"meta/core#/$defs/uriReferenceString","deprecated":true}}}],
|
|
11
|
+
["https://json-schema.org/draft/2020-12/meta/core", {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/core","$dynamicAnchor":"meta","title":"Core vocabulary meta-schema","type":["object","boolean"],"properties":{"$id":{"$ref":"#/$defs/uriReferenceString","$comment":"Non-empty fragments not allowed.","pattern":"^[^#]*#?$"},"$schema":{"$ref":"#/$defs/uriString"},"$ref":{"$ref":"#/$defs/uriReferenceString"},"$anchor":{"$ref":"#/$defs/anchorString"},"$dynamicRef":{"$ref":"#/$defs/uriReferenceString"},"$dynamicAnchor":{"$ref":"#/$defs/anchorString"},"$vocabulary":{"type":"object","propertyNames":{"$ref":"#/$defs/uriString"},"additionalProperties":{"type":"boolean"}},"$comment":{"type":"string"},"$defs":{"type":"object","additionalProperties":{"$dynamicRef":"#meta"}}},"$defs":{"anchorString":{"type":"string","pattern":"^[A-Za-z_][-A-Za-z0-9._]*$"},"uriString":{"type":"string","format":"uri"},"uriReferenceString":{"type":"string","format":"uri-reference"}}}],
|
|
12
|
+
["https://json-schema.org/draft/2020-12/meta/applicator", {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/applicator","$dynamicAnchor":"meta","title":"Applicator vocabulary meta-schema","type":["object","boolean"],"properties":{"prefixItems":{"$ref":"#/$defs/schemaArray"},"items":{"$dynamicRef":"#meta"},"contains":{"$dynamicRef":"#meta"},"additionalProperties":{"$dynamicRef":"#meta"},"properties":{"type":"object","additionalProperties":{"$dynamicRef":"#meta"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$dynamicRef":"#meta"},"propertyNames":{"format":"regex"},"default":{}},"dependentSchemas":{"type":"object","additionalProperties":{"$dynamicRef":"#meta"},"default":{}},"propertyNames":{"$dynamicRef":"#meta"},"if":{"$dynamicRef":"#meta"},"then":{"$dynamicRef":"#meta"},"else":{"$dynamicRef":"#meta"},"allOf":{"$ref":"#/$defs/schemaArray"},"anyOf":{"$ref":"#/$defs/schemaArray"},"oneOf":{"$ref":"#/$defs/schemaArray"},"not":{"$dynamicRef":"#meta"}},"$defs":{"schemaArray":{"type":"array","minItems":1,"items":{"$dynamicRef":"#meta"}}}}],
|
|
13
|
+
["https://json-schema.org/draft/2020-12/meta/validation", {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/validation","$dynamicAnchor":"meta","title":"Validation vocabulary meta-schema","type":["object","boolean"],"properties":{"type":{"anyOf":[{"$ref":"#/$defs/simpleTypes"},{"type":"array","items":{"$ref":"#/$defs/simpleTypes"},"minItems":1,"uniqueItems":true}]},"const":true,"enum":{"type":"array","items":true},"multipleOf":{"type":"number","exclusiveMinimum":0},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"number"},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"number"},"maxLength":{"$ref":"#/$defs/nonNegativeInteger"},"minLength":{"$ref":"#/$defs/nonNegativeIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"maxItems":{"$ref":"#/$defs/nonNegativeInteger"},"minItems":{"$ref":"#/$defs/nonNegativeIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"maxContains":{"$ref":"#/$defs/nonNegativeInteger"},"minContains":{"$ref":"#/$defs/nonNegativeInteger","default":1},"maxProperties":{"$ref":"#/$defs/nonNegativeInteger"},"minProperties":{"$ref":"#/$defs/nonNegativeIntegerDefault0"},"required":{"$ref":"#/$defs/stringArray"},"dependentRequired":{"type":"object","additionalProperties":{"$ref":"#/$defs/stringArray"}}},"$defs":{"nonNegativeInteger":{"type":"integer","minimum":0},"nonNegativeIntegerDefault0":{"$ref":"#/$defs/nonNegativeInteger","default":0},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"uniqueItems":true,"default":[]}}}],
|
|
14
|
+
["https://json-schema.org/draft/2020-12/meta/meta-data", {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/meta-data","$dynamicAnchor":"meta","title":"Meta-data vocabulary meta-schema","type":["object","boolean"],"properties":{"title":{"type":"string"},"description":{"type":"string"},"default":true,"deprecated":{"type":"boolean","default":false},"readOnly":{"type":"boolean","default":false},"writeOnly":{"type":"boolean","default":false},"examples":{"type":"array","items":true}}}],
|
|
15
|
+
["https://json-schema.org/draft/2020-12/meta/format-annotation", {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/format-annotation","$dynamicAnchor":"meta","title":"Format vocabulary meta-schema for annotation results","type":["object","boolean"],"properties":{"format":{"type":"string"}}}],
|
|
16
|
+
["https://json-schema.org/draft/2020-12/meta/content", {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/content","$dynamicAnchor":"meta","title":"Content vocabulary meta-schema","type":["object","boolean"],"properties":{"contentEncoding":{"type":"string"},"contentMediaType":{"type":"string"},"contentSchema":{"$dynamicRef":"#meta"}}}],
|
|
17
|
+
["https://json-schema.org/draft/2020-12/meta/unevaluated", {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/unevaluated","$dynamicAnchor":"meta","title":"Unevaluated applicator vocabulary meta-schema","type":["object","boolean"],"properties":{"unevaluatedItems":{"$dynamicRef":"#meta"},"unevaluatedProperties":{"$dynamicRef":"#meta"}}}],
|
|
18
|
+
["http://json-schema.org/draft-07/schema#", {"$schema":"http://json-schema.org/draft-07/schema#","$id":"http://json-schema.org/draft-07/schema#","title":"Core schema meta-schema","definitions":{"schemaArray":{"type":"array","minItems":1,"items":{"$ref":"#"}},"nonNegativeInteger":{"type":"integer","minimum":0},"nonNegativeIntegerDefault0":{"allOf":[{"$ref":"#/definitions/nonNegativeInteger"},{"default":0}]},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"uniqueItems":true,"default":[]}},"type":["object","boolean"],"properties":{"$id":{"type":"string","format":"uri-reference"},"$schema":{"type":"string","format":"uri"},"$ref":{"type":"string","format":"uri-reference"},"$comment":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"default":true,"readOnly":{"type":"boolean","default":false},"writeOnly":{"type":"boolean","default":false},"examples":{"type":"array","items":true},"multipleOf":{"type":"number","exclusiveMinimum":0},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"number"},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"number"},"maxLength":{"$ref":"#/definitions/nonNegativeInteger"},"minLength":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"additionalItems":{"$ref":"#"},"items":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/schemaArray"}],"default":true},"maxItems":{"$ref":"#/definitions/nonNegativeInteger"},"minItems":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"contains":{"$ref":"#"},"maxProperties":{"$ref":"#/definitions/nonNegativeInteger"},"minProperties":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"required":{"$ref":"#/definitions/stringArray"},"additionalProperties":{"$ref":"#"},"definitions":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"properties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$ref":"#"},"propertyNames":{"format":"regex"},"default":{}},"dependencies":{"type":"object","additionalProperties":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/stringArray"}]}},"propertyNames":{"$ref":"#"},"const":true,"enum":{"type":"array","items":true,"minItems":1,"uniqueItems":true},"type":{"anyOf":[{"$ref":"#/definitions/simpleTypes"},{"type":"array","items":{"$ref":"#/definitions/simpleTypes"},"minItems":1,"uniqueItems":true}]},"format":{"type":"string"},"contentMediaType":{"type":"string"},"contentEncoding":{"type":"string"},"if":{"$ref":"#"},"then":{"$ref":"#"},"else":{"$ref":"#"},"allOf":{"$ref":"#/definitions/schemaArray"},"anyOf":{"$ref":"#/definitions/schemaArray"},"oneOf":{"$ref":"#/definitions/schemaArray"},"not":{"$ref":"#"}},"default":true}],
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
module.exports = { METASCHEMAS };
|
package/lib/safe-regex-source.js
CHANGED
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
// into standalone output without a runtime `fs` read. Kept in sync by
|
|
6
6
|
// `tests/test_safe_regex_source_sync.js`.
|
|
7
7
|
|
|
8
|
-
module.exports = "'use strict'\n\n// Linear-time regex engine for JSON Schema `pattern`, used in place of JS RegExp\n// so an adversarial input cannot trigger catastrophic backtracking (ReDoS).\n//\n// It is a Pike VM: the pattern compiles to a small instruction program, and the\n// VM simulates all NFA threads in lockstep over the input, deduping by program\n// counter. Runtime is O(input * program), with no backtracking.\n//\n// Supported (the RE2 subset, which is what ata's native path also accepts):\n// literals, ., character classes, \\d \\w \\s \\D \\W \\S, anchors ^ $, quantifiers\n// * + ? {n} {n,} {n,m} (greedy or lazy, same language for a boolean test),\n// groups ( ) (?: ), alternation |. Backreferences and lookaround are not\n// supported by linear engines; compileSafe throws on them so the caller can\n// decide (ata's codegen rejects such schemas rather than risk a hang).\n\nconst WS = [[9, 13], [32, 32], [160, 160]]\nconst DIGIT = [[48, 57]]\nconst WORD = [[48, 57], [65, 90], [97, 122], [95, 95]]\n\nfunction parse (src) {\n let i = 0\n const len = src.length\n const peek = () => src[i]\n const eof = () => i >= len\n\n function parseAlt () {\n const opts = [parseConcat()]\n while (!eof() && peek() === '|') { i++; opts.push(parseConcat()) }\n return opts.length === 1 ? opts[0] : { t: 'alt', opts }\n }\n\n function parseConcat () {\n const parts = []\n while (!eof() && peek() !== '|' && peek() !== ')') parts.push(parseRepeat())\n if (parts.length === 0) return { t: 'empty' }\n return parts.length === 1 ? parts[0] : { t: 'concat', parts }\n }\n\n function parseRepeat () {\n let node = parseAtom()\n while (!eof()) {\n const ch = peek()\n if (ch === '*') { i++; node = { t: 'star', child: node } }\n else if (ch === '+') { i++; node = { t: 'plus', child: node } }\n else if (ch === '?') { i++; node = { t: 'quest', child: node } }\n else if (ch === '{') {\n const saved = i\n const q = tryQuantifier()\n if (!q) { i = saved; break }\n node = { t: 'repeat', child: node, min: q.min, max: q.max }\n } else break\n // a trailing ? makes the quantifier lazy; same language for a boolean test\n if (!eof() && peek() === '?') i++\n }\n return node\n }\n\n function tryQuantifier () {\n // assumes current char is '{'\n i++\n let min = ''\n while (!eof() && /[0-9]/.test(peek())) { min += peek(); i++ }\n if (min === '') return null\n let max\n if (peek() === '}') { i++; return { min: +min, max: +min } }\n if (peek() === ',') {\n i++\n let m = ''\n while (!eof() && /[0-9]/.test(peek())) { m += peek(); i++ }\n if (peek() !== '}') return null\n i++\n max = m === '' ? Infinity : +m\n return { min: +min, max }\n }\n return null\n }\n\n function parseAtom () {\n const ch = peek()\n if (ch === '(') {\n i++\n if (src[i] === '?') {\n if (src[i + 1] === ':') { i += 2 }\n else throw new Error('unsupported group (lookaround/named) in pattern')\n }\n const child = parseAlt()\n if (peek() !== ')') throw new Error('unbalanced ( in pattern')\n i++\n return { t: 'group', child }\n }\n if (ch === '[') return parseClass()\n if (ch === '.') { i++; return { t: 'any' } }\n if (ch === '^') { i++; return { t: 'bol' } }\n if (ch === '$') { i++; return { t: 'eol' } }\n if (ch === '\\\\') return parseEscape(false)\n if (ch === ')' || ch === '|') return { t: 'empty' }\n i++\n return { t: 'char', c: ch.charCodeAt(0) }\n }\n\n function parseClass () {\n i++ // [\n let neg = false\n if (peek() === '^') { neg = true; i++ }\n const ranges = []\n while (!eof() && peek() !== ']') {\n let lo\n if (peek() === '\\\\') {\n const esc = parseEscape(true)\n if (esc.t === 'classpart') { for (const r of esc.ranges) ranges.push(r); continue }\n lo = esc.c\n } else { lo = peek().charCodeAt(0); i++ }\n if (peek() === '-' && src[i + 1] !== ']' && i + 1 < len) {\n i++ // -\n let hi\n if (peek() === '\\\\') { const e = parseEscape(true); hi = e.c } else { hi = peek().charCodeAt(0); i++ }\n ranges.push([lo, hi])\n } else {\n ranges.push([lo, lo])\n }\n }\n if (peek() !== ']') throw new Error('unbalanced [ in pattern')\n i++\n return { t: 'class', neg, ranges }\n }\n\n function parseEscape (inClass) {\n i++ // backslash\n if (eof()) throw new Error('trailing backslash in pattern')\n const ch = peek(); i++\n switch (ch) {\n case 'd': return inClass ? { t: 'classpart', ranges: DIGIT } : { t: 'class', neg: false, ranges: DIGIT }\n case 'w': return inClass ? { t: 'classpart', ranges: WORD } : { t: 'class', neg: false, ranges: WORD }\n case 's': return inClass ? { t: 'classpart', ranges: WS } : { t: 'class', neg: false, ranges: WS }\n case 'D': if (inClass) throw new Error('\\\\D inside a class is not supported'); return { t: 'class', neg: true, ranges: DIGIT }\n case 'W': if (inClass) throw new Error('\\\\W inside a class is not supported'); return { t: 'class', neg: true, ranges: WORD }\n case 'S': if (inClass) throw new Error('\\\\S inside a class is not supported'); return { t: 'class', neg: true, ranges: WS }\n case 'n': return { t: 'char', c: 10 }\n case 'r': return { t: 'char', c: 13 }\n case 't': return { t: 'char', c: 9 }\n case 'f': return { t: 'char', c: 12 }\n case 'v': return { t: 'char', c: 11 }\n case '0': return { t: 'char', c: 0 }\n case 'x': { const h = src.slice(i, i + 2); i += 2; return { t: 'char', c: parseInt(h, 16) } }\n case 'u': { const h = src.slice(i, i + 4); i += 4; return { t: 'char', c: parseInt(h, 16) } }\n case 'b': if (inClass) return { t: 'char', c: 8 }; throw new Error('\\\\b word boundary is not supported')\n default:\n if (/[1-9]/.test(ch)) throw new Error('backreferences are not supported in pattern')\n return { t: 'char', c: ch.charCodeAt(0) }\n }\n }\n\n const ast = parseAlt()\n if (!eof()) throw new Error('unexpected \"' + peek() + '\" in pattern')\n return ast\n}\n\nfunction compileProg (ast) {\n const prog = []\n const emit = (op, extra) => { const idx = prog.length; prog.push(Object.assign({ op }, extra)); return idx }\n\n function rec (n) {\n switch (n.t) {\n case 'empty': break\n case 'char': emit('char', { c: n.c }); break\n case 'any': emit('any'); break\n case 'class': emit('class', { neg: n.neg, ranges: n.ranges }); break\n case 'bol': emit('bol'); break\n case 'eol': emit('eol'); break\n case 'group': rec(n.child); break\n case 'concat': for (const p of n.parts) rec(p); break\n case 'alt': {\n const jmps = []\n for (let k = 0; k < n.opts.length; k++) {\n if (k < n.opts.length - 1) {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.opts[k])\n jmps.push(emit('jmp', { x: 0 }))\n prog[sp].y = prog.length\n } else {\n rec(n.opts[k])\n }\n }\n for (const j of jmps) prog[j].x = prog.length\n break\n }\n case 'star': {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.child)\n emit('jmp', { x: sp })\n prog[sp].y = prog.length\n break\n }\n case 'plus': {\n const start = prog.length\n rec(n.child)\n const sp = emit('split', { x: start, y: 0 })\n prog[sp].y = prog.length\n break\n }\n case 'quest': {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.child)\n prog[sp].y = prog.length\n break\n }\n case 'repeat': {\n for (let k = 0; k < n.min; k++) rec(n.child)\n if (n.max === Infinity) {\n if (n.min === 0) rec({ t: 'star', child: n.child })\n else rec({ t: 'star', child: n.child })\n } else {\n for (let k = 0; k < n.max - n.min; k++) rec({ t: 'quest', child: n.child })\n }\n break\n }\n }\n }\n\n rec(ast)\n emit('match')\n return prog\n}\n\nfunction matchClass (instr, c) {\n let inside = false\n const r = instr.ranges\n for (let k = 0; k < r.length; k++) { if (c >= r[k][0] && c <= r[k][1]) { inside = true; break } }\n return instr.neg ? !inside : inside\n}\n\nfunction makeRunner (prog) {\n const n = prog.length\n const lastGen = new Int32Array(n).fill(-1)\n let gen = 0\n const stack = []\n\n function addThread (list, pc, pos, len) {\n stack.length = 0\n stack.push(pc)\n while (stack.length) {\n const p = stack.pop()\n if (lastGen[p] === gen) continue\n lastGen[p] = gen\n const I = prog[p]\n switch (I.op) {\n case 'jmp': stack.push(I.x); break\n case 'split': stack.push(I.y); stack.push(I.x); break\n case 'bol': if (pos === 0) stack.push(p + 1); break\n case 'eol': if (pos === len) stack.push(p + 1); break\n default: list.push(p)\n }\n }\n }\n\n return function test (s) {\n const len = s.length\n let clist = []\n let nlist = []\n gen++\n addThread(clist, 0, 0, len)\n for (let pos = 0; pos <= len; pos++) {\n const c = pos < len ? s.charCodeAt(pos) : -1\n gen++\n nlist.length = 0\n for (let k = 0; k < clist.length; k++) {\n const pc = clist[k]\n const I = prog[pc]\n if (I.op === 'match') return true\n else if (I.op === 'char') { if (c === I.c) addThread(nlist, pc + 1, pos + 1, len) }\n else if (I.op === 'any') { if (c !== -1 && c !== 10) addThread(nlist, pc + 1, pos + 1, len) }\n else if (I.op === 'class') { if (c !== -1 && matchClass(I, c)) addThread(nlist, pc + 1, pos + 1, len) }\n }\n if (pos < len) addThread(nlist, 0, pos + 1, len)\n const tmp = clist; clist = nlist; nlist = tmp\n }\n return false\n }\n}\n\nfunction compileSafe (pattern) {\n const prog = compileProg(parse(pattern))\n const runner = makeRunner(prog)\n // `__ataSafe` brands the result so the standalone serializer can tell a safe\n // matcher apart from a RegExp and emit `__ataSafeRe(source)` instead.\n return { test: runner, source: pattern, __ataSafe: true }\n}\n\n// True when the linear engine can represent `src`. Used by the codegen to decide\n// between the safe matcher and a JS RegExp fallback for patterns outside the\n// supported (RE2) subset (backreferences, lookaround, etc.).\nfunction patternIsSafe (src) {\n try { compileSafe(src); return true } catch { return false }\n}\n\nmodule.exports = { compileSafe, patternIsSafe }\n";
|
|
8
|
+
module.exports = "'use strict'\n\n// Linear-time regex engine for JSON Schema `pattern`, used in place of JS RegExp\n// so an adversarial input cannot trigger catastrophic backtracking (ReDoS).\n//\n// It is a Pike VM: the pattern compiles to a small instruction program, and the\n// VM simulates all NFA threads in lockstep over the input, deduping by program\n// counter. Runtime is O(input * program), with no backtracking.\n//\n// Supported (the RE2 subset, which is what ata's native path also accepts):\n// literals, ., character classes, \\d \\w \\s \\D \\W \\S, anchors ^ $, quantifiers\n// * + ? {n} {n,} {n,m} (greedy or lazy, same language for a boolean test),\n// groups ( ) (?: ), alternation |. Backreferences and lookaround are not\n// supported by linear engines; compileSafe throws on them so the caller can\n// decide (ata's codegen rejects such schemas rather than risk a hang).\n\nconst WS = [[9, 13], [32, 32], [160, 160]]\nconst DIGIT = [[48, 57]]\nconst WORD = [[48, 57], [65, 90], [97, 122], [95, 95]]\n\nfunction parse (src) {\n let i = 0\n const len = src.length\n const peek = () => src[i]\n const eof = () => i >= len\n\n function parseAlt () {\n const opts = [parseConcat()]\n while (!eof() && peek() === '|') { i++; opts.push(parseConcat()) }\n return opts.length === 1 ? opts[0] : { t: 'alt', opts }\n }\n\n function parseConcat () {\n const parts = []\n while (!eof() && peek() !== '|' && peek() !== ')') parts.push(parseRepeat())\n if (parts.length === 0) return { t: 'empty' }\n return parts.length === 1 ? parts[0] : { t: 'concat', parts }\n }\n\n function parseRepeat () {\n let node = parseAtom()\n while (!eof()) {\n const ch = peek()\n if (ch === '*') { i++; node = { t: 'star', child: node } }\n else if (ch === '+') { i++; node = { t: 'plus', child: node } }\n else if (ch === '?') { i++; node = { t: 'quest', child: node } }\n else if (ch === '{') {\n const saved = i\n const q = tryQuantifier()\n if (!q) { i = saved; break }\n node = { t: 'repeat', child: node, min: q.min, max: q.max }\n } else break\n // a trailing ? makes the quantifier lazy; same language for a boolean test\n if (!eof() && peek() === '?') i++\n }\n return node\n }\n\n function tryQuantifier () {\n // assumes current char is '{'\n i++\n let min = ''\n while (!eof() && /[0-9]/.test(peek())) { min += peek(); i++ }\n if (min === '') return null\n let max\n if (peek() === '}') { i++; return { min: +min, max: +min } }\n if (peek() === ',') {\n i++\n let m = ''\n while (!eof() && /[0-9]/.test(peek())) { m += peek(); i++ }\n if (peek() !== '}') return null\n i++\n max = m === '' ? Infinity : +m\n return { min: +min, max }\n }\n return null\n }\n\n function parseAtom () {\n const ch = peek()\n if (ch === '(') {\n i++\n if (src[i] === '?') {\n if (src[i + 1] === ':') { i += 2 }\n else throw new Error('unsupported group (lookaround/named) in pattern')\n }\n const child = parseAlt()\n if (peek() !== ')') throw new Error('unbalanced ( in pattern')\n i++\n return { t: 'group', child }\n }\n if (ch === '[') return parseClass()\n if (ch === '.') { i++; return { t: 'any' } }\n if (ch === '^') { i++; return { t: 'bol' } }\n if (ch === '$') { i++; return { t: 'eol' } }\n if (ch === '\\\\') return parseEscape(false)\n if (ch === ')' || ch === '|') return { t: 'empty' }\n i++\n return { t: 'char', c: ch.charCodeAt(0) }\n }\n\n function parseClass () {\n i++ // [\n let neg = false\n if (peek() === '^') { neg = true; i++ }\n const ranges = []\n while (!eof() && peek() !== ']') {\n let lo\n if (peek() === '\\\\') {\n const esc = parseEscape(true)\n if (esc.t === 'classpart') { for (const r of esc.ranges) ranges.push(r); continue }\n lo = esc.c\n } else { lo = peek().charCodeAt(0); i++ }\n if (peek() === '-' && src[i + 1] !== ']' && i + 1 < len) {\n i++ // -\n let hi\n if (peek() === '\\\\') { const e = parseEscape(true); hi = e.c } else { hi = peek().charCodeAt(0); i++ }\n ranges.push([lo, hi])\n } else {\n ranges.push([lo, lo])\n }\n }\n if (peek() !== ']') throw new Error('unbalanced [ in pattern')\n i++\n return { t: 'class', neg, ranges }\n }\n\n function parseEscape (inClass) {\n i++ // backslash\n if (eof()) throw new Error('trailing backslash in pattern')\n const ch = peek(); i++\n switch (ch) {\n case 'd': return inClass ? { t: 'classpart', ranges: DIGIT } : { t: 'class', neg: false, ranges: DIGIT }\n case 'w': return inClass ? { t: 'classpart', ranges: WORD } : { t: 'class', neg: false, ranges: WORD }\n case 's': return inClass ? { t: 'classpart', ranges: WS } : { t: 'class', neg: false, ranges: WS }\n case 'D': if (inClass) throw new Error('\\\\D inside a class is not supported'); return { t: 'class', neg: true, ranges: DIGIT }\n case 'W': if (inClass) throw new Error('\\\\W inside a class is not supported'); return { t: 'class', neg: true, ranges: WORD }\n case 'S': if (inClass) throw new Error('\\\\S inside a class is not supported'); return { t: 'class', neg: true, ranges: WS }\n case 'n': return { t: 'char', c: 10 }\n case 'r': return { t: 'char', c: 13 }\n case 't': return { t: 'char', c: 9 }\n case 'f': return { t: 'char', c: 12 }\n case 'v': return { t: 'char', c: 11 }\n case '0': return { t: 'char', c: 0 }\n case 'x': { const h = src.slice(i, i + 2); i += 2; return { t: 'char', c: parseInt(h, 16) } }\n case 'u': { const h = src.slice(i, i + 4); i += 4; return { t: 'char', c: parseInt(h, 16) } }\n case 'b': if (inClass) return { t: 'char', c: 8 }; throw new Error('\\\\b word boundary is not supported')\n default:\n if (/[1-9]/.test(ch)) throw new Error('backreferences are not supported in pattern')\n return { t: 'char', c: ch.charCodeAt(0) }\n }\n }\n\n const ast = parseAlt()\n if (!eof()) throw new Error('unexpected \"' + peek() + '\" in pattern')\n return ast\n}\n\nfunction compileProg (ast) {\n const prog = []\n const emit = (op, extra) => { const idx = prog.length; prog.push(Object.assign({ op }, extra)); return idx }\n\n function rec (n) {\n switch (n.t) {\n case 'empty': break\n case 'char': emit('char', { c: n.c }); break\n case 'any': emit('any'); break\n case 'class': emit('class', { neg: n.neg, ranges: n.ranges }); break\n case 'bol': emit('bol'); break\n case 'eol': emit('eol'); break\n case 'group': rec(n.child); break\n case 'concat': for (const p of n.parts) rec(p); break\n case 'alt': {\n const jmps = []\n for (let k = 0; k < n.opts.length; k++) {\n if (k < n.opts.length - 1) {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.opts[k])\n jmps.push(emit('jmp', { x: 0 }))\n prog[sp].y = prog.length\n } else {\n rec(n.opts[k])\n }\n }\n for (const j of jmps) prog[j].x = prog.length\n break\n }\n case 'star': {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.child)\n emit('jmp', { x: sp })\n prog[sp].y = prog.length\n break\n }\n case 'plus': {\n const start = prog.length\n rec(n.child)\n const sp = emit('split', { x: start, y: 0 })\n prog[sp].y = prog.length\n break\n }\n case 'quest': {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.child)\n prog[sp].y = prog.length\n break\n }\n case 'repeat': {\n for (let k = 0; k < n.min; k++) rec(n.child)\n if (n.max === Infinity) {\n if (n.min === 0) rec({ t: 'star', child: n.child })\n else rec({ t: 'star', child: n.child })\n } else {\n for (let k = 0; k < n.max - n.min; k++) rec({ t: 'quest', child: n.child })\n }\n break\n }\n }\n }\n\n rec(ast)\n emit('match')\n return prog\n}\n\n// Numeric opcodes for the runner. The program is compiled once into flat\n// typed arrays so the inner loop does no property lookups or string compares.\nconst OP_CHAR = 0\nconst OP_ANY = 1\nconst OP_CLASS = 2\nconst OP_SPLIT = 3\nconst OP_JMP = 4\nconst OP_BOL = 5\nconst OP_EOL = 6\nconst OP_MATCH = 7\n\nfunction classMatcher (instr) {\n // ASCII is answered from a bitmap; anything above 0x7f walks the ranges.\n const bits = new Uint8Array(128)\n const r = instr.ranges\n for (let k = 0; k < r.length; k++) {\n const hi = Math.min(r[k][1], 127)\n for (let c = r[k][0]; c <= hi; c++) bits[c] = 1\n }\n return { bits, ranges: r, neg: instr.neg }\n}\n\nfunction matchClass (cls, c) {\n let inside\n if (c < 128) {\n inside = cls.bits[c] === 1\n } else {\n inside = false\n const r = cls.ranges\n for (let k = 0; k < r.length; k++) { if (c >= r[k][0] && c <= r[k][1]) { inside = true; break } }\n }\n return cls.neg ? !inside : inside\n}\n\nfunction makeRunner (prog) {\n const n = prog.length\n const ops = new Uint8Array(n)\n const xs = new Int32Array(n)\n const ys = new Int32Array(n)\n const cs = new Int32Array(n)\n const classes = new Array(n)\n for (let i = 0; i < n; i++) {\n const I = prog[i]\n switch (I.op) {\n case 'char': ops[i] = OP_CHAR; cs[i] = I.c; break\n case 'any': ops[i] = OP_ANY; break\n case 'class': ops[i] = OP_CLASS; classes[i] = classMatcher(I); break\n case 'split': ops[i] = OP_SPLIT; xs[i] = I.x; ys[i] = I.y; break\n case 'jmp': ops[i] = OP_JMP; xs[i] = I.x; break\n case 'bol': ops[i] = OP_BOL; break\n case 'eol': ops[i] = OP_EOL; break\n case 'match': ops[i] = OP_MATCH; break\n }\n }\n\n const lastGen = new Int32Array(n).fill(-1)\n let gen = 0\n // Each unvisited instruction is popped once and pushes at most two, so the\n // stack never holds more than 2n + 1 entries.\n const stack = new Int32Array(2 * n + 2)\n // Thread lists hold at most one entry per instruction per step.\n let clist = new Int32Array(n)\n let nlist = new Int32Array(n)\n let clen = 0\n let nlen = 0\n\n // Follows epsilon edges from `pc` and records every consuming instruction\n // (or match) reached in `list`. `lastGen` dedupes per step.\n function addThread (list, len0, pc, pos, len) {\n // Most transitions land directly on a consuming instruction; skip the\n // stack walk for those.\n if (ops[pc] <= OP_CLASS || ops[pc] === OP_MATCH) {\n if (lastGen[pc] === gen) return len0\n lastGen[pc] = gen\n list[len0] = pc\n return len0 + 1\n }\n let sp = 0\n stack[sp++] = pc\n let count = len0\n while (sp > 0) {\n const p = stack[--sp]\n if (lastGen[p] === gen) continue\n lastGen[p] = gen\n switch (ops[p]) {\n case OP_JMP: stack[sp++] = xs[p]; break\n case OP_SPLIT: stack[sp++] = ys[p]; stack[sp++] = xs[p]; break\n case OP_BOL: if (pos === 0) stack[sp++] = p + 1; break\n case OP_EOL: if (pos === len) stack[sp++] = p + 1; break\n default: list[count++] = p\n }\n }\n return count\n }\n\n // A pattern is anchored when starting it anywhere but position 0 yields no\n // thread, which is the case for `^...` and its alternations. The probe sits\n // at position 1 of a length-1 string so that only `^` can fail. For anchored\n // patterns the per-position restart below is skipped, and an empty thread\n // list means the match has already failed.\n gen++\n const anchored = addThread(nlist, 0, 0, 1, 1) === 0\n\n function testNFA (s) {\n const len = s.length\n gen++\n clen = addThread(clist, 0, 0, 0, len)\n for (let pos = 0; pos <= len; pos++) {\n const c = pos < len ? s.charCodeAt(pos) : -1\n gen++\n nlen = 0\n for (let k = 0; k < clen; k++) {\n const pc = clist[k]\n switch (ops[pc]) {\n case OP_MATCH: return true\n case OP_CHAR: if (c === cs[pc]) nlen = addThread(nlist, nlen, pc + 1, pos + 1, len); break\n case OP_ANY: if (c !== -1 && c !== 10) nlen = addThread(nlist, nlen, pc + 1, pos + 1, len); break\n case OP_CLASS: if (c !== -1 && matchClass(classes[pc], c)) nlen = addThread(nlist, nlen, pc + 1, pos + 1, len); break\n }\n }\n if (pos < len) {\n if (!anchored) nlen = addThread(nlist, nlen, 0, pos + 1, len)\n else if (nlen === 0) return false\n }\n const tmp = clist; clist = nlist; nlist = tmp\n clen = nlen\n }\n return false\n }\n\n // Lazy DFA on top of the NFA. A DFA state is the set of consuming\n // instructions live at a position; transitions are computed on first use\n // and cached per ASCII character. `^` and `$` depend on position, so the\n // closure is taken with flags for \"at start\" and \"at end\", which gives two\n // start states and two transition tables per state. The state count is\n // capped; past the cap the matcher falls back to the NFA walk above, so the\n // time bound stays linear either way.\n const MAX_STATES = 256\n const states = []\n const stateIds = new Map()\n let overflow = false\n\n function closure (list, count, pc, atStart, atEnd) {\n // Same walk as addThread, with the position replaced by the two flags.\n let sp = 0\n stack[sp++] = pc\n while (sp > 0) {\n const p = stack[--sp]\n if (lastGen[p] === gen) continue\n lastGen[p] = gen\n switch (ops[p]) {\n case OP_JMP: stack[sp++] = xs[p]; break\n case OP_SPLIT: stack[sp++] = ys[p]; stack[sp++] = xs[p]; break\n case OP_BOL: if (atStart) stack[sp++] = p + 1; break\n case OP_EOL: if (atEnd) stack[sp++] = p + 1; break\n default: list[count++] = p\n }\n }\n return count\n }\n\n function internState (list, count) {\n const pcs = Array.from(list.subarray(0, count)).sort((a, b) => a - b)\n const key = pcs.join(',')\n let id = stateIds.get(key)\n if (id !== undefined) return id\n if (states.length >= MAX_STATES) { overflow = true; return -1 }\n id = states.length\n let isMatch = false\n for (let k = 0; k < pcs.length; k++) if (ops[pcs[k]] === OP_MATCH) { isMatch = true; break }\n states.push({ pcs: Int32Array.from(pcs), isMatch, next: new Int32Array(128).fill(-2), nextEnd: new Int32Array(128).fill(-2) })\n stateIds.set(key, id)\n return id\n }\n\n function step (state, c, atEnd) {\n gen++\n let count = 0\n const pcs = state.pcs\n for (let k = 0; k < pcs.length; k++) {\n const pc = pcs[k]\n switch (ops[pc]) {\n case OP_CHAR: if (c === cs[pc]) count = closure(nlist, count, pc + 1, false, atEnd); break\n case OP_ANY: if (c !== 10) count = closure(nlist, count, pc + 1, false, atEnd); break\n case OP_CLASS: if (matchClass(classes[pc], c)) count = closure(nlist, count, pc + 1, false, atEnd); break\n }\n }\n if (!anchored) count = closure(nlist, count, 0, false, atEnd)\n return internState(nlist, count)\n }\n\n let startEmpty = -2\n let startNonEmpty = -2\n\n function startState (atEnd) {\n gen++\n const count = closure(nlist, 0, 0, true, atEnd)\n return internState(nlist, count)\n }\n\n function testDFA (s) {\n const len = s.length\n let id\n if (len === 0) {\n if (startEmpty === -2) startEmpty = startState(true)\n id = startEmpty\n } else {\n if (startNonEmpty === -2) startNonEmpty = startState(false)\n id = startNonEmpty\n }\n if (id < 0) return testNFA(s)\n let state = states[id]\n for (let pos = 0; pos < len; pos++) {\n if (state.isMatch) return true\n const c = s.charCodeAt(pos)\n const atEnd = pos + 1 === len\n let nid\n if (c < 128) {\n const table = atEnd ? state.nextEnd : state.next\n nid = table[c]\n if (nid === -2) { nid = step(state, c, atEnd); table[c] = nid }\n } else {\n nid = step(state, c, atEnd)\n }\n if (nid < 0) return testNFA(s)\n state = states[nid]\n if (anchored && state.pcs.length === 0) return false\n }\n return state.isMatch\n }\n\n return function test (s) {\n return overflow ? testNFA(s) : testDFA(s)\n }\n}\n\nfunction compileSafe (pattern) {\n const prog = compileProg(parse(pattern))\n const runner = makeRunner(prog)\n // `__ataSafe` brands the result so the standalone serializer can tell a safe\n // matcher apart from a RegExp and emit `__ataSafeRe(source)` instead.\n return { test: runner, source: pattern, __ataSafe: true }\n}\n\n// True when the linear engine can represent `src`. Used by the codegen to decide\n// between the safe matcher and a JS RegExp fallback for patterns outside the\n// supported (RE2) subset (backreferences, lookaround, etc.).\nfunction patternIsSafe (src) {\n try { compileSafe(src); return true } catch { return false }\n}\n\nmodule.exports = { compileSafe, patternIsSafe }\n";
|