Files
reverse-engineering-anything/tests/boundary/providers/native/plistSourceFormat.test.ts
T
4e14452ddb fix(native): report unidentified text plists as unknown (#4320)
* fix(native): report unidentified text plists as unknown

Require explicit XML identification in file output instead of treating all
text as XML. Preserve decoded plist values and source-format provenance
when plutil falls back to XML conversion.

Add provider-boundary regressions and real macOS CLI controls for OpenStep,
data-bearing OpenStep, XML, UTF-16 XML and binary property lists.

Fixes #4294

* fix(native): keep XML plists without a declaration labeled xml

`file -b` names XML only when a document declaration is present, so the
narrowed `\bXML\b` check reported a declaration-less XML plist as unknown and
broke the real-plutil `decodes xml plists that plutil cannot express as JSON`
CLI test. When `file` reports only text, read the source head once and label it
XML only when its first non-whitespace character (after a UTF-8 BOM) is `<`;
OpenStep and other text stay unknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(native): distinguish OpenStep data from XML source markup

Recognize XML markup instead of treating every leading angle bracket as XML.
Keep data-only OpenStep sources unknown, honor explicit JSON identification,
and decode BOM-marked UTF-16 prefixes before detecting declarationless XML.
Add source-format regressions and macOS CLI cases for data-only sources.

---------

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:52:25 +08:00

156 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
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 =
"<plist><dict><key>Name</key><string>Example</string><key>Items</key><array><string>a</string><string>b</string></array><key>Blob</key><data>3q2+7w==</data></dict></plist>";
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;
}
it.for<PlistSourceCase>([
{ description: "ASCII text\n", expected: "unknown", xmlFallback: false },
{
description: "JSON text data\n",
contents: JSON.stringify(VALUE),
expected: "json",
xmlFallback: false,
},
...["<deadbeef>\n", "<de ad\nbe ef>\n", "<>\n"].map((contents) => ({
description: "ASCII text\n",
contents,
expected: "unknown",
xmlFallback: true,
convertedXml: `<plist><data>${contents.startsWith("<>") ? "" : DATA.base64}</data></plist>`,
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: `<!-- source plist -->\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",
expected: "binary",
xmlFallback: true,
},
])(
"keeps source format $expected for $description (XML fallback: $xmlFallback)",
async (
{
description,
contents = OPENSTEP,
expected,
xmlFallback,
convertedXml = XML,
expectedValue = { ...VALUE, ...(xmlFallback ? { Blob: DATA } : {}) },
},
{ 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(
xmlFallback ? ["file", "plutil", "plutil"] : ["file", "plutil"],
);
expect(result.limitations).toEqual(
xmlFallback ? [expect.stringContaining("XML conversion")] : [],
);
},
);