Files
rea/src/domain/xmlDocument.test.ts
T
morluto 00266efe13 fix(plist): preserve Unicode separators during XML normalization
Use one XML 1.0 line-ending rule in the shared XML boundary and plist source-span owner. Normalize literal CRLF and CR while retaining NEL, line separator and paragraph separator as observed characters. Keep character references, raw scalar locations and prototype-key omission intact.

Validation: 229 XML/plist source tests, 60 adjacent Ghidra/Apktool/SAZ source tests, and 33 compiled CLI/provider tests passed. Full check:ci passed. Real plutil XML and binary fixtures reproduced the baseline loss and passed CLI plus SDK-validated stdio MCP checks with unchanged input digests. Independent source review found no blocker. Binary conversion losses tracked by #5701, #5703 and #5705 remain separate work.

Closes #5702
2026-10-21 11:42:25 +08:00

90 lines
3.2 KiB
TypeScript

import { expect, it } from "vitest";
import { parseXmlDocument } from "./xmlDocument.js";
it.each([
'<root attr="�">😀 � &amp; &#xFFFD;</root>',
"<root><!-- � --><![CDATA[�]]></root>",
'<?xml version="1.0"?><root>\r\n �\rtext</root>',
"<!DOCTYPE root><root>�</root>",
'<!DOCTYPE root [<!ENTITY unused "�">]><root>�</root>',
])("preserves legal Unicode and source-located DOM nodes: %j", (source) => {
const parsed = parseXmlDocument(source, {
doctype: "allow",
lineEndings: "preserve",
});
if (!parsed.ok) throw parsed.error;
expect(parsed.value.documentElement?.tagName).toBe("root");
expect(parsed.value.documentElement?.lineNumber).toBeDefined();
expect(parsed.value.documentElement?.textContent).toContain("�");
});
it.each([
"<root attr=unquoted>�</root>",
'<root a="x"b="�"/>',
'<root attr="a" attr="�"/>',
"<root><broken>�</root>",
"<root>� & unknown</root>",
"<root>� &missing;</root>",
"<root>�\u0000</root>",
"<root>�&#xD800;</root>",
"<root>�\uFFFF</root>",
"<root>�]]></root>",
"<root><!-- invalid -- comment � --></root>",
"<root>�</root><second/>",
"<!DOCTYPE root [BOGUS]><root>�</root>",
"<!DOCTYPE root [<!ELEMENT broken>]><root>�</root>",
"<!DOCTYPE root SYSTEM><root>�</root>",
])("rejects malformed XML without warning-message matching: %j", (source) => {
const parsed = parseXmlDocument(source, {
doctype: "allow",
lineEndings: "preserve",
});
expect(parsed.ok).toBe(false);
if (parsed.ok) throw new Error("Expected refusal");
expect(parsed.error.reason).toBe("malformed");
expect(parsed.error.cause).toBeDefined();
});
it("keeps literal line endings and character-reference values distinct", () => {
const parsed = parseXmlDocument("<root>\r\n&#13;\r�</root>", {
doctype: "allow",
lineEndings: "preserve",
});
if (!parsed.ok) throw parsed.error;
expect(parsed.value.documentElement?.textContent).toBe("\r\n\r\r�");
});
it("applies the consumer's explicit DTD policy", () => {
const parsed = parseXmlDocument(
'<!DOCTYPE root [<!ENTITY value "unused">]><root/>',
{ doctype: "reject", lineEndings: "preserve" },
);
expect(parsed.ok).toBe(false);
if (parsed.ok) throw new Error("Expected unsupported DTD");
expect(parsed.error.reason).toBe("doctype");
});
it("applies XML line-ending normalization only when the consumer selects it", () => {
const parsed = parseXmlDocument('<root attr="a\r\nb">\r\nx</root>', {
doctype: "allow",
lineEndings: "normalize",
});
if (!parsed.ok) throw parsed.error;
expect(parsed.value.documentElement?.getAttribute("attr")).toBe("a b");
expect(parsed.value.documentElement?.textContent).toBe("\nx");
});
it("preserves Unicode separators under XML 1.0 normalization", () => {
const parsed = parseXmlDocument(
'<root attr="a\u0085b\u2028c\u2029d">a\r\u0085b\r\u2028c\r\n&#13;d\u2029</root>',
{ doctype: "allow", lineEndings: "normalize" },
);
if (!parsed.ok) throw parsed.error;
expect(parsed.value.documentElement?.getAttribute("attr")).toBe(
"a\u0085b\u2028c\u2029d",
);
expect(parsed.value.documentElement?.textContent).toBe(
"a\n\u0085b\n\u2028c\n\rd\u2029",
);
});