`cmdVerifyKeyLinks` compiled `must_haves.key_links[].pattern` from plan frontmatter with `new RegExp()` and tested it against whole file contents, so a nested-quantifier pattern such as `(a+)+$` hung `verify-phase` indefinitely (CWE-1333). JavaScript has no regex-execution timeout.
Untrusted patterns now run on RE2 (re2js), whose match time is linear in input length — the class is closed by the engine, not by a heuristic screen. The screen lost in the ADR-0174 consolidation was deliberately NOT restored: it never worked, since `(a|a)*$`, `((a+))+$`, `(a+){2,}$` and `(a{1,3})+$` all evade it. A refused pattern's matcher returns false for every input, so it cannot report a match no matter what the caller does.
The engine is vendored at gsd-core/bin/lib/vendor/re2js.cjs because gsd-core/bin/** is copied into installed trees with no node_modules; runtime dependencies are unchanged. New ESLint rule local/no-external-require-in-bin enforces that invariant, which had been documented in a comment since the #3024/#2071 bug class and enforced nowhere.
Backreferences and look-around are unsupported by RE2 by construction — disclosed in a Changed changeset.
Closes #3477
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
125 lines
4.9 KiB
JavaScript
125 lines
4.9 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
/**
|
|
* lint-vendored-deps.cjs — freshness gate for gsd-core/bin/lib/vendor/.
|
|
*
|
|
* #3477 follow-up: gsd-core/bin/** is copied by the installer into trees
|
|
* that have NO node_modules, so it must carry zero external requires
|
|
* (local/no-external-require-in-bin, eslint-rules/no-external-require-in-bin.cjs).
|
|
* `re2js` (src/pattern.cts's RE2 engine) is vendored verbatim under
|
|
* gsd-core/bin/lib/vendor/ instead — see gsd-core/bin/lib/vendor/README.md.
|
|
*
|
|
* A vendored artifact that silently drifts from its upstream package is
|
|
* just as dangerous as never vendoring it in the first place (a stale
|
|
* copy ships a different engine than the one actually reviewed/audited).
|
|
* This guard fails CI when:
|
|
* 1. gsd-core/bin/lib/vendor/re2js.cjs no longer matches
|
|
* node_modules/re2js/build/index.cjs byte-for-byte.
|
|
* 2. gsd-core/bin/lib/vendor/re2js.d.cts no longer matches
|
|
* node_modules/re2js/build/index.d.cts byte-for-byte.
|
|
* 3. src/vendor/re2js.d.cts (the source-side twin tsc needs to resolve
|
|
* types for src/pattern.cts's relative './vendor/re2js.cjs' import —
|
|
* module resolution for a .cts source is relative to src/, not the
|
|
* output dir) no longer matches gsd-core/bin/lib/vendor/re2js.d.cts.
|
|
* 4. The `re2js` version pinned in package.json `devDependencies` no
|
|
* longer matches the version actually installed at
|
|
* node_modules/re2js/package.json (read there, per the dispatch
|
|
* brief, rather than duplicating a second pin).
|
|
*
|
|
* Usage: node scripts/lint-vendored-deps.cjs
|
|
* Exit 0 when every vendored copy is fresh; 1 otherwise.
|
|
*/
|
|
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
|
|
const ROOT = path.join(__dirname, '..');
|
|
|
|
const REFRESH_COMMAND =
|
|
'cp node_modules/re2js/build/index.cjs gsd-core/bin/lib/vendor/re2js.cjs && '
|
|
+ 'cp node_modules/re2js/build/index.d.cts gsd-core/bin/lib/vendor/re2js.d.cts && '
|
|
+ 'cp node_modules/re2js/build/index.d.cts src/vendor/re2js.d.cts';
|
|
|
|
/**
|
|
* Compare two files byte-for-byte. Returns null when equal, or a short
|
|
* mismatch description (missing file / byte-length delta) otherwise.
|
|
* @param {string} relA
|
|
* @param {string} relB
|
|
* @returns {string | null}
|
|
*/
|
|
function compareFiles(relA, relB) {
|
|
const absA = path.join(ROOT, relA);
|
|
const absB = path.join(ROOT, relB);
|
|
if (!fs.existsSync(absA)) return `${relA} does not exist`;
|
|
if (!fs.existsSync(absB)) return `${relB} does not exist`;
|
|
const a = fs.readFileSync(absA);
|
|
const b = fs.readFileSync(absB);
|
|
if (a.equals(b)) return null;
|
|
return `${relA} (${a.length} bytes) != ${relB} (${b.length} bytes)`;
|
|
}
|
|
|
|
/**
|
|
* Strip a leading semver range operator (^, ~, >=, >, <=, <, =) from a
|
|
* package.json dependency spec, leaving a bare version.
|
|
* @param {string} spec
|
|
* @returns {string}
|
|
*/
|
|
function stripRangeOperator(spec) {
|
|
return String(spec || '').trim().replace(/^[\^~]|^>=|^<=|^>|^<|^=/, '').trim();
|
|
}
|
|
|
|
function main() {
|
|
const findings = [];
|
|
|
|
const cjsDrift = compareFiles('gsd-core/bin/lib/vendor/re2js.cjs', 'node_modules/re2js/build/index.cjs');
|
|
if (cjsDrift) findings.push(cjsDrift);
|
|
|
|
const dctsDrift = compareFiles('gsd-core/bin/lib/vendor/re2js.d.cts', 'node_modules/re2js/build/index.d.cts');
|
|
if (dctsDrift) findings.push(dctsDrift);
|
|
|
|
const srcTwinDrift = compareFiles('src/vendor/re2js.d.cts', 'gsd-core/bin/lib/vendor/re2js.d.cts');
|
|
if (srcTwinDrift) findings.push(srcTwinDrift);
|
|
|
|
const pkgPath = path.join(ROOT, 'package.json');
|
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
const pinnedSpec = pkg.devDependencies && pkg.devDependencies.re2js;
|
|
if (!pinnedSpec) {
|
|
findings.push('package.json devDependencies.re2js is missing');
|
|
} else {
|
|
const installedPkgPath = path.join(ROOT, 'node_modules', 're2js', 'package.json');
|
|
if (!fs.existsSync(installedPkgPath)) {
|
|
findings.push('node_modules/re2js/package.json does not exist (run npm install)');
|
|
} else {
|
|
const installed = JSON.parse(fs.readFileSync(installedPkgPath, 'utf8'));
|
|
const pinned = stripRangeOperator(pinnedSpec);
|
|
if (pinned !== installed.version) {
|
|
findings.push(
|
|
`package.json devDependencies.re2js ("${pinnedSpec}" -> "${pinned}") != `
|
|
+ `node_modules/re2js/package.json version ("${installed.version}")`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (findings.length > 0) {
|
|
const detail = findings.map((f) => ` ${f}`).join('\n');
|
|
throw new ExitError(
|
|
1,
|
|
'lint-vendored-deps: gsd-core/bin/lib/vendor/re2js.* has drifted from its\n'
|
|
+ 'upstream package (or its version pin). Refresh with:\n'
|
|
+ ` ${REFRESH_COMMAND}\n`
|
|
+ 'Findings:\n'
|
|
+ detail,
|
|
);
|
|
}
|
|
|
|
process.stdout.write('ok lint-vendored-deps: gsd-core/bin/lib/vendor/re2js.* matches node_modules/re2js and its pinned version\n');
|
|
return 0;
|
|
}
|
|
|
|
if (require.main === module) runMain(main);
|
|
|
|
module.exports = { compareFiles, stripRangeOperator };
|