fix: integrate 14 approved fix PRs (668,671,673,675,678-681,685-690) (#705)

* fix(javascript): preserve array index positions

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* fix(javascript): respect object property overwrite order

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* test(javascript): cover uncertain spread discriminants

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* fix(javascript): keep nonfinite values outside JSON lattice

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* fix(dotnet): keep non-CIL method implementations unknown

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* test(package): separate CIL reconstruction from native imports

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* fix(javascript): invalidate values on nontrivial binding writes

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* fix(artifacts): align native DMG mount and detach lifecycle

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* fix(browser): retain all literal worker import arguments

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* fix(native): preserve final demangled symbol whitespace

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* fix(dotnet): preserve C identifier underscores in native matching

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* fix(browser): respect metadata key and overwrite semantics

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* fix(javascript): keep optional parameter input unknown

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* fix(dotnet): report observed method metadata changes

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* docs(dotnet): synchronize generated conformance skill digest

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* fix(browser): reject malformed indexed source-map offsets

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* fix(native): preserve numeric-looking Mach-O identifiers

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* docs: regenerate catalogs for integrated fix batch

* docs: regenerate catalogs after merging main (#702)

---------

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
This commit is contained in:
morlutoandRudyCelekli authored and GitHub committed 2026-10-06 16:31:00 +08:00
1 parent 5bcb6645b5
commit cf061159ab
32 files changed
+797 -73

No files matched your search

+8
View File
@@ -437,3 +437,11 @@ The managed-code track advances as reviewable pull requests:
Each implementation PR updates generated product facts only for behavior it
actually ships and states which real-tool checks were performed.
### Method metadata during build comparison
Managed member comparisons report a `metadata` dimension when an observed
MethodDef flags or implementation flags value differs, including accessibility
or synchronization changes. This is separate from the CIL/signature matching
tiers: identical instructions do not establish unchanged method metadata. The
reported difference is static metadata evidence, not proof of runtime behavior.
+3 -3
View File
@@ -7,7 +7,7 @@
"client": "2.3.1",
"core": "2.3.1"
},
"skill_version": "27"
"skill_version": "24"
},
"tools": {
"total": 117,
@@ -558,9 +558,9 @@
"mcp_prompts": 6
},
"digests": {
"tools_sha256": "eec70858c8de9e04fc3a87f38ad9e2f0b75ed8717afae80d1837a348f1232086",
"tools_sha256": "be74884c49199aa22146a94dfbf8c11dd0ae0b87c0896e32a10b9ff691fdd342",
"prompts_sha256": "4762db348821fec787a5b8c6423f2c48a71f6f0ec2afee97b16ad69030ec504b",
"combined_sha256": "55ea35002d0e7ac5267b58794116927ef784d3d0a64c015fbf523ce680fd1960",
"combined_sha256": "f63ae9b3480f206700cf334c54b3267aff8d0c3a7f4e782d8b5f7952cfdd5271",
"providers_sha256": "c1a6849b2524347e6cf9e004a17ba1805f86114b2da72eb689a075afbbb1aada"
}
}
@@ -1,5 +1,5 @@
{
"manifest_id": "ecm_cbd9c43ea4e600851d7d3035788008fa1ec06dd7072b6c8057692d02e8a76781",
"manifest_id": "ecm_8d5469adca2b281028f4b94f0e82c8a104c988d97542a91b51d903e764d6c779",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@@ -13,7 +13,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "65e107a997c825a5ddc661b1d8f788fc1890a35ed3b55754033b33c8fe20b0c3"
"sha256": "73d1f6ba53bb3d3a115a5a5895dd21975494147b05f3360cc19d35437d060cd0"
}
],
"claims": [
+6 -1
View File
@@ -12,10 +12,15 @@ const buildManagedFixtures = async (workspace) => {
writeFile(
managedPath,
buildManagedPeFixture({
methods: [
{ name: "Main", flags: 0x0016 },
{ name: "NativeMessageBox", flags: 0x2016, rva: 0 },
],
pinvoke: {
moduleName: "user32.dll",
importName: "MessageBoxW",
mappingFlags: 0x0345,
memberRow: 2,
},
}),
),
@@ -59,7 +64,7 @@ const verifyManagedMembers = async ({ cli, managedPath, environment }) => {
);
if (
managedMembers.operation !== "inspect_managed_members" ||
managedMembers.normalized_result?.methods?.length !== 1
managedMembers.normalized_result?.methods?.length !== 2
)
throw new Error("packaged managed member CLI failed");
return managedMembers;
+6 -7
View File
@@ -2,9 +2,9 @@
name: reverse-engineer-anything
description: Reverse engineer native, managed, Electron/JavaScript, packaged, and browser applications with REA. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
metadata:
version: "27"
version: "24"
tool_count: 117
catalog_digest: "55ea35002d0e7ac5267b58794116927ef784d3d0a64c015fbf523ce680fd1960"
catalog_digest: "f63ae9b3480f206700cf334c54b3267aff8d0c3a7f4e782d8b5f7952cfdd5271"
---
# REA
@@ -37,11 +37,10 @@ name to one clear installed artifact when possible; ask only when matches are
ambiguous. Never choose an example app on the user's behalf.
In a target-free session, use `open_binary` to bind any archive/package or
native target whose analysis tool operates on the active target. `tools/list`
includes the complete catalog, including unavailable tools. When choosing a
capability, call `binary_session` with no arguments and read the desired tool's
entry in `result.tool_availability`. Follow its availability reason and
remediation before calling an unavailable operation.
native target whose analysis tool operates on the active target. Do not call a
tool hidden from `tools/list`; inspect `binary_session` with
`detail: "capabilities"` for the exact remediation when a desired capability
is unavailable.
## Work summary-first
+17 -4
View File
@@ -1,4 +1,4 @@
import { mkdtemp, rm } from "node:fs/promises";
import { mkdtemp, realpath, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, join } from "node:path";
import type { Readable } from "node:stream";
@@ -134,7 +134,7 @@ export class NativeDmgArtifactReader implements ArtifactReader {
async attach(signal?: AbortSignal): Promise<void> {
await runChecked(this.host, ["verify", this.path], signal);
this.#provenance.push(command(["verify", this.path], ["read"]));
this.#mountRoot = await mkdtemp(join(tmpdir(), "rea-dmg-"));
this.#mountRoot = await realpath(await mkdtemp(join(tmpdir(), "rea-dmg-")));
try {
const attached = await runChecked(
this.host,
@@ -150,8 +150,21 @@ export class NativeDmgArtifactReader implements ArtifactReader {
signal,
);
const parsed = attachOutputSchema.parse(parse(attached.stdout));
this.#devices = parsed["system-entities"].map(
(entity) => entity["dev-entry"],
const devices = [
...new Set(
parsed["system-entities"].map((entity) => entity["dev-entry"]),
),
];
// Detaching an observed whole disk also detaches its partitions. Do not
// subsequently detach the now-unavailable child devices.
this.#devices = devices.filter(
(device) =>
!devices.some(
(parent) =>
/^\/dev\/disk\d+$/u.test(parent) &&
device.startsWith(`${parent}s`) &&
/^\d+$/u.test(device.slice(parent.length + 1)),
),
);
if (this.#devices.length === 0)
throw new ArtifactReaderFailure(
+10 -1
View File
@@ -284,7 +284,13 @@ const validSourceMapEnvelope = (text: string): boolean => {
const pending: unknown[] = [...parsed.sections];
while (pending.length > 0) {
const section = pending.pop();
if (!isRecord(section) || !isRecord(section.offset) || !("map" in section))
if (
!isRecord(section) ||
!isRecord(section.offset) ||
!validSourceMapOffset(section.offset.line) ||
!validSourceMapOffset(section.offset.column) ||
!("map" in section)
)
return false;
const map = section.map;
if (!isRecord(map) || map.version !== 3) return false;
@@ -295,6 +301,9 @@ const validSourceMapEnvelope = (text: string): boolean => {
return true;
};
const validSourceMapOffset = (value: unknown): boolean =>
typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
const validSourceMapLeaf = (map: Readonly<Record<string, unknown>>): boolean =>
typeof map.mappings === "string" &&
Array.isArray(map.sources) &&
@@ -290,3 +290,186 @@ describe("primitive addition values", () => {
});
});
});
describe("array positional value recovery", () => {
it("keeps holes at their actual destructuring indices", () => {
const ir = analyzeJavaScriptSemantics(`
const list = ["zero", , "two"];
const [first, second, third] = list;
`);
expect(topLevelBinding(ir, "first").value).toEqual({
status: "literal",
value: "zero",
});
expect(topLevelBinding(ir, "second").value.status).toBe("unknown");
expect(topLevelBinding(ir, "third").value).toEqual({
status: "literal",
value: "two",
});
});
it("projects canonical array indices without treating arbitrary strings as indices", () => {
const ir = analyzeJavaScriptSemantics(`
const list = ["zero", "one"];
const numeric = list[1];
const quoted = list["1"];
const leadingZero = list["01"];
const negative = list[-1];
`);
for (const name of ["numeric", "quoted"])
expect(topLevelBinding(ir, name).value).toEqual({
status: "literal",
value: "one",
});
for (const name of ["leadingZero", "negative"])
expect(topLevelBinding(ir, name).value.status).toBe("unknown");
});
it("does not shift trailing values across an unknown spread", () => {
const ir = analyzeJavaScriptSemantics(`
const values = ["first", ...unknownValues, "last"];
const [first, second] = values;
`);
expect(topLevelBinding(ir, "first").value).toEqual({
status: "literal",
value: "first",
});
expect(topLevelBinding(ir, "second").value.status).toBe("unknown");
});
});
describe("object value overwrite boundaries", () => {
it("uses the last named property and keeps later exact overwrites", () => {
const ir = analyzeJavaScriptSemantics(`
const duplicate = { key: "old", key: "new" };
const selected = duplicate.key;
const later = { key: "old", ...unknownObject, key: "final" };
const final = later.key;
`);
expect(topLevelBinding(ir, "selected").value).toEqual({
status: "literal",
value: "new",
});
expect(topLevelBinding(ir, "final").value).toEqual({
status: "literal",
value: "final",
});
});
it("does not retain an exact earlier value after an unknown overwrite", () => {
const ir = analyzeJavaScriptSemantics(`
const spread = { key: "old", ...unknownObject };
const dynamic = { key: "old", [unknownKey]: "new" };
const method = { key: "old", key() { return "new"; } };
const spreadValue = spread.key;
const dynamicValue = dynamic.key;
const methodValue = method.key;
`);
for (const name of ["spreadValue", "dynamicValue", "methodValue"])
expect(topLevelBinding(ir, name).value.status).toBe("unknown");
});
it("admits literal computed keys while excluding prototype setters", () => {
const ir = analyzeJavaScriptSemantics(`
const object = { ["key"]: "value", __proto__: { prototype: true } };
const selected = object.key;
const prototype = object.__proto__;
`);
expect(topLevelBinding(ir, "selected").value).toEqual({
status: "literal",
value: "value",
});
expect(topLevelBinding(ir, "prototype").value.status).toBe("unknown");
});
});
it("retains shorthand __proto__ as an own data property", () => {
const ir = analyzeJavaScriptSemantics(`
const __proto__ = "own";
const object = { __proto__ };
const selected = object.__proto__;
`);
expect(topLevelBinding(ir, "selected").value).toEqual({
status: "literal",
value: "own",
});
});
describe("nonfinite static values", () => {
it.each(['+"not-a-number"', '-"not-a-number"', "1e308 + 1e308", "1e309"])(
"keeps %s unknown instead of publishing a non-JSON number",
(expression) => {
const ir = analyzeJavaScriptSemantics(`const answer = ${expression};`);
expect(topLevelBinding(ir, "answer").value.status).toBe("unknown");
},
);
it("does not collapse an uncertain nonfinite branch to its finite alternative", () => {
const ir = analyzeJavaScriptSemantics(
'const answer = condition ? 1 : +"invalid";',
);
expect(topLevelBinding(ir, "answer").value.status).toBe("ambiguous");
});
});
describe("binding write invalidation", () => {
it.each([
"let value = 1; value++; const observed = value;",
"let value = 1; --value; const observed = value;",
'let value = "old"; [value] = ["new"]; const observed = value;',
'let value = "old"; ({key: value} = {key: "new"}); const observed = value;',
'let value = "old"; for (value of ["new"]) {} const observed = value;',
'let value = "old"; for (value in {new: true}) {} const observed = value;',
])("does not retain an exact initializer across %s", (source) => {
const ir = analyzeJavaScriptSemantics(source);
expect(topLevelBinding(ir, "observed").value.status).toBe("ambiguous");
expect(
topLevelBinding(ir, "value").definitions.some(
({ kind }) => kind === "assignment",
),
).toBe(true);
});
it("does not treat destructuring keys or member owners as assigned bindings", () => {
const ir = analyzeJavaScriptSemantics(`
const key = "stable";
const owner = "stable";
let target = "old";
({key: target} = {key: "new"});
({owner: object.slot} = {owner: "new"});
const preservedKey = key;
const preservedOwner = owner;
`);
for (const name of ["preservedKey", "preservedOwner"])
expect(topLevelBinding(ir, name).value).toEqual({
status: "literal",
value: "stable",
});
});
});
describe("JavaScript semantic analysis: unknown default inputs", () => {
it.each([
'function launch(mode = "safe") { return mode; }',
'function launch({ mode } = { mode: "safe" }) { return mode; }',
'function launch([mode] = ["safe"]) { return mode; }',
])("does not assume an optional argument is omitted: %s", (source) => {
const ir = analyzeJavaScriptSemantics(source);
expect(onlyCallable(ir, "launch").returnSites[0]?.value.status).toBe(
"unknown",
);
});
it("preserves known declaration inputs and unknown catch bindings", () => {
const ir = analyzeJavaScriptSemantics(`
const { mode: retained = "fallback" } = { mode: "actual" };
function caught() {
try { risky(); } catch ({ message = "fallback" }) { return message; }
}
`);
expect(topLevelBinding(ir, "retained").value).toMatchObject({
status: "literal",
value: "actual",
});
expect(onlyCallable(ir, "caught").returnSites[0]?.value.status).toBe(
"unknown",
);
});
});
+40 -4
View File
@@ -253,8 +253,41 @@ const bindInnerDeclaration = (
mutable: true,
kind: "catch",
});
else if (t.isAssignmentExpression(node) && t.isIdentifier(node.left))
addAssignment(node.left, node.right, scope, state);
else if (t.isAssignmentExpression(node)) {
if (t.isIdentifier(node.left))
addAssignment(node.left, node.right, scope, state);
else
for (const identifier of assignedPatternIdentifiers(node.left))
addAssignment(identifier, node, scope, state);
} else if (t.isUpdateExpression(node) && t.isIdentifier(node.argument))
addAssignment(node.argument, node, scope, state);
else if (t.isForOfStatement(node) || t.isForInStatement(node))
for (const identifier of assignedPatternIdentifiers(node.left))
addAssignment(identifier, node, scope, state);
};
const assignedPatternIdentifiers = (
pattern: t.Node,
): readonly t.Identifier[] => {
const identifiers: t.Identifier[] = [];
const pending = [pattern];
while (pending.length > 0) {
const node = pending.pop();
if (node === undefined) continue;
if (t.isIdentifier(node)) identifiers.push(node);
else if (t.isRestElement(node)) pending.push(node.argument);
else if (t.isAssignmentPattern(node)) pending.push(node.left);
else if (t.isArrayPattern(node)) {
for (const element of node.elements)
if (element !== null) pending.push(element);
} else if (t.isObjectPattern(node)) {
for (const property of node.properties)
pending.push(
t.isRestElement(property) ? property.argument : property.value,
);
}
}
return identifiers;
};
const nestedScope = (
@@ -398,7 +431,10 @@ const bindPattern = (input: BindPatternInput): void => {
bindPattern({
...input,
pattern: pattern.left,
initializer: initializer ?? pattern.right,
initializer:
kind === "parameter" || kind === "catch"
? initializer
: (initializer ?? pattern.right),
});
return;
}
@@ -488,7 +524,7 @@ const createBinding = (
const addAssignment = (
identifier: t.Identifier,
initializer: t.Expression,
initializer: t.Node,
scope: JavaScriptSemanticScopeState,
state: JavaScriptSemanticAnalysisState,
): void => {
+9 -1
View File
@@ -11,6 +11,13 @@ import { semanticPrimitiveKey } from "./javascriptSemanticProvenance.js";
export const semanticPrimitiveSet = (
values: readonly JavaScriptSemanticPrimitive[],
): JavaScriptSemanticValue => {
if (
values.some((value) => typeof value === "number" && !Number.isFinite(value))
)
return {
status: "unknown",
reason: "Nonfinite numbers are outside the JSON primitive lattice.",
};
const unique = [
...new Map(
values.map((value) => [semanticPrimitiveKey(value), value]),
@@ -40,7 +47,8 @@ export const semanticPrimitiveValue = (
):
| { readonly found: true; readonly value: JavaScriptSemanticPrimitive }
| { readonly found: false } => {
if (t.isStringLiteral(node) || t.isNumericLiteral(node))
if (t.isStringLiteral(node)) return { found: true, value: node.value };
if (t.isNumericLiteral(node) && Number.isFinite(node.value))
return { found: true, value: node.value };
if (t.isBooleanLiteral(node)) return { found: true, value: node.value };
if (t.isNullLiteral(node)) return { found: true, value: null };
+55 -18
View File
@@ -12,10 +12,7 @@ import {
type JavaScriptSemanticAnalysisState,
type JavaScriptSemanticBindingState,
} from "./javascriptSemanticState.js";
import {
propertyName,
stringValue,
} from "./javascriptStaticAnalysisHelpers.js";
import { stringValue } from "./javascriptStaticAnalysisHelpers.js";
import { compareCodePoints } from "./canonicalOrdering.js";
import { semanticStaticPropertyName } from "./javascriptAstValues.js";
import {
@@ -153,32 +150,57 @@ const evaluateObject = (
node: t.ObjectExpression,
context: EvaluationContext,
): JavaScriptSemanticValue => {
const properties: JavaScriptSemanticProperty[] = [];
const propertiesByName = new Map<string, JavaScriptSemanticProperty>();
let unknownProperties = false;
let omittedProperties: number | null = 0;
const invalidateEarlierProperties = (): void => {
for (const [name] of propertiesByName)
propertiesByName.set(name, {
name,
value: {
status: "unknown",
reason: "A later property may overwrite this value.",
},
});
};
for (const property of node.properties) {
if (t.isSpreadElement(property)) {
unknownProperties = true;
omittedProperties = null;
invalidateEarlierProperties();
continue;
}
if (property.computed) {
const name = semanticStaticPropertyName(property.key, property.computed);
if (name === "" && !t.isStringLiteral(property.key, { value: "" })) {
unknownProperties = true;
if (omittedProperties !== null) omittedProperties += 1;
invalidateEarlierProperties();
continue;
}
// This spelling changes the prototype instead of defining an own slot.
if (
t.isObjectProperty(property) &&
!property.computed &&
!property.shorthand &&
name === "__proto__"
) {
unknownProperties = true;
if (omittedProperties !== null) omittedProperties += 1;
continue;
}
const name = propertyName(property.key);
if (name === "" || !t.isObjectProperty(property)) {
unknownProperties = true;
if (omittedProperties !== null) omittedProperties += 1;
continue;
}
properties.push({
propertiesByName.set(name, {
name,
value: evaluateExpression(property.value, nestedContext(context)),
value: t.isObjectProperty(property)
? evaluateExpression(property.value, nestedContext(context))
: {
status: "unknown",
reason: "Object method or accessor value is not a primitive.",
},
});
}
properties.sort((left, right) => compareCodePoints(left.name, right.name));
const properties = [...propertiesByName.values()].sort((left, right) =>
compareCodePoints(left.name, right.name),
);
return unknownProperties
? {
status: "object",
@@ -205,11 +227,16 @@ const evaluateArray = (
if (t.isSpreadElement(element)) {
unknownItems = true;
omittedItems = null;
continue;
// Subsequent elements have no fixed index after an unknown-length spread.
break;
}
if (element === null) {
unknownItems = true;
if (omittedItems !== null) omittedItems += 1;
items.push({
status: "unknown",
reason: "Array hole has no primitive value.",
});
continue;
}
items.push(evaluateExpression(element, nestedContext(context)));
@@ -292,8 +319,18 @@ const projectValue = (
reason: `Object property ${key} was not observed.`,
};
current = property.value;
} else if (current.status === "array" && typeof key === "number") {
const item = current.items[key];
} else if (current.status === "array") {
const index = typeof key === "number" ? key : Number(key);
if (
!Number.isSafeInteger(index) ||
index < 0 ||
String(index) !== String(key)
)
return {
status: "unknown",
reason: `Array property ${String(key)} is not a canonical index.`,
};
const item = current.items[index];
if (item === undefined)
return {
status: "unknown",
+1
View File
@@ -125,6 +125,7 @@ const methodComparisonItemSchema = comparisonItemSchema(
item_id: prefixedDigestSchema("mmc_method"),
dimensions: z.array(
z.enum([
"metadata",
"signature",
"cil",
"opcode-shape",
@@ -36,6 +36,8 @@ const methodDimensions = (
readonly bodyUnknown: boolean;
} => {
const dimensions: MethodItem["dimensions"] = [];
if (left.flags !== right.flags || left.impl_flags !== right.impl_flags)
dimensions.push("metadata");
if (left.signature.raw_sha256 !== right.signature.raw_sha256)
dimensions.push("signature");
const leftBodyComplete =
+6 -5
View File
@@ -100,10 +100,11 @@ export const collectNativeSymbols = (
return { symbols, accepted, unsupported };
};
const normalizeSymbol = (value: string): string => value.replace(/^_+/u, "");
const sameSymbolName = (left: string, right: string): boolean =>
normalizeSymbol(left) === normalizeSymbol(right);
const sameSymbolName = (expected: string, native: NativeSymbol): boolean =>
expected ===
(native.source === "macho-export" && native.name.startsWith("_")
? native.name.slice(1)
: native.name);
const normalizeModule = (value: string): string =>
basename(value)
@@ -164,7 +165,7 @@ const matchPinvoke = (
): PinvokeCandidateMatch => {
const names = candidateNames(managed);
const allCandidates = symbols.filter((symbol) =>
names.some((name) => sameSymbolName(name, symbol.name)),
names.some((name) => sameSymbolName(name, symbol)),
);
const [first, ...remaining] = allCandidates;
if (first === undefined)
+46
View File
@@ -323,3 +323,49 @@ it("keeps bare module locations unresolved without inventing URL-relative paths"
)?.resolved_url,
).toBe(`${origin}/assets/worker.js`);
});
it("retains each literal importScripts argument in source order", () => {
const result = analyzeCapturedWebBundle(
inspection(
'importScripts("first.js", dynamicValue, "second.js", "first.js");',
),
);
expect(
result.observations.chunks.edges
.filter(({ kind }) => kind === "worker_import")
.map(({ specifier }) => specifier),
).toEqual(["first.js", "second.js"]);
});
describe("web metadata key semantics", () => {
it("keeps computed metadata keys and overwrite order conservative", () => {
const result = analyzeCapturedWebBundle(
inspection(`
const path = "description";
const routes = [{ [path]: "/not-a-route" }, { ["path"]: "/real" }];
document.modelContext.registerTool({
name: "old", name: "new",
inputSchema: { properties: { [name]: {}, literal: {} } }
});
document.modelContext.registerTool({ name: "old", ...dynamic });
document.modelContext.registerTool({ name: "old", [name]: "dynamic" });
document.modelContext.registerTool({ name: "old", get name() {} });
document.modelContext.registerTool({ ...dynamic, ["name"]: "restored" });
document.modelContext.registerTool({ name: "valid", [""]: 0 });
`),
);
expect(result.observations.routes.map(({ value }) => value)).toEqual([
"/real",
]);
expect(result.observations.webmcp_declarations).toEqual([
expect.objectContaining({
name: "new",
schema_property_names: ["literal"],
}),
expect.objectContaining({ name: null }),
expect.objectContaining({ name: "restored" }),
expect.objectContaining({ name: "valid" }),
]);
});
});
+13 -4
View File
@@ -1,6 +1,7 @@
import * as t from "@babel/types";
import { sanitizeBrowserUrl } from "./browserObservation.js";
import { semanticStaticPropertyName } from "./javascriptAstValues.js";
/** Name of a call/new callee from its AST node. */
export const calleeName = (callee: t.Node): string => {
@@ -30,10 +31,18 @@ export const objectValue = (
object: t.ObjectExpression,
name: string,
): t.ObjectProperty["value"] | undefined => {
for (const property of object.properties)
if (t.isObjectProperty(property) && propertyName(property.key) === name)
return property.value;
return undefined;
let value: t.ObjectProperty["value"] | undefined;
for (const property of object.properties) {
if (t.isSpreadElement(property)) {
value = undefined;
continue;
}
const key = semanticStaticPropertyName(property.key, property.computed);
if (key === "" && !t.isStringLiteral(property.key)) value = undefined;
else if (key === name)
value = t.isObjectProperty(property) ? property.value : undefined;
}
return value;
};
export const objectString = (
+11 -14
View File
@@ -4,6 +4,7 @@ import * as t from "@babel/types";
import { sanitizeEndpointCandidate } from "./browserObservation.js";
import type { WebPageInspection } from "./browserObservation.js";
import type { WebBundleAnalysis } from "./webBundleAnalysis.js";
import { semanticStaticPropertyName } from "./javascriptAstValues.js";
import {
calleeName,
endpointArgument,
@@ -11,7 +12,6 @@ import {
locationFields,
objectString,
objectValue,
propertyName,
resolveSpecifier,
isUrlLikeModuleSpecifier,
stringArgument,
@@ -118,17 +118,12 @@ const inspectCall = (
node,
accumulator,
});
if (
(name === "importScripts" || name.endsWith(".importScripts")) &&
first !== undefined
)
addEdge({
script,
specifier: first,
kind: "worker_import",
node,
accumulator,
});
if (name === "importScripts" || name.endsWith(".importScripts"))
for (const argument of node.arguments) {
const specifier = stringArgument(argument);
if (specifier === undefined) continue;
addEdge({ script, specifier, kind: "worker_import", node, accumulator });
}
if (
routeCallNames.some(
(candidate) => name === candidate || name.endsWith(`.${candidate}`),
@@ -172,7 +167,7 @@ const inspectRouteProperty = (
node: t.ObjectProperty,
accumulator: AnalysisAccumulator,
): void => {
const key = propertyName(node.key);
const key = semanticStaticPropertyName(node.key, node.computed);
if ((key === "path" || key === "route") && t.isStringLiteral(node.value))
addFinding({
collection: accumulator.routes,
@@ -251,7 +246,9 @@ const addWebMcpDeclaration = (
const schemaPropertyNames = t.isObjectExpression(propertyObject)
? propertyObject.properties.flatMap((property) =>
t.isObjectProperty(property)
? [propertyName(property.key)].filter(Boolean)
? [
semanticStaticPropertyName(property.key, property.computed),
].filter(Boolean)
: [],
)
: [];
+36
View File
@@ -165,3 +165,39 @@ it("preserves leading U+FEFF as metadata name content", () => {
"\uFEFFFixture.\uFEFFProgram.\uFEFFMain",
);
});
it.each([
{ implFlags: 1, flags: 0, label: "native" },
{ implFlags: 3, flags: 0, label: "runtime" },
{ implFlags: 4, flags: 0, label: "unmanaged" },
{ implFlags: 0, flags: 0x2000, label: "pinvoke" },
])(
"does not interpret $label implementations as CIL",
({ implFlags, flags }) => {
const bytes = buildManagedPeFixture();
const original = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
const row = original.methods[0]?.row_offset;
expect(row).toBeDefined();
if (row === undefined) return;
bytes.writeUInt16LE(implFlags, row + 4);
bytes.writeUInt16LE(bytes.readUInt16LE(row + 6) | flags, row + 6);
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.methods[0]?.body).toMatchObject({
status: "partial",
header_format: "unknown",
rva: original.methods[0]?.rva,
il_sha256: null,
normalized_il_sha256: null,
anchors: [],
});
expect(result.methods[0]?.body.issue).toMatch(/CIL/u);
expect(result.call_edges).toEqual([]);
expect(result.field_accesses).toEqual([]);
},
);
+1 -1
View File
@@ -153,7 +153,7 @@ export const parseMethods = ({
impl_flags: implFlags,
flags,
signature: signature(sig),
body: methodBody(bytes, pe, rva),
body: methodBody(bytes, pe, rva, { implFlags, flags }),
});
core.set(token, { token, name, declaringType: declared?.fullName ?? null });
}
+15 -1
View File
@@ -60,12 +60,26 @@ const readMethodBodyHeader = (
};
};
/** Decode admitted managed CIL, retaining unavailable implementation metadata as partial. */
export const methodBody = (
bytes: Buffer,
pe: ManagedPeLayout,
rva: number,
implementation: { readonly implFlags: number; readonly flags: number } = {
implFlags: 0,
flags: 0,
},
): ManagedMethodBody => {
if (rva === 0) return emptyMethodBody(rva, "absent", null);
if (
(implementation.implFlags & 7) !== 0 ||
(implementation.flags & 0x2000) !== 0
)
return emptyMethodBody(
rva,
"partial",
"CIL decoding is unavailable for native, runtime, unmanaged, or P/Invoke implementations",
);
try {
const offset = pe.rvaToOffset(rva, 1, "method.body");
const header = readMethodBodyHeader(bytes, offset);
@@ -148,7 +162,7 @@ export const methodBody = (
const emptyMethodBody = (
rva: number,
status: "absent" | "malformed",
status: "absent" | "malformed" | "partial",
issue: string | null,
): ManagedMethodBody => ({
status,
+1 -1
View File
@@ -30,7 +30,7 @@ interface GeneratedPayload {
}
const GENERATED_PAYLOAD_JSON =
'{\n "auxiliaryProviders": [\n {\n "capabilities": [\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": true,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "inventory_artifact",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": true,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "inspect_artifact",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": true,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "extract_artifact",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": false,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "decode_interface_builder",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": false,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "inspect_keyed_archive",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DLine truncated
'{\n "auxiliaryProviders": [\n {\n "capabilities": [\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": true,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "inventory_artifact",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": true,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "inspect_artifact",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": true,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "extract_artifact",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": false,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "decode_interface_builder",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": false,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",\n "ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."\n ],\n "operation": "inspect_keyed_archive",\n "provider": {\n "id": "rea-artifact-graph",\n "name": "REA safe artifact graph provider",\n "version": "1"\n },\n "reason": null\n },\n {\n "available": true,\n "effects": {\n "changesPermissions": false,\n "launchesProcess": true,\n "mayAccessNetwork": false,\n "mayShowUi": false,\n "mayWriteFilesystem": false,\n "mutatesArtifact": false,\n "requiresRoot": false\n },\n "limitations": [\n "DLine truncated
const generatedPayload: unknown = JSON.parse(GENERATED_PAYLOAD_JSON);
if (!isGeneratedPayload(generatedPayload))
throw new TypeError("Generated MCP catalog payload is invalid");
+1 -1
View File
@@ -5,5 +5,5 @@ export const PACKAGE_METADATA = {
serverSdkVersion: "2.3.1",
clientSdkVersion: "2.3.1",
coreSdkVersion: "2.3.1",
skillVersion: "27",
skillVersion: "24",
} as const;
+27
View File
@@ -0,0 +1,27 @@
import { expect, it } from "vitest";
import { parseDemangledSymbols } from "./demangle.js";
it.each(["ordinary name ", "ordinary\t", " "])(
"preserves unchanged final symbol text: %j",
(input) => {
expect(parseDemangledSymbols([input], `${input}\n`)).toEqual([
{ input, output: input, status: "unchanged" },
]);
},
);
it("removes only the line terminator and preserves mixed output ordering", () => {
expect(
parseDemangledSymbols(
["$s4main5helloyyF", "plain "],
"main.hello() -> ()\r\nplain \r\n",
),
).toEqual([
{
input: "$s4main5helloyyF",
output: "main.hello() -> ()",
status: "demangled",
},
{ input: "plain ", output: "plain ", status: "unchanged" },
]);
});
+1 -1
View File
@@ -11,7 +11,7 @@ export const parseDemangledSymbols = (
inputs: readonly string[],
output: string,
) => {
const lines = output.trimEnd().split(/\r?\n/u);
const lines = output.replace(/\r?\n$/u, "").split(/\r?\n/u);
if (lines.length !== inputs.length)
throw new TypeError("swift-demangle output count does not match input");
return inputs.map((input, index) => {
+4 -1
View File
@@ -219,7 +219,10 @@ const parseFields = (block: string): Record<string, string | number | null> => {
const match = /^((?:time stamp)|\S+(?:\s+version)?)\s+(.+)$/u.exec(line);
if (match?.[1] === undefined || match[2] === undefined) continue;
const value = match[2].trim();
fields[match[1]] = numeric(value) ?? value;
fields[match[1]] =
match[1] === "segname" || match[1] === "sectname"
? value
: (numeric(value) ?? value);
}
return fields;
};
@@ -225,6 +225,47 @@ describe("web source-map fetching and validation", () => {
});
});
describe("indexed source-map offsets", () => {
it.each([
{ line: 0, column: -1 },
{ line: -1, column: 0 },
{ line: 0, column: "1" },
{ line: "0", column: 0 },
{ line: 0, column: 0.5 },
{ line: 0.5, column: 0 },
{ line: 0 },
{ line: 0, column: Number.MAX_SAFE_INTEGER + 1 },
])("retains an invalid-map item for malformed offset %j", async (offset) => {
const result = await fetchWebSourceMaps([request], input(), undefined, {
fetch: () =>
Promise.resolve(
new Response(
JSON.stringify({
version: 3,
sections: [
{
offset,
map: {
version: 3,
names: [],
sources: ["original.js"],
sourcesContent: ["export const stable = 1;"],
mappings: "AAAA",
},
},
],
}),
{ status: 200 },
),
),
});
expect(result).toMatchObject({
status: "unavailable",
items: [{ status: "invalid", artifact: null, mappings: [] }],
});
});
});
describe("web source-map sourcesContent validation", () => {
it.each([
["a scalar sourcesContent value", { sourcesContent: "source" }],
@@ -70,6 +70,25 @@ describe("JavaScript export return-shape comparison", () => {
});
it("keeps additions unknown when spread coverage is incomplete", async () => {
const [left, right] = await analyzeSources({
left: `
const dynamic = getDynamic();
export default () => ({ ...dynamic, type: "heading" });
`,
right: `
const dynamic = getDynamic();
export default () => ({ depth: 1, ...dynamic, type: "heading" });
`,
});
const result = compare(left, right);
expect(result.changes).toEqual([
expect.objectContaining({ status: "unknown", path: "/depth" }),
]);
expect(result.coverage.status).toBe("partial");
});
it("does not pair discriminants that an unknown trailing spread can overwrite", async () => {
const [left, right] = await analyzeSources({
left: `
const dynamic = getDynamic();
@@ -83,9 +102,29 @@ describe("JavaScript export return-shape comparison", () => {
const result = compare(left, right);
expect(result.changes).toEqual([
expect.objectContaining({ status: "unknown", path: "/depth" }),
expect.objectContaining({
status: "unknown",
path: "",
discriminant: null,
}),
expect.objectContaining({
status: "unknown",
path: "",
discriminant: null,
}),
]);
expect(result.coverage.status).toBe("partial");
expect(result.summary).toEqual({
added: 0,
removed: 0,
changed: 0,
unknown: 2,
});
expect(result.coverage).toMatchObject({
status: "partial",
paired_variants: 0,
unpaired_left_variants: 1,
unpaired_right_variants: 1,
});
});
it("does not pair ambiguous variants or invent behavior for no-return exports", async () => {
@@ -18,6 +18,42 @@ import { inspectManagedMembersBytes } from "../../../../src/dotnet/ManagedMember
import { managedPeFixtureTarget } from "../../../../src/dotnet/ManagedPe.fixture.js";
import { buildManagedPeFixture } from "../../../../src/dotnet/ManagedPe.fixture.js";
it.each([
["accessibility", 6, 0x0011],
["synchronized implementation", 4, 0x0020],
])(
"reports changed observed method %s despite identical CIL",
async (_name, flagOffset, flags) => {
const directory = await createTestTempDirectory("rea-managed-flags-");
const leftPath = join(directory, "left.dll");
const rightPath = join(directory, "right.dll");
const left = buildManagedPeFixture();
const inspection = inspectManagedMembersBytes(
left,
managedPeFixtureTarget(left, leftPath),
);
const method = inspection.methods[0];
if (method === undefined) throw new Error("missing method fixture");
const right = Buffer.from(left);
right.writeUInt16LE(flags, method.row_offset + flagOffset);
await writeFile(leftPath, left);
await writeFile(rightPath, right);
const result = await compareManagedMemberPaths({ leftPath, rightPath });
expect(result.ok).toBe(true);
if (!result.ok) return;
const comparison = managedMemberComparisonResultSchema.parse(
result.value.normalized_result,
);
expect(comparison.methods[0]).toMatchObject({
status: "changed",
dimensions: ["metadata"],
});
expect(comparison.methods[0]?.left?.normalized_il_sha256).toBe(
comparison.methods[0]?.right?.normalized_il_sha256,
);
},
);
describe("managed member comparison path workflow", () => {
it("preserves undecoded signatures as unknown through content-addressed partial Evidence", () => {
const observe = (signature: number, path: string) => {
@@ -181,3 +181,53 @@ it("retains U+FEFF metadata through the filesystem target and provider", async (
fields: [expect.objectContaining({ name: "\uFEFFcounter" })],
});
});
it("keeps non-CIL implementation bytes unknown through the real file provider", async () => {
const directory = await createTestTempDirectory("rea-managed-native-body-");
const path = join(directory, "fixture.exe");
const bytes = buildManagedPeFixture();
await writeFile(path, bytes);
const originalTarget = await parseBinaryTarget(path);
expect(originalTarget.ok).toBe(true);
if (!originalTarget.ok) return;
const original = await new ManagedStaticProvider()
.createClient(originalTarget.value)
.execute("inspect_managed_members", {});
expect(original.ok).toBe(true);
if (!original.ok) return;
const row = asManagedMemberResult(original.value).methods[0]?.row_offset;
expect(row).toBeDefined();
if (row === undefined) return;
bytes.writeUInt16LE(1, row + 4);
await writeFile(path, bytes);
const nativeTarget = await parseBinaryTarget(path);
expect(nativeTarget.ok).toBe(true);
if (!nativeTarget.ok) return;
const native = await new ManagedStaticProvider()
.createClient(nativeTarget.value)
.execute("inspect_managed_members", {});
expect(native.ok).toBe(true);
if (!native.ok) return;
const result = asManagedMemberResult(native.value);
expect(result.methods[0]?.body).toMatchObject({
status: "partial",
rva: 0x2800,
il_sha256: null,
anchors: [],
});
expect(result.call_edges).toEqual([]);
expect(result.field_accesses).toEqual([]);
bytes.writeUInt32LE(0, row);
await writeFile(path, bytes);
const absentTarget = await parseBinaryTarget(path);
expect(absentTarget.ok).toBe(true);
if (!absentTarget.ok) return;
const absent = await new ManagedStaticProvider()
.createClient(absentTarget.value)
.execute("inspect_managed_members", {});
expect(absent.ok).toBe(true);
if (!absent.ok) return;
expect(asManagedMemberResult(absent.value).methods[0]?.body.status).toBe(
"absent",
);
});
@@ -1,4 +1,4 @@
import { mkdir, writeFile } from "node:fs/promises";
import { mkdir, realpath, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { build } from "plist";
@@ -81,3 +81,47 @@ describe("native DMG artifact reader", () => {
).rejects.toThrow("cleanup could not detach every device");
});
});
it("owns the canonical mount root and detaches each observed whole image once", async () => {
const calls: string[][] = [];
const host: NativeDmgHost = {
async run(arguments_) {
calls.push([...arguments_]);
if (arguments_[0] !== "attach") return { stdout: "", exitCode: 0 };
const mountRoot = arguments_[arguments_.indexOf("-mountroot") + 1];
if (mountRoot === undefined) throw new Error("missing mount root");
expect(mountRoot).toBe(await realpath(mountRoot));
const mountPoint = join(await realpath(mountRoot), "Fixture");
await mkdir(mountPoint);
await writeFile(join(mountPoint, "hello.txt"), "hello");
return {
stdout: build({
"system-entities": [
{ "dev-entry": "/dev/disk41" },
{ "dev-entry": "/dev/disk41s1" },
{ "dev-entry": "/dev/disk41s2", "mount-point": mountPoint },
{ "dev-entry": "/dev/disk42" },
{ "dev-entry": "/dev/disk42s1" },
],
}),
exitCode: 0,
};
},
};
const reader = await NativeDmgArtifactReader.create(
"/tmp/image.dmg",
undefined,
host,
);
try {
const entries = [];
for await (const entry of reader.entries()) entries.push(entry.path);
expect(entries).toContain("image.dmg/Fixture/hello.txt");
} finally {
await reader.close();
}
expect(calls.filter((call) => call[0] === "detach")).toEqual([
["detach", "/dev/disk42"],
["detach", "/dev/disk41"],
]);
});
@@ -1,5 +1,6 @@
import { expect, it } from "vitest";
import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js";
import { parseOtoolLoadCommands } from "../../../../src/native/parsers/otool.js";
import { parseDyldSymbols } from "../../../../src/native/parsers/dyldInfo.js";
import {
NativeFixtureRunner as FixtureRunner,
@@ -166,3 +167,36 @@ it("retains symbol names containing spaces in dyld inventory rows", () => {
},
]);
});
it("preserves numeric-looking segment and section identifiers from otool", () => {
const parsed = parseOtoolLoadCommands(`Load command 1
cmd LC_SEGMENT_64
cmdsize 152
segname 0001
vmaddr 0x0000000000004000
vmsize 0x0000000000004000
fileoff 16384
filesize 16384
maxprot 0x00000003
initprot 0x00000003
nsects 1
flags 0x4
Section
sectname 0002
segname 0001
addr 0x0000000000004000
size 0x0000000000000008
offset 16384
align 2^0 (1)
reloff 0
nreloc 0
flags 0x00000000
reserved1 0
reserved2 0
`);
expect(parsed.segments[0]).toMatchObject({
name: "0001",
file_offset: 16384,
sections: [{ segment: "0001", name: "0002", size: 8 }],
});
});
@@ -8,6 +8,7 @@ import {
managedNativeVerificationResultSchema,
verifyManagedNativeBoundaries,
} from "../../../../src/domain/managedNativeVerification.js";
import { managedNativeBoundaryInspectionSchema } from "../../../../src/domain/managedArtifact.js";
import { inspectMachoSchema } from "../../../../src/domain/nativeInspection.js";
const exampleInput = () =>
@@ -263,3 +264,48 @@ describe("managed/native verification result algebra", () => {
).toBe(false);
});
});
it("does not erase a C identifier underscore beyond the Mach-O ABI prefix", () => {
const result = verifyManagedNativeBoundaries({
...exampleInput(),
native_observations: [nativeEvidenceWithExports(["__open_native"])],
});
expect(result.summary).toMatchObject({ inferred: 0, unresolved: 1 });
expect(result.pinvoke_imports[0]).toMatchObject({
status: "unresolved",
candidates: [],
matched_native: null,
});
});
it("does not treat a raw Mach-O ABI name as an exact underscored C identifier", () => {
const input = exampleInput();
const managed = input.managed_boundaries;
const normalized = managedNativeBoundaryInspectionSchema.parse(
managed.normalized_result,
);
const imports = normalized.pinvoke_imports;
const result = verifyManagedNativeBoundaries({
...input,
managed_boundaries: createEvidence(undefined, managed.provider, {
operation: managed.operation,
parameters: managed.parameters,
result: {
...normalized,
pinvoke_imports: imports.map((item) => ({
...item,
import_name: "_open_native",
no_mangle: true,
})),
},
rawResult: null,
}),
native_observations: [nativeEvidenceWithExports(["_open_native"])],
});
expect(result.summary).toMatchObject({
verified: 0,
inferred: 0,
unresolved: 1,
});
expect(result.pinvoke_imports[0]?.candidates).toEqual([]);
});