`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>
This commit is contained in:
@@ -1,4 +1,12 @@
|
||||
[
|
||||
{
|
||||
"path": "gsd-core/bin/lib/vendor/re2js.d.cts",
|
||||
"reason": "Vendored third-party type declaration (#3477): a .d.cts carries no executable code, so no ESLint rule is meaningful; the vendored .cjs it describes is globally ignored as verbatim upstream output. Freshness is enforced by scripts/lint-vendored-deps.cjs, not by lint rules."
|
||||
},
|
||||
{
|
||||
"path": "src/vendor/re2js.d.cts",
|
||||
"reason": "Vendored third-party type declaration (#3477): a .d.cts carries no executable code, so no ESLint rule is meaningful; the vendored .cjs it describes is globally ignored as verbatim upstream output. Freshness is enforced by scripts/lint-vendored-deps.cjs, not by lint rules."
|
||||
},
|
||||
{
|
||||
"path": "tests/fixtures/brand-typing/bad-calibrated-as-sample-basis.cts",
|
||||
"reason": "Deliberate MUST-NOT-COMPILE type-error fixture (#3059): type-aware linting would fail by design; it exists to prove the compiler rejects it."
|
||||
|
||||
124
scripts/lint-vendored-deps.cjs
Normal file
124
scripts/lint-vendored-deps.cjs
Normal file
@@ -0,0 +1,124 @@
|
||||
#!/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 };
|
||||
Reference in New Issue
Block a user