diff --git a/native/windows/README.md b/native/windows/README.md index 86103ce9ae..ec2fb1bde5 100644 --- a/native/windows/README.md +++ b/native/windows/README.md @@ -154,7 +154,9 @@ node scripts/verify-windows-native-inspection.mjs This artifact injects one access-denied disposition result while using a real NTFS directory. The verifier checks retained ownership, recovery on a later query, cached successful admission, and cleanup on environment teardown. It -does not reproduce or identify the host-dependent cause reported in #1701. +renames the retained probe and recreates its old path, checking that cleanup +removes the owned inode while preserving the replacement directory. +It does not reproduce or identify the host-dependent cause reported in #1701. The fixture is stored under `.cache/` and is excluded from the shipped addon. The native lane checks DACL readback independently, source write/replacement diff --git a/scripts/verify-windows-native-inspection.mjs b/scripts/verify-windows-native-inspection.mjs index 5b3c2e7260..94adaf5a60 100644 --- a/scripts/verify-windows-native-inspection.mjs +++ b/scripts/verify-windows-native-inspection.mjs @@ -5,7 +5,9 @@ import { mkdir, mkdtemp, readdir, + rmdir, rm, + stat, symlink, writeFile, } from "node:fs/promises"; @@ -52,7 +54,7 @@ try { join(workspace, "dist", "windows", "WindowsNativeLoader.js"), ).href; const source = `import assert from 'node:assert/strict'; -import {readdirSync,renameSync,statSync} from 'node:fs'; +import {existsSync,mkdirSync,readdirSync,renameSync,statSync} from 'node:fs'; import {join} from 'node:path'; import {systemWindowsNativeAuthority} from ${JSON.stringify(loader)}; const temp=process.env.TEMP; @@ -63,14 +65,19 @@ assert.match(first.reason,/Delete owned object by handle failed.*Win32 5/s); const [name,...extra]=readdirSync(temp);assert.ok(name);assert.equal(extra.length,0); const path=join(temp,name);const identity=statSync(path,{bigint:true}); // Ownership follows the retained NTFS handle even when the directory moves. +// A replacement at its old pathname must remain caller-owned. const moved=path+'-renamed';renameSync(path,moved); assert.equal(statSync(moved,{bigint:true}).ino,identity.ino); +mkdirSync(path);const replacement=statSync(path,{bigint:true}); +assert.notEqual(replacement.ino,identity.ino); if(process.argv[2]!=='teardown'){ const second=systemWindowsNativeAuthority();assert.equal(second.available,true); - assert.equal(readdirSync(temp).length,0); + assert.equal(existsSync(moved),false); + assert.deepEqual(readdirSync(temp),[name]); + assert.equal(statSync(path,{bigint:true}).ino,replacement.ino); assert.equal(systemWindowsNativeAuthority(),second); } -console.log(JSON.stringify({mode:process.argv[2],firstFailed:true,retainedOwner:true,inode:identity.ino.toString()})); +console.log(JSON.stringify({mode:process.argv[2],firstFailed:true,retainedOwner:true,inode:identity.ino.toString(),replacementName:name,replacementInode:replacement.ino.toString()})); `; const fixture = join(workspace, "probe.mjs"); await writeFile(fixture, source); @@ -86,10 +93,17 @@ console.log(JSON.stringify({mode:process.argv[2],firstFailed:true,retainedOwner: }), ); assert.equal(observed.retainedOwner, true); + assert.deepEqual(await readdir(temp), [observed.replacementName]); + const replacement = join(temp, observed.replacementName); + assert.equal( + (await stat(replacement, { bigint: true })).ino.toString(), + observed.replacementInode, + ); + await rmdir(replacement); assert.deepEqual(await readdir(temp), []); } process.stdout.write( - `${JSON.stringify({ ok: true, controlledDispositionFailure: true, retainedOwner: true, retryRecovers: true, successfulAdmissionCached: true, environmentTeardownCleansProbe: true, issue1701HostCauseReproduced: false })}\n`, + `${JSON.stringify({ ok: true, controlledDispositionFailure: true, retainedOwner: true, renamedOwnerCleanup: true, replacementPreserved: true, retryRecovers: true, successfulAdmissionCached: true, environmentTeardownCleansProbe: true, issue1701HostCauseReproduced: false })}\n`, ); } finally { await rm(workspace, { recursive: true, force: true }); diff --git a/src/domain/android/androidAnalysis.ts b/src/domain/android/androidAnalysis.ts index caa3bf5ee0..eaf521a50f 100644 --- a/src/domain/android/androidAnalysis.ts +++ b/src/domain/android/androidAnalysis.ts @@ -98,7 +98,7 @@ const textCoverage = z.strictObject({ text: z.string(), reported_total_bytes: z.number().int().nonnegative().nullable(), }); -/** A DEX prototype: parameter descriptors followed by the return descriptor. */ +/** A DEX prototype; hex parentheses also keep the advertised class valid in v mode. */ export const dexDescriptorSchema = z .string() .regex( diff --git a/src/domain/javascript/electronWrappedCallPaths.ts b/src/domain/javascript/electronWrappedCallPaths.ts index 051595eb6e..979ab14555 100644 --- a/src/domain/javascript/electronWrappedCallPaths.ts +++ b/src/domain/javascript/electronWrappedCallPaths.ts @@ -1,4 +1,5 @@ import * as t from "@babel/types"; +import { runtimeFunctionParameters } from "./javascriptAstValues.js"; import { JavaScriptSymbolicBindings } from "./javascriptSymbolicBindings.js"; import { symbolicVariants, @@ -447,7 +448,9 @@ export const collectElectronWrappedCallPaths = ( const entry = `${key.value}.${name}`; functionBody( fn, - fn.node.params.map(() => symbolicUnknown("public API argument")), + runtimeFunctionParameters(fn.node).map(() => + symbolicUnknown("public API argument"), + ), { ...memberContext, entryNode: node, diff --git a/src/domain/javascript/javascriptAstFingerprint.ts b/src/domain/javascript/javascriptAstFingerprint.ts index c85700da92..b9e53f9759 100644 --- a/src/domain/javascript/javascriptAstFingerprint.ts +++ b/src/domain/javascript/javascriptAstFingerprint.ts @@ -7,6 +7,7 @@ import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { propertyName, + runtimeFunctionParameters, semanticStaticPropertyKey, } from "./javascriptAstValues.js"; import { stringValue } from "./javascriptStaticAnalysisHelpers.js"; @@ -38,15 +39,13 @@ export const collectJavaScriptExports = ( bindings?: JavaScriptSemanticAnalysisState, ): StaticExports => { const exports = new Set(); + const parameter = t.isFunction(node) + ? runtimeFunctionParameters(node)[1] + : undefined; const factoryExports = - bindings !== undefined && - t.isFunction(node) && - t.isIdentifier(node.params[1]) - ? resolveSemanticBindingState( - bindings, - node.params[1], - node.params[1].name, - )?.bindingId + bindings !== undefined && t.isFunction(node) && t.isIdentifier(parameter) + ? resolveSemanticBindingState(bindings, parameter, parameter.name) + ?.bindingId : undefined; const isExportsAlias = (reference: t.Node | null): boolean => { if (!t.isIdentifier(reference)) return false; diff --git a/src/domain/javascript/javascriptAstValues.ts b/src/domain/javascript/javascriptAstValues.ts index cb3a533fe4..26ff26a499 100644 --- a/src/domain/javascript/javascriptAstValues.ts +++ b/src/domain/javascript/javascriptAstValues.ts @@ -1,6 +1,14 @@ import * as t from "@babel/types"; import type { JavaScriptSemanticPrimitive } from "./javascriptSemanticValueTypes.js"; +/** TypeScript receiver annotations occupy no positional runtime parameter slot. */ +export const runtimeFunctionParameters = ( + node: t.Function, +): t.Function["params"] => + t.isIdentifier(node.params[0], { name: "this" }) + ? node.params.slice(1) + : node.params; + /** * Remove wrappers that affect TypeScript or Flow checking but not runtime * expression identity. The returned depth lets bounded evaluators account for diff --git a/src/domain/javascript/javascriptBootstrap.ts b/src/domain/javascript/javascriptBootstrap.ts index cdb33d0245..2cc20ffdf8 100644 --- a/src/domain/javascript/javascriptBootstrap.ts +++ b/src/domain/javascript/javascriptBootstrap.ts @@ -1,6 +1,9 @@ import * as t from "@babel/types"; -import { semanticStaticPropertyKey } from "./javascriptAstValues.js"; +import { + semanticStaticPropertyKey, + runtimeFunctionParameters, +} from "./javascriptAstValues.js"; import { moduleFactory } from "./javascriptStaticAnalysisHelpers.js"; import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; import { @@ -27,7 +30,7 @@ export const javascriptBootstrapAt = ( (!t.isFunctionExpression(call.callee) && !t.isArrowFunctionExpression(call.callee)) || !t.isBlockStatement(call.callee.body) || - call.callee.params.length !== 0 || + runtimeFunctionParameters(call.callee).length !== 0 || call.arguments.length !== 0 ) return undefined; @@ -63,10 +66,10 @@ export const javascriptBootstrapAt = ( !t.isFunctionDeclaration(loader) || loader.id === null || loader.id === undefined || - loader.params.length !== 1 + runtimeFunctionParameters(loader).length !== 1 ) continue; - const parameter = loader.params[0]; + const parameter = runtimeFunctionParameters(loader)[0]; if (!t.isIdentifier(parameter)) continue; const loaderName = loader.id.name; const loaderBindingId = bindingId(state, loader.id); diff --git a/src/domain/javascript/javascriptModuleInterop.ts b/src/domain/javascript/javascriptModuleInterop.ts index c2e26d6fe1..6f8eb9980c 100644 --- a/src/domain/javascript/javascriptModuleInterop.ts +++ b/src/domain/javascript/javascriptModuleInterop.ts @@ -26,6 +26,7 @@ import { import { SemanticPropertyPathCoverage } from "./javascriptSemanticPropertyPaths.js"; import { readExactJavaScriptLiteral, + runtimeFunctionParameters, unwrapJavaScriptExpression, } from "./javascriptAstValues.js"; @@ -662,10 +663,10 @@ const identityFunction = ( !t.isFunction(node) || node.async || node.generator || - node.params.length !== 1 + runtimeFunctionParameters(node).length !== 1 ) return false; - const parameter = node.params[0]; + const parameter = runtimeFunctionParameters(node)[0]; const result = t.isBlockStatement(node.body) ? node.body.body.length === 1 && t.isReturnStatement(node.body.body[0]) ? node.body.body[0].argument diff --git a/src/domain/javascript/javascriptSemanticAnalysis.ts b/src/domain/javascript/javascriptSemanticAnalysis.ts index 7e20c0e137..578789ad58 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.ts @@ -18,6 +18,7 @@ import { } from "./javascriptSemanticProjection.js"; import { semanticObjectPatternKeys, + runtimeFunctionParameters, semanticStaticPropertyKey, semanticWriteTargets, } from "./javascriptAstValues.js"; @@ -717,7 +718,7 @@ function* collectDefinitionsSteps( functionBodies.push({ body: node, scope: nested, - parameters: parent.params, + parameters: runtimeFunctionParameters(parent), }); } const scope = currentSemanticScope(stack); @@ -1076,7 +1077,7 @@ const scopeKind = ( }; const functionParametersHaveExpressions = (node: t.Function): boolean => { - const pending: t.Node[] = [...node.params]; + const pending: t.Node[] = [...runtimeFunctionParameters(node)]; while (pending.length > 0) { const parameter = pending.pop(); if (t.isAssignmentPattern(parameter)) return true; @@ -1133,7 +1134,7 @@ const bindFunctionLocals = ( scope: JavaScriptSemanticScopeState, state: JavaScriptSemanticAnalysisState, ): void => { - for (const parameter of node.params) + for (const parameter of runtimeFunctionParameters(node)) bindPattern({ pattern: t.isTSParameterProperty(parameter) ? parameter.parameter diff --git a/src/domain/javascript/javascriptSemanticCallResolution.ts b/src/domain/javascript/javascriptSemanticCallResolution.ts index 9292ea2658..3ed09dca0c 100644 --- a/src/domain/javascript/javascriptSemanticCallResolution.ts +++ b/src/domain/javascript/javascriptSemanticCallResolution.ts @@ -6,7 +6,10 @@ import type { } from "./javascriptSemanticIr.js"; import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { semanticCallableIdForNode } from "./javascriptSemanticProjection.js"; -import { unwrapJavaScriptExpression } from "./javascriptAstValues.js"; +import { + unwrapJavaScriptExpression, + runtimeFunctionParameters, +} from "./javascriptAstValues.js"; import { resolveSemanticBindingState, type JavaScriptSemanticAnalysisState, @@ -194,14 +197,15 @@ const parameterAcceptsArgument = ( ): boolean => { const node = state.callableNodesById.get(callable.callableId); if (!t.isFunction(node)) return false; - const parameterIndex = node.params.findIndex((parameter) => + const parameters = runtimeFunctionParameters(node); + const parameterIndex = parameters.findIndex((parameter) => rangeContains(range(parameter), location), ); if (parameterIndex === argumentIndex) return true; return ( parameterIndex >= 0 && parameterIndex < argumentIndex && - t.isRestElement(node.params[parameterIndex]) + t.isRestElement(parameters[parameterIndex]) ); }; diff --git a/src/domain/javascript/javascriptSemanticFingerprints.ts b/src/domain/javascript/javascriptSemanticFingerprints.ts index 8b6edc0365..c8615788c2 100644 --- a/src/domain/javascript/javascriptSemanticFingerprints.ts +++ b/src/domain/javascript/javascriptSemanticFingerprints.ts @@ -1,6 +1,7 @@ import { createHash } from "node:crypto"; import * as t from "@babel/types"; +import { runtimeFunctionParameters } from "./javascriptAstValues.js"; import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import type { JavaScriptSemanticCallAnalysis } from "./javascriptSemanticCalls.js"; @@ -279,7 +280,7 @@ const relationShape = ( ]; const callableParameterArity = (node: t.Node): number => - t.isFunction(node) ? node.params.length : 0; + t.isFunction(node) ? runtimeFunctionParameters(node).length : 0; const isAsyncCallable = (node: t.Node): boolean => t.isFunction(node) && node.async; diff --git a/src/domain/javascript/javascriptSemanticIterationSources.ts b/src/domain/javascript/javascriptSemanticIterationSources.ts index ea3708e515..0521f76b8d 100644 --- a/src/domain/javascript/javascriptSemanticIterationSources.ts +++ b/src/domain/javascript/javascriptSemanticIterationSources.ts @@ -2,6 +2,7 @@ import * as t from "@babel/types"; import { semanticObjectPatternKeys, + runtimeFunctionParameters, semanticStaticPropertyKey, } from "./javascriptAstValues.js"; import { @@ -212,7 +213,8 @@ export const semanticIterationSources = ( else if (t.isYieldExpression(node) && node.delegate && node.argument != null) add(sources, { node: node.argument, projection: [] }, node, {}); else if (t.isFunction(node)) - for (const parameter of node.params) visit(context, parameter, undefined); + for (const parameter of runtimeFunctionParameters(node)) + visit(context, parameter, undefined); return sources; }; diff --git a/src/domain/javascript/javascriptStaticAnalysisBundler.ts b/src/domain/javascript/javascriptStaticAnalysisBundler.ts index 773f88d7d2..b49f4c2ae7 100644 --- a/src/domain/javascript/javascriptStaticAnalysisBundler.ts +++ b/src/domain/javascript/javascriptStaticAnalysisBundler.ts @@ -1,4 +1,5 @@ import * as t from "@babel/types"; +import { runtimeFunctionParameters } from "./javascriptAstValues.js"; import { javascriptBootstrapAt } from "./javascriptBootstrap.js"; import { resolveSemanticBindingState, @@ -304,7 +305,7 @@ const runtimeMetadata = (node: t.Node | null | undefined): RuntimeMetadata => { asyncChunkKeys: [], unknownAsyncChunkKeys: 0, }; - const parameter = node.params[0]; + const parameter = runtimeFunctionParameters(node)[0]; const requireName = t.isIdentifier(parameter) ? parameter.name : null; if (requireName === null) return { diff --git a/src/domain/javascript/javascriptStaticAnalysisHelpers.ts b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts index 71628bc8ce..e2cc73f202 100644 --- a/src/domain/javascript/javascriptStaticAnalysisHelpers.ts +++ b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts @@ -13,6 +13,7 @@ import type { } from "./javascriptStaticAnalysisTypes.js"; import { readExactJavaScriptLiteral, + runtimeFunctionParameters, semanticStaticPropertyKey, semanticStaticPropertyName, unwrapJavaScriptExpression, @@ -147,7 +148,7 @@ export const modulePropertyName = ( export const factoryRequireName = ( factory: t.FunctionExpression | t.ArrowFunctionExpression | t.ObjectMethod, ): string | null => { - const parameter = factory.params[2]; + const parameter = runtimeFunctionParameters(factory)[2]; return t.isIdentifier(parameter) ? parameter.name : null; }; diff --git a/src/domain/javascript/javascriptSymbolicBindings.ts b/src/domain/javascript/javascriptSymbolicBindings.ts index 62c70f953e..faddf545fc 100644 --- a/src/domain/javascript/javascriptSymbolicBindings.ts +++ b/src/domain/javascript/javascriptSymbolicBindings.ts @@ -1,6 +1,7 @@ import * as t from "@babel/types"; import { semanticStaticPropertyKey, + runtimeFunctionParameters, unwrapJavaScriptExpression, } from "./javascriptAstValues.js"; import { javascriptBootstrapAt } from "./javascriptBootstrap.js"; @@ -387,7 +388,7 @@ export class JavaScriptSymbolicBindings { invocationNode?: t.Node, ): SymbolicEnvironment { const values = new Map(); - fn.node.params.forEach((param, index) => { + runtimeFunctionParameters(fn.node).forEach((param, index) => { const target = t.isRestElement(param) ? param.argument : param; if (!t.isIdentifier(target)) return; const binding = resolveSemanticBindingState( @@ -866,9 +867,7 @@ export class JavaScriptSymbolicBindings { ? {} : { scopeId: this.state.scopeByNode.get(factory)!.scopeId }), }; - const first = factory.params[0]; - const second = factory.params[1]; - const third = factory.params[2]; + const [first, second, third] = runtimeFunctionParameters(factory); const binding = (n: t.Node | undefined) => t.isIdentifier(n) ? resolveSemanticBindingState(this.state, n, n.name)?.bindingId diff --git a/src/ghidra/GhidraAnalysisSeeds.test.ts b/src/ghidra/GhidraAnalysisSeeds.test.ts index 37437ebf7c..7693573732 100644 --- a/src/ghidra/GhidraAnalysisSeeds.test.ts +++ b/src/ghidra/GhidraAnalysisSeeds.test.ts @@ -23,7 +23,7 @@ import { type GhidraSeedCommitment, type GhidraSeedReport, } from "./GhidraAnalysisSeeds.js"; -import type { GhidraInstallationHost } from "./GhidraInstallation.js"; +import { ghidraProfileInstallationHost as host } from "./GhidraInstallation.fixture.js"; import { GhidraProvider } from "./GhidraProvider.js"; const directories: string[] = []; @@ -193,19 +193,6 @@ describe("Ghidra seed session verification", () => { }); describe("Ghidra seed profile", () => { - const host: GhidraInstallationHost = { - platform: "linux", - architecture: "x64", - readText: () => "application.version=12.1.4\napplication.java.min=21\n", - executable: () => true, - probeJava: () => ({ - version: "21.0.11", - major: 21, - home: "/jdk-21", - bits: 64, - runtime: "jdk", - }), - }; const pe: BinaryTarget = { path: "/fixture.exe", sha256: "a".repeat(64), diff --git a/src/ghidra/GhidraInstallation.fixture.ts b/src/ghidra/GhidraInstallation.fixture.ts new file mode 100644 index 0000000000..cb81fa0de1 --- /dev/null +++ b/src/ghidra/GhidraInstallation.fixture.ts @@ -0,0 +1,16 @@ +import type { GhidraInstallationHost } from "./GhidraInstallation.js"; + +/** Simulated valid installation for profile tests; never starts Ghidra or Java. */ +export const ghidraProfileInstallationHost: GhidraInstallationHost = { + platform: "linux", + architecture: "x64", + readText: () => "application.version=12.1.4\napplication.java.min=21\n", + executable: () => true, + probeJava: () => ({ + version: "21.0.11", + major: 21, + home: "/jdk-21", + bits: 64, + runtime: "jdk", + }), +}; diff --git a/src/ghidra/GhidraLanguageOverride.test.ts b/src/ghidra/GhidraLanguageOverride.test.ts index 02c5bf245f..86909112e2 100644 --- a/src/ghidra/GhidraLanguageOverride.test.ts +++ b/src/ghidra/GhidraLanguageOverride.test.ts @@ -11,7 +11,7 @@ import { ghidraProfileLanguageOverride, sameGhidraLanguageOverride, } from "./GhidraAnalysisProfile.js"; -import type { GhidraInstallationHost } from "./GhidraInstallation.js"; +import { ghidraProfileInstallationHost as host } from "./GhidraInstallation.fixture.js"; import { admitGhidraLanguageOverride, parseGhidraLanguageDefinitions, @@ -39,21 +39,6 @@ const X86_LDEFS = ` `; -// Fake installation inspection; the language catalog is a real directory. -const host: GhidraInstallationHost = { - platform: "linux", - architecture: "x64", - readText: () => "application.version=12.1.4\napplication.java.min=21\n", - executable: () => true, - probeJava: () => ({ - version: "21.0.11", - major: 21, - home: "/jdk-21", - bits: 64, - runtime: "jdk", - }), -}; - const pe: BinaryTarget = { path: "/fixture.exe", sha256: "a".repeat(64), diff --git a/src/ghidra/GhidraMipsProfile.test.ts b/src/ghidra/GhidraMipsProfile.test.ts index 800de9490d..57c04daefd 100644 --- a/src/ghidra/GhidraMipsProfile.test.ts +++ b/src/ghidra/GhidraMipsProfile.test.ts @@ -14,7 +14,7 @@ import { ghidraMipsProfileParameters, ghidraPinnedMipsLanguage, } from "./GhidraMipsProfile.js"; -import type { GhidraInstallationHost } from "./GhidraInstallation.js"; +import { ghidraProfileInstallationHost as host } from "./GhidraInstallation.fixture.js"; import { GHIDRA_SESSION_CAPABILITIES } from "./GhidraSessionValues.js"; import type { GhidraMipsSoftFloatReport } from "./GhidraMipsProfile.js"; import { @@ -22,20 +22,6 @@ import { ghidraProfileLanguageOverride, } from "./GhidraAnalysisProfile.js"; -// Fake installation inspection tests admission/profile contracts, not Ghidra. -const host: GhidraInstallationHost = { - platform: "linux", - architecture: "x64", - readText: () => "application.version=12.1.4\napplication.java.min=21\n", - executable: () => true, - probeJava: () => ({ - version: "21.0.11", - major: 21, - home: "/jdk-21", - bits: 64, - runtime: "jdk", - }), -}; const parsed = parseConfig({ GHIDRA_INSTALL_DIR: "/ghidra" }); if (!parsed.ok) throw parsed.error; const config = parsed.value; @@ -155,7 +141,13 @@ describe("bounded Ghidra MIPS admission", () => { reason: expect.stringContaining(reason), }); const resolved = await ghidra.resolveAnalysisProfile(value); - expect(resolved.ok).toBe(false); + expect(resolved).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisUnsupportedTargetError", + reason: expect.stringContaining(reason), + }, + }); }, ); @@ -180,7 +172,13 @@ describe("recordless legacy MIPS32 admission", () => { status: "unsupported", }); const resolved = await ghidra.resolveAnalysisProfile(object); - expect(resolved.ok).toBe(false); + expect(resolved).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisUnsupportedTargetError", + reason: expect.stringContaining("ET_EXEC"), + }, + }); }); it.each([ diff --git a/src/ghidra/GhidraPowerPcProfile.test.ts b/src/ghidra/GhidraPowerPcProfile.test.ts index 831a47f3bc..0be7aa898e 100644 --- a/src/ghidra/GhidraPowerPcProfile.test.ts +++ b/src/ghidra/GhidraPowerPcProfile.test.ts @@ -16,7 +16,7 @@ import { ghidraProfileLanguageOverride, } from "./GhidraAnalysisProfile.js"; import { GhidraProvider } from "./GhidraProvider.js"; -import type { GhidraInstallationHost } from "./GhidraInstallation.js"; +import { ghidraProfileInstallationHost as host } from "./GhidraInstallation.fixture.js"; // Abbreviated from Ghidra 12.1.4 Processors/PowerPC/data/languages/ppc.ldefs. const PPC_LDEFS = ` @@ -32,20 +32,6 @@ const PPC_LDEFS = ` `; -// Fake installation inspection tests admission/profile contracts, not Ghidra. -const host: GhidraInstallationHost = { - platform: "linux", - architecture: "x64", - readText: () => "application.version=12.1.4\n", - executable: () => true, - probeJava: () => ({ - version: "21.0.11", - major: 21, - home: "/jdk-21", - bits: 64, - runtime: "jdk", - }), -}; const configFor = (environment: NodeJS.ProcessEnv) => { const parsed = parseConfig({ GHIDRA_INSTALL_DIR: "/ghidra", ...environment }); if (!parsed.ok) throw parsed.error; @@ -65,6 +51,7 @@ const target = (change: Partial = {}): BinaryTarget => ({ describe("bounded Ghidra PowerPC admission", () => { it("binds the generic language instead of the loader's e500 choice", async () => { const ghidra = new GhidraProvider(config, silentLogger, {}, host); + expect(ghidra.inspectAvailability()).toMatchObject({ status: "available" }); expect(ghidra.inspectTargetSupport(target()).status).toBe("supported"); const resolved = await ghidra.resolveAnalysisProfile(target()); if (!resolved.ok) throw resolved.error; @@ -152,7 +139,13 @@ describe("bounded Ghidra PowerPC admission", () => { reason: expect.stringContaining(reason), }); const resolved = await ghidra.resolveAnalysisProfile(value); - expect(resolved.ok).toBe(false); + expect(resolved).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisUnsupportedTargetError", + reason: expect.stringContaining(reason), + }, + }); }, ); diff --git a/src/ghidra/GhidraRiscvProfile.test.ts b/src/ghidra/GhidraRiscvProfile.test.ts index 718fe8d46d..3ada4d15b9 100644 --- a/src/ghidra/GhidraRiscvProfile.test.ts +++ b/src/ghidra/GhidraRiscvProfile.test.ts @@ -13,7 +13,7 @@ import { hopperLoaderArgsForTarget } from "../hopper/HopperAnalysisProfile.js"; import { IdaProvider } from "../ida/IdaProvider.js"; import { GhidraProvider } from "./GhidraProvider.js"; import { ghidraRiscvLimitations } from "./GhidraRiscvProfile.js"; -import type { GhidraInstallationHost } from "./GhidraInstallation.js"; +import { ghidraProfileInstallationHost as host } from "./GhidraInstallation.fixture.js"; import { ghidraPinnedLanguage, ghidraProfileLanguageOverride, @@ -49,20 +49,6 @@ const RISCV_LDEFS = ` `; -// Fake installation inspection tests admission/profile contracts, not Ghidra. -const host: GhidraInstallationHost = { - platform: "linux", - architecture: "x64", - readText: () => "application.version=12.1.4\napplication.java.min=21\n", - executable: () => true, - probeJava: () => ({ - version: "21.0.11", - major: 21, - home: "/jdk-21", - bits: 64, - runtime: "jdk", - }), -}; const configOf = ( installDir: string, environment: Record = {}, diff --git a/tests/acceptance/applications/typescriptThisParametersCli.test.ts b/tests/acceptance/applications/typescriptThisParametersCli.test.ts new file mode 100644 index 0000000000..a90dddec5c --- /dev/null +++ b/tests/acceptance/applications/typescriptThisParametersCli.test.ts @@ -0,0 +1,140 @@ +import { join } from "node:path"; + +import { expect } from "vitest"; + +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; +import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js"; +import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js"; + +cliTest.for(["CLI", "stdio MCP"] as const)( + "maps arguments to runtime TypeScript parameters through %s", + async (surface, { cli, processes }) => { + const root = await createTestTempDirectory("rea-this-parameters-"); + await writeFixtureFiles(root, { + "bundle.mts": [ + "self.webpackChunkApp.push([[1], { 7: function(this: void, module: unknown, exports: Record, require: (id: number) => unknown) { exports.answer = 42; } }, function(this: void, require: (id: number) => unknown) { require(7); }]);", + ].join("\n"), + "interop.cts": [ + "function identity(this: void, value: T) { return value; }", + 'const electron = identity(require("electron"));', + 'electron.ipcRenderer.send("typed-identity");', + ].join("\n"), + "ipc-wrapper.cts": [ + 'const { ipcRenderer } = require("electron");', + "function send(this: void, channel: string) { ipcRenderer.send(channel); }", + "function sendRest(this: void, ...channels: string[]) { ipcRenderer.send(...channels); }", + 'send("typed-channel");', + 'sendRest("typed-rest");', + ].join("\n"), + "entry.mts": [ + "function first(this: void, value: number) { return value; }", + "function rest(this: void, ...values: number[]) { return values; }", + "function destructured({ left }: { left: number }, right: number) { return left + right; }", + "function mixed(this: void, head: number, ...tail: number[]) { return [head, ...tail]; }", + "const a = first(7);", + "const b = rest(1, 2);", + "const c = destructured({ left: 3 }, 4);", + "const d = mixed(9, 10, 11);", + "console.log(JSON.stringify([a, b, c, d, first.length, rest.length, mixed.length]));", + ].join("\n"), + }); + const control = await processes.run( + process.execPath, + [join(root, "entry.mts")], + { timeoutMs: 15_000 }, + ); + expect(control.exitCode, control.stderr).toBe(0); + expect(JSON.parse(control.stdout)).toEqual([ + 7, + [1, 2], + 7, + [9, 10, 11], + 1, + 0, + 1, + ]); + const analysis = await analyzeJavaScriptDirectory(surface, cli, root); + const graph = analysis.semantic_graph; + const parameters = graph.nodes.filter( + (node) => + node.kind === "parameter" && node.identity.module_path === "entry.mts", + ); + expect(parameters.map((node) => node.label).sort()).toEqual([ + "head", + "left", + "right", + "tail", + "value", + "values", + ]); + const parameterIds = new Set(parameters.map((node) => node.node_id)); + const flows = graph.relations.filter( + (relation) => + relation.relation === "argument-to-parameter" && + parameterIds.has(relation.target_node_id), + ); + expect( + flows + .map((flow) => { + const source = graph.nodes.find( + (node) => node.node_id === flow.source_node_id, + ); + const target = parameters.find( + (node) => node.node_id === flow.target_node_id, + ); + const owner = graph.nodes.find( + (node) => node.node_id === target?.function_node_id, + ); + return [ + source?.identity.source_range?.start.line, + source?.label, + owner?.label, + target?.label, + ]; + }) + .sort(), + ).toEqual([ + [5, "argument 0", "first", "value"], + [6, "argument 0", "rest", "values"], + [6, "argument 1", "rest", "values"], + [7, "argument 0", "destructured", "left"], + [7, "argument 1", "destructured", "right"], + [8, "argument 0", "mixed", "head"], + [8, "argument 1", "mixed", "tail"], + [8, "argument 2", "mixed", "tail"], + ]); + expect( + analysis.graph.nodes + .flatMap((node) => node.observations) + .some( + (observation) => + Array.isArray(observation.properties.exports) && + observation.properties.exports.includes("answer"), + ), + ).toBe(true); + const channels = analysis.graph.nodes + .filter((node) => node.kind === "ipc-channel") + .flatMap((node) => + node.observations.map((observation) => observation.properties.channel), + ); + expect(channels).toEqual( + expect.arrayContaining(["typed-channel", "typed-rest", "typed-identity"]), + ); + for (const [name, arity] of [ + ["first", 1], + ["rest", 1], + ["destructured", 2], + ["mixed", 2], + ] as const) { + const callable = graph.nodes.find( + (node) => node.kind === "function" && node.label === name, + ); + expect(callable, name).toBeDefined(); + const fingerprint = graph.fingerprints.find( + (value) => value.function_node_id === callable?.node_id, + ); + expect(fingerprint?.components.parameter_arity, name).toBe(arity); + } + }, +); diff --git a/tests/boundary/filesystem/ciTestReports.test.ts b/tests/boundary/filesystem/ciTestReports.test.ts index 6a59424480..48ba18c114 100644 --- a/tests/boundary/filesystem/ciTestReports.test.ts +++ b/tests/boundary/filesystem/ciTestReports.test.ts @@ -17,6 +17,32 @@ import { workflowGit } from "../../support/workflowGit.js"; const execute = promisify(execFile); const script = new URL("../../../scripts/ci/test-report.mjs", import.meta.url); + +// Each report invocation owns its CI state and summary destination. Inherited +// workflow flags must not change fixture outcomes or append to the real job. +const executeTimingReport = async ( + root: string, + environment: NodeJS.ProcessEnv = {}, +) => { + const summary = join(root, "summary.md"); + await writeFile(summary, ""); + const output = await execute(process.execPath, [fileURLToPath(script)], { + cwd: root, + env: { + ...process.env, + SHARD_RESULT: "success", + MERGE_RESULT: "success", + SOURCE_REQUIRED: "false", + SOURCE_RESULT: "", + CI_VERIFY_TEST_INVENTORY: "false", + CI_TEST_SELECTION: "", + ...environment, + GITHUB_STEP_SUMMARY: summary, + }, + }); + return { ...output, summary: await readFile(summary, "utf8") }; +}; + const stepsSchema = z.array( z.object({ name: z.string().optional(), @@ -225,14 +251,8 @@ it("rejects successful shard reports with missing or duplicate portable files", files: names.map((name) => ({ name, durationMs: 1 })), }), ); - return execute(process.execPath, [fileURLToPath(script)], { - cwd: root, - env: { - ...process.env, - SHARD_RESULT: "success", - MERGE_RESULT: "success", - CI_VERIFY_TEST_INVENTORY: "true", - }, + return executeTimingReport(root, { + CI_VERIFY_TEST_INVENTORY: "true", }); }; await expect( @@ -308,17 +328,11 @@ it("exports portable timing history and removes it on a failed, partial, or inva source?: string, ) => { await writeFile(join(root, "test-timings.json"), JSON.stringify(value)); - await writeFile(summary, ""); - return execute(process.execPath, [fileURLToPath(script)], { - cwd: root, - env: { - ...process.env, - GITHUB_STEP_SUMMARY: summary, - SHARD_RESULT: shards, - SOURCE_REQUIRED: String(source !== undefined), - SOURCE_RESULT: source ?? "", - MERGE_RESULT: merge, - }, + return executeTimingReport(root, { + SHARD_RESULT: shards, + SOURCE_REQUIRED: String(source !== undefined), + SOURCE_RESULT: source ?? "", + MERGE_RESULT: merge, }); }; await run(report); @@ -458,25 +472,17 @@ it("recovers complete module costs from real blob reports without executing test expect(file.durationMs).toBeCloseTo( Object.values(file.phases).reduce((sum, value) => sum + value, 0), ); - const partial = await execute(process.execPath, [fileURLToPath(script)], { - cwd: root, - env: { - ...process.env, - SHARD_RESULT: "skipped", - SOURCE_REQUIRED: "true", - SOURCE_RESULT: "success", - MERGE_RESULT: "success", - }, + const partial = await executeTimingReport(root, { + SHARD_RESULT: "skipped", + SOURCE_REQUIRED: "true", + SOURCE_RESULT: "success", }); - expect(partial.stdout).toContain("Partial test diagnostics"); - expect(partial.stdout).toContain("sample.test.js"); + expect(partial.summary).toContain("Partial test diagnostics"); + expect(partial.summary).toContain("sample.test.js"); await expect( readFile(join(root, ".cache/ci-test-history/test-results.json")), ).rejects.toMatchObject({ code: "ENOENT" }); - await execute(process.execPath, [fileURLToPath(script)], { - cwd: root, - env: { ...process.env, SHARD_RESULT: "success", MERGE_RESULT: "success" }, - }); + await executeTimingReport(root); expect( parseTestDurations( await readFile( @@ -508,10 +514,7 @@ it("recovers complete module costs from real blob reports without executing test files: [{ state: "failed" }], }); // Even a caller accidentally marking the merge successful cannot publish this report. - await execute(process.execPath, [fileURLToPath(script)], { - cwd: root, - env: { ...process.env, SHARD_RESULT: "success", MERGE_RESULT: "success" }, - }); + await executeTimingReport(root); await expect( readFile(join(root, ".cache/ci-test-history/test-results.json")), ).rejects.toMatchObject({ code: "ENOENT" }); @@ -543,21 +546,17 @@ it("records failed omissions from full reports without treating partial or unhan }; const run = async (report: unknown, source = "failure") => { await writeFile(join(root, "test-timings.json"), JSON.stringify(report)); - await execute(process.execPath, [fileURLToPath(script)], { - cwd: root, - env: { - ...process.env, - SHARD_RESULT: "failure", - SOURCE_REQUIRED: "true", - SOURCE_RESULT: source, - MERGE_RESULT: "failure", - CI_VERIFY_TEST_INVENTORY: "true", - CI_TEST_SELECTION: JSON.stringify( - planTestSelection(["src/sqlite/Provider.ts"], "pull_request", "fast"), - ), - GITHUB_SHA: "merge-sha", - CI_PR_HEAD_SHA: "pr-head-sha", - }, + await executeTimingReport(root, { + SHARD_RESULT: "failure", + SOURCE_REQUIRED: "true", + SOURCE_RESULT: source, + MERGE_RESULT: "failure", + CI_VERIFY_TEST_INVENTORY: "true", + CI_TEST_SELECTION: JSON.stringify( + planTestSelection(["src/sqlite/Provider.ts"], "pull_request", "fast"), + ), + GITHUB_SHA: "merge-sha", + CI_PR_HEAD_SHA: "pr-head-sha", }); return JSON.parse( await readFile(