mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(target): decode app Info.plist executables with an XML parser
The pattern-based reader decoded entities twice, ignored numeric character references and CDATA, and could match a commented or nested CFBundleExecutable key, so valid app bundles reported a missing program. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,8 @@ import { execFile } from "node:child_process";
|
||||
import { extname, isAbsolute, join, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { parse as parsePlist } from "plist";
|
||||
|
||||
import { isPathWithinRoot } from "../domain/localPath.js";
|
||||
import { BinaryTargetError } from "../domain/configurationErrors.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
@@ -264,14 +266,21 @@ const resolveAppBundle = async (
|
||||
}
|
||||
};
|
||||
|
||||
/** Decode the top-level executable name with an XML parser, not a pattern. */
|
||||
const parseXmlPlistExecutable = (plist: string): string => {
|
||||
const match =
|
||||
/<key>\s*CFBundleExecutable\s*<\/key>\s*<string>([^<]+)<\/string>/u.exec(
|
||||
plist,
|
||||
);
|
||||
if (match?.[1] === undefined)
|
||||
const value = parsePlist(plist);
|
||||
const executable =
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!Array.isArray(value) &&
|
||||
!(value instanceof Date) &&
|
||||
!(value instanceof Uint8Array) &&
|
||||
Object.hasOwn(value, "CFBundleExecutable")
|
||||
? value.CFBundleExecutable
|
||||
: undefined;
|
||||
if (typeof executable !== "string")
|
||||
throw new Error("CFBundleExecutable is missing");
|
||||
return decodeXml(match[1]);
|
||||
return executable;
|
||||
};
|
||||
|
||||
const readBinaryPlistExecutable = async (plistPath: string): Promise<string> =>
|
||||
@@ -295,14 +304,6 @@ const isSafeExecutableName = (name: string): boolean =>
|
||||
!name.includes("\0") &&
|
||||
!/[/\\]/u.test(name);
|
||||
|
||||
const decodeXml = (value: string): string =>
|
||||
value
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll(""", '"')
|
||||
.replaceAll("'", "'");
|
||||
|
||||
const readExecutableMetadata = async (
|
||||
handle: FileHandle,
|
||||
hostArchitecture: NodeJS.Architecture,
|
||||
|
||||
@@ -187,6 +187,62 @@ describe("app executable filename fidelity", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["an escaped entity reference", "a&lt;b", "a<b"],
|
||||
["a numeric character reference", "a b", "a b"],
|
||||
["a CDATA section", "<![CDATA[a&b]]>", "a&b"],
|
||||
])(
|
||||
"decodes an XML plist executable name containing %s",
|
||||
async (_case, encoded, name) => {
|
||||
const directory = await createTestTempDirectory("rea-app-xml-name-");
|
||||
const app = join(directory, "Encoded.app");
|
||||
const contents = join(app, "Contents");
|
||||
const executable = join(contents, "MacOS", name);
|
||||
await mkdir(join(contents, "MacOS"), { recursive: true });
|
||||
await writeFile(
|
||||
join(contents, "Info.plist"),
|
||||
`<plist><dict><key>CFBundleExecutable</key><string>${encoded}</string></dict></plist>`,
|
||||
);
|
||||
await writeFile(executable, thinMach(0xfeedfacf, 0x0100000c));
|
||||
const result = await parseBinaryTarget(app, directory, "arm64");
|
||||
expect(result.ok && result.value).toMatchObject({
|
||||
path: await realpath(executable),
|
||||
format: "mach-o",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("reads the top-level executable key rather than commented or nested text", async () => {
|
||||
const directory = await createTestTempDirectory("rea-app-xml-key-");
|
||||
const app = join(directory, "Nested.app");
|
||||
const contents = join(app, "Contents");
|
||||
const executable = join(contents, "MacOS", "App");
|
||||
await mkdir(join(contents, "MacOS"), { recursive: true });
|
||||
await writeFile(
|
||||
join(contents, "Info.plist"),
|
||||
[
|
||||
"<plist><dict>",
|
||||
"<!-- <key>CFBundleExecutable</key><string>Old</string> -->",
|
||||
"<key>NSExtension</key><dict><key>CFBundleExecutable</key><string>Inner</string></dict>",
|
||||
"<key>CFBundleExecutable</key><string>App</string>",
|
||||
"</dict></plist>",
|
||||
].join(""),
|
||||
);
|
||||
await writeFile(executable, thinMach(0xfeedfacf, 0x0100000c));
|
||||
await writeFile(
|
||||
join(contents, "MacOS", "Old"),
|
||||
thinMach(0xfeedfacf, 0x0100000c),
|
||||
);
|
||||
await writeFile(
|
||||
join(contents, "MacOS", "Inner"),
|
||||
thinMach(0xfeedfacf, 0x0100000c),
|
||||
);
|
||||
const result = await parseBinaryTarget(app, directory, "arm64");
|
||||
expect(result.ok && result.value).toMatchObject({
|
||||
path: await realpath(executable),
|
||||
});
|
||||
});
|
||||
|
||||
it.skipIf(process.platform !== "darwin")(
|
||||
"preserves executable filename whitespace from native binary plists",
|
||||
async () => {
|
||||
|
||||
Reference in New Issue
Block a user