fix(native): keep plist XML element types for large numbers
The XML fallback inferred an integer from `Number.isInteger`, so an integral `<real>` beyond 2^53 was misreported as an unknown integer. Read the `<integer>` and `<real>` literals from plutil's XML text instead: an unsafe integral number that matches one `<integer>` literal and no `<real>` gets its exact decimal; a number that could be either stays as decoded with a limitation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
430c297ca4
commit
f85056d83e
|
|
@ -26,13 +26,13 @@ it("reports no limitation when every JSON integer is exact", () => {
|
|||
expect(parsed.value.limitations).toEqual([]);
|
||||
});
|
||||
|
||||
it("marks rounded XML integers as unknown instead of reporting them", () => {
|
||||
it("recovers exact XML integers from their element literals", () => {
|
||||
const parsed = parsePlistXml(
|
||||
"<plist><dict><key>Max</key><integer>18446744073709551615</integer><key>Safe</key><integer>7</integer><key>Blob</key><data>AA==</data></dict></plist>",
|
||||
);
|
||||
if (!parsed.ok) throw new Error("expected plist XML to parse");
|
||||
expect(parsed.value.value).toEqual({
|
||||
Max: { $plist_type: "integer", decimal: null },
|
||||
Max: { $plist_type: "integer", decimal: "18446744073709551615" },
|
||||
Safe: 7,
|
||||
Blob: { $plist_type: "data", base64: "AA==" },
|
||||
});
|
||||
|
|
@ -41,3 +41,33 @@ it("marks rounded XML integers as unknown instead of reporting them", () => {
|
|||
expect.stringContaining("1 integer value(s)"),
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps integral XML reals beyond the safe range as reals", () => {
|
||||
const parsed = parsePlistXml(
|
||||
"<plist><dict><key>Real</key><real>9007199254740992</real><key>Blob</key><data>AA==</data></dict></plist>",
|
||||
);
|
||||
if (!parsed.ok) throw new Error("expected plist XML to parse");
|
||||
expect(parsed.value.value).toEqual({
|
||||
Real: 9007199254740992,
|
||||
Blob: { $plist_type: "data", base64: "AA==" },
|
||||
});
|
||||
expect(parsed.value.limitations).toEqual([
|
||||
expect.stringContaining("XML conversion"),
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports XML numbers whose integer or real origin is ambiguous", () => {
|
||||
const parsed = parsePlistXml(
|
||||
"<plist><array><integer>9007199254740993</integer><real>9007199254740992</real><data>AA==</data></array></plist>",
|
||||
);
|
||||
if (!parsed.ok) throw new Error("expected plist XML to parse");
|
||||
expect(parsed.value.value).toEqual([
|
||||
9007199254740992,
|
||||
9007199254740992,
|
||||
{ $plist_type: "data", base64: "AA==" },
|
||||
]);
|
||||
expect(parsed.value.limitations).toEqual([
|
||||
expect.stringContaining("XML conversion"),
|
||||
expect.stringContaining("2 number(s)"),
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -93,13 +93,38 @@ export const parsePlistXml = (
|
|||
),
|
||||
);
|
||||
}
|
||||
let unknownIntegerCount = 0;
|
||||
// The XML decoder parses integers into numbers before REA sees their text,
|
||||
// so a value outside the exact range is already rounded and stays unknown.
|
||||
// The XML decoder turns <integer> and <real> into numbers before REA sees
|
||||
// them, rounding integers beyond the exact range. plutil escapes "<" in keys
|
||||
// and strings, so the element literals can be read from the XML text.
|
||||
const unsafeIntegers = new Map<number, Set<string>>();
|
||||
for (const [, decimal = ""] of output.matchAll(
|
||||
/<integer>\s*([+-]?\d+)\s*<\/integer>/gu,
|
||||
)) {
|
||||
const rounded = Number(decimal);
|
||||
if (Number.isSafeInteger(rounded)) continue;
|
||||
const exact = BigInt(decimal).toString();
|
||||
unsafeIntegers.set(
|
||||
rounded,
|
||||
(unsafeIntegers.get(rounded) ?? new Set()).add(exact),
|
||||
);
|
||||
}
|
||||
const reals = new Set(
|
||||
[...output.matchAll(/<real>([^<]*)<\/real>/gu)].map(([, text = ""]) =>
|
||||
Number.parseFloat(text),
|
||||
),
|
||||
);
|
||||
let exactIntegerCount = 0;
|
||||
let ambiguousNumberCount = 0;
|
||||
const value = mapJsonNumbers(projected.value, (item) => {
|
||||
if (!Number.isInteger(item) || Number.isSafeInteger(item)) return item;
|
||||
unknownIntegerCount += 1;
|
||||
return { $plist_type: "integer", decimal: null };
|
||||
const decimals = unsafeIntegers.get(item);
|
||||
if (decimals === undefined) return item;
|
||||
const [decimal] = decimals;
|
||||
if (decimals.size !== 1 || decimal === undefined || reals.has(item)) {
|
||||
ambiguousNumberCount += 1;
|
||||
return item;
|
||||
}
|
||||
exactIntegerCount += 1;
|
||||
return { $plist_type: "integer", decimal };
|
||||
});
|
||||
return ok({
|
||||
value,
|
||||
|
|
@ -111,10 +136,13 @@ export const parsePlistXml = (
|
|||
: [
|
||||
`${String(projected.unknownRealCount)} non-finite real value(s) are reported as { "$plist_type": "real", "value": null } because the XML decoder does not distinguish NaN from infinity.`,
|
||||
]),
|
||||
...(unknownIntegerCount === 0
|
||||
...(exactIntegerCount === 0
|
||||
? []
|
||||
: [largeIntegerLimitation(exactIntegerCount)]),
|
||||
...(ambiguousNumberCount === 0
|
||||
? []
|
||||
: [
|
||||
`${String(unknownIntegerCount)} integer value(s) exceed the exact range of a JSON number and are reported as { "$plist_type": "integer", "decimal": null } because the XML decoder rounds them.`,
|
||||
`${String(ambiguousNumberCount)} number(s) beyond the exact range of a JSON number are reported as decoded because the XML literal they came from, a rounded <integer> or a <real>, cannot be identified.`,
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -134,9 +134,9 @@ describe.skipIf(process.platform !== "darwin")(
|
|||
describe.skipIf(process.platform !== "darwin")(
|
||||
"native plist CLI integers",
|
||||
() => {
|
||||
cliTest(
|
||||
"reports exact plist integers beyond the JSON number range",
|
||||
async ({ cli }) => {
|
||||
cliTest.for([false, true])(
|
||||
"reports exact plist integers beyond the JSON number range (data: %s)",
|
||||
async (withData, { cli }) => {
|
||||
const directory = await createTestTempDirectory("rea-plist-integers-");
|
||||
const app = join(directory, "Example.app");
|
||||
const contents = join(app, "Contents");
|
||||
|
|
@ -153,6 +153,7 @@ describe.skipIf(process.platform !== "darwin")(
|
|||
"<key>Max</key><integer>18446744073709551615</integer>",
|
||||
"<key>Min</key><integer>-9223372036854775808</integer>",
|
||||
"<key>Count</key><integer>42</integer>",
|
||||
...(withData ? ["<key>Blob</key><data>AAEC</data>"] : []),
|
||||
"</dict></plist>",
|
||||
].join(""),
|
||||
);
|
||||
|
|
@ -171,7 +172,10 @@ describe.skipIf(process.platform !== "darwin")(
|
|||
Min: { $plist_type: "integer", decimal: "-9223372036854775808" },
|
||||
Count: 42,
|
||||
},
|
||||
limitations: [expect.stringContaining("2 integer value(s)")],
|
||||
limitations: [
|
||||
...(withData ? [expect.stringContaining("XML conversion")] : []),
|
||||
expect.stringContaining("2 integer value(s)"),
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue