import { execFile } from "node:child_process"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { promisify } from "node:util"; 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"; describe.skipIf(process.platform !== "darwin")( "native plist CLI selection", () => { // Targets are app-relative: the bundle, its program file, or a plist file. cliTest.for([ ["default", "", undefined, "default"], ["alternate", "", "Contents/Alternate.plist", "alternate"], ["missing", "", "Contents/Missing.plist", undefined], ["program default", "Contents/MacOS/App", undefined, "default"], [ "program alternate", "Contents/MacOS/App", "Contents/Alternate.plist", "alternate", ], ["plist file", "Contents/Alternate.plist", undefined, "alternate"], ] as const)( "honors the $0 plist selection", async ([, target, selected, identifier], { cli }) => { const directory = await createTestTempDirectory("rea-plist-cli-"); const app = join(directory, "Example.app"); const contents = join(app, "Contents"); await mkdir(join(contents, "MacOS"), { recursive: true }); await writeFile( join(contents, "MacOS/App"), thinMach(0xfeedfacf, 0x0100000c), ); for (const [name, value] of [ ["Info", "default"], ["Alternate", "alternate"], ]) { await writeFile( join(contents, `${name}.plist`), `CFBundleExecutableAppCFBundleIdentifier${value}`, ); } const result = await cli.run({ arguments: [ "inspect-plist", join(app, target), ...(selected === undefined ? [] : ["--relative-path", selected]), "--json", ], environment: { REA_LOG_LEVEL: "silent", REA_ANALYSIS_PROVIDER: "auto", }, }); if (identifier === undefined) { expect(result.exitCode).toBe(1); expect(result.json).toMatchObject({ error: "Analysis failed" }); } else { expect(result.exitCode).toBe(0); expect(result.json).toMatchObject({ normalized_result: { source_path: join( app, selected ?? (target.endsWith(".plist") ? target : "Contents/Info.plist"), ), value: { CFBundleIdentifier: identifier }, }, }); } }, ); }, ); describe.skipIf(process.platform !== "darwin")( "native plist CLI source formats", () => { cliTest( "retains non-finite values through real plutil XML conversion", async ({ cli }) => { const directory = await createTestTempDirectory("rea-plist-reals-"); const path = join(directory, "selected.plist"); const source = Buffer.from(` positive+infinity negative-INFINITY nannancontrol1.25 `); await writeFile(path, source); const result = await cli.run({ arguments: ["inspect-plist", path, "--json"], environment: { REA_LOG_LEVEL: "silent", REA_ANALYSIS_PROVIDER: "auto", }, }); expect(result.exitCode, result.stderr).toBe(0); expect(result.json).toMatchObject({ normalized_result: { value: { positive: { $plist_type: "real", value: "+infinity" }, negative: { $plist_type: "real", value: "-infinity" }, nan: { $plist_type: "real", value: "nan" }, control: 1.25, }, limitations: expect.arrayContaining([ expect.stringContaining("3 non-finite real value(s)"), ]), }, }); expect(await readFile(path)).toEqual(source); }, ); cliTest.for([ ["openstep", "unknown"], ["openstep-data", "unknown"], ["openstep-data-root", "unknown"], ["openstep-empty-data", "unknown"], ["xml", "xml"], ["xml16", "xml"], ["binary", "binary"], ] as const)( "reports the original $0 plist format through the compiled CLI", async ([encoding, format], { cli }) => { const directory = await createTestTempDirectory("rea-plist-format-"); const target = join(directory, "target"); const plist = join(directory, "selected.plist"); const withData = encoding === "openstep-data"; const dataRoot = encoding === "openstep-data-root"; const emptyData = encoding === "openstep-empty-data"; await writeFile(target, thinMach(0xfeedfacf, 0x0100000c)); await writeFile( plist, dataRoot ? "\n" : emptyData ? "<>\n" : `{ Name = Example; Items = (a, b); ${withData ? "Blob = ;" : ""} }\n`, ); if (encoding === "binary" || encoding.startsWith("xml")) { await promisify(execFile)("/usr/bin/plutil", [ "-convert", encoding === "binary" ? "binary1" : "xml1", "--", plist, ]); if (encoding === "xml16") { const { stdout } = await promisify(execFile)("/usr/bin/plutil", [ "-convert", "xml1", "-o", "-", "--", plist, ]); await writeFile( plist, Buffer.concat([ Buffer.from([0xfe, 0xff]), Buffer.from( stdout.replace("UTF-8", "UTF-16"), "utf16le", ).swap16(), ]), ); } } const result = await cli.run({ arguments: [ "inspect-plist", target, "--relative-path", plist, "--json", ], environment: { REA_LOG_LEVEL: "silent", REA_ANALYSIS_PROVIDER: "auto", }, }); expect(result.exitCode, result.stderr).toBe(0); expect(result.json).toMatchObject({ normalized_result: { format, source_path: plist, value: dataRoot || emptyData ? { $plist_type: "data", base64: emptyData ? "" : "3q2+7w==" } : { Name: "Example", Items: ["a", "b"], ...(withData ? { Blob: { $plist_type: "data", base64: "3q2+7w==" } } : {}), }, }, }); }, ); }, ); describe.skipIf(process.platform !== "darwin")( "native plist CLI typed values", () => { cliTest.for(["xml", "binary"] as const)( "decodes $0 plists that plutil cannot express as JSON", async (format, { cli }) => { const directory = await createTestTempDirectory("rea-plist-types-"); const app = join(directory, "Example.app"); const contents = join(app, "Contents"); await mkdir(join(contents, "MacOS"), { recursive: true }); await writeFile( join(contents, "MacOS/App"), thinMach(0xfeedfacf, 0x0100000c), ); const plist = join(contents, "Info.plist"); await writeFile( plist, [ "", "CFBundleExecutableApp", "CFBundleIdentifiercom.example.types", "BlobAAEC", "Separatorsa\u0085b\u2028c\u2029d", "Built2020-01-01T00:00:00Z", // Parse the unsigned extended year as a UTC field, without JS string-date inference. "Far11507-08-17T05:20:00Z", "Rationan", "Nested", "Ratio0.5", "Unbounded-infinity", "empty key", "", "", ].join(""), ); if (format === "binary") await promisify(execFile)("/usr/bin/plutil", [ "-convert", "binary1", "--", plist, ]); const result = await cli.run({ arguments: ["inspect-plist", app, "--json"], environment: { REA_LOG_LEVEL: "silent", REA_ANALYSIS_PROVIDER: "auto", }, }); expect(result.exitCode).toBe(0); expect(result.json).toMatchObject({ normalized_result: { format, value: { CFBundleIdentifier: "com.example.types", Blob: { $plist_type: "data", base64: "AAEC" }, Separators: "a\u0085b\u2028c\u2029d", Built: { $plist_type: "date", iso8601: "2020-01-01T00:00:00.000Z", }, Far: { $plist_type: "date", iso8601: "+011507-08-17T05:20:00.000Z", }, Ratio: { $plist_type: "real", value: "nan" }, Nested: [ { Ratio: 0.5, Unbounded: { $plist_type: "real", value: "-infinity" }, "": "empty key", }, ], }, bundle: { identifier: "com.example.types", executable: "App" }, provenance: format === "xml" ? [ { tool: "file" }, { command: expect.arrayContaining(["json"]), exit: { code: 1 }, }, { command: expect.arrayContaining(["xml1"]), exit: { code: 0 }, }, ] : [{ tool: "file" }], limitations: format === "xml" ? [ expect.stringContaining("XML conversion"), expect.stringContaining("2 non-finite real"), ] : [expect.stringContaining("2 non-finite real")], }, }); }, ); }, ); describe.skipIf(process.platform !== "darwin")( "native plist CLI integers", () => { cliTest.for([false, true])( "reports exact plist integers and integral reals beyond the JSON number range (data: %s)", async (withData, { cli }) => { const directory = await createTestTempDirectory("rea-plist-integers-"); const app = join(directory, "Example.app"); const contents = join(app, "Contents"); await mkdir(join(contents, "MacOS"), { recursive: true }); await writeFile( join(contents, "MacOS/App"), thinMach(0xfeedfacf, 0x0100000c), ); await writeFile( join(contents, "Info.plist"), [ "", "CFBundleExecutableApp", "Max18446744073709551615", "Min-9223372036854775808", "Count42", "Real9007199254740992", "Large1e20", ...(withData ? ["BlobAAEC"] : []), "", ].join(""), ); const result = await cli.run({ arguments: ["inspect-plist", app, "--json"], environment: { REA_LOG_LEVEL: "silent", REA_ANALYSIS_PROVIDER: "auto", }, }); expect(result.exitCode).toBe(0); expect(result.json).toMatchObject({ normalized_result: { value: { Max: { $plist_type: "integer", decimal: "18446744073709551615" }, Min: { $plist_type: "integer", decimal: "-9223372036854775808" }, Count: 42, Real: 9007199254740992, Large: 1e20, }, limitations: [ ...(withData ? [expect.stringContaining("XML conversion")] : []), expect.stringContaining("2 integer value(s)"), ], }, }); }, ); }, );