Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
104 lines
8.7 KiB
JavaScript
104 lines
8.7 KiB
JavaScript
// Row 148 round 3 (sage-0621): a stack verdict for every regex in markdown-preview.js.
|
|
// Each regex is taken from the module's own source text (the needle must occur in it), then run on
|
|
// its worst shapes at 4 MiB, one process per (regex, shape), so one run's heap cannot help another.
|
|
// A verdict is "returns" or "RangeError" (or "other: <message>" / "timeout").
|
|
// Usage: node stack-audit.mjs --module F --out stack-audit.md [--kib 4096] [--timeout 60]
|
|
// node stack-audit.mjs one <needle-index> <shape-index> <kib> --module F (internal)
|
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const args = process.argv.slice(2);
|
|
const opt = (name, dflt) => { const i = args.indexOf('--' + name); return i < 0 ? dflt : args[i + 1]; };
|
|
const MODULE = resolve(opt('module'));
|
|
const src = readFileSync(MODULE, 'utf8');
|
|
const SEP = String.fromCharCode(0x2028);
|
|
|
|
// [literal as written in the module, how it is used, shapes]. A shape is [name, n => string].
|
|
const R = (n, c) => c.repeat(n);
|
|
const REGEXES = [
|
|
['/[!-/:-@[-`{-~]/', 'test', [['letters', n => R(n, 'a')], ['punct', n => R(n, '!')]]],
|
|
['/^( {0,3})(`{3,}|~{3,})(.*)$/s', 'test', [['backticks', n => R(n, '`')], ['tildes', n => R(n, '~')], ['backticks + x', n => R(n, '`') + 'x'], ['backticks + separator', n => R(n, '`') + SEP], ['fence + blanks', n => '```' + R(n, ' ')]]],
|
|
['/^ {0,3}(#{1,6})(?:[ \\t]+(.*))?$/s', 'test', [['hashes', n => R(n, '#')], ['hash + blanks + x', n => '#' + R(n, ' ') + 'x'], ['hash + blanks', n => '#' + R(n, ' ')], ['hash + blanks + separator', n => '# ' + R(n, ' ') + SEP]]],
|
|
['/^ {0,3}(=+|-+)[ \\t]*$/', 'test', [['equals', n => R(n, '=')], ['dashes', n => R(n, '-')], ['equals + blanks + x', n => '=' + R(n, ' ') + 'x'], ['dash + tabs + x', n => '-' + R(n, '\t') + 'x']]],
|
|
['/^ {0,3}> ?/', 'test', [['quotes', n => R(n, '>')], ['quote + blanks', n => '>' + R(n, ' ')], ['blanks + quote', n => R(n, ' ') + '>']]],
|
|
['/^( {0,3})([-+*]|\\d{1,9}[.)])([ \\t]+|$)(.*)$/s', 'exec', [['dash + blanks', n => '-' + R(n, ' ')], ['dash + blanks + x', n => '-' + R(n, ' ') + 'x'], ['digits + dot', n => R(n, '1') + '.'], ['dash + tabs + separator', n => '-' + R(n, '\t') + SEP], ['dash + blanks + separator', n => '- ' + R(n, ' ') + SEP]]],
|
|
['/[\\u0000-\\u0020\\u007f-\\u009f\\\\\\u200b-\\u200f\\u2028-\\u202e\\u2066-\\u2069\\ufeff]/', 'test', [['letters', n => R(n, 'a')], ['letters + separator', n => R(n, 'a') + SEP]]],
|
|
['/^[A-Za-z][A-Za-z0-9+.-]*:/', 'test', [['letters', n => R(n, 'a')], ['letters + colon', n => R(n, 'a') + ':']]],
|
|
['/^https?:\\/\\/[^/?#]/i', 'test', [['h', n => R(n, 'h')], ['http + letters', n => 'http://' + R(n, 'a')]]],
|
|
['/[ \\t\\n]/', 'test', [['letters', n => R(n, 'a')]]],
|
|
['/[\\n<]/', 'test', [['letters', n => R(n, 'a')]]],
|
|
['/\\s/', 'test', [['letters', n => R(n, 'a')]]],
|
|
['/\\\\([!-/:-@[-`{-~])/g', 'replace', [['escaped punct', n => R(n / 2, '\\!')], ['backslashes', n => R(n, '\\')], ['escaped letters', n => R(n / 2, '\\a')]]],
|
|
['/[A-Za-z0-9]/', 'test', [['punct', n => R(n, '!')], ['letters', n => R(n, 'a')]]],
|
|
['/\\n/g', 'replace', [['newlines', n => R(n, '\n')], ['letters', n => R(n, 'a')]]],
|
|
['/^(`+|~+)[ \\t]*$/', 'exec', [['backticks', n => R(n, '`')], ['backticks + blank', n => R(n, '`') + ' '], ['backticks + x', n => R(n, '`') + 'x'], ['backtick + blanks + x', n => '`' + R(n, ' ') + 'x'], ['tildes + x', n => R(n, '~') + 'x']]],
|
|
['/\\r\\n?/g', 'replace', [['CR', n => R(n, '\r')], ['CRLF', n => R(n / 2, '\r\n')], ['letters', n => R(n, 'a')]]],
|
|
['/^[ \\t]+/', 'replace', [['blanks', n => R(n, ' ')], ['blanks + x', n => R(n, ' ') + 'x'], ['tabs', n => R(n, '\t')], ['letters', n => R(n, 'a')]]],
|
|
['/\\t/g', 'replace', [['tabs', n => R(n, '\t')], ['letters', n => R(n, 'a')]]],
|
|
];
|
|
// The regex round 3 removed, kept here as the control: the audit must report it.
|
|
const REMOVED = ['/^ {0,3}([-*_])(?:[ \\t]*\\1){2,}[ \\t]*$/', 'test', [['dashes', n => R(n, '-')], ['dashes + blank', n => R(n, '-') + ' '], ['spaced dashes', n => R(n / 2, '- ')], ['spaced stars', n => R(n / 2, '* ')], ['underscores', n => R(n, '_')]]];
|
|
|
|
if (args[0] === 'one') {
|
|
const [, ni, si, kib] = args;
|
|
const entry = ni === 'removed' ? REMOVED : REGEXES[Number(ni)];
|
|
const re = new Function('return ' + entry[0])();
|
|
const text = entry[2][Number(si)][1](Number(kib) * 1024);
|
|
const t0 = performance.now();
|
|
try {
|
|
if (entry[1] === 'test') re.test(text);
|
|
else if (entry[1] === 'exec') re.exec(text);
|
|
else if (entry[1] === 'replace') text.replace(re, w => w);
|
|
console.log(JSON.stringify({ verdict: 'returns', ms: +(performance.now() - t0).toFixed(1) }));
|
|
} catch (e) {
|
|
console.log(JSON.stringify({ verdict: e instanceof RangeError ? 'RangeError' : 'other: ' + e.message, ms: +(performance.now() - t0).toFixed(1) }));
|
|
}
|
|
process.exit(0);
|
|
}
|
|
|
|
const KIB = Number(opt('kib', '4096')), TIMEOUT = Number(opt('timeout', '60')), OUT = resolve(opt('out', 'stack-audit.md'));
|
|
const SELF = fileURLToPath(import.meta.url);
|
|
const lineOf = needle => { const out = []; src.split('\n').forEach((l, i) => { if (l.includes(needle)) out.push(i + 1); }); return out; };
|
|
const quantifiedGroup = lit => /\)[*+{]/.test(lit) || /\\[1-9]/.test(lit);
|
|
const L = ['# Stack verdict for every regex in markdown-preview.js (sage-0621)', '',
|
|
`Module ${MODULE}. Each regex is taken from the module's source text (it must occur there), then run alone on its worst shapes at ${KIB} KiB (${KIB * 1024} characters), one process per regex and shape, timeout ${TIMEOUT} s. A verdict is "returns" or "RangeError".`,
|
|
'Column "quantified group or backreference" is a text test on the literal: a `)` followed by `*`, `+` or `{`, or a `\\1`-style backreference. A `)?` is a group that matches at most once and is not counted.', '',
|
|
'| line(s) | regex | quantified group or backreference | shape | verdict | ms |', '|---|---|---|---|---|---|'];
|
|
let bad = 0, total = 0;
|
|
const run = (ni, si) => {
|
|
const r = spawnSync(process.execPath, [SELF, 'one', String(ni), String(si), String(KIB), '--module', MODULE], { encoding: 'utf8', timeout: TIMEOUT * 1000, killSignal: 'SIGKILL' });
|
|
if (r.error && r.error.code === 'ETIMEDOUT') return { verdict: 'timeout', ms: TIMEOUT * 1000 };
|
|
if (r.status !== 0) return { verdict: 'process failed: ' + (r.stderr || '').slice(0, 80), ms: 0 };
|
|
return JSON.parse(r.stdout);
|
|
};
|
|
const missing = [];
|
|
REGEXES.forEach((entry, ni) => {
|
|
const lines = lineOf(entry[0]);
|
|
if (!lines.length) missing.push(entry[0]);
|
|
entry[2].forEach((shape, si) => {
|
|
const v = run(ni, si); total++;
|
|
if (v.verdict !== 'returns') bad++;
|
|
L.push(`| ${lines.join(', ') || 'NOT FOUND'} | \`${entry[0].replace(/\|/g, '\\|')}\` | ${quantifiedGroup(entry[0]) ? 'yes' : 'no'} | ${shape[0]} | ${v.verdict} | ${v.ms} |`);
|
|
});
|
|
});
|
|
L.push('', '## Control: the regex round 3 removed (it must not be in the module, and it must fail here)', '', '| line(s) in module | regex | quantified group or backreference | shape | verdict | ms |', '|---|---|---|---|---|---|');
|
|
const inModule = lineOf(REMOVED[0]);
|
|
let controlFails = 0;
|
|
REMOVED[2].forEach((shape, si) => {
|
|
const v = run('removed', si);
|
|
if (v.verdict === 'RangeError') controlFails++;
|
|
L.push(`| ${inModule.join(', ') || 'none'} | \`${REMOVED[0]}\` | ${quantifiedGroup(REMOVED[0]) ? 'yes' : 'no'} | ${shape[0]} | ${v.verdict} | ${v.ms} |`);
|
|
});
|
|
L.push('', `${total} regex and shape runs on the module's regexes: ${bad} not "returns". Regexes missing from the module's source: ${missing.length}${missing.length ? ' (' + missing.join(', ') + ')' : ''}. Regexes in the module with a quantified group or backreference: ${REGEXES.filter(e => quantifiedGroup(e[0])).length}. The removed regex is ${inModule.length ? 'STILL IN THE MODULE' : 'not in the module'} and fails ${controlFails} of ${REMOVED[2].length} control shapes.`);
|
|
// The scan reads code only: whole-line comments and a trailing " // comment" are cut first.
|
|
const code = src.split('\n').filter(l => !l.trim().startsWith('//') && !l.trim().startsWith('*')).map(l => l.replace(/\s\/\/\s.*$/, '')).join('\n');
|
|
const literals = [...code.matchAll(/(?<![\w)\]$"'`])\/(?![/*])(?:\\.|\[(?:\\.|[^\]\\])*\]|[^/\\\n[])+\/[a-z]*/g)].map(m => m[0]);
|
|
const known = new Set(REGEXES.map(e => e[0]));
|
|
const unlisted = literals.filter(l => !known.has(l));
|
|
L.push('', `Regex literals found by a scan of the module source that are not in the table above: ${unlisted.length}${unlisted.length ? ' - ' + unlisted.map(x => '`' + x + '`').join(', ') : ''}.`);
|
|
writeFileSync(OUT, L.join('\n') + '\n');
|
|
console.log(`${total} runs, ${bad} not returns, missing ${missing.length}, unlisted ${unlisted.length}, control fails ${controlFails}/${REMOVED[2].length}`);
|
|
process.exit(bad || missing.length || unlisted.length || inModule.length ? 1 : 0);
|