import { mkdir, writeFile } from "node:fs/promises"; import { join, resolve } from "node:path"; import { Client } from "@modelcontextprotocol/client"; import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; import { describe, expect, onTestFinished } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { cliTest } from "../../support/cli/cliFixture.js"; const encodingFixture = () => { const xml = 'CFBundleExecutableAppCFBundleIdentifierdev.rea.encoding'; const utf16 = Buffer.from(xml, "utf16le"); const utf8 = xml.replace("UTF-16", "UTF-8"); const cases = [ { name: "UTF-8 BOM", bytes: Buffer.concat([ Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(utf8), ]), valid: true, }, { name: "UTF-16 LE", bytes: Buffer.concat([Buffer.from([0xff, 0xfe]), utf16]), valid: true, }, { name: "UTF-16 BE", bytes: Buffer.concat([ Buffer.from([0xfe, 0xff]), Buffer.from(utf16).swap16(), ]), valid: true, }, { name: "unsupported declaration", bytes: Buffer.from(utf8.replace("UTF-8", "Shift_JIS")), valid: false, }, { name: "declaration mismatch", bytes: Buffer.from(xml), valid: false, }, { name: "truncated UTF-16", bytes: Buffer.concat([Buffer.from([0xff, 0xfe]), utf16.subarray(0, -1)]), valid: false, }, { name: "malformed UTF-8 executable", bytes: Buffer.concat([ Buffer.from(utf8.split("App")[0] + ""), Buffer.from([0xff]), Buffer.from("" + utf8.split("App")[1]), ]), valid: false, }, ]; return { utf16, cases }; }; describe.skipIf(process.platform !== "darwin")( "app bundle XML byte boundaries", () => { cliTest( "resolves real compiled bundles without changing executable names during decoding", async ({ cli, processes }) => { const directory = await createTestTempDirectory("rea-bundle-encoding-"); const app = join(directory, "Example.app"); const contents = join(app, "Contents"); await mkdir(join(contents, "MacOS"), { recursive: true }); const compiled = await processes.run("/usr/bin/xcrun", [ "clang", resolve("tests/conformance/interface-builder/fixture.c"), "-o", join(contents, "MacOS", "App"), ]); expect(compiled.exitCode, compiled.stderr).toBe(0); const { utf16, cases } = encodingFixture(); for (const { name, bytes, valid } of cases) { await writeFile(join(contents, "Info.plist"), bytes); const result = await cli.run({ arguments: ["inspect-plist", app, "--json"], environment: { REA_LOG_LEVEL: "silent", REA_ANALYSIS_PROVIDER: "auto", }, }); expect( result.exitCode, `${name}: ${result.stdout}\n${result.stderr}`, ).toBe(valid ? 0 : 1); if (valid) expect(result.json).toMatchObject({ normalized_result: { value: { CFBundleExecutable: "App", CFBundleIdentifier: "dev.rea.encoding", }, bundle: { executable: "App" }, }, }); else expect(result.json).toMatchObject({ error: "Analysis failed", details: { path: join(contents, "Info.plist"), reason: expect.stringContaining( "app Info.plist is malformed, unsupported, or lacks CFBundleExecutable", ), }, }); } 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: "bundle-encoding-parity", version: "1", }); onTestFinished(async () => { try { await client.close(); } finally { await transport.close(); } }); await client.connect(transport); await writeFile( join(contents, "Info.plist"), Buffer.concat([ Buffer.from([0xfe, 0xff]), Buffer.from(utf16).swap16(), ]), ); const opened = await client.callTool({ name: "open_binary", arguments: { path: app }, }); expect(opened.isError, JSON.stringify(opened)).not.toBe(true); const inspected = await client.callTool({ name: "inspect_plist", arguments: {}, }); expect(inspected.isError, JSON.stringify(inspected)).not.toBe(true); expect(inspected.structuredContent).toMatchObject({ normalized_result: { value: { CFBundleExecutable: "App", CFBundleIdentifier: "dev.rea.encoding", }, }, }); await writeFile( join(contents, "Info.plist"), Buffer.concat([Buffer.from([0xff, 0xfe]), utf16.subarray(0, -1)]), ); const invalid = await client.callTool({ name: "open_binary", arguments: { path: app }, }); expect(invalid.isError).toBe(true); expect(parseMcpToolError(invalid)).toMatchObject({ error: { details: { path: join(contents, "Info.plist"), reason: expect.stringContaining( "app Info.plist is malformed, unsupported, or lacks CFBundleExecutable", ), }, }, }); }, 45_000, ); }, );