mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(artifacts): preserve NIB field and class identities (#2106)
This commit is contained in:
1 parent
6c816af119
commit
f8e95799f9
2 files changed
+120
-2
No files matched your search
@@ -134,7 +134,14 @@ export const decodeNibArchive = (
|
||||
const value = values.get(index);
|
||||
if (value === undefined) continue;
|
||||
const key = keys[value.key_index];
|
||||
if (key !== undefined) fields[key] = value.value;
|
||||
if (key !== undefined)
|
||||
// Archive keys are data, including the legacy __proto__ setter name.
|
||||
Object.defineProperty(fields, key, {
|
||||
value: value.value,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
return [{ id, class_name: className, values: fields }];
|
||||
});
|
||||
@@ -519,7 +526,8 @@ const decodeUtf8 = (
|
||||
label: string,
|
||||
): string => {
|
||||
try {
|
||||
return new TextDecoder("utf-8", { fatal: true }).decode(
|
||||
// These are length-prefixed names, not standalone text documents.
|
||||
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
|
||||
bytes.subarray(start, end),
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { encodeNibArchiveFixture } from "../../../src/artifacts/apple/NibArchive.fixture.js";
|
||||
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
||||
import { interfaceBuilderAnalysisSchema } from "../../../src/domain/apple/interfaceBuilderGraph.js";
|
||||
import { thinMach } from "../../../src/domain/binaryTarget.fixture.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
cliTest(
|
||||
"preserves literal NIB fields and UTF-8 names through CLI and stdio MCP",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-nib-field-identity-");
|
||||
const app = join(root, "Fixture.app");
|
||||
const contents = join(app, "Contents");
|
||||
await mkdir(join(contents, "Resources"), { recursive: true });
|
||||
await mkdir(join(contents, "MacOS"));
|
||||
await writeFile(
|
||||
join(contents, "MacOS/App"),
|
||||
thinMach(0xfeedfacf, 0x0100000c),
|
||||
);
|
||||
await writeFile(
|
||||
join(contents, "Info.plist"),
|
||||
"<plist><dict><key>CFBundleExecutable</key><string>App</string><key>CFBundleIdentifier</key><string>dev.rea.nib-fields</string></dict></plist>",
|
||||
);
|
||||
const archive = encodeNibArchiveFixture({
|
||||
classes: ["UIView", "\uFEFFCustomView"],
|
||||
objects: [
|
||||
{
|
||||
classIndex: 0,
|
||||
values: {
|
||||
["__proto__"]: { ref: 1 },
|
||||
constructor: 7,
|
||||
enabled: true,
|
||||
"\uFEFFenabled": false,
|
||||
},
|
||||
},
|
||||
{ classIndex: 1, values: { ["__proto__"]: 23 } },
|
||||
],
|
||||
});
|
||||
const path = join(contents, "Resources/Panel.nib");
|
||||
await writeFile(path, archive);
|
||||
const result = await cli.run({
|
||||
arguments: ["decode-interface-builder", app, "--json"],
|
||||
environment: { REA_LOG_LEVEL: "silent", REA_ANALYSIS_PROVIDER: "auto" },
|
||||
});
|
||||
expect(result.exitCode, result.stdout + result.stderr).toBe(0);
|
||||
const contract = toolContract("decode_interface_builder");
|
||||
const evidence = contract.outputSchema.parse(result.json);
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [resolve("scripts/rea.mjs"), "mcp"],
|
||||
env: {
|
||||
PATH: process.env.PATH ?? "",
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_ANALYSIS_PROVIDER: "auto",
|
||||
},
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "nib-field-identity", version: "1" });
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const opened = await client.callTool({
|
||||
name: "open_binary",
|
||||
arguments: { path: app },
|
||||
});
|
||||
expect(opened.isError, JSON.stringify(opened)).not.toBe(true);
|
||||
const response = await client.callTool({
|
||||
name: contract.name,
|
||||
arguments: {},
|
||||
});
|
||||
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
||||
const mcp = contract.outputSchema.parse(response.structuredContent);
|
||||
expect(mcp.normalized_result).toEqual(evidence.normalized_result);
|
||||
} finally {
|
||||
try {
|
||||
await client.close();
|
||||
} finally {
|
||||
await transport.close();
|
||||
}
|
||||
}
|
||||
const report = interfaceBuilderAnalysisSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
expect(report.documents[0]?.archive_sha256).toBe(
|
||||
createHash("sha256").update(archive).digest("hex"),
|
||||
);
|
||||
const objects = report.graph.nodes.filter(
|
||||
({ attributes }) => attributes.interface_builder_object_id !== undefined,
|
||||
);
|
||||
expect(objects).toHaveLength(2);
|
||||
expect(objects[0]?.attributes.nibValues).toEqual({
|
||||
["__proto__"]: { objectID: "1" },
|
||||
constructor: 7,
|
||||
enabled: true,
|
||||
"\uFEFFenabled": false,
|
||||
});
|
||||
expect(objects[1]?.attributes).toMatchObject({
|
||||
class_name: "\uFEFFCustomView",
|
||||
nibValues: { ["__proto__"]: 23 },
|
||||
});
|
||||
expect(await readFile(path)).toEqual(archive);
|
||||
},
|
||||
60_000,
|
||||
);
|
||||
Reference in new issue
Block a user