Files
reverse-engineering-anything/src/domain/xmlDocument.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

61 lines
1.9 KiB
TypeScript

import { DOMParser, onErrorStopParsing, type Document } from "@xmldom/xmldom";
import { SaxesParser } from "saxes";
import { err, ok, type Result } from "./result.js";
/** XML 1.0 literal line endings; Unicode separators remain source characters. */
export const XML_LINE_ENDING = /\r\n?|\n/gu;
export class XmlDocumentParseError extends Error {
readonly _tag = "XmlDocumentParseError";
constructor(
readonly reason: "malformed" | "doctype",
cause?: unknown,
) {
super(
reason === "doctype"
? "XML document types are unsupported"
: `XML is not well-formed${cause instanceof Error ? `: ${cause.message}` : ""}`,
{ cause },
);
}
}
/** One boundary owns strict XML syntax and the source-located DOM projection. */
export const parseXmlDocument = (
source: string,
policy: {
readonly doctype: "allow" | "reject";
readonly lineEndings: "preserve" | "normalize";
},
): Result<Document, XmlDocumentParseError> => {
try {
const syntax = new SaxesParser({
defaultXMLVersion: "1.0",
forceXMLVersion: true,
});
syntax.on("doctype", () => {
if (policy.doctype === "reject")
throw new XmlDocumentParseError("doctype");
});
syntax.write(source).close();
// Saxes owns well-formedness outside DTDs. The DOM parser additionally
// checks DTD syntax and builds source-located nodes; its advisory warnings
// (including literal U+FFFD) are not byte-decoding or syntax failures.
return ok(
new DOMParser({
normalizeLineEndings:
policy.lineEndings === "preserve"
? (text) => text
: (text) => text.replace(XML_LINE_ENDING, "\n"),
onError: onErrorStopParsing,
}).parseFromString(source, "application/xml"),
);
} catch (cause: unknown) {
return err(
cause instanceof XmlDocumentParseError
? cause
: new XmlDocumentParseError("malformed", cause),
);
}
};