mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 17:28:08 +00:00
fix(plist): decode XML data as CoreFoundation does
XML <data> used WHATWG atob, which pads an incomplete trailing quartet into bytes the native reader never produces and refuses non-alphabet characters that CFPropertyList skips. Decode at the shared XML value owner by skipping non-alphabet characters and emitting only completed quartets, shortened by the trailing padding run. Fixes #4978
This commit is contained in:
1 parent
b83b9caf4a
commit
87bfcbd70f
3 files changed
+53
-5
No files matched your search
@@ -24,6 +24,20 @@ it.each([
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["AQID", [1, 2, 3]],
|
||||
[" A Q I D\n", [1, 2, 3]],
|
||||
["AQI", []],
|
||||
["AQ!ID", [1, 2, 3]],
|
||||
["AQ==", [1]],
|
||||
["AQI=", [1, 2]],
|
||||
["AQ= =\n", [1]],
|
||||
["", []],
|
||||
])("decodes data text %j as CFPropertyList does", (text, expected) => {
|
||||
const parsed = parseXmlPropertyList(`<plist><data>${text}</data></plist>`);
|
||||
expect(parsed.value).toEqual(Uint8Array.from(expected));
|
||||
});
|
||||
|
||||
it("collects source integers and CDATA reals before rounding and duplicate assignment", () => {
|
||||
const parsed = parseXmlPropertyList(
|
||||
"<plist><dict><key>a</key><integer>0x20000000000001</integer><key>a</key><integer>9007199254740992</integer><key>b</key><real>9007199254<![CDATA[740992]]></real></dict></plist>",
|
||||
|
||||
@@ -7,6 +7,32 @@ import {
|
||||
parsePlistNonFiniteRealLiteral,
|
||||
} from "./apple/plistNumbers.js";
|
||||
|
||||
const BASE64_DIGITS =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
const C_WHITESPACE = new Set([" ", "\t", "\n", "\v", "\f", "\r"]);
|
||||
|
||||
// CFPropertyList skips non-alphabet characters and emits only completed
|
||||
// quartets; atob would pad or refuse data the native reader accepts.
|
||||
const decodePlistData = (text: string): Uint8Array => {
|
||||
const bytes: number[] = [];
|
||||
let quartet = 0;
|
||||
let digits = 0;
|
||||
let padding = 0;
|
||||
for (const character of text) {
|
||||
if (character === "=") padding += 1;
|
||||
else if (!C_WHITESPACE.has(character)) padding = 0;
|
||||
const digit = character === "=" ? 0 : BASE64_DIGITS.indexOf(character);
|
||||
if (digit < 0) continue;
|
||||
quartet = ((quartet << 6) | digit) & 0xffffff;
|
||||
digits += 1;
|
||||
if (digits % 4 !== 0) continue;
|
||||
bytes.push(quartet >> 16);
|
||||
if (padding < 2) bytes.push((quartet >> 8) & 0xff);
|
||||
if (padding < 1) bytes.push(quartet & 0xff);
|
||||
}
|
||||
return Uint8Array.from(bytes);
|
||||
};
|
||||
|
||||
/** Interpret an already validated, depth-bounded DOM, owning values and number facts together. */
|
||||
export const decodeXmlPropertyList = (document: Document) => {
|
||||
const root = document.documentElement;
|
||||
@@ -132,10 +158,8 @@ export const decodeXmlPropertyList = (document: Document) => {
|
||||
if (literal.complete) numbers.real(value);
|
||||
return value;
|
||||
}
|
||||
case "data": {
|
||||
const binary = atob(content(node, textNode).text.replace(/\s+/gu, ""));
|
||||
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
||||
}
|
||||
case "data":
|
||||
return decodePlistData(content(node, textNode).text);
|
||||
case "date": {
|
||||
const value = requireContent(node).nodeValue;
|
||||
return new Date(value === null ? 0 : value);
|
||||
|
||||
@@ -34,8 +34,18 @@ cliTest.for([
|
||||
element: `<integer>1${"0".repeat(400)}</integer>`,
|
||||
expected: { $plist_type: "integer", decimal: `1${"0".repeat(400)}` },
|
||||
},
|
||||
{
|
||||
name: "incomplete data quartet",
|
||||
element: "<data>AQI</data>",
|
||||
expected: { $plist_type: "data", base64: "" },
|
||||
},
|
||||
{
|
||||
name: "data with a non-alphabet character",
|
||||
element: "<data>AQ!ID</data>",
|
||||
expected: { $plist_type: "data", base64: "AQID" },
|
||||
},
|
||||
])(
|
||||
"preserves $name XML numbers through CLI and MCP",
|
||||
"preserves $name XML scalars through CLI and MCP",
|
||||
async (scenario, { cli }) => {
|
||||
const root = await createTestTempDirectory("rea-keyed-xml-numbers-");
|
||||
const bytes = Buffer.from(
|
||||
|
||||
Reference in new issue
Block a user