Files
rea/scripts/lib/reverse-tools-mutation-targets.mjs
4aa51bfe19 fix(reverse): reconcile cleanup ownership and mutation diagnostics (#2799)
* feat(process): add owned debugger shutdown lifecycle

* feat(reverse): add local inspection commands

* feat(gdb): add persistent debugger integration

* feat(cutter): add Cutter bridge integration

* test(reverse): enforce complete integration coverage

* fix(process): preserve debugger shutdown ownership

* fix(reverse): retain evidence across interrupted commands

* fix(gdb): honor captured provider environment

* test(process): cover debugger shutdown retries

* fix(reverse): retain evidence for interrupted providers

* test(process): cover debugger shutdown retries

* fix(reverse): retain evidence for interrupted providers

* fix(server): snapshot provider environment at creation

* fix(reverse): close review gaps with complete coverage

* fix(reverse): address inspection review findings

* fix(gdb): bound sessions and retain cleanup ownership

* fix(ci): require reverse-tools quality lane

* fix(process): unref inherited debugger output pipes

* fix(reverse): address remaining PR 1960 findings

* fix(gdb): address remaining PR 1961 findings

* fix(reverse): account for unrestricted provider execution

* fix(process): retain debugger ownership until safe shutdown

* fix(process): preserve replacement debugger sessions

* fix(reverse): preserve admission and output provenance

* fix(process): retain exited debugger until output settles

* fix(mcp): bound provider admission and debugger cleanup

* fix(reverse): preserve failure provenance and classify E2BIG

* fix(gdb): preserve startup cleanup and detach safety

* fix(process): retain debugger ownership after detach failure

* fix(reverse): preserve mutation and failure provenance

* fix(gdb): address debugger review findings

* fix(reverse): rebind sessions after external mutation

* fix(process): cap debugger output after exit

* fix(gdb): protect one-shot detach verification

* feat(debugger): add reverse tool coverage gate

* test(debugger): cover integrated failure paths

* fix(gdb): bound and preserve session observations

* fix(gdb): address review regressions

* style: format merged integration files

* fix(reverse): preserve session and output evidence

* refactor(gdb): share repeated test and partial result setup

* fix(gdb): invalidate TTY proof for Python aliases

* fix(reverse): retain failure output byte counts

* test(process): synchronize late debugger output

* fix(gdb): invalidate TTY proof for wrapped commands

* fix(gdb): recognize wit command abbreviation

* test(reverse): wait for cancelled descendant readiness

* test(reverse): bound descendant cancellation cleanup

* style(reverse): format integration tests and adapters

* style: format reverse engineering integration files

* test(reverse): remove duplicate environment harness

* test(gdb): share session startup assertions

* fix(gdb): invalidate output proof for command abbreviations

* test(reverse): share composition edge case scenarios

* fix(ci): recognize verification and mutation tool entries

* test(rizin): cover successful fake inferior detach

* test(cli): align full output failure envelope

* test(cli): isolate reverse command lifecycle service

* test(process): share debugger output fixture setup

* fix(binary): preserve high-bit Windows signature bytes

* style(binary): format Windows signature parser

* test(mcp): preserve call coverage in unchanged comparison

* fix(ci): include reverse tools Vitest configs in lint cache

* fix(mcp): preserve target content on delivery errors

* ci: align duplication ceiling with expanded suite

* test(reverse): allow signal admission mutants

* fix(reverse): bound retained cleanup failures to process owners

* fix(test): replace drifting mutation ranges with owned selectors

* fix(reverse): admit persistent commands and retain cleanup ownership

* fix(cutter): recover timed-out commands from exact receipts

* fix(cutter): reject unsupported bridge request kinds

* fix(ci): repair client format and shared verification fixtures

* fix(native): preserve debugger cleanup ownership and bridge authentication

* test(native): retain diagnostics without perfect coverage requirements

* chore(licenses): refresh locked dependency inventory and notices

* fix(test): exclude transient command locks from mutation sandboxes

* fix(gdb): admit only platforms with safe detach verification

* fix(cutter): retain bounded completion receipts until acknowledgement

* fix(cutter): acknowledge commands rejected before UI dispatch

* test(reverse): fail cancellation checks before fixture teardown

* test(gdb): clean up the retained broken-stdin fixture owner

* docs(test): clarify focused provider verification scope

* fix(rizin): retain close cleanup failures

* fix(gdb): wait for inferior stop and bound MI buffering

* fix(cutter): retain failed receipt acknowledgements

* fix(rizin): return retained session identity on rebind failure

* fix(gdb): preserve startup cancellation and lossy status

* fix(rizin): retain launch provenance and assemble frames once

* fix(cutter): recover receipts across callers and bound discovery

* docs: describe debugger evidence and bridge recovery

* fix(process): keep cleanup incomplete while writers remain

Stopping a debugger no longer reports success when inherited output
streams stay open, so BinarySession cannot reopen a binary a child is
still changing. Concurrent Cutter cleanup retries share one receipt
acknowledgement instead of treating a consumed receipt as failure.

* fix(debuggers): recover cleanup with captured process identities

* fix(analysis): retire sessions after unrestricted commands

* fix(cli): preserve completed evidence after cleanup recovery

* fix(cutter): bound frame collection and coalesce cleanup

* fix(windows): resolve selected provider paths absolutely

* fix(gdb): report decoding loss in failed startup evidence

* fix(rizin): scope decoding loss to collected command frames

* test(mcp): cover snapshot retirement in reverse tools CI

---------

Co-authored-by: Daniel Gonçalves Araujo <47118053+dandgabr@users.noreply.github.com>
Co-authored-by: REA Maintainers <noreply@github.com>
Co-authored-by: OpenAI Codex <codex@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-14 04:47:34 +08:00

183 lines
5.8 KiB
JavaScript

import { readFileSync } from "node:fs";
import ts from "typescript6";
const compact = (text) => text.replace(/\s+/gu, "");
const nameOf = (node, source) => node.name?.getText(source);
const methodOwner = (node, source) => {
for (let parent = node; parent !== undefined; parent = parent.parent) {
if (ts.isMethodDeclaration(parent) || ts.isConstructorDeclaration(parent))
return {
owner: nameOf(parent.parent, source),
body: parent.body,
method: ts.isConstructorDeclaration(parent)
? "constructor"
: nameOf(parent, source),
};
}
return undefined;
};
const withinOwner = (node, selector, source) => {
if (selector.owner === undefined) return true;
const owner = methodOwner(node, source);
return (
owner?.owner === selector.owner &&
(selector.method === undefined || owner.method === selector.method)
);
};
const contains = (node, predicate) =>
predicate(node) ||
ts.forEachChild(node, (child) => contains(child, predicate)) === true;
const inBlockWithBinding = (node, binding) => {
if (binding === undefined) return true;
let block = node.parent;
while (block !== undefined && !ts.isBlock(block)) block = block.parent;
return block?.statements.some(
(statement) =>
ts.isVariableStatement(statement) &&
statement.declarationList.declarations.some(
(declaration) =>
ts.isIdentifier(declaration.name) &&
declaration.name.text === binding,
),
);
};
const isCommandHandler = (node, command) => {
if (!ts.isArrowFunction(node)) return false;
const property = node.parent;
if (
!ts.isPropertyAssignment(property) ||
!ts.isIdentifier(property.name) ||
property.name.text !== "run"
)
return false;
const call = property.parent.parent;
if (
!ts.isCallExpression(call) ||
!ts.isPropertyAccessExpression(call.expression) ||
!ts.isIdentifier(call.expression.expression) ||
call.expression.expression.text !== "cli" ||
call.expression.name.text !== "command"
)
return false;
const selected = call.arguments[0];
return (
selected !== undefined &&
ts.isPropertyAccessExpression(selected) &&
ts.isIdentifier(selected.expression) &&
selected.expression.text === "CLI_COMMANDS" &&
selected.name.text === command
);
};
const matches = (node, selector, source) => {
if (!withinOwner(node, selector, source)) return false;
if (
selector.direct === true &&
node.parent !== methodOwner(node, source)?.body
)
return false;
switch (selector.kind) {
case "expression":
return (
ts.isBinaryExpression(node) &&
compact(node.getText(source)) === compact(selector.text)
);
case "declaration":
return (
ts.isVariableDeclaration(node) &&
nameOf(node, source) === selector.name &&
inBlockWithBinding(node, selector.withinBlockWithBinding)
);
case "method":
return (
ts.isMethodDeclaration(node) && nameOf(node, source) === selector.name
);
case "constructor":
return ts.isConstructorDeclaration(node);
case "guard":
return (
ts.isIfStatement(node) &&
compact(node.expression.getText(source)) === compact(selector.condition)
);
case "call":
return (
ts.isCallExpression(node) &&
compact(node.expression.getText(source)) === compact(selector.callee) &&
(selector.event === undefined ||
(node.arguments[0] !== undefined &&
ts.isStringLiteral(node.arguments[0]) &&
node.arguments[0].text === selector.event))
);
case "try":
return (
ts.isTryStatement(node) &&
contains(node.tryBlock, (child) =>
selector.assignment !== undefined
? ts.isBinaryExpression(child) &&
child.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
ts.isIdentifier(child.left) &&
child.left.text === selector.assignment
: ts.isCallExpression(child) &&
compact(child.expression.getText(source)) ===
compact(selector.callee),
)
);
case "command-handler":
return isCommandHandler(node, selector.command);
default:
throw new Error(
`Unknown mutation selector kind: ${String(selector.kind)}`,
);
}
};
/** Parse a source once and resolve one owned region for every requested selector. */
export const mutationRanges = (path, selectors) => {
if (!Array.isArray(selectors) || selectors.length === 0)
throw new Error(`Mutation selectors must be nonempty: ${path}`);
for (const selector of selectors)
if (
["method", "constructor", "guard", "call", "try"].includes(
selector.kind,
) &&
typeof selector.owner !== "string"
)
throw new Error(
`Mutation selector requires a named class owner: ${path}`,
);
const source = ts.createSourceFile(
path,
readFileSync(path, "utf8"),
ts.ScriptTarget.Latest,
true,
);
if (source.parseDiagnostics.length !== 0)
throw new Error(
`Cannot select mutations from malformed TypeScript: ${path}`,
);
const found = selectors.map(() => []);
const visit = (node) => {
for (const [index, selector] of selectors.entries())
if (matches(node, selector, source)) found[index].push(node);
ts.forEachChild(node, visit);
};
visit(source);
return selectors.map((selector, index) => {
const candidates = found[index];
if (candidates.length !== 1)
throw new Error(
`Expected one mutation target in ${path}: ${JSON.stringify(selector)}; found ${candidates.length}`,
);
const node = candidates[0];
const first =
source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
const last = source.getLineAndCharacterOfPosition(node.getEnd()).line + 1;
return `${path}:${first}-${last}`;
});
};