import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { binaryPlistObjectTable } from "../../tests/fixtures/binaryPlistObjectTable.js";
import { resolveAppBundleExecutable } from "./AppBundleExecutable.js";
const binaryPlistExecutableWithThreeByteUid = (): Buffer => {
const ascii = (value: string): Buffer => {
const bytes = Buffer.from(value, "ascii");
return bytes.length < 15
? Buffer.concat([Buffer.from([0x50 | bytes.length]), bytes])
: Buffer.concat([Buffer.from([0x5f, 0x10, bytes.length]), bytes]);
};
const objects = [
Buffer.from([0xd2, 1, 3, 2, 4]),
ascii("CFBundleExecutable"),
ascii("Fixture"),
ascii("ignored"),
Buffer.from([0x82, 1, 2, 3]),
];
const tableOffset =
8 + objects.reduce((sum, object) => sum + object.length, 0);
const bytes = Buffer.alloc(tableOffset + objects.length + 32);
let cursor = 8;
const offsets = objects.map((object) => {
const offset = cursor;
object.copy(bytes, cursor);
cursor += object.length;
return offset;
});
return binaryPlistObjectTable(bytes, offsets, tableOffset);
};
it.each([
["macOS", ["Contents"], ["Contents", "MacOS"]],
["iOS", [], []],
] as const)(
"resolves the %s bundle layout when Info.plist contains processing instructions",
async (_kind, plistDirectory, programDirectory) => {
const root = await createTestTempDirectory("rea-app-plist-pi-");
const bundle = join(root, "Fixture.app");
const plistPath = join(bundle, ...plistDirectory, "Info.plist");
const executable = join(bundle, ...programDirectory, "Fixture");
await mkdir(join(bundle, ...plistDirectory), { recursive: true });
await mkdir(join(bundle, ...programDirectory), { recursive: true });
await writeFile(
plistPath,
'CFBundleExecutableFixture',
);
await writeFile(executable, "inert fixture; never executed");
const result = await resolveAppBundleExecutable(bundle);
expect(result).toEqual({
ok: true,
value: { executable, infoPlist: plistPath },
});
},
);
describe("app bundle Info.plist magic", () => {
it("decodes an executable when an unrelated UID uses a three-byte width", async () => {
const root = await createTestTempDirectory("rea-app-three-byte-uid-");
const bundle = join(root, "Fixture.app");
const plistPath = join(bundle, "Contents", "Info.plist");
const executable = join(bundle, "Contents", "MacOS", "Fixture");
await mkdir(join(bundle, "Contents", "MacOS"), { recursive: true });
await writeFile(plistPath, binaryPlistExecutableWithThreeByteUid());
await writeFile(executable, "inert fixture; never executed");
expect(await resolveAppBundleExecutable(bundle)).toEqual({
ok: true,
value: { executable, infoPlist: plistPath },
});
});
it("does not decode a high-bit fold of bplist as a binary Info.plist", async () => {
const root = await mkdtemp(join(tmpdir(), "rea-bplist-fold-"));
try {
const bundle = join(root, "Fixture.app");
const plistPath = join(bundle, "Contents", "Info.plist");
await mkdir(join(bundle, "Contents"), { recursive: true });
const plist = Buffer.from("bplist00");
plist[0] = (plist[0] ?? 0) | 0x80;
await writeFile(plistPath, plist);
let decoded = false;
const result = await resolveAppBundleExecutable(bundle, {
readFile: async () => plist,
realpath: async (path) => path,
decodeBinaryPlist: async () => {
decoded = true;
return "Fixture";
},
});
expect(decoded).toBe(false);
expect(result.ok).toBe(false);
} finally {
await rm(root, { recursive: true, force: true });
}
});
});
describe("app bundle program directory containment", () => {
it("rejects an escaping programs directory before resolving its child", async () => {
const root = await mkdtemp(join(tmpdir(), "rea-app-programs-"));
try {
const bundle = join(root, "Fixture.app");
const programs = join(bundle, "Contents", "MacOS");
const executable = join(programs, "Missing");
const canonicalBundle = join(root, "Canonical.app");
const outsidePrograms = join(root, "outside");
const plist =
"CFBundleExecutableMissing";
await mkdir(join(bundle, "Contents"), { recursive: true });
await writeFile(join(bundle, "Contents", "Info.plist"), plist);
let childResolved = false;
const result = await resolveAppBundleExecutable(bundle, {
readFile: async () => Buffer.from(plist),
realpath: async (path) => {
if (path === bundle) return canonicalBundle;
if (path === programs) return outsidePrograms;
if (path === executable) {
childResolved = true;
throw Object.assign(new Error("missing executable"), {
code: "ENOENT",
});
}
return path;
},
});
expect(result).toMatchObject({
ok: false,
error: {
reason: "app program directory leaves Contents/MacOS",
},
});
expect(childResolved).toBe(false);
} finally {
await rm(root, { recursive: true, force: true });
}
});
});