ata-validator 1.6.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +32 -14
- package/build.d.ts +12 -0
- package/index.d.ts +16 -4
- package/index.js +58 -13
- package/lib/aot-build.js +1 -0
- package/lib/aot.js +69 -19
- package/lib/buffer-gate.js +132 -0
- package/lib/draft7.js +24 -2
- package/lib/enrich-error.js +14 -4
- package/lib/error-codes.js +16 -9
- package/lib/interpreter.js +432 -200
- package/lib/js-compiler.js +240 -62
- package/lib/metaschemas.js +21 -0
- package/lib/safe-regex-source.js +1 -1
- package/lib/safe-regex.js +209 -29
- package/lib/version.js +1 -1
- package/package.json +8 -8
- package/t.d.ts +16 -5
package/lib/safe-regex-source.js
CHANGED
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
// into standalone output without a runtime `fs` read. Kept in sync by
|
|
6
6
|
// `tests/test_safe_regex_source_sync.js`.
|
|
7
7
|
|
|
8
|
-
module.exports = "'use strict'\n\n// Linear-time regex engine for JSON Schema `pattern`, used in place of JS RegExp\n// so an adversarial input cannot trigger catastrophic backtracking (ReDoS).\n//\n// It is a Pike VM: the pattern compiles to a small instruction program, and the\n// VM simulates all NFA threads in lockstep over the input, deduping by program\n// counter. Runtime is O(input * program), with no backtracking.\n//\n// Supported (the RE2 subset, which is what ata's native path also accepts):\n// literals, ., character classes, \\d \\w \\s \\D \\W \\S, anchors ^ $, quantifiers\n// * + ? {n} {n,} {n,m} (greedy or lazy, same language for a boolean test),\n// groups ( ) (?: ), alternation |. Backreferences and lookaround are not\n// supported by linear engines; compileSafe throws on them so the caller can\n// decide (ata's codegen rejects such schemas rather than risk a hang).\n\nconst WS = [[9, 13], [32, 32], [160, 160]]\nconst DIGIT = [[48, 57]]\nconst WORD = [[48, 57], [65, 90], [97, 122], [95, 95]]\n\nfunction parse (src) {\n let i = 0\n const len = src.length\n const peek = () => src[i]\n const eof = () => i >= len\n\n function parseAlt () {\n const opts = [parseConcat()]\n while (!eof() && peek() === '|') { i++; opts.push(parseConcat()) }\n return opts.length === 1 ? opts[0] : { t: 'alt', opts }\n }\n\n function parseConcat () {\n const parts = []\n while (!eof() && peek() !== '|' && peek() !== ')') parts.push(parseRepeat())\n if (parts.length === 0) return { t: 'empty' }\n return parts.length === 1 ? parts[0] : { t: 'concat', parts }\n }\n\n function parseRepeat () {\n let node = parseAtom()\n while (!eof()) {\n const ch = peek()\n if (ch === '*') { i++; node = { t: 'star', child: node } }\n else if (ch === '+') { i++; node = { t: 'plus', child: node } }\n else if (ch === '?') { i++; node = { t: 'quest', child: node } }\n else if (ch === '{') {\n const saved = i\n const q = tryQuantifier()\n if (!q) { i = saved; break }\n node = { t: 'repeat', child: node, min: q.min, max: q.max }\n } else break\n // a trailing ? makes the quantifier lazy; same language for a boolean test\n if (!eof() && peek() === '?') i++\n }\n return node\n }\n\n function tryQuantifier () {\n // assumes current char is '{'\n i++\n let min = ''\n while (!eof() && /[0-9]/.test(peek())) { min += peek(); i++ }\n if (min === '') return null\n let max\n if (peek() === '}') { i++; return { min: +min, max: +min } }\n if (peek() === ',') {\n i++\n let m = ''\n while (!eof() && /[0-9]/.test(peek())) { m += peek(); i++ }\n if (peek() !== '}') return null\n i++\n max = m === '' ? Infinity : +m\n return { min: +min, max }\n }\n return null\n }\n\n function parseAtom () {\n const ch = peek()\n if (ch === '(') {\n i++\n if (src[i] === '?') {\n if (src[i + 1] === ':') { i += 2 }\n else throw new Error('unsupported group (lookaround/named) in pattern')\n }\n const child = parseAlt()\n if (peek() !== ')') throw new Error('unbalanced ( in pattern')\n i++\n return { t: 'group', child }\n }\n if (ch === '[') return parseClass()\n if (ch === '.') { i++; return { t: 'any' } }\n if (ch === '^') { i++; return { t: 'bol' } }\n if (ch === '$') { i++; return { t: 'eol' } }\n if (ch === '\\\\') return parseEscape(false)\n if (ch === ')' || ch === '|') return { t: 'empty' }\n i++\n return { t: 'char', c: ch.charCodeAt(0) }\n }\n\n function parseClass () {\n i++ // [\n let neg = false\n if (peek() === '^') { neg = true; i++ }\n const ranges = []\n while (!eof() && peek() !== ']') {\n let lo\n if (peek() === '\\\\') {\n const esc = parseEscape(true)\n if (esc.t === 'classpart') { for (const r of esc.ranges) ranges.push(r); continue }\n lo = esc.c\n } else { lo = peek().charCodeAt(0); i++ }\n if (peek() === '-' && src[i + 1] !== ']' && i + 1 < len) {\n i++ // -\n let hi\n if (peek() === '\\\\') { const e = parseEscape(true); hi = e.c } else { hi = peek().charCodeAt(0); i++ }\n ranges.push([lo, hi])\n } else {\n ranges.push([lo, lo])\n }\n }\n if (peek() !== ']') throw new Error('unbalanced [ in pattern')\n i++\n return { t: 'class', neg, ranges }\n }\n\n function parseEscape (inClass) {\n i++ // backslash\n if (eof()) throw new Error('trailing backslash in pattern')\n const ch = peek(); i++\n switch (ch) {\n case 'd': return inClass ? { t: 'classpart', ranges: DIGIT } : { t: 'class', neg: false, ranges: DIGIT }\n case 'w': return inClass ? { t: 'classpart', ranges: WORD } : { t: 'class', neg: false, ranges: WORD }\n case 's': return inClass ? { t: 'classpart', ranges: WS } : { t: 'class', neg: false, ranges: WS }\n case 'D': if (inClass) throw new Error('\\\\D inside a class is not supported'); return { t: 'class', neg: true, ranges: DIGIT }\n case 'W': if (inClass) throw new Error('\\\\W inside a class is not supported'); return { t: 'class', neg: true, ranges: WORD }\n case 'S': if (inClass) throw new Error('\\\\S inside a class is not supported'); return { t: 'class', neg: true, ranges: WS }\n case 'n': return { t: 'char', c: 10 }\n case 'r': return { t: 'char', c: 13 }\n case 't': return { t: 'char', c: 9 }\n case 'f': return { t: 'char', c: 12 }\n case 'v': return { t: 'char', c: 11 }\n case '0': return { t: 'char', c: 0 }\n case 'x': { const h = src.slice(i, i + 2); i += 2; return { t: 'char', c: parseInt(h, 16) } }\n case 'u': { const h = src.slice(i, i + 4); i += 4; return { t: 'char', c: parseInt(h, 16) } }\n case 'b': if (inClass) return { t: 'char', c: 8 }; throw new Error('\\\\b word boundary is not supported')\n default:\n if (/[1-9]/.test(ch)) throw new Error('backreferences are not supported in pattern')\n return { t: 'char', c: ch.charCodeAt(0) }\n }\n }\n\n const ast = parseAlt()\n if (!eof()) throw new Error('unexpected \"' + peek() + '\" in pattern')\n return ast\n}\n\nfunction compileProg (ast) {\n const prog = []\n const emit = (op, extra) => { const idx = prog.length; prog.push(Object.assign({ op }, extra)); return idx }\n\n function rec (n) {\n switch (n.t) {\n case 'empty': break\n case 'char': emit('char', { c: n.c }); break\n case 'any': emit('any'); break\n case 'class': emit('class', { neg: n.neg, ranges: n.ranges }); break\n case 'bol': emit('bol'); break\n case 'eol': emit('eol'); break\n case 'group': rec(n.child); break\n case 'concat': for (const p of n.parts) rec(p); break\n case 'alt': {\n const jmps = []\n for (let k = 0; k < n.opts.length; k++) {\n if (k < n.opts.length - 1) {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.opts[k])\n jmps.push(emit('jmp', { x: 0 }))\n prog[sp].y = prog.length\n } else {\n rec(n.opts[k])\n }\n }\n for (const j of jmps) prog[j].x = prog.length\n break\n }\n case 'star': {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.child)\n emit('jmp', { x: sp })\n prog[sp].y = prog.length\n break\n }\n case 'plus': {\n const start = prog.length\n rec(n.child)\n const sp = emit('split', { x: start, y: 0 })\n prog[sp].y = prog.length\n break\n }\n case 'quest': {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.child)\n prog[sp].y = prog.length\n break\n }\n case 'repeat': {\n for (let k = 0; k < n.min; k++) rec(n.child)\n if (n.max === Infinity) {\n if (n.min === 0) rec({ t: 'star', child: n.child })\n else rec({ t: 'star', child: n.child })\n } else {\n for (let k = 0; k < n.max - n.min; k++) rec({ t: 'quest', child: n.child })\n }\n break\n }\n }\n }\n\n rec(ast)\n emit('match')\n return prog\n}\n\nfunction matchClass (instr, c) {\n let inside = false\n const r = instr.ranges\n for (let k = 0; k < r.length; k++) { if (c >= r[k][0] && c <= r[k][1]) { inside = true; break } }\n return instr.neg ? !inside : inside\n}\n\nfunction makeRunner (prog) {\n const n = prog.length\n const lastGen = new Int32Array(n).fill(-1)\n let gen = 0\n const stack = []\n\n function addThread (list, pc, pos, len) {\n stack.length = 0\n stack.push(pc)\n while (stack.length) {\n const p = stack.pop()\n if (lastGen[p] === gen) continue\n lastGen[p] = gen\n const I = prog[p]\n switch (I.op) {\n case 'jmp': stack.push(I.x); break\n case 'split': stack.push(I.y); stack.push(I.x); break\n case 'bol': if (pos === 0) stack.push(p + 1); break\n case 'eol': if (pos === len) stack.push(p + 1); break\n default: list.push(p)\n }\n }\n }\n\n return function test (s) {\n const len = s.length\n let clist = []\n let nlist = []\n gen++\n addThread(clist, 0, 0, len)\n for (let pos = 0; pos <= len; pos++) {\n const c = pos < len ? s.charCodeAt(pos) : -1\n gen++\n nlist.length = 0\n for (let k = 0; k < clist.length; k++) {\n const pc = clist[k]\n const I = prog[pc]\n if (I.op === 'match') return true\n else if (I.op === 'char') { if (c === I.c) addThread(nlist, pc + 1, pos + 1, len) }\n else if (I.op === 'any') { if (c !== -1 && c !== 10) addThread(nlist, pc + 1, pos + 1, len) }\n else if (I.op === 'class') { if (c !== -1 && matchClass(I, c)) addThread(nlist, pc + 1, pos + 1, len) }\n }\n if (pos < len) addThread(nlist, 0, pos + 1, len)\n const tmp = clist; clist = nlist; nlist = tmp\n }\n return false\n }\n}\n\nfunction compileSafe (pattern) {\n const prog = compileProg(parse(pattern))\n const runner = makeRunner(prog)\n // `__ataSafe` brands the result so the standalone serializer can tell a safe\n // matcher apart from a RegExp and emit `__ataSafeRe(source)` instead.\n return { test: runner, source: pattern, __ataSafe: true }\n}\n\n// True when the linear engine can represent `src`. Used by the codegen to decide\n// between the safe matcher and a JS RegExp fallback for patterns outside the\n// supported (RE2) subset (backreferences, lookaround, etc.).\nfunction patternIsSafe (src) {\n try { compileSafe(src); return true } catch { return false }\n}\n\nmodule.exports = { compileSafe, patternIsSafe }\n";
|
|
8
|
+
module.exports = "'use strict'\n\n// Linear-time regex engine for JSON Schema `pattern`, used in place of JS RegExp\n// so an adversarial input cannot trigger catastrophic backtracking (ReDoS).\n//\n// It is a Pike VM: the pattern compiles to a small instruction program, and the\n// VM simulates all NFA threads in lockstep over the input, deduping by program\n// counter. Runtime is O(input * program), with no backtracking.\n//\n// Supported (the RE2 subset, which is what ata's native path also accepts):\n// literals, ., character classes, \\d \\w \\s \\D \\W \\S, anchors ^ $, quantifiers\n// * + ? {n} {n,} {n,m} (greedy or lazy, same language for a boolean test),\n// groups ( ) (?: ), alternation |. Backreferences and lookaround are not\n// supported by linear engines; compileSafe throws on them so the caller can\n// decide (ata's codegen rejects such schemas rather than risk a hang).\n\nconst WS = [[9, 13], [32, 32], [160, 160]]\nconst DIGIT = [[48, 57]]\nconst WORD = [[48, 57], [65, 90], [97, 122], [95, 95]]\n\nfunction parse (src) {\n let i = 0\n const len = src.length\n const peek = () => src[i]\n const eof = () => i >= len\n\n function parseAlt () {\n const opts = [parseConcat()]\n while (!eof() && peek() === '|') { i++; opts.push(parseConcat()) }\n return opts.length === 1 ? opts[0] : { t: 'alt', opts }\n }\n\n function parseConcat () {\n const parts = []\n while (!eof() && peek() !== '|' && peek() !== ')') parts.push(parseRepeat())\n if (parts.length === 0) return { t: 'empty' }\n return parts.length === 1 ? parts[0] : { t: 'concat', parts }\n }\n\n function parseRepeat () {\n let node = parseAtom()\n while (!eof()) {\n const ch = peek()\n if (ch === '*') { i++; node = { t: 'star', child: node } }\n else if (ch === '+') { i++; node = { t: 'plus', child: node } }\n else if (ch === '?') { i++; node = { t: 'quest', child: node } }\n else if (ch === '{') {\n const saved = i\n const q = tryQuantifier()\n if (!q) { i = saved; break }\n node = { t: 'repeat', child: node, min: q.min, max: q.max }\n } else break\n // a trailing ? makes the quantifier lazy; same language for a boolean test\n if (!eof() && peek() === '?') i++\n }\n return node\n }\n\n function tryQuantifier () {\n // assumes current char is '{'\n i++\n let min = ''\n while (!eof() && /[0-9]/.test(peek())) { min += peek(); i++ }\n if (min === '') return null\n let max\n if (peek() === '}') { i++; return { min: +min, max: +min } }\n if (peek() === ',') {\n i++\n let m = ''\n while (!eof() && /[0-9]/.test(peek())) { m += peek(); i++ }\n if (peek() !== '}') return null\n i++\n max = m === '' ? Infinity : +m\n return { min: +min, max }\n }\n return null\n }\n\n function parseAtom () {\n const ch = peek()\n if (ch === '(') {\n i++\n if (src[i] === '?') {\n if (src[i + 1] === ':') { i += 2 }\n else throw new Error('unsupported group (lookaround/named) in pattern')\n }\n const child = parseAlt()\n if (peek() !== ')') throw new Error('unbalanced ( in pattern')\n i++\n return { t: 'group', child }\n }\n if (ch === '[') return parseClass()\n if (ch === '.') { i++; return { t: 'any' } }\n if (ch === '^') { i++; return { t: 'bol' } }\n if (ch === '$') { i++; return { t: 'eol' } }\n if (ch === '\\\\') return parseEscape(false)\n if (ch === ')' || ch === '|') return { t: 'empty' }\n i++\n return { t: 'char', c: ch.charCodeAt(0) }\n }\n\n function parseClass () {\n i++ // [\n let neg = false\n if (peek() === '^') { neg = true; i++ }\n const ranges = []\n while (!eof() && peek() !== ']') {\n let lo\n if (peek() === '\\\\') {\n const esc = parseEscape(true)\n if (esc.t === 'classpart') { for (const r of esc.ranges) ranges.push(r); continue }\n lo = esc.c\n } else { lo = peek().charCodeAt(0); i++ }\n if (peek() === '-' && src[i + 1] !== ']' && i + 1 < len) {\n i++ // -\n let hi\n if (peek() === '\\\\') { const e = parseEscape(true); hi = e.c } else { hi = peek().charCodeAt(0); i++ }\n ranges.push([lo, hi])\n } else {\n ranges.push([lo, lo])\n }\n }\n if (peek() !== ']') throw new Error('unbalanced [ in pattern')\n i++\n return { t: 'class', neg, ranges }\n }\n\n function parseEscape (inClass) {\n i++ // backslash\n if (eof()) throw new Error('trailing backslash in pattern')\n const ch = peek(); i++\n switch (ch) {\n case 'd': return inClass ? { t: 'classpart', ranges: DIGIT } : { t: 'class', neg: false, ranges: DIGIT }\n case 'w': return inClass ? { t: 'classpart', ranges: WORD } : { t: 'class', neg: false, ranges: WORD }\n case 's': return inClass ? { t: 'classpart', ranges: WS } : { t: 'class', neg: false, ranges: WS }\n case 'D': if (inClass) throw new Error('\\\\D inside a class is not supported'); return { t: 'class', neg: true, ranges: DIGIT }\n case 'W': if (inClass) throw new Error('\\\\W inside a class is not supported'); return { t: 'class', neg: true, ranges: WORD }\n case 'S': if (inClass) throw new Error('\\\\S inside a class is not supported'); return { t: 'class', neg: true, ranges: WS }\n case 'n': return { t: 'char', c: 10 }\n case 'r': return { t: 'char', c: 13 }\n case 't': return { t: 'char', c: 9 }\n case 'f': return { t: 'char', c: 12 }\n case 'v': return { t: 'char', c: 11 }\n case '0': return { t: 'char', c: 0 }\n case 'x': { const h = src.slice(i, i + 2); i += 2; return { t: 'char', c: parseInt(h, 16) } }\n case 'u': { const h = src.slice(i, i + 4); i += 4; return { t: 'char', c: parseInt(h, 16) } }\n case 'b': if (inClass) return { t: 'char', c: 8 }; throw new Error('\\\\b word boundary is not supported')\n default:\n if (/[1-9]/.test(ch)) throw new Error('backreferences are not supported in pattern')\n return { t: 'char', c: ch.charCodeAt(0) }\n }\n }\n\n const ast = parseAlt()\n if (!eof()) throw new Error('unexpected \"' + peek() + '\" in pattern')\n return ast\n}\n\nfunction compileProg (ast) {\n const prog = []\n const emit = (op, extra) => { const idx = prog.length; prog.push(Object.assign({ op }, extra)); return idx }\n\n function rec (n) {\n switch (n.t) {\n case 'empty': break\n case 'char': emit('char', { c: n.c }); break\n case 'any': emit('any'); break\n case 'class': emit('class', { neg: n.neg, ranges: n.ranges }); break\n case 'bol': emit('bol'); break\n case 'eol': emit('eol'); break\n case 'group': rec(n.child); break\n case 'concat': for (const p of n.parts) rec(p); break\n case 'alt': {\n const jmps = []\n for (let k = 0; k < n.opts.length; k++) {\n if (k < n.opts.length - 1) {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.opts[k])\n jmps.push(emit('jmp', { x: 0 }))\n prog[sp].y = prog.length\n } else {\n rec(n.opts[k])\n }\n }\n for (const j of jmps) prog[j].x = prog.length\n break\n }\n case 'star': {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.child)\n emit('jmp', { x: sp })\n prog[sp].y = prog.length\n break\n }\n case 'plus': {\n const start = prog.length\n rec(n.child)\n const sp = emit('split', { x: start, y: 0 })\n prog[sp].y = prog.length\n break\n }\n case 'quest': {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.child)\n prog[sp].y = prog.length\n break\n }\n case 'repeat': {\n for (let k = 0; k < n.min; k++) rec(n.child)\n if (n.max === Infinity) {\n if (n.min === 0) rec({ t: 'star', child: n.child })\n else rec({ t: 'star', child: n.child })\n } else {\n for (let k = 0; k < n.max - n.min; k++) rec({ t: 'quest', child: n.child })\n }\n break\n }\n }\n }\n\n rec(ast)\n emit('match')\n return prog\n}\n\n// Numeric opcodes for the runner. The program is compiled once into flat\n// typed arrays so the inner loop does no property lookups or string compares.\nconst OP_CHAR = 0\nconst OP_ANY = 1\nconst OP_CLASS = 2\nconst OP_SPLIT = 3\nconst OP_JMP = 4\nconst OP_BOL = 5\nconst OP_EOL = 6\nconst OP_MATCH = 7\n\nfunction classMatcher (instr) {\n // ASCII is answered from a bitmap; anything above 0x7f walks the ranges.\n const bits = new Uint8Array(128)\n const r = instr.ranges\n for (let k = 0; k < r.length; k++) {\n const hi = Math.min(r[k][1], 127)\n for (let c = r[k][0]; c <= hi; c++) bits[c] = 1\n }\n return { bits, ranges: r, neg: instr.neg }\n}\n\nfunction matchClass (cls, c) {\n let inside\n if (c < 128) {\n inside = cls.bits[c] === 1\n } else {\n inside = false\n const r = cls.ranges\n for (let k = 0; k < r.length; k++) { if (c >= r[k][0] && c <= r[k][1]) { inside = true; break } }\n }\n return cls.neg ? !inside : inside\n}\n\nfunction makeRunner (prog) {\n const n = prog.length\n const ops = new Uint8Array(n)\n const xs = new Int32Array(n)\n const ys = new Int32Array(n)\n const cs = new Int32Array(n)\n const classes = new Array(n)\n for (let i = 0; i < n; i++) {\n const I = prog[i]\n switch (I.op) {\n case 'char': ops[i] = OP_CHAR; cs[i] = I.c; break\n case 'any': ops[i] = OP_ANY; break\n case 'class': ops[i] = OP_CLASS; classes[i] = classMatcher(I); break\n case 'split': ops[i] = OP_SPLIT; xs[i] = I.x; ys[i] = I.y; break\n case 'jmp': ops[i] = OP_JMP; xs[i] = I.x; break\n case 'bol': ops[i] = OP_BOL; break\n case 'eol': ops[i] = OP_EOL; break\n case 'match': ops[i] = OP_MATCH; break\n }\n }\n\n const lastGen = new Int32Array(n).fill(-1)\n let gen = 0\n // Each unvisited instruction is popped once and pushes at most two, so the\n // stack never holds more than 2n + 1 entries.\n const stack = new Int32Array(2 * n + 2)\n // Thread lists hold at most one entry per instruction per step.\n let clist = new Int32Array(n)\n let nlist = new Int32Array(n)\n let clen = 0\n let nlen = 0\n\n // Follows epsilon edges from `pc` and records every consuming instruction\n // (or match) reached in `list`. `lastGen` dedupes per step.\n function addThread (list, len0, pc, pos, len) {\n // Most transitions land directly on a consuming instruction; skip the\n // stack walk for those.\n if (ops[pc] <= OP_CLASS || ops[pc] === OP_MATCH) {\n if (lastGen[pc] === gen) return len0\n lastGen[pc] = gen\n list[len0] = pc\n return len0 + 1\n }\n let sp = 0\n stack[sp++] = pc\n let count = len0\n while (sp > 0) {\n const p = stack[--sp]\n if (lastGen[p] === gen) continue\n lastGen[p] = gen\n switch (ops[p]) {\n case OP_JMP: stack[sp++] = xs[p]; break\n case OP_SPLIT: stack[sp++] = ys[p]; stack[sp++] = xs[p]; break\n case OP_BOL: if (pos === 0) stack[sp++] = p + 1; break\n case OP_EOL: if (pos === len) stack[sp++] = p + 1; break\n default: list[count++] = p\n }\n }\n return count\n }\n\n // A pattern is anchored when starting it anywhere but position 0 yields no\n // thread, which is the case for `^...` and its alternations. The probe sits\n // at position 1 of a length-1 string so that only `^` can fail. For anchored\n // patterns the per-position restart below is skipped, and an empty thread\n // list means the match has already failed.\n gen++\n const anchored = addThread(nlist, 0, 0, 1, 1) === 0\n\n function testNFA (s) {\n const len = s.length\n gen++\n clen = addThread(clist, 0, 0, 0, len)\n for (let pos = 0; pos <= len; pos++) {\n const c = pos < len ? s.charCodeAt(pos) : -1\n gen++\n nlen = 0\n for (let k = 0; k < clen; k++) {\n const pc = clist[k]\n switch (ops[pc]) {\n case OP_MATCH: return true\n case OP_CHAR: if (c === cs[pc]) nlen = addThread(nlist, nlen, pc + 1, pos + 1, len); break\n case OP_ANY: if (c !== -1 && c !== 10) nlen = addThread(nlist, nlen, pc + 1, pos + 1, len); break\n case OP_CLASS: if (c !== -1 && matchClass(classes[pc], c)) nlen = addThread(nlist, nlen, pc + 1, pos + 1, len); break\n }\n }\n if (pos < len) {\n if (!anchored) nlen = addThread(nlist, nlen, 0, pos + 1, len)\n else if (nlen === 0) return false\n }\n const tmp = clist; clist = nlist; nlist = tmp\n clen = nlen\n }\n return false\n }\n\n // Lazy DFA on top of the NFA. A DFA state is the set of consuming\n // instructions live at a position; transitions are computed on first use\n // and cached per ASCII character. `^` and `$` depend on position, so the\n // closure is taken with flags for \"at start\" and \"at end\", which gives two\n // start states and two transition tables per state. The state count is\n // capped; past the cap the matcher falls back to the NFA walk above, so the\n // time bound stays linear either way.\n const MAX_STATES = 256\n const states = []\n const stateIds = new Map()\n let overflow = false\n\n function closure (list, count, pc, atStart, atEnd) {\n // Same walk as addThread, with the position replaced by the two flags.\n let sp = 0\n stack[sp++] = pc\n while (sp > 0) {\n const p = stack[--sp]\n if (lastGen[p] === gen) continue\n lastGen[p] = gen\n switch (ops[p]) {\n case OP_JMP: stack[sp++] = xs[p]; break\n case OP_SPLIT: stack[sp++] = ys[p]; stack[sp++] = xs[p]; break\n case OP_BOL: if (atStart) stack[sp++] = p + 1; break\n case OP_EOL: if (atEnd) stack[sp++] = p + 1; break\n default: list[count++] = p\n }\n }\n return count\n }\n\n function internState (list, count) {\n const pcs = Array.from(list.subarray(0, count)).sort((a, b) => a - b)\n const key = pcs.join(',')\n let id = stateIds.get(key)\n if (id !== undefined) return id\n if (states.length >= MAX_STATES) { overflow = true; return -1 }\n id = states.length\n let isMatch = false\n for (let k = 0; k < pcs.length; k++) if (ops[pcs[k]] === OP_MATCH) { isMatch = true; break }\n states.push({ pcs: Int32Array.from(pcs), isMatch, next: new Int32Array(128).fill(-2), nextEnd: new Int32Array(128).fill(-2) })\n stateIds.set(key, id)\n return id\n }\n\n function step (state, c, atEnd) {\n gen++\n let count = 0\n const pcs = state.pcs\n for (let k = 0; k < pcs.length; k++) {\n const pc = pcs[k]\n switch (ops[pc]) {\n case OP_CHAR: if (c === cs[pc]) count = closure(nlist, count, pc + 1, false, atEnd); break\n case OP_ANY: if (c !== 10) count = closure(nlist, count, pc + 1, false, atEnd); break\n case OP_CLASS: if (matchClass(classes[pc], c)) count = closure(nlist, count, pc + 1, false, atEnd); break\n }\n }\n if (!anchored) count = closure(nlist, count, 0, false, atEnd)\n return internState(nlist, count)\n }\n\n let startEmpty = -2\n let startNonEmpty = -2\n\n function startState (atEnd) {\n gen++\n const count = closure(nlist, 0, 0, true, atEnd)\n return internState(nlist, count)\n }\n\n function testDFA (s) {\n const len = s.length\n let id\n if (len === 0) {\n if (startEmpty === -2) startEmpty = startState(true)\n id = startEmpty\n } else {\n if (startNonEmpty === -2) startNonEmpty = startState(false)\n id = startNonEmpty\n }\n if (id < 0) return testNFA(s)\n let state = states[id]\n for (let pos = 0; pos < len; pos++) {\n if (state.isMatch) return true\n const c = s.charCodeAt(pos)\n const atEnd = pos + 1 === len\n let nid\n if (c < 128) {\n const table = atEnd ? state.nextEnd : state.next\n nid = table[c]\n if (nid === -2) { nid = step(state, c, atEnd); table[c] = nid }\n } else {\n nid = step(state, c, atEnd)\n }\n if (nid < 0) return testNFA(s)\n state = states[nid]\n if (anchored && state.pcs.length === 0) return false\n }\n return state.isMatch\n }\n\n return function test (s) {\n return overflow ? testNFA(s) : testDFA(s)\n }\n}\n\nfunction compileSafe (pattern) {\n const prog = compileProg(parse(pattern))\n const runner = makeRunner(prog)\n // `__ataSafe` brands the result so the standalone serializer can tell a safe\n // matcher apart from a RegExp and emit `__ataSafeRe(source)` instead.\n return { test: runner, source: pattern, __ataSafe: true }\n}\n\n// True when the linear engine can represent `src`. Used by the codegen to decide\n// between the safe matcher and a JS RegExp fallback for patterns outside the\n// supported (RE2) subset (backreferences, lookaround, etc.).\nfunction patternIsSafe (src) {\n try { compileSafe(src); return true } catch { return false }\n}\n\nmodule.exports = { compileSafe, patternIsSafe }\n";
|
package/lib/safe-regex.js
CHANGED
|
@@ -226,60 +226,240 @@ function compileProg (ast) {
|
|
|
226
226
|
return prog
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
|
|
230
|
-
|
|
229
|
+
// Numeric opcodes for the runner. The program is compiled once into flat
|
|
230
|
+
// typed arrays so the inner loop does no property lookups or string compares.
|
|
231
|
+
const OP_CHAR = 0
|
|
232
|
+
const OP_ANY = 1
|
|
233
|
+
const OP_CLASS = 2
|
|
234
|
+
const OP_SPLIT = 3
|
|
235
|
+
const OP_JMP = 4
|
|
236
|
+
const OP_BOL = 5
|
|
237
|
+
const OP_EOL = 6
|
|
238
|
+
const OP_MATCH = 7
|
|
239
|
+
|
|
240
|
+
function classMatcher (instr) {
|
|
241
|
+
// ASCII is answered from a bitmap; anything above 0x7f walks the ranges.
|
|
242
|
+
const bits = new Uint8Array(128)
|
|
231
243
|
const r = instr.ranges
|
|
232
|
-
for (let k = 0; k < r.length; k++) {
|
|
233
|
-
|
|
244
|
+
for (let k = 0; k < r.length; k++) {
|
|
245
|
+
const hi = Math.min(r[k][1], 127)
|
|
246
|
+
for (let c = r[k][0]; c <= hi; c++) bits[c] = 1
|
|
247
|
+
}
|
|
248
|
+
return { bits, ranges: r, neg: instr.neg }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function matchClass (cls, c) {
|
|
252
|
+
let inside
|
|
253
|
+
if (c < 128) {
|
|
254
|
+
inside = cls.bits[c] === 1
|
|
255
|
+
} else {
|
|
256
|
+
inside = false
|
|
257
|
+
const r = cls.ranges
|
|
258
|
+
for (let k = 0; k < r.length; k++) { if (c >= r[k][0] && c <= r[k][1]) { inside = true; break } }
|
|
259
|
+
}
|
|
260
|
+
return cls.neg ? !inside : inside
|
|
234
261
|
}
|
|
235
262
|
|
|
236
263
|
function makeRunner (prog) {
|
|
237
264
|
const n = prog.length
|
|
265
|
+
const ops = new Uint8Array(n)
|
|
266
|
+
const xs = new Int32Array(n)
|
|
267
|
+
const ys = new Int32Array(n)
|
|
268
|
+
const cs = new Int32Array(n)
|
|
269
|
+
const classes = new Array(n)
|
|
270
|
+
for (let i = 0; i < n; i++) {
|
|
271
|
+
const I = prog[i]
|
|
272
|
+
switch (I.op) {
|
|
273
|
+
case 'char': ops[i] = OP_CHAR; cs[i] = I.c; break
|
|
274
|
+
case 'any': ops[i] = OP_ANY; break
|
|
275
|
+
case 'class': ops[i] = OP_CLASS; classes[i] = classMatcher(I); break
|
|
276
|
+
case 'split': ops[i] = OP_SPLIT; xs[i] = I.x; ys[i] = I.y; break
|
|
277
|
+
case 'jmp': ops[i] = OP_JMP; xs[i] = I.x; break
|
|
278
|
+
case 'bol': ops[i] = OP_BOL; break
|
|
279
|
+
case 'eol': ops[i] = OP_EOL; break
|
|
280
|
+
case 'match': ops[i] = OP_MATCH; break
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
238
284
|
const lastGen = new Int32Array(n).fill(-1)
|
|
239
285
|
let gen = 0
|
|
240
|
-
|
|
286
|
+
// Each unvisited instruction is popped once and pushes at most two, so the
|
|
287
|
+
// stack never holds more than 2n + 1 entries.
|
|
288
|
+
const stack = new Int32Array(2 * n + 2)
|
|
289
|
+
// Thread lists hold at most one entry per instruction per step.
|
|
290
|
+
let clist = new Int32Array(n)
|
|
291
|
+
let nlist = new Int32Array(n)
|
|
292
|
+
let clen = 0
|
|
293
|
+
let nlen = 0
|
|
241
294
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
295
|
+
// Follows epsilon edges from `pc` and records every consuming instruction
|
|
296
|
+
// (or match) reached in `list`. `lastGen` dedupes per step.
|
|
297
|
+
function addThread (list, len0, pc, pos, len) {
|
|
298
|
+
// Most transitions land directly on a consuming instruction; skip the
|
|
299
|
+
// stack walk for those.
|
|
300
|
+
if (ops[pc] <= OP_CLASS || ops[pc] === OP_MATCH) {
|
|
301
|
+
if (lastGen[pc] === gen) return len0
|
|
302
|
+
lastGen[pc] = gen
|
|
303
|
+
list[len0] = pc
|
|
304
|
+
return len0 + 1
|
|
305
|
+
}
|
|
306
|
+
let sp = 0
|
|
307
|
+
stack[sp++] = pc
|
|
308
|
+
let count = len0
|
|
309
|
+
while (sp > 0) {
|
|
310
|
+
const p = stack[--sp]
|
|
247
311
|
if (lastGen[p] === gen) continue
|
|
248
312
|
lastGen[p] = gen
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
case
|
|
252
|
-
case
|
|
253
|
-
case
|
|
254
|
-
|
|
255
|
-
default: list.push(p)
|
|
313
|
+
switch (ops[p]) {
|
|
314
|
+
case OP_JMP: stack[sp++] = xs[p]; break
|
|
315
|
+
case OP_SPLIT: stack[sp++] = ys[p]; stack[sp++] = xs[p]; break
|
|
316
|
+
case OP_BOL: if (pos === 0) stack[sp++] = p + 1; break
|
|
317
|
+
case OP_EOL: if (pos === len) stack[sp++] = p + 1; break
|
|
318
|
+
default: list[count++] = p
|
|
256
319
|
}
|
|
257
320
|
}
|
|
321
|
+
return count
|
|
258
322
|
}
|
|
259
323
|
|
|
260
|
-
|
|
324
|
+
// A pattern is anchored when starting it anywhere but position 0 yields no
|
|
325
|
+
// thread, which is the case for `^...` and its alternations. The probe sits
|
|
326
|
+
// at position 1 of a length-1 string so that only `^` can fail. For anchored
|
|
327
|
+
// patterns the per-position restart below is skipped, and an empty thread
|
|
328
|
+
// list means the match has already failed.
|
|
329
|
+
gen++
|
|
330
|
+
const anchored = addThread(nlist, 0, 0, 1, 1) === 0
|
|
331
|
+
|
|
332
|
+
function testNFA (s) {
|
|
261
333
|
const len = s.length
|
|
262
|
-
let clist = []
|
|
263
|
-
let nlist = []
|
|
264
334
|
gen++
|
|
265
|
-
addThread(clist, 0, 0, len)
|
|
335
|
+
clen = addThread(clist, 0, 0, 0, len)
|
|
266
336
|
for (let pos = 0; pos <= len; pos++) {
|
|
267
337
|
const c = pos < len ? s.charCodeAt(pos) : -1
|
|
268
338
|
gen++
|
|
269
|
-
|
|
270
|
-
for (let k = 0; k <
|
|
339
|
+
nlen = 0
|
|
340
|
+
for (let k = 0; k < clen; k++) {
|
|
271
341
|
const pc = clist[k]
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
342
|
+
switch (ops[pc]) {
|
|
343
|
+
case OP_MATCH: return true
|
|
344
|
+
case OP_CHAR: if (c === cs[pc]) nlen = addThread(nlist, nlen, pc + 1, pos + 1, len); break
|
|
345
|
+
case OP_ANY: if (c !== -1 && c !== 10) nlen = addThread(nlist, nlen, pc + 1, pos + 1, len); break
|
|
346
|
+
case OP_CLASS: if (c !== -1 && matchClass(classes[pc], c)) nlen = addThread(nlist, nlen, pc + 1, pos + 1, len); break
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (pos < len) {
|
|
350
|
+
if (!anchored) nlen = addThread(nlist, nlen, 0, pos + 1, len)
|
|
351
|
+
else if (nlen === 0) return false
|
|
277
352
|
}
|
|
278
|
-
if (pos < len) addThread(nlist, 0, pos + 1, len)
|
|
279
353
|
const tmp = clist; clist = nlist; nlist = tmp
|
|
354
|
+
clen = nlen
|
|
280
355
|
}
|
|
281
356
|
return false
|
|
282
357
|
}
|
|
358
|
+
|
|
359
|
+
// Lazy DFA on top of the NFA. A DFA state is the set of consuming
|
|
360
|
+
// instructions live at a position; transitions are computed on first use
|
|
361
|
+
// and cached per ASCII character. `^` and `$` depend on position, so the
|
|
362
|
+
// closure is taken with flags for "at start" and "at end", which gives two
|
|
363
|
+
// start states and two transition tables per state. The state count is
|
|
364
|
+
// capped; past the cap the matcher falls back to the NFA walk above, so the
|
|
365
|
+
// time bound stays linear either way.
|
|
366
|
+
const MAX_STATES = 256
|
|
367
|
+
const states = []
|
|
368
|
+
const stateIds = new Map()
|
|
369
|
+
let overflow = false
|
|
370
|
+
|
|
371
|
+
function closure (list, count, pc, atStart, atEnd) {
|
|
372
|
+
// Same walk as addThread, with the position replaced by the two flags.
|
|
373
|
+
let sp = 0
|
|
374
|
+
stack[sp++] = pc
|
|
375
|
+
while (sp > 0) {
|
|
376
|
+
const p = stack[--sp]
|
|
377
|
+
if (lastGen[p] === gen) continue
|
|
378
|
+
lastGen[p] = gen
|
|
379
|
+
switch (ops[p]) {
|
|
380
|
+
case OP_JMP: stack[sp++] = xs[p]; break
|
|
381
|
+
case OP_SPLIT: stack[sp++] = ys[p]; stack[sp++] = xs[p]; break
|
|
382
|
+
case OP_BOL: if (atStart) stack[sp++] = p + 1; break
|
|
383
|
+
case OP_EOL: if (atEnd) stack[sp++] = p + 1; break
|
|
384
|
+
default: list[count++] = p
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return count
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function internState (list, count) {
|
|
391
|
+
const pcs = Array.from(list.subarray(0, count)).sort((a, b) => a - b)
|
|
392
|
+
const key = pcs.join(',')
|
|
393
|
+
let id = stateIds.get(key)
|
|
394
|
+
if (id !== undefined) return id
|
|
395
|
+
if (states.length >= MAX_STATES) { overflow = true; return -1 }
|
|
396
|
+
id = states.length
|
|
397
|
+
let isMatch = false
|
|
398
|
+
for (let k = 0; k < pcs.length; k++) if (ops[pcs[k]] === OP_MATCH) { isMatch = true; break }
|
|
399
|
+
states.push({ pcs: Int32Array.from(pcs), isMatch, next: new Int32Array(128).fill(-2), nextEnd: new Int32Array(128).fill(-2) })
|
|
400
|
+
stateIds.set(key, id)
|
|
401
|
+
return id
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function step (state, c, atEnd) {
|
|
405
|
+
gen++
|
|
406
|
+
let count = 0
|
|
407
|
+
const pcs = state.pcs
|
|
408
|
+
for (let k = 0; k < pcs.length; k++) {
|
|
409
|
+
const pc = pcs[k]
|
|
410
|
+
switch (ops[pc]) {
|
|
411
|
+
case OP_CHAR: if (c === cs[pc]) count = closure(nlist, count, pc + 1, false, atEnd); break
|
|
412
|
+
case OP_ANY: if (c !== 10) count = closure(nlist, count, pc + 1, false, atEnd); break
|
|
413
|
+
case OP_CLASS: if (matchClass(classes[pc], c)) count = closure(nlist, count, pc + 1, false, atEnd); break
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (!anchored) count = closure(nlist, count, 0, false, atEnd)
|
|
417
|
+
return internState(nlist, count)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
let startEmpty = -2
|
|
421
|
+
let startNonEmpty = -2
|
|
422
|
+
|
|
423
|
+
function startState (atEnd) {
|
|
424
|
+
gen++
|
|
425
|
+
const count = closure(nlist, 0, 0, true, atEnd)
|
|
426
|
+
return internState(nlist, count)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function testDFA (s) {
|
|
430
|
+
const len = s.length
|
|
431
|
+
let id
|
|
432
|
+
if (len === 0) {
|
|
433
|
+
if (startEmpty === -2) startEmpty = startState(true)
|
|
434
|
+
id = startEmpty
|
|
435
|
+
} else {
|
|
436
|
+
if (startNonEmpty === -2) startNonEmpty = startState(false)
|
|
437
|
+
id = startNonEmpty
|
|
438
|
+
}
|
|
439
|
+
if (id < 0) return testNFA(s)
|
|
440
|
+
let state = states[id]
|
|
441
|
+
for (let pos = 0; pos < len; pos++) {
|
|
442
|
+
if (state.isMatch) return true
|
|
443
|
+
const c = s.charCodeAt(pos)
|
|
444
|
+
const atEnd = pos + 1 === len
|
|
445
|
+
let nid
|
|
446
|
+
if (c < 128) {
|
|
447
|
+
const table = atEnd ? state.nextEnd : state.next
|
|
448
|
+
nid = table[c]
|
|
449
|
+
if (nid === -2) { nid = step(state, c, atEnd); table[c] = nid }
|
|
450
|
+
} else {
|
|
451
|
+
nid = step(state, c, atEnd)
|
|
452
|
+
}
|
|
453
|
+
if (nid < 0) return testNFA(s)
|
|
454
|
+
state = states[nid]
|
|
455
|
+
if (anchored && state.pcs.length === 0) return false
|
|
456
|
+
}
|
|
457
|
+
return state.isMatch
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return function test (s) {
|
|
461
|
+
return overflow ? testNFA(s) : testDFA(s)
|
|
462
|
+
}
|
|
283
463
|
}
|
|
284
464
|
|
|
285
465
|
function compileSafe (pattern) {
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
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",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"release:check": "node scripts/regen-safe-regex-source.js && node tests/test_pack_purity.js && node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_source_sync.js && node tests/test_version_sync.js",
|
|
45
45
|
"build": "cmake-js build --target ata",
|
|
46
46
|
"rebuild": "cmake-js rebuild --target ata",
|
|
47
|
-
"test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.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_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_format_engine_parity.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_order.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_error_messages.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",
|
|
47
|
+
"test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.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_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_pattern_properties_errors.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.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_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.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",
|
|
48
48
|
"bench:size": "node benchmark/bench_aot_size.mjs",
|
|
49
49
|
"test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
|
|
50
50
|
"test:compat": "node tests/test_compat.js",
|
|
@@ -112,12 +112,12 @@
|
|
|
112
112
|
"LICENSE"
|
|
113
113
|
],
|
|
114
114
|
"optionalDependencies": {
|
|
115
|
-
"@ata-validator/native-darwin-arm64": "1.
|
|
116
|
-
"@ata-validator/native-linux-x64-gnu": "1.
|
|
117
|
-
"@ata-validator/native-linux-arm64-gnu": "1.
|
|
118
|
-
"@ata-validator/native-linux-x64-musl": "1.
|
|
119
|
-
"@ata-validator/native-linux-arm64-musl": "1.
|
|
120
|
-
"@ata-validator/native-win32-x64": "1.
|
|
115
|
+
"@ata-validator/native-darwin-arm64": "1.7.0",
|
|
116
|
+
"@ata-validator/native-linux-x64-gnu": "1.7.0",
|
|
117
|
+
"@ata-validator/native-linux-arm64-gnu": "1.7.0",
|
|
118
|
+
"@ata-validator/native-linux-x64-musl": "1.7.0",
|
|
119
|
+
"@ata-validator/native-linux-arm64-musl": "1.7.0",
|
|
120
|
+
"@ata-validator/native-win32-x64": "1.7.0"
|
|
121
121
|
},
|
|
122
122
|
"peerDependencies": {
|
|
123
123
|
"yaml": "^2.0.0"
|
package/t.d.ts
CHANGED
|
@@ -34,7 +34,18 @@ export type TOptional<S> = S & { readonly [OPTIONAL]: true };
|
|
|
34
34
|
|
|
35
35
|
export type TSchema = JSONSchema;
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Shared by every builder option bag. Annotations and vendor keywords (custom
|
|
39
|
+
* keywords outside the JSON Schema vocabulary) are carried through to the
|
|
40
|
+
* emitted schema, so the option types accept them.
|
|
41
|
+
*/
|
|
42
|
+
export interface CommonOptions {
|
|
43
|
+
description?: string;
|
|
44
|
+
title?: string;
|
|
45
|
+
[keyword: string]: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface StringOptions extends CommonOptions {
|
|
38
49
|
minLength?: number;
|
|
39
50
|
maxLength?: number;
|
|
40
51
|
pattern?: string;
|
|
@@ -45,7 +56,7 @@ export interface StringOptions {
|
|
|
45
56
|
examples?: readonly string[];
|
|
46
57
|
}
|
|
47
58
|
|
|
48
|
-
export interface NumericOptions {
|
|
59
|
+
export interface NumericOptions extends CommonOptions {
|
|
49
60
|
minimum?: number;
|
|
50
61
|
maximum?: number;
|
|
51
62
|
exclusiveMinimum?: number;
|
|
@@ -57,7 +68,7 @@ export interface NumericOptions {
|
|
|
57
68
|
examples?: readonly number[];
|
|
58
69
|
}
|
|
59
70
|
|
|
60
|
-
export interface ArrayOptions {
|
|
71
|
+
export interface ArrayOptions extends CommonOptions {
|
|
61
72
|
minItems?: number;
|
|
62
73
|
maxItems?: number;
|
|
63
74
|
uniqueItems?: boolean;
|
|
@@ -65,13 +76,13 @@ export interface ArrayOptions {
|
|
|
65
76
|
title?: string;
|
|
66
77
|
}
|
|
67
78
|
|
|
68
|
-
export interface ObjectOptions {
|
|
79
|
+
export interface ObjectOptions extends CommonOptions {
|
|
69
80
|
additionalProperties?: boolean | JSONSchema;
|
|
70
81
|
description?: string;
|
|
71
82
|
title?: string;
|
|
72
83
|
}
|
|
73
84
|
|
|
74
|
-
export interface RecordOptions {
|
|
85
|
+
export interface RecordOptions extends CommonOptions {
|
|
75
86
|
minProperties?: number;
|
|
76
87
|
maxProperties?: number;
|
|
77
88
|
description?: string;
|