mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
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:
1 parent
5bcb6645b5
commit
cf061159ab
32 files changed
+797
-73
No files matched your search
@@ -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.
|
||||
@@ -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": [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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" }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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)
|
||||
: [],
|
||||
)
|
||||
: [];
|
||||
|
||||
@@ -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([]);
|
||||
},
|
||||
);
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
@@ -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" },
|
||||
]);
|
||||
});
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
Reference in new issue
Block a user