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.
@@ -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 => JSON.stringify(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 = JSON.stringify(d)
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 = JSON.stringify(cv)
186
- checks.push((d) => JSON.stringify(d) === cs)
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 === 'object' && d !== null && key in 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 && schema.properties) {
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 = 0; i < d.length; 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) => typeof d !== 'number' || d % div === 0)
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: "#" — treat as permissive to avoid infinite recursion
601
- if (ref === '#') return () => true
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
- // Bail if schema contains features that codegen can't handle correctly
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
- fmtEntries.push({ name: closureNames[i], fn: closureValues[i] })
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}!==0`)
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}!==0`)
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
- if (v === 'd') {
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
- lines.push(`if(JSON.stringify(${v})!==${JSON.stringify(JSON.stringify(cv))})return false`)
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
- for (const key of schema.required) {
1651
- lines.push(`if(typeof ${v}!=='object'||${v}===null||!(${JSON.stringify(key)} in ${v}))return false`)
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) lines.push(isNum ? `if(${v}%${schema.multipleOf}!==0)return false` : `if(typeof ${v}==='number'&&${v}%${schema.multipleOf}!==0)return false`)
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}=0;${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`
2020
- : `if(Array.isArray(${v})){for(let ${idx}=0;${idx}<${v}.length;${idx}++){const ${elem}=${v}[${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 (!codegenSafe(schema, schemaMap)) return null
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
- if (schema.additionalProperties === false && schema.properties) {
3231
- const allowed = Object.keys(schema.properties).map(k => `${JSON.stringify(k)}`).join(',')
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 inner = `const _k${ci}=Object.keys(${v});const _a${ci}=new Set([${allowed}]);for(let _i=0;_i<_k${ci}.length;_i++){if(!_a${ci}.has(_k${ci}[_i])){_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}}}`
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
- genCodeE(sub, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix+'/patternProperties')
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 (!codegenSafe(schema, schemaMap)) return null
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
- lines.push(`if(${matchers[i].check}){_m${pi}=true;if(!_ppf${pi}_${i}(${v}[${kVar}])){${fail('pattern', 'patternProperties', `{pattern:'${ppEntries[i][0]}'}`, `'patternProperties: value invalid for key '+${kVar}`)}}}`)
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}&&!_ppf${pi}_${i}(${v}[${kVar}])){${fail('pattern', 'patternProperties', `{pattern:'${ppEntries[i][0]}'}`, `'patternProperties: value invalid for key '+${kVar}`)}}`)
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 };
@@ -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";