fix(plist): keep character references beyond Unicode literal

Reserving the __proto__ placeholder decodes the whole document, so
<![CDATA[&#x110000;]]> or a comment with an out-of-range reference
threw a RangeError, and every XML plist reader failed on a valid
document. References beyond U+10FFFF now stay as written. The XML
decoder still rejects them outside comments and CDATA.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822 2026-10-07 08:12:59 +00:00
parent 6729ee7636
commit 5afd8a850b
2 changed files with 24 additions and 3 deletions

View File

@ -49,6 +49,16 @@ it("keeps an entity-encoded key that decodes to the placeholder", () => {
});
});
it("keeps entity-like text beyond Unicode in comments and CDATA", () => {
const text = plist(
"<!-- &#99999999999; --><dict><key>raw</key><string><![CDATA[&#x110000;]]></string><key>__proto__</key><string>x</string></dict>",
);
expect(parseXmlPropertyList(text)).toEqual({
value: { raw: "&#x110000;" },
omittedPrototypeKeys: 1,
});
});
it("omits own __proto__ entries that JSON decoding creates", () => {
const decoded: unknown = JSON.parse(
'{"__proto__":{"x":1},"a":[{"__proto__":2,"b":3}]}',

View File

@ -56,13 +56,23 @@ const XML_ENTITIES: Readonly<Record<string, string>> = {
quot: '"',
};
/**
* Expand XML character and predefined entity references. A reference beyond
* Unicode stays as written: comments and CDATA may hold such text literally,
* and the XML decoder rejects it anywhere else.
*/
const decodeXmlText = (text: string): string =>
text.replace(
/&(?:#x([\da-f]+)|#(\d+)|([a-z]+));/giu,
(entity, hex?: string, decimal?: string, name?: string) => {
if (hex !== undefined) return String.fromCodePoint(parseInt(hex, 16));
if (decimal !== undefined)
return String.fromCodePoint(parseInt(decimal, 10));
const codePoint =
hex !== undefined
? parseInt(hex, 16)
: decimal !== undefined
? parseInt(decimal, 10)
: undefined;
if (codePoint !== undefined)
return codePoint <= 0x10ffff ? String.fromCodePoint(codePoint) : entity;
return (name === undefined ? undefined : XML_ENTITIES[name]) ?? entity;
},
);
@ -77,6 +87,7 @@ export const parseXmlPropertyList = (
): PropertyListWithoutPrototypeKeys => {
// The decoded text holds every key the decoder can produce, including
// entity-encoded spellings of the placeholder, so no source key aliases it.
// Decoding comment or CDATA text too can only lengthen the placeholder.
const decoded = decodeXmlText(text);
let placeholder = "__rea_prototype_key__";
while (decoded.includes(placeholder)) placeholder = `_${placeholder}`;