mirror of
https://github.com/morluto/rea.git
synced 2026-11-06 14:56:32 +00:00
* 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>
156 lines
4.9 KiB
TypeScript
156 lines
4.9 KiB
TypeScript
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")] : [],
|
||
);
|
||
},
|
||
);
|