fix(native): report why a selected plist cannot be inspected
`inspect_plist` turned a missing selected plist, a directory, or a file that is not a property list into a generic execution failure that advised retrying and running `rea doctor`. A bare Mach-O without Contents/Info.plist failed the same way. Check the resolved path before running plutil and keep plutil's decode diagnostic. A caller-selected path now fails as invalid input naming the path and the reason; a missing default Info.plist is unavailable for the target. The CLI no longer sends Contents/Info.plist as an explicit selection, so its default matches the MCP default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
cfd2b0c75d
commit
0674facd46
|
|
@ -135,8 +135,10 @@ const registerNativeCommands = (cli: CliInstance, logger: Logger): void => {
|
|||
options: z.object({
|
||||
relativePath: z
|
||||
.string()
|
||||
.default("Contents/Info.plist")
|
||||
.describe("Plist path relative to the app root"),
|
||||
.optional()
|
||||
.describe(
|
||||
"Plist path relative to the app root (default: Contents/Info.plist)",
|
||||
),
|
||||
}),
|
||||
alias: { relativePath: "relative-path" },
|
||||
run: ({ args, options }) =>
|
||||
|
|
@ -144,7 +146,9 @@ const registerNativeCommands = (cli: CliInstance, logger: Logger): void => {
|
|||
runProviderAnalysis(
|
||||
args.path,
|
||||
"inspect_plist",
|
||||
{ path: options.relativePath },
|
||||
options.relativePath === undefined
|
||||
? {}
|
||||
: { path: options.relativePath },
|
||||
logger,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import type { EvidenceLocation } from "../domain/evidence.js";
|
|||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
|
|
@ -419,6 +420,8 @@ class NativeMacOSClient implements AnalysisClient {
|
|||
);
|
||||
const plist = await resolvePlistPath(this.target, requested);
|
||||
if (!plist.ok) return plist;
|
||||
const selected = await requireRegularPlist(plist.value, requested);
|
||||
if (!selected.ok) return selected;
|
||||
const classified = await this.#run(
|
||||
"inspect_plist",
|
||||
"file",
|
||||
|
|
@ -445,9 +448,17 @@ class NativeMacOSClient implements AnalysisClient {
|
|||
"inspect_plist",
|
||||
"plutil",
|
||||
["-convert", "xml1", "-o", "-", "--", plist.value],
|
||||
{ signal },
|
||||
{ signal, acceptNonZero: !jsonDecoded },
|
||||
);
|
||||
if (xml !== undefined && !xml.ok) return xml;
|
||||
if (xml !== undefined && xml.value.exitCode !== 0)
|
||||
return err(
|
||||
unreadablePlist(
|
||||
plist.value,
|
||||
requested,
|
||||
`plutil could not decode it as a property list (${plutilDiagnostic(xml.value.stderr, plist.value)})`,
|
||||
),
|
||||
);
|
||||
const parsed =
|
||||
xml === undefined
|
||||
? parsePlistJson(json.value.stdout)
|
||||
|
|
@ -637,6 +648,57 @@ const parseEntitlements = (output: string): JsonValue | null => {
|
|||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Report a plist that cannot be decoded as the caller's selection, or as the
|
||||
* target's missing default, rather than as a failed tool run.
|
||||
*/
|
||||
const unreadablePlist = (
|
||||
path: string,
|
||||
requested: string | undefined,
|
||||
reason: string,
|
||||
): AnalysisError => {
|
||||
const sentence = reason.endsWith(".") ? reason : `${reason}.`;
|
||||
return requested === undefined
|
||||
? new AnalysisCapabilityUnavailableError(
|
||||
IDENTITY.id,
|
||||
"inspect_plist",
|
||||
`The target has no readable Contents/Info.plist at ${path}: ${sentence} Pass path to inspect another plist.`,
|
||||
)
|
||||
: new AnalysisInputError("inspect_plist", undefined, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_value",
|
||||
message: `Cannot inspect ${path}: ${sentence}`,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
/** plutil prefixes its diagnostic with the path the message already names. */
|
||||
const plutilDiagnostic = (stderr: string, path: string): string => {
|
||||
const text = stderr.trim();
|
||||
return text.startsWith(`${path}: `) ? text.slice(path.length + 2) : text;
|
||||
};
|
||||
|
||||
const requireRegularPlist = async (
|
||||
path: string,
|
||||
requested: string | undefined,
|
||||
): Promise<Result<null, AnalysisError>> => {
|
||||
try {
|
||||
if ((await stat(path)).isFile()) return ok(null);
|
||||
return err(unreadablePlist(path, requested, "it is not a regular file"));
|
||||
} catch (cause: unknown) {
|
||||
if (
|
||||
cause instanceof Error &&
|
||||
"code" in cause &&
|
||||
(cause.code === "ENOENT" || cause.code === "ENOTDIR")
|
||||
)
|
||||
return err(unreadablePlist(path, requested, "no file exists there"));
|
||||
return err(
|
||||
new ProviderAdapterError(IDENTITY.id, "inspect_plist", { cause }),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const resolvePlistPath = async (
|
||||
target: BinaryTarget,
|
||||
requested: string | undefined,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect } from "vitest";
|
||||
|
||||
import { thinMach } from "../../../src/domain/binaryTarget.fixture.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
const environment = {
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_ANALYSIS_PROVIDER: "auto",
|
||||
} as const;
|
||||
|
||||
/** An app bundle whose Contents holds a directory and a non-plist file. */
|
||||
const fixtureApp = async () => {
|
||||
const app = join(await createTestTempDirectory("rea-plist-fail-"), "A.app");
|
||||
const contents = join(app, "Contents");
|
||||
await mkdir(join(contents, "MacOS"), { recursive: true });
|
||||
await mkdir(join(contents, "Folder.plist"));
|
||||
await writeFile(join(contents, "MacOS/App"), thinMach(0xfeedfacf, 0x100000c));
|
||||
await writeFile(
|
||||
join(contents, "Info.plist"),
|
||||
"<plist><dict><key>CFBundleExecutable</key><string>App</string></dict></plist>",
|
||||
);
|
||||
await writeFile(join(contents, "Text.plist"), "not a property list\n");
|
||||
return app;
|
||||
};
|
||||
|
||||
describe.skipIf(process.platform !== "darwin")(
|
||||
"native plist CLI selection failures",
|
||||
() => {
|
||||
cliTest.for([
|
||||
["Contents/Missing.plist", "no file exists there."],
|
||||
["Contents/Folder.plist", "it is not a regular file."],
|
||||
["Contents/Text.plist", "plutil could not decode it as a property list"],
|
||||
] as const)(
|
||||
"reports why the selected %s cannot be inspected",
|
||||
async ([selected, reason], { cli }) => {
|
||||
const app = await fixtureApp();
|
||||
const result = await cli.run({
|
||||
arguments: [
|
||||
"inspect-plist",
|
||||
app,
|
||||
"--relative-path",
|
||||
selected,
|
||||
"--json",
|
||||
],
|
||||
environment,
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.json).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
operation: "inspect_plist",
|
||||
issues: [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_value",
|
||||
message: expect.stringContaining(
|
||||
`Cannot inspect ${join(app, selected)}: ${reason}`,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"reports a missing default Info.plist as unavailable for the target",
|
||||
async ({ cli }) => {
|
||||
const directory = await createTestTempDirectory("rea-plist-bare-");
|
||||
const program = join(directory, "tool");
|
||||
await writeFile(program, thinMach(0xfeedfacf, 0x100000c));
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-plist", program, "--json"],
|
||||
environment,
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.json).toMatchObject({
|
||||
code: "capability_unavailable",
|
||||
details: {
|
||||
operation: "inspect_plist",
|
||||
reason: expect.stringContaining(
|
||||
`no readable Contents/Info.plist at ${join(directory, "Contents/Info.plist")}: no file exists there.`,
|
||||
),
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
Loading…
Reference in New Issue