ata-validator 0.17.2 → 0.17.4
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 +14 -0
- package/bin/ata.js +5 -0
- package/index.js +63 -8
- package/lib/js-compiler.js +78 -21
- package/lib/safe-regex.js +300 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to ata-validator are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/), and this project adheres to semantic versioning.
|
|
4
4
|
|
|
5
|
+
## 0.17.4 - 2026-05-25
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Standalone output now embeds user-supplied format functions. `toStandaloneModule` and `bundleCompact` referenced the `_uf_<name>` format helpers without declaring them, so the generated module threw `_uf_<name> is not defined` on the first validation, and their error path skipped the custom format entirely (so `validate` disagreed with `isValid`). Both now serialize the format functions via `Function#toString` and run them on the error path too, matching `bundleStandalone`.
|
|
10
|
+
- Compiled validators report per-property errors for schema-valued `additionalProperties`. The AOT error path previously emitted a single generic `validation failed`; it now validates each undeclared property against the subschema and reports the precise `/<key>` error, matching the runtime validator.
|
|
11
|
+
- `ata --version` (and `-V`) prints the CLI version instead of failing with `unknown command`.
|
|
12
|
+
|
|
13
|
+
## 0.17.3 - 2026-05-25
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- User-supplied `pattern`, `patternProperties`, and `propertyNames` regexes now run through a linear-time matching engine, so a crafted schema or input can no longer trigger catastrophic backtracking (ReDoS). Patterns the engine cannot represent, such as those using backreferences, fall back to the native `RegExp`. The built-in `format` checks (`email`, `uri`, `uri-reference`, `hostname`, `ipv4`, `ipv6`, `date`, `date-time`, `time`, `duration`, `uuid`) were routed through the same engine and stay linear on adversarial input.
|
|
18
|
+
|
|
5
19
|
## 0.17.2 - 2026-05-24
|
|
6
20
|
|
|
7
21
|
### Fixed
|
package/bin/ata.js
CHANGED
|
@@ -45,6 +45,7 @@ Build options:
|
|
|
45
45
|
--dual Emit both .compiled.mjs (with source) and .compiled.min.mjs (without)
|
|
46
46
|
|
|
47
47
|
-h, --help Show this message
|
|
48
|
+
-V, --version Print the ata-validator version
|
|
48
49
|
|
|
49
50
|
Examples:
|
|
50
51
|
ata compile schemas/user.json -o src/generated/user.validator.mjs
|
|
@@ -386,6 +387,10 @@ function main() {
|
|
|
386
387
|
|
|
387
388
|
const cmd = argv[0];
|
|
388
389
|
if (cmd === '-h' || cmd === '--help' || cmd === 'help') { usage(); return; }
|
|
390
|
+
if (cmd === '-V' || cmd === '--version' || cmd === 'version') {
|
|
391
|
+
process.stdout.write(require('../package.json').version + '\n');
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
389
394
|
|
|
390
395
|
const rest = argv.slice(1);
|
|
391
396
|
let args;
|
package/index.js
CHANGED
|
@@ -299,6 +299,27 @@ const _CP_LEN_SOURCE = `function _cpLen(s) {
|
|
|
299
299
|
return len;
|
|
300
300
|
}`;
|
|
301
301
|
|
|
302
|
+
// The linear-time regex engine, inlined verbatim into standalone output so a
|
|
303
|
+
// compiled module that uses safe `pattern` matchers has no runtime dependency on
|
|
304
|
+
// ata-validator. Read from the source file (one copy, no drift) with the strict
|
|
305
|
+
// directive and CommonJS exports stripped, plus the `__ataSafeRe` alias the
|
|
306
|
+
// emitted code calls. The engine has no eval/new Function, so the embed is
|
|
307
|
+
// CSP-safe.
|
|
308
|
+
const SAFE_REGEX_EMBED = (() => {
|
|
309
|
+
const raw = require("fs").readFileSync(require("path").join(__dirname, "lib", "safe-regex.js"), "utf8");
|
|
310
|
+
const body = raw
|
|
311
|
+
.replace(/^'use strict'\s*\n/, "")
|
|
312
|
+
.replace(/\nmodule\.exports[^\n]*\n?/, "\n");
|
|
313
|
+
return body.trimEnd() + "\nconst __ataSafeRe = compileSafe;";
|
|
314
|
+
})();
|
|
315
|
+
|
|
316
|
+
// Returns the engine embed when any supplied compiled function references the
|
|
317
|
+
// safe matcher (jsFn._usesSafeRe), else an empty string so non-pattern modules
|
|
318
|
+
// pay zero bytes.
|
|
319
|
+
function safeRePrelude(...fns) {
|
|
320
|
+
return fns.some((f) => f && f._usesSafeRe) ? SAFE_REGEX_EMBED + "\n" : "";
|
|
321
|
+
}
|
|
322
|
+
|
|
302
323
|
// Above this size, simdjson On Demand (selective field access) beats JSON.parse
|
|
303
324
|
// (which must materialize the full JS object tree). Buffer.from + NAPI ~2x faster.
|
|
304
325
|
const SIMDJSON_THRESHOLD = 8192;
|
|
@@ -1152,7 +1173,7 @@ class Validator {
|
|
|
1152
1173
|
return `// Auto-generated by ata-validator — do not edit
|
|
1153
1174
|
'use strict';
|
|
1154
1175
|
${_CP_LEN_SOURCE}
|
|
1155
|
-
${preambleSrc}
|
|
1176
|
+
${safeRePrelude(jsFn, jsErrFn)}${preambleSrc}
|
|
1156
1177
|
const boolFn = function(d) {
|
|
1157
1178
|
${src}
|
|
1158
1179
|
};
|
|
@@ -1188,11 +1209,12 @@ module.exports = { boolFn, hybridFactory, errFn };
|
|
|
1188
1209
|
const src = jsFn._source;
|
|
1189
1210
|
|
|
1190
1211
|
let errCore = '';
|
|
1212
|
+
let jsErrFn = null;
|
|
1191
1213
|
if (!abortEarly) {
|
|
1192
|
-
|
|
1214
|
+
jsErrFn = compileToJSCodegenWithErrors(
|
|
1193
1215
|
typeof this._schemaObj === 'object' ? this._schemaObj : {},
|
|
1194
1216
|
null,
|
|
1195
|
-
|
|
1217
|
+
this._userFormats,
|
|
1196
1218
|
(source && sourceMap && schemaFile) ? { sourceMap, schemaFile } : null,
|
|
1197
1219
|
);
|
|
1198
1220
|
const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
|
|
@@ -1220,7 +1242,9 @@ module.exports = { boolFn, hybridFactory, errFn };
|
|
|
1220
1242
|
lines.push(`const ${name} = ${JSON.stringify(val)};`);
|
|
1221
1243
|
continue;
|
|
1222
1244
|
}
|
|
1223
|
-
if (val
|
|
1245
|
+
if (val && val.__ataSafe) {
|
|
1246
|
+
lines.push(`const ${name} = __ataSafeRe(${JSON.stringify(val.source)});`);
|
|
1247
|
+
} else if (val instanceof RegExp) {
|
|
1224
1248
|
const flags = val.flags;
|
|
1225
1249
|
lines.push(`const ${name} = new RegExp(${JSON.stringify(val.source)}${flags ? ', ' + JSON.stringify(flags) : ''});`);
|
|
1226
1250
|
} else if (val instanceof Set) {
|
|
@@ -1242,6 +1266,16 @@ module.exports = { boolFn, hybridFactory, errFn };
|
|
|
1242
1266
|
// them at module scope too, or _fn references undefined names (e.g. _af1_b0).
|
|
1243
1267
|
const preambleDecls = jsFn._preambleSource ? jsFn._preambleSource + '\n' : '';
|
|
1244
1268
|
|
|
1269
|
+
// User-supplied format functions are referenced as _uf_<name> by both the
|
|
1270
|
+
// boolean (_fn) and error (errFn) bodies. Embed them via Function#toString
|
|
1271
|
+
// so the standalone module stays self-contained.
|
|
1272
|
+
let formatDecls = '';
|
|
1273
|
+
if (jsFn._formatClosures && jsFn._formatClosures.length > 0) {
|
|
1274
|
+
formatDecls = jsFn._formatClosures
|
|
1275
|
+
.map(({ name, fn }) => `const ${name} = ${fn.toString()};`)
|
|
1276
|
+
.join('\n') + '\n';
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1245
1279
|
const validBody = errCore
|
|
1246
1280
|
? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
|
|
1247
1281
|
: 'return _fn(data) ? VALID : ABORT';
|
|
@@ -1254,7 +1288,7 @@ module.exports = { boolFn, hybridFactory, errFn };
|
|
|
1254
1288
|
// Schema is embedded; runtime has zero dependency on ata-validator.
|
|
1255
1289
|
'use strict';
|
|
1256
1290
|
${_CP_LEN_SOURCE}
|
|
1257
|
-
${schemaSourceConst}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
1291
|
+
${safeRePrelude(jsFn, jsErrFn)}${schemaSourceConst}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
1258
1292
|
const ABORT = Object.freeze({
|
|
1259
1293
|
valid: false,
|
|
1260
1294
|
errors: Object.freeze([Object.freeze({
|
|
@@ -1264,7 +1298,7 @@ const ABORT = Object.freeze({
|
|
|
1264
1298
|
path: '',
|
|
1265
1299
|
})]),
|
|
1266
1300
|
});
|
|
1267
|
-
${closureDecls}${preambleDecls}const _fn = function(d) {
|
|
1301
|
+
${closureDecls}${preambleDecls}${formatDecls}const _fn = function(d) {
|
|
1268
1302
|
${src}
|
|
1269
1303
|
};
|
|
1270
1304
|
${errCore}function isValid(data) { return _fn(data); }
|
|
@@ -1490,6 +1524,7 @@ Validator.bundleStandalone = function (schemas, opts) {
|
|
|
1490
1524
|
const bundleOpts = haveIds ? { ...(opts || {}), schemas } : (opts || {});
|
|
1491
1525
|
const format = (opts && opts.format) || 'cjs';
|
|
1492
1526
|
const R = "Object.freeze({valid:true,errors:Object.freeze([])})";
|
|
1527
|
+
let bundleUsesSafeRe = false;
|
|
1493
1528
|
const fns = schemas.map((schema) => {
|
|
1494
1529
|
const v = new Validator(schema, bundleOpts);
|
|
1495
1530
|
v._ensureCompiled();
|
|
@@ -1500,6 +1535,7 @@ Validator.bundleStandalone = function (schemas, opts) {
|
|
|
1500
1535
|
v._schemaMap,
|
|
1501
1536
|
v._userFormats,
|
|
1502
1537
|
);
|
|
1538
|
+
if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
|
|
1503
1539
|
const errBody =
|
|
1504
1540
|
jsErrFn && jsErrFn._errSource
|
|
1505
1541
|
? jsErrFn._errSource
|
|
@@ -1525,10 +1561,11 @@ Validator.bundleStandalone = function (schemas, opts) {
|
|
|
1525
1561
|
return `(function(R){${preamble}var E=function(d){var _all=true;${errBody}};return function(d){${jsFn._hybridSource}}})(R)`;
|
|
1526
1562
|
});
|
|
1527
1563
|
const arr = `[${fns.join(",")}]`;
|
|
1564
|
+
const safeEmbed = bundleUsesSafeRe ? SAFE_REGEX_EMBED + "\n" : "";
|
|
1528
1565
|
if (format === 'esm') {
|
|
1529
|
-
return `// Auto-generated by ata-validator — do not edit\
|
|
1566
|
+
return `// Auto-generated by ata-validator — do not edit\n${safeEmbed}const R=${R};\nconst validators=${arr};\nexport default validators;\nexport { validators };\n`;
|
|
1530
1567
|
}
|
|
1531
|
-
return `'use strict';\
|
|
1568
|
+
return `'use strict';\n${safeEmbed}var R=${R};\nmodule.exports=[${fns.join(",")}];\n`;
|
|
1532
1569
|
};
|
|
1533
1570
|
|
|
1534
1571
|
// Compact bundle: deduplicated code. Shared template functions + per-schema params.
|
|
@@ -1538,6 +1575,7 @@ Validator.bundleCompact = function (schemas, opts) {
|
|
|
1538
1575
|
const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
|
|
1539
1576
|
const bundleOpts = haveIds ? { ...(opts || {}), schemas } : (opts || {});
|
|
1540
1577
|
const format = (opts && opts.format) || 'cjs';
|
|
1578
|
+
let bundleUsesSafeRe = false;
|
|
1541
1579
|
// Analyze schemas and group by structure
|
|
1542
1580
|
const entries = schemas.map((schema) => {
|
|
1543
1581
|
const v = new Validator(schema, bundleOpts);
|
|
@@ -1547,7 +1585,9 @@ Validator.bundleCompact = function (schemas, opts) {
|
|
|
1547
1585
|
const jsErrFn = compileToJSCodegenWithErrors(
|
|
1548
1586
|
typeof schema === "string" ? JSON.parse(schema) : schema,
|
|
1549
1587
|
v._schemaMap,
|
|
1588
|
+
v._userFormats,
|
|
1550
1589
|
);
|
|
1590
|
+
if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
|
|
1551
1591
|
// Hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) must travel with the
|
|
1552
1592
|
// hybrid body or it references undefined names. Prepending keeps dedup honest:
|
|
1553
1593
|
// schemas with different branch sets no longer collide on body alone.
|
|
@@ -1557,6 +1597,7 @@ Validator.bundleCompact = function (schemas, opts) {
|
|
|
1557
1597
|
return {
|
|
1558
1598
|
hybrid,
|
|
1559
1599
|
err: jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : null,
|
|
1600
|
+
fmt: jsFn._formatClosures || null,
|
|
1560
1601
|
};
|
|
1561
1602
|
});
|
|
1562
1603
|
|
|
@@ -1591,9 +1632,23 @@ Validator.bundleCompact = function (schemas, opts) {
|
|
|
1591
1632
|
let out = isEsm
|
|
1592
1633
|
? "// Auto-generated by ata-validator — do not edit\n"
|
|
1593
1634
|
: "'use strict';\n";
|
|
1635
|
+
if (bundleUsesSafeRe) out += SAFE_REGEX_EMBED + "\n";
|
|
1594
1636
|
const declKW = isEsm ? "const" : "var";
|
|
1595
1637
|
out += `${declKW} R=Object.freeze({valid:true,errors:Object.freeze([])});\n`;
|
|
1596
1638
|
|
|
1639
|
+
// User format functions are referenced as _uf_<name> by the hybrid and error
|
|
1640
|
+
// bodies. Collect them across all schemas (deduped by name) and embed via
|
|
1641
|
+
// Function#toString so the bundle stays self-contained.
|
|
1642
|
+
const fmtSeen = new Set();
|
|
1643
|
+
for (const e of entries) {
|
|
1644
|
+
if (!e || !e.fmt) continue;
|
|
1645
|
+
for (const { name, fn } of e.fmt) {
|
|
1646
|
+
if (fmtSeen.has(name)) continue;
|
|
1647
|
+
fmtSeen.add(name);
|
|
1648
|
+
out += `${declKW} ${name}=${fn.toString()};\n`;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1597
1652
|
// Shared hybrid factories
|
|
1598
1653
|
out += `${declKW} H=[\n`;
|
|
1599
1654
|
out += bodies
|
package/lib/js-compiler.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { codeFor } = require('./error-codes')
|
|
4
|
+
const { compileSafe, patternIsSafe } = require('./safe-regex')
|
|
5
|
+
|
|
6
|
+
// Closure value for a patternProperties/propertyNames regex: the linear-time
|
|
7
|
+
// safe matcher when the pattern is in the supported subset (and flag the ctx so
|
|
8
|
+
// standalone embeds the engine), else a plain RegExp. Both expose `.test`.
|
|
9
|
+
function safeReClosure (ctx, src) {
|
|
10
|
+
if (patternIsSafe(src)) { ctx.usesSafeRe = true; return compileSafe(src) }
|
|
11
|
+
return new RegExp(src)
|
|
12
|
+
}
|
|
4
13
|
|
|
5
14
|
// Compile a JSON Schema into a pure JS validator function.
|
|
6
15
|
// Closure-based validator — no new Function() or eval().
|
|
@@ -328,7 +337,7 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
328
337
|
}
|
|
329
338
|
if (schema.pattern) {
|
|
330
339
|
try {
|
|
331
|
-
const re = new RegExp(schema.pattern)
|
|
340
|
+
const re = patternIsSafe(schema.pattern) ? compileSafe(schema.pattern) : new RegExp(schema.pattern)
|
|
332
341
|
checks.push((d) => typeof d !== 'string' || re.test(d))
|
|
333
342
|
} catch {
|
|
334
343
|
return null
|
|
@@ -962,6 +971,12 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
|
962
971
|
|
|
963
972
|
// Pre-create regex objects once
|
|
964
973
|
for (const code of ctx.helperCode) {
|
|
974
|
+
const safeMatch = code.match(/^const (_re\d+)=__ataSafeRe\((.+)\)$/)
|
|
975
|
+
if (safeMatch) {
|
|
976
|
+
closureNames.push(safeMatch[1])
|
|
977
|
+
closureValues.push(compileSafe(JSON.parse(safeMatch[2])))
|
|
978
|
+
continue
|
|
979
|
+
}
|
|
965
980
|
const match = code.match(/^const (_re\d+)=new RegExp\((.+)\)$/)
|
|
966
981
|
if (match) {
|
|
967
982
|
closureNames.push(match[1])
|
|
@@ -1005,6 +1020,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
|
1005
1020
|
boolFn._source = helperStr + body
|
|
1006
1021
|
boolFn._preambleSource = preambleStr
|
|
1007
1022
|
boolFn._hybridSource = helperStr + hybridBody
|
|
1023
|
+
boolFn._usesSafeRe = !!ctx.usesSafeRe
|
|
1008
1024
|
// Custom-format closure entries that the bundle output needs to recreate.
|
|
1009
1025
|
// Stored as { name, fn } so consumers can serialize via Function#toString.
|
|
1010
1026
|
if (ctx.userFormats) {
|
|
@@ -1454,7 +1470,12 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1454
1470
|
if (!ctx.regExpMap.has(pattern)) {
|
|
1455
1471
|
const ri = ctx.varCounter++
|
|
1456
1472
|
ctx.regExpMap.set(pattern, ri)
|
|
1457
|
-
|
|
1473
|
+
if (patternIsSafe(schema.pattern)) {
|
|
1474
|
+
ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
|
|
1475
|
+
ctx.usesSafeRe = true
|
|
1476
|
+
} else {
|
|
1477
|
+
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
|
|
1478
|
+
}
|
|
1458
1479
|
}
|
|
1459
1480
|
const ri = ctx.regExpMap.get(pattern);
|
|
1460
1481
|
lines.push(isStr ? `if(!_re${ri}.test(${v}))return false` : `if(typeof ${v}==='string'&&!_re${ri}.test(${v}))return false`)
|
|
@@ -1552,7 +1573,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1552
1573
|
} else {
|
|
1553
1574
|
const ri = ctx.varCounter++
|
|
1554
1575
|
ctx.closureVars.push(`_re${ri}`)
|
|
1555
|
-
ctx.closureVals.push(
|
|
1576
|
+
ctx.closureVals.push(safeReClosure(ctx, pat))
|
|
1556
1577
|
matchers.push({ check: `_re${ri}.test(${kVar})` })
|
|
1557
1578
|
}
|
|
1558
1579
|
}
|
|
@@ -1587,7 +1608,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1587
1608
|
} else {
|
|
1588
1609
|
const ri = ctx.varCounter++
|
|
1589
1610
|
ctx.closureVars.push(`_re${ri}`)
|
|
1590
|
-
ctx.closureVals.push(
|
|
1611
|
+
ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
|
|
1591
1612
|
lines.push(`if(!_re${ri}.test(${kVar}))return false`)
|
|
1592
1613
|
}
|
|
1593
1614
|
}
|
|
@@ -1630,7 +1651,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1630
1651
|
} else {
|
|
1631
1652
|
const ri = ctx.varCounter++
|
|
1632
1653
|
ctx.closureVars.push(`_re${ri}`)
|
|
1633
|
-
ctx.closureVals.push(
|
|
1654
|
+
ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
|
|
1634
1655
|
lines.push(`if(!_re${ri}.test(${kVar}))return false`)
|
|
1635
1656
|
}
|
|
1636
1657
|
}
|
|
@@ -1674,7 +1695,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1674
1695
|
} else {
|
|
1675
1696
|
const ri = ctx.varCounter++
|
|
1676
1697
|
ctx.closureVars.push(`_re${ri}`)
|
|
1677
|
-
ctx.closureVals.push(
|
|
1698
|
+
ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
|
|
1678
1699
|
lines.push(`if(!_re${ri}.test(_k${ki}))return false`)
|
|
1679
1700
|
}
|
|
1680
1701
|
}
|
|
@@ -2102,7 +2123,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2102
2123
|
for (const pat of allPatterns) {
|
|
2103
2124
|
const ri = ctx.varCounter++
|
|
2104
2125
|
ctx.closureVars.push(`_ure${ri}`)
|
|
2105
|
-
ctx.closureVals.push(
|
|
2126
|
+
ctx.closureVals.push(safeReClosure(ctx, pat))
|
|
2106
2127
|
reVars.push(`_ure${ri}`)
|
|
2107
2128
|
}
|
|
2108
2129
|
if (schema.if && !schema.then && !schema.else) {
|
|
@@ -2119,7 +2140,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2119
2140
|
for (const pat of ifPatterns) {
|
|
2120
2141
|
const ri = ctx.varCounter++
|
|
2121
2142
|
ctx.closureVars.push(`_ure${ri}`)
|
|
2122
|
-
ctx.closureVals.push(
|
|
2143
|
+
ctx.closureVals.push(safeReClosure(ctx, pat))
|
|
2123
2144
|
ifReVars.push(`_ure${ri}`)
|
|
2124
2145
|
}
|
|
2125
2146
|
const rootReVars = []
|
|
@@ -2127,7 +2148,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2127
2148
|
for (const pat of Object.keys(schema.patternProperties)) {
|
|
2128
2149
|
const ri = ctx.varCounter++
|
|
2129
2150
|
ctx.closureVars.push(`_ure${ri}`)
|
|
2130
|
-
ctx.closureVals.push(
|
|
2151
|
+
ctx.closureVals.push(safeReClosure(ctx, pat))
|
|
2131
2152
|
rootReVars.push(`_ure${ri}`)
|
|
2132
2153
|
}
|
|
2133
2154
|
}
|
|
@@ -2537,9 +2558,6 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
2537
2558
|
if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
|
|
2538
2559
|
// Bail on self-referencing schemas — error codegen doesn't support recursion
|
|
2539
2560
|
if (s.includes('"$ref":"#"')) return null
|
|
2540
|
-
// Bail on additionalProperties as a schema — genCodeE does not emit the
|
|
2541
|
-
// per-key sub-schema loop. Let the closure path handle detailed errors.
|
|
2542
|
-
if (hasAdditionalPropertiesSchema(schema)) return null
|
|
2543
2561
|
}
|
|
2544
2562
|
if (typeof schema === 'boolean') {
|
|
2545
2563
|
return schema
|
|
@@ -2621,8 +2639,17 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
2621
2639
|
`\n return{valid:_e.length===0,errors:_e}`
|
|
2622
2640
|
}
|
|
2623
2641
|
try {
|
|
2624
|
-
|
|
2642
|
+
let fn
|
|
2643
|
+
if (ctx.usesSafeRe) {
|
|
2644
|
+
// Inlined helperCode references __ataSafeRe; bind the safe-regex factory.
|
|
2645
|
+
// Standalone keeps the inline _errSource — the embed defines __ataSafeRe.
|
|
2646
|
+
const built = new Function('__ataSafeRe', 'd', '_all', body)
|
|
2647
|
+
fn = (d, _all) => built(compileSafe, d, _all)
|
|
2648
|
+
} else {
|
|
2649
|
+
fn = new Function('d', '_all', body)
|
|
2650
|
+
}
|
|
2625
2651
|
fn._errSource = body
|
|
2652
|
+
fn._usesSafeRe = !!ctx.usesSafeRe
|
|
2626
2653
|
return fn
|
|
2627
2654
|
} catch {
|
|
2628
2655
|
return null
|
|
@@ -2856,7 +2883,12 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2856
2883
|
if (!ctx.regExpMap.has(pattern)) {
|
|
2857
2884
|
const ri = ctx.varCounter++
|
|
2858
2885
|
ctx.regExpMap.set(pattern, ri)
|
|
2859
|
-
|
|
2886
|
+
if (patternIsSafe(schema.pattern)) {
|
|
2887
|
+
ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
|
|
2888
|
+
ctx.usesSafeRe = true
|
|
2889
|
+
} else {
|
|
2890
|
+
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
|
|
2891
|
+
}
|
|
2860
2892
|
}
|
|
2861
2893
|
const ri = ctx.regExpMap.get(pattern);
|
|
2862
2894
|
const c = isStr ? `!_re${ri}.test(${v})` : `typeof ${v}==='string'&&!_re${ri}.test(${v})`
|
|
@@ -2961,7 +2993,12 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2961
2993
|
if (!ctx.regExpMap.has(pattern)) {
|
|
2962
2994
|
const ri = ctx.varCounter++
|
|
2963
2995
|
ctx.regExpMap.set(pattern, ri)
|
|
2964
|
-
|
|
2996
|
+
if (patternIsSafe(pat)) {
|
|
2997
|
+
ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
|
|
2998
|
+
ctx.usesSafeRe = true
|
|
2999
|
+
} else {
|
|
3000
|
+
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
|
|
3001
|
+
}
|
|
2965
3002
|
}
|
|
2966
3003
|
const ri = ctx.regExpMap.get(pattern);
|
|
2967
3004
|
const ki = ctx.varCounter++
|
|
@@ -2972,6 +3009,21 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2972
3009
|
}
|
|
2973
3010
|
}
|
|
2974
3011
|
|
|
3012
|
+
// additionalProperties as a schema — validate each property not declared in
|
|
3013
|
+
// `properties` against the subschema, with a dynamic /<key> path. codegenSafe
|
|
3014
|
+
// guarantees no patternProperties or composition here, so "additional" is
|
|
3015
|
+
// simply "not declared".
|
|
3016
|
+
if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null) {
|
|
3017
|
+
const ki = ctx.varCounter++
|
|
3018
|
+
const known = Object.keys(schema.properties || {})
|
|
3019
|
+
const guard = known.length ? `const _ak${ki}=new Set([${known.map((k) => JSON.stringify(k)).join(',')}]);` : ''
|
|
3020
|
+
const keep = known.length ? `if(!_ak${ki}.has(_k${ki})){` : `{`
|
|
3021
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${guard}for(const _k${ki} in ${v}){${keep}`)
|
|
3022
|
+
const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
|
|
3023
|
+
genCodeE(schema.additionalProperties, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix + '/additionalProperties')
|
|
3024
|
+
lines.push(`}}}`)
|
|
3025
|
+
}
|
|
3026
|
+
|
|
2975
3027
|
// dependentSchemas
|
|
2976
3028
|
if (schema.dependentSchemas) {
|
|
2977
3029
|
for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
|
|
@@ -2997,7 +3049,12 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2997
3049
|
if (!ctx.regExpMap.has(pattern)) {
|
|
2998
3050
|
const ri = ctx.varCounter++
|
|
2999
3051
|
ctx.regExpMap.set(pattern, ri)
|
|
3000
|
-
|
|
3052
|
+
if (patternIsSafe(pn.pattern)) {
|
|
3053
|
+
ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
|
|
3054
|
+
ctx.usesSafeRe = true
|
|
3055
|
+
} else {
|
|
3056
|
+
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
|
|
3057
|
+
}
|
|
3001
3058
|
}
|
|
3002
3059
|
const ri = ctx.regExpMap.get(pattern);
|
|
3003
3060
|
lines.push(`if(!_re${ri}.test(_k${ki})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
|
|
@@ -3419,7 +3476,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3419
3476
|
const ri = ctx.varCounter++
|
|
3420
3477
|
const reVar = `_re${ri}`
|
|
3421
3478
|
ctx.closureVars.push(reVar)
|
|
3422
|
-
ctx.closureVals.push(new RegExp(schema.pattern))
|
|
3479
|
+
ctx.closureVals.push(patternIsSafe(schema.pattern) ? compileSafe(schema.pattern) : new RegExp(schema.pattern))
|
|
3423
3480
|
const c = isStr ? `!${reVar}.test(${v})` : `typeof ${v}==='string'&&!${reVar}.test(${v})`
|
|
3424
3481
|
lines.push(`if(${c}){${fail('pattern', 'pattern', `{pattern:${JSON.stringify(schema.pattern)}}`, `'must match pattern "${schema.pattern}"'`)}}`)
|
|
3425
3482
|
}
|
|
@@ -3544,7 +3601,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3544
3601
|
} else {
|
|
3545
3602
|
const ri = ctx.varCounter++
|
|
3546
3603
|
ctx.closureVars.push(`_re${ri}`)
|
|
3547
|
-
ctx.closureVals.push(
|
|
3604
|
+
ctx.closureVals.push(safeReClosure(ctx, pat))
|
|
3548
3605
|
matchers.push({ check: `_re${ri}.test(_k${pi})` })
|
|
3549
3606
|
}
|
|
3550
3607
|
}
|
|
@@ -3588,7 +3645,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3588
3645
|
} else {
|
|
3589
3646
|
const ri = ctx.varCounter++
|
|
3590
3647
|
ctx.closureVars.push(`_re${ri}`)
|
|
3591
|
-
ctx.closureVals.push(
|
|
3648
|
+
ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
|
|
3592
3649
|
lines.push(`if(!_re${ri}.test(${kVar})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
|
|
3593
3650
|
}
|
|
3594
3651
|
}
|
|
@@ -3620,7 +3677,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3620
3677
|
} else {
|
|
3621
3678
|
const ri = ctx.varCounter++
|
|
3622
3679
|
ctx.closureVars.push(`_re${ri}`)
|
|
3623
|
-
ctx.closureVals.push(
|
|
3680
|
+
ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
|
|
3624
3681
|
lines.push(`if(!_re${ri}.test(${kVar})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
|
|
3625
3682
|
}
|
|
3626
3683
|
}
|
|
@@ -3662,7 +3719,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3662
3719
|
if (pn.pattern) {
|
|
3663
3720
|
const ri = ctx.varCounter++
|
|
3664
3721
|
ctx.closureVars.push(`_re${ri}`)
|
|
3665
|
-
ctx.closureVals.push(
|
|
3722
|
+
ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
|
|
3666
3723
|
lines.push(`if(!_re${ri}.test(_k${ki})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
|
|
3667
3724
|
}
|
|
3668
3725
|
if (pn.const !== undefined) {
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Linear-time regex engine for JSON Schema `pattern`, used in place of JS RegExp
|
|
4
|
+
// so an adversarial input cannot trigger catastrophic backtracking (ReDoS).
|
|
5
|
+
//
|
|
6
|
+
// It is a Pike VM: the pattern compiles to a small instruction program, and the
|
|
7
|
+
// VM simulates all NFA threads in lockstep over the input, deduping by program
|
|
8
|
+
// counter. Runtime is O(input * program), with no backtracking.
|
|
9
|
+
//
|
|
10
|
+
// Supported (the RE2 subset, which is what ata's native path also accepts):
|
|
11
|
+
// literals, ., character classes, \d \w \s \D \W \S, anchors ^ $, quantifiers
|
|
12
|
+
// * + ? {n} {n,} {n,m} (greedy or lazy, same language for a boolean test),
|
|
13
|
+
// groups ( ) (?: ), alternation |. Backreferences and lookaround are not
|
|
14
|
+
// supported by linear engines; compileSafe throws on them so the caller can
|
|
15
|
+
// decide (ata's codegen rejects such schemas rather than risk a hang).
|
|
16
|
+
|
|
17
|
+
const WS = [[9, 13], [32, 32], [160, 160]]
|
|
18
|
+
const DIGIT = [[48, 57]]
|
|
19
|
+
const WORD = [[48, 57], [65, 90], [97, 122], [95, 95]]
|
|
20
|
+
|
|
21
|
+
function parse (src) {
|
|
22
|
+
let i = 0
|
|
23
|
+
const len = src.length
|
|
24
|
+
const peek = () => src[i]
|
|
25
|
+
const eof = () => i >= len
|
|
26
|
+
|
|
27
|
+
function parseAlt () {
|
|
28
|
+
const opts = [parseConcat()]
|
|
29
|
+
while (!eof() && peek() === '|') { i++; opts.push(parseConcat()) }
|
|
30
|
+
return opts.length === 1 ? opts[0] : { t: 'alt', opts }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseConcat () {
|
|
34
|
+
const parts = []
|
|
35
|
+
while (!eof() && peek() !== '|' && peek() !== ')') parts.push(parseRepeat())
|
|
36
|
+
if (parts.length === 0) return { t: 'empty' }
|
|
37
|
+
return parts.length === 1 ? parts[0] : { t: 'concat', parts }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseRepeat () {
|
|
41
|
+
let node = parseAtom()
|
|
42
|
+
while (!eof()) {
|
|
43
|
+
const ch = peek()
|
|
44
|
+
if (ch === '*') { i++; node = { t: 'star', child: node } }
|
|
45
|
+
else if (ch === '+') { i++; node = { t: 'plus', child: node } }
|
|
46
|
+
else if (ch === '?') { i++; node = { t: 'quest', child: node } }
|
|
47
|
+
else if (ch === '{') {
|
|
48
|
+
const saved = i
|
|
49
|
+
const q = tryQuantifier()
|
|
50
|
+
if (!q) { i = saved; break }
|
|
51
|
+
node = { t: 'repeat', child: node, min: q.min, max: q.max }
|
|
52
|
+
} else break
|
|
53
|
+
// a trailing ? makes the quantifier lazy; same language for a boolean test
|
|
54
|
+
if (!eof() && peek() === '?') i++
|
|
55
|
+
}
|
|
56
|
+
return node
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function tryQuantifier () {
|
|
60
|
+
// assumes current char is '{'
|
|
61
|
+
i++
|
|
62
|
+
let min = ''
|
|
63
|
+
while (!eof() && /[0-9]/.test(peek())) { min += peek(); i++ }
|
|
64
|
+
if (min === '') return null
|
|
65
|
+
let max
|
|
66
|
+
if (peek() === '}') { i++; return { min: +min, max: +min } }
|
|
67
|
+
if (peek() === ',') {
|
|
68
|
+
i++
|
|
69
|
+
let m = ''
|
|
70
|
+
while (!eof() && /[0-9]/.test(peek())) { m += peek(); i++ }
|
|
71
|
+
if (peek() !== '}') return null
|
|
72
|
+
i++
|
|
73
|
+
max = m === '' ? Infinity : +m
|
|
74
|
+
return { min: +min, max }
|
|
75
|
+
}
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseAtom () {
|
|
80
|
+
const ch = peek()
|
|
81
|
+
if (ch === '(') {
|
|
82
|
+
i++
|
|
83
|
+
if (src[i] === '?') {
|
|
84
|
+
if (src[i + 1] === ':') { i += 2 }
|
|
85
|
+
else throw new Error('unsupported group (lookaround/named) in pattern')
|
|
86
|
+
}
|
|
87
|
+
const child = parseAlt()
|
|
88
|
+
if (peek() !== ')') throw new Error('unbalanced ( in pattern')
|
|
89
|
+
i++
|
|
90
|
+
return { t: 'group', child }
|
|
91
|
+
}
|
|
92
|
+
if (ch === '[') return parseClass()
|
|
93
|
+
if (ch === '.') { i++; return { t: 'any' } }
|
|
94
|
+
if (ch === '^') { i++; return { t: 'bol' } }
|
|
95
|
+
if (ch === '$') { i++; return { t: 'eol' } }
|
|
96
|
+
if (ch === '\\') return parseEscape(false)
|
|
97
|
+
if (ch === ')' || ch === '|') return { t: 'empty' }
|
|
98
|
+
i++
|
|
99
|
+
return { t: 'char', c: ch.charCodeAt(0) }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseClass () {
|
|
103
|
+
i++ // [
|
|
104
|
+
let neg = false
|
|
105
|
+
if (peek() === '^') { neg = true; i++ }
|
|
106
|
+
const ranges = []
|
|
107
|
+
while (!eof() && peek() !== ']') {
|
|
108
|
+
let lo
|
|
109
|
+
if (peek() === '\\') {
|
|
110
|
+
const esc = parseEscape(true)
|
|
111
|
+
if (esc.t === 'classpart') { for (const r of esc.ranges) ranges.push(r); continue }
|
|
112
|
+
lo = esc.c
|
|
113
|
+
} else { lo = peek().charCodeAt(0); i++ }
|
|
114
|
+
if (peek() === '-' && src[i + 1] !== ']' && i + 1 < len) {
|
|
115
|
+
i++ // -
|
|
116
|
+
let hi
|
|
117
|
+
if (peek() === '\\') { const e = parseEscape(true); hi = e.c } else { hi = peek().charCodeAt(0); i++ }
|
|
118
|
+
ranges.push([lo, hi])
|
|
119
|
+
} else {
|
|
120
|
+
ranges.push([lo, lo])
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (peek() !== ']') throw new Error('unbalanced [ in pattern')
|
|
124
|
+
i++
|
|
125
|
+
return { t: 'class', neg, ranges }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseEscape (inClass) {
|
|
129
|
+
i++ // backslash
|
|
130
|
+
if (eof()) throw new Error('trailing backslash in pattern')
|
|
131
|
+
const ch = peek(); i++
|
|
132
|
+
switch (ch) {
|
|
133
|
+
case 'd': return inClass ? { t: 'classpart', ranges: DIGIT } : { t: 'class', neg: false, ranges: DIGIT }
|
|
134
|
+
case 'w': return inClass ? { t: 'classpart', ranges: WORD } : { t: 'class', neg: false, ranges: WORD }
|
|
135
|
+
case 's': return inClass ? { t: 'classpart', ranges: WS } : { t: 'class', neg: false, ranges: WS }
|
|
136
|
+
case 'D': if (inClass) throw new Error('\\D inside a class is not supported'); return { t: 'class', neg: true, ranges: DIGIT }
|
|
137
|
+
case 'W': if (inClass) throw new Error('\\W inside a class is not supported'); return { t: 'class', neg: true, ranges: WORD }
|
|
138
|
+
case 'S': if (inClass) throw new Error('\\S inside a class is not supported'); return { t: 'class', neg: true, ranges: WS }
|
|
139
|
+
case 'n': return { t: 'char', c: 10 }
|
|
140
|
+
case 'r': return { t: 'char', c: 13 }
|
|
141
|
+
case 't': return { t: 'char', c: 9 }
|
|
142
|
+
case 'f': return { t: 'char', c: 12 }
|
|
143
|
+
case 'v': return { t: 'char', c: 11 }
|
|
144
|
+
case '0': return { t: 'char', c: 0 }
|
|
145
|
+
case 'x': { const h = src.slice(i, i + 2); i += 2; return { t: 'char', c: parseInt(h, 16) } }
|
|
146
|
+
case 'u': { const h = src.slice(i, i + 4); i += 4; return { t: 'char', c: parseInt(h, 16) } }
|
|
147
|
+
case 'b': if (inClass) return { t: 'char', c: 8 }; throw new Error('\\b word boundary is not supported')
|
|
148
|
+
default:
|
|
149
|
+
if (/[1-9]/.test(ch)) throw new Error('backreferences are not supported in pattern')
|
|
150
|
+
return { t: 'char', c: ch.charCodeAt(0) }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const ast = parseAlt()
|
|
155
|
+
if (!eof()) throw new Error('unexpected "' + peek() + '" in pattern')
|
|
156
|
+
return ast
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function compileProg (ast) {
|
|
160
|
+
const prog = []
|
|
161
|
+
const emit = (op, extra) => { const idx = prog.length; prog.push(Object.assign({ op }, extra)); return idx }
|
|
162
|
+
|
|
163
|
+
function rec (n) {
|
|
164
|
+
switch (n.t) {
|
|
165
|
+
case 'empty': break
|
|
166
|
+
case 'char': emit('char', { c: n.c }); break
|
|
167
|
+
case 'any': emit('any'); break
|
|
168
|
+
case 'class': emit('class', { neg: n.neg, ranges: n.ranges }); break
|
|
169
|
+
case 'bol': emit('bol'); break
|
|
170
|
+
case 'eol': emit('eol'); break
|
|
171
|
+
case 'group': rec(n.child); break
|
|
172
|
+
case 'concat': for (const p of n.parts) rec(p); break
|
|
173
|
+
case 'alt': {
|
|
174
|
+
const jmps = []
|
|
175
|
+
for (let k = 0; k < n.opts.length; k++) {
|
|
176
|
+
if (k < n.opts.length - 1) {
|
|
177
|
+
const sp = emit('split', { x: 0, y: 0 })
|
|
178
|
+
prog[sp].x = prog.length
|
|
179
|
+
rec(n.opts[k])
|
|
180
|
+
jmps.push(emit('jmp', { x: 0 }))
|
|
181
|
+
prog[sp].y = prog.length
|
|
182
|
+
} else {
|
|
183
|
+
rec(n.opts[k])
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (const j of jmps) prog[j].x = prog.length
|
|
187
|
+
break
|
|
188
|
+
}
|
|
189
|
+
case 'star': {
|
|
190
|
+
const sp = emit('split', { x: 0, y: 0 })
|
|
191
|
+
prog[sp].x = prog.length
|
|
192
|
+
rec(n.child)
|
|
193
|
+
emit('jmp', { x: sp })
|
|
194
|
+
prog[sp].y = prog.length
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
case 'plus': {
|
|
198
|
+
const start = prog.length
|
|
199
|
+
rec(n.child)
|
|
200
|
+
const sp = emit('split', { x: start, y: 0 })
|
|
201
|
+
prog[sp].y = prog.length
|
|
202
|
+
break
|
|
203
|
+
}
|
|
204
|
+
case 'quest': {
|
|
205
|
+
const sp = emit('split', { x: 0, y: 0 })
|
|
206
|
+
prog[sp].x = prog.length
|
|
207
|
+
rec(n.child)
|
|
208
|
+
prog[sp].y = prog.length
|
|
209
|
+
break
|
|
210
|
+
}
|
|
211
|
+
case 'repeat': {
|
|
212
|
+
for (let k = 0; k < n.min; k++) rec(n.child)
|
|
213
|
+
if (n.max === Infinity) {
|
|
214
|
+
if (n.min === 0) rec({ t: 'star', child: n.child })
|
|
215
|
+
else rec({ t: 'star', child: n.child })
|
|
216
|
+
} else {
|
|
217
|
+
for (let k = 0; k < n.max - n.min; k++) rec({ t: 'quest', child: n.child })
|
|
218
|
+
}
|
|
219
|
+
break
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
rec(ast)
|
|
225
|
+
emit('match')
|
|
226
|
+
return prog
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function matchClass (instr, c) {
|
|
230
|
+
let inside = false
|
|
231
|
+
const r = instr.ranges
|
|
232
|
+
for (let k = 0; k < r.length; k++) { if (c >= r[k][0] && c <= r[k][1]) { inside = true; break } }
|
|
233
|
+
return instr.neg ? !inside : inside
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function makeRunner (prog) {
|
|
237
|
+
const n = prog.length
|
|
238
|
+
const lastGen = new Int32Array(n).fill(-1)
|
|
239
|
+
let gen = 0
|
|
240
|
+
const stack = []
|
|
241
|
+
|
|
242
|
+
function addThread (list, pc, pos, len) {
|
|
243
|
+
stack.length = 0
|
|
244
|
+
stack.push(pc)
|
|
245
|
+
while (stack.length) {
|
|
246
|
+
const p = stack.pop()
|
|
247
|
+
if (lastGen[p] === gen) continue
|
|
248
|
+
lastGen[p] = gen
|
|
249
|
+
const I = prog[p]
|
|
250
|
+
switch (I.op) {
|
|
251
|
+
case 'jmp': stack.push(I.x); break
|
|
252
|
+
case 'split': stack.push(I.y); stack.push(I.x); break
|
|
253
|
+
case 'bol': if (pos === 0) stack.push(p + 1); break
|
|
254
|
+
case 'eol': if (pos === len) stack.push(p + 1); break
|
|
255
|
+
default: list.push(p)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return function test (s) {
|
|
261
|
+
const len = s.length
|
|
262
|
+
let clist = []
|
|
263
|
+
let nlist = []
|
|
264
|
+
gen++
|
|
265
|
+
addThread(clist, 0, 0, len)
|
|
266
|
+
for (let pos = 0; pos <= len; pos++) {
|
|
267
|
+
const c = pos < len ? s.charCodeAt(pos) : -1
|
|
268
|
+
gen++
|
|
269
|
+
nlist.length = 0
|
|
270
|
+
for (let k = 0; k < clist.length; k++) {
|
|
271
|
+
const pc = clist[k]
|
|
272
|
+
const I = prog[pc]
|
|
273
|
+
if (I.op === 'match') return true
|
|
274
|
+
else if (I.op === 'char') { if (c === I.c) addThread(nlist, pc + 1, pos + 1, len) }
|
|
275
|
+
else if (I.op === 'any') { if (c !== -1 && c !== 10) addThread(nlist, pc + 1, pos + 1, len) }
|
|
276
|
+
else if (I.op === 'class') { if (c !== -1 && matchClass(I, c)) addThread(nlist, pc + 1, pos + 1, len) }
|
|
277
|
+
}
|
|
278
|
+
if (pos < len) addThread(nlist, 0, pos + 1, len)
|
|
279
|
+
const tmp = clist; clist = nlist; nlist = tmp
|
|
280
|
+
}
|
|
281
|
+
return false
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function compileSafe (pattern) {
|
|
286
|
+
const prog = compileProg(parse(pattern))
|
|
287
|
+
const runner = makeRunner(prog)
|
|
288
|
+
// `__ataSafe` brands the result so the standalone serializer can tell a safe
|
|
289
|
+
// matcher apart from a RegExp and emit `__ataSafeRe(source)` instead.
|
|
290
|
+
return { test: runner, source: pattern, __ataSafe: true }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// True when the linear engine can represent `src`. Used by the codegen to decide
|
|
294
|
+
// between the safe matcher and a JS RegExp fallback for patterns outside the
|
|
295
|
+
// supported (RE2) subset (backreferences, lookaround, etc.).
|
|
296
|
+
function patternIsSafe (src) {
|
|
297
|
+
try { compileSafe(src); return true } catch { return false }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
module.exports = { compileSafe, patternIsSafe }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.4",
|
|
4
4
|
"description": "JSON Schema validation with first-class TypeScript and zero runtime cost. AOT compile to per-schema ESM modules with zero validator dependency. Generic Validator<T> for TypeBox/Zod/Valibot composition. Optional runtime API. Standard Schema V1 compatible.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.mjs",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"rebuild": "cmake-js rebuild --target ata",
|
|
43
43
|
"prebuild": "pkg-prebuilds-copy --baseDir build/Release --source ata.node --name=ata --strip --napi_version=10",
|
|
44
44
|
"prebuild-all": "npm run prebuild -- --arch x64 && npm run prebuild -- --arch arm64",
|
|
45
|
-
"test": "node test.js && node tests/test_no_native.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_rich_errors_optout.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node benchmark/bench_aot_size.mjs",
|
|
45
|
+
"test": "node test.js && node tests/test_no_native.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_additional_props_errors.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_rich_errors_optout.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
|
|
46
46
|
"bench:size": "node benchmark/bench_aot_size.mjs",
|
|
47
47
|
"test:suite": "node tests/run_suite.js",
|
|
48
48
|
"test:compat": "node tests/test_compat.js",
|