import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it } from "vitest"; import { buildBinary } from "plist"; import { inspectPlistSchema } from "../../../../src/domain/native/nativeInspection.js"; import { ok } from "../../../../src/domain/result.js"; import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js"; import { NativeFixtureRunner, nativeMachoTarget, } from "../../../fixtures/nativeCommands.js"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; const VALUE = { Name: "Example", Items: ["a", "b"] }; const XML = "NameExampleItemsabBlob3q2+7w=="; const DATA = { $plist_type: "data", base64: "3q2+7w==" }; class PlistSourceRunner extends NativeFixtureRunner { constructor( description: string, private readonly xmlFallback: boolean, private readonly convertedXml: string = XML, ) { super({ file: description, plutil: JSON.stringify(VALUE) }); } override async run(tool: string, arguments_: readonly string[]) { const result = await super.run(tool, arguments_); if (!result.ok || tool !== "plutil" || !this.xmlFallback) return result; const json = arguments_.includes("json"); const stdout = json ? "" : this.convertedXml; return ok({ ...result.value, stdout, stdoutBytes: Buffer.byteLength(stdout), exitCode: json ? 1 : 0, }); } } const OPENSTEP = "{ Name = Example; Items = (a, b); }\n"; interface PlistSourceCase { readonly description: string; readonly contents?: string | Buffer; readonly expected: string; readonly xmlFallback: boolean; readonly convertedXml?: string; readonly expectedValue?: unknown; readonly expectedTools?: readonly string[]; readonly expectedLimitations?: readonly unknown[]; } it.for([ { description: "ASCII text\n", expected: "unknown", xmlFallback: false }, { description: "JSON text data\n", contents: JSON.stringify(VALUE), expected: "json", xmlFallback: false, }, ...["\n", "\n", "<>\n"].map((contents) => ({ description: "ASCII text\n", contents, expected: "unknown", xmlFallback: true, convertedXml: `${contents.startsWith("<>") ? "" : DATA.base64}`, expectedValue: { ...DATA, base64: contents.startsWith("<>") ? "" : DATA.base64, }, })), // `file` reports XML without a declaration as plain text. { description: "ASCII text, with no line terminators\n", contents: ` \n${XML}`, expected: "xml", xmlFallback: true, }, { description: "ASCII text\n", contents: `\n${XML}`, expected: "xml", xmlFallback: true, }, ...[false, true].map((bigEndian) => ({ description: `Unicode text, UTF-16, ${bigEndian ? "big" : "little"}-endian text\n`, contents: bigEndian ? Buffer.from(`\uFEFF${XML}`, "utf16le").swap16() : Buffer.from(`\uFEFF${XML}`, "utf16le"), expected: "xml", xmlFallback: true, })), { description: "ASCII text\n", expected: "unknown", xmlFallback: true }, { description: "Unicode text, UTF-8 text\n", expected: "unknown", xmlFallback: false, }, { description: "XML 1.0 document text, ASCII text\n", expected: "xml", xmlFallback: false, }, { description: "XML 1.0 document text, Unicode text, UTF-16, big-endian text\n", expected: "xml", xmlFallback: true, }, { description: "Apple binary property list\n", contents: Buffer.from( buildBinary({ nel: "a\u0085b", ls: "a\u2028b", ps: "a\u2029b", lf: "a\nb", payload: Buffer.from([0]), }), ), expected: "binary", xmlFallback: false, expectedTools: ["file"], expectedValue: { nel: "a\u0085b", ls: "a\u2028b", ps: "a\u2029b", lf: "a\nb", payload: { $plist_type: "data", base64: "AA==" }, }, expectedLimitations: [], }, { description: "XML 1.0 document text, ASCII text\n", contents: "nela\u0085blsa\u2028bpsa\u2029blfa\nbpayloadAA==", expected: "xml", xmlFallback: true, convertedXml: "nela\u0085blsa\u2028bpsa\u2029blfa\nbpayloadAA==", expectedValue: { nel: "a\u0085b", ls: "a\u2028b", ps: "a\u2029b", lf: "a\nb", payload: { $plist_type: "data", base64: "AA==" }, }, }, ])( "keeps source format $expected for $description (XML fallback: $xmlFallback)", async ( { description, contents = OPENSTEP, expected, xmlFallback, convertedXml = XML, expectedValue = { ...VALUE, ...(xmlFallback ? { Blob: DATA } : {}) }, expectedTools, expectedLimitations, }, { onTestFinished }, ) => { const root = await createTestTempDirectory("rea-plist-source-format-"); const path = join(root, "selected.plist"); await writeFile(path, contents); const client = new NativeMacOSProvider( {}, new PlistSourceRunner(description, xmlFallback, convertedXml), "darwin", ).createClient(nativeMachoTarget(join(root, "target"))); onTestFinished(async () => { const closed = await client.close(); if (!closed.ok) throw closed.error; }); const inspected = await client.execute("inspect_plist", { path }); if (!inspected.ok) throw inspected.error; const result = inspectPlistSchema.parse(inspected.value.result); expect(result.format).toBe(expected); expect(result.source_path).toBe(path); expect(result.value).toEqual(expectedValue); expect(result.provenance.map(({ tool }) => tool)).toEqual( expectedTools ?? (xmlFallback ? ["file", "plutil", "plutil"] : ["file", "plutil"]), ); expect(result.limitations).toEqual( expectedLimitations ?? (xmlFallback ? [expect.stringContaining("XML conversion")] : []), ); }, );