From f2b7bbb31279a820f4cf32d439570f8e5381d010 Mon Sep 17 00:00:00 2001
From: morluto <76467478+morluto@users.noreply.github.com>
Date: Sun, 18 Oct 2026 07:55:51 +0800
Subject: [PATCH] fix(plist): retain known binary non-finite value identity
Make XML numeric uncertainty explicit at its producing boundary, separate
from decoded IEEE NaN and signed infinities. Shared projection preserves
known values using canonical numeric labels; unresolved XML numeric
values remain explicit unknowns. Remove the superseded known-value
wrapper and spelling alias, keeping numeric interpretation with its owner.
Cover public CLI/MCP binary archives, lookalike dictionaries, authored
32/64-bit IEEE payloads, signed zero, and XML parsing/overflow uncertainty.
Document canonical labels and unrepresented NaN payload/sign details.
Fixes #4919. Related: #4906, #4852.
Validation: baseline 2 failures/5 controls through public CLI/MCP; stable
final 439 passed across25files, check:fast, docs:check and discovery.
Native macOS plutil and existing codesign controls executed; no
Linux/Windows run or real Interface Builder engine qualification.
---
docs/native-investigation.md | 28 +++---
src/domain/apple/plistNumbers.ts | 14 +--
src/domain/apple/plistValue.test.ts | 91 +++++++++++++++++--
src/domain/apple/plistValue.ts | 38 ++++----
src/domain/propertyListKeys.ts | 5 +-
src/domain/propertyListXml.test.ts | 62 ++++++++-----
src/domain/propertyListXml.ts | 16 +++-
src/native/parsers/plist.test.ts | 4 +-
.../keyedArchiveScalarKinds.test.ts | 66 +++++++++++++-
9 files changed, 243 insertions(+), 81 deletions(-)
diff --git a/docs/native-investigation.md b/docs/native-investigation.md
index fed3529bf5..aa138587e8 100644
--- a/docs/native-investigation.md
+++ b/docs/native-investigation.md
@@ -634,17 +634,23 @@ because a JSON number cannot carry the sign of zero. `inspect_plist` and
Builder node attributes use the same value. Positive zero and integer zero,
including an `-0` literal, remain the number `0`.
-An XML `` spelled the way CFPropertyList reads and writes non-finite
-reals (`nan`, `+infinity`, `-infinity`, and the parser's `infinity`, `inf`,
-`+inf` and `-inf`, compared case-insensitively) is reported as
-`{ "$plist_type": "real", "value": "nan" | "+infinity" | "-infinity" }` with
-the kind and sign preserved using CFPropertyList's canonical writer spelling.
-Case and `inf` aliases are normalized; the original archive bytes retain the
-source literal. A non-finite number without a completely observed spelling,
-such as a directly decoded binary plist double or XML text the decoder could
-not read as a number, is reported as `{ "$plist_type": "real", "value": null }`. Both are
-counted in separate limitations. Native `inspect_plist` can recover canonical
-non-finite values from plutil's XML conversion even for a binary source.
+Known non-finite reals are reported as
+`{ "$plist_type": "real", "value": "nan" | "+infinity" | "-infinity" }`.
+These are canonical numeric labels, not a claim about original source spelling.
+Binary IEEE reals retain NaN versus signed infinity; NaN payload bits and sign
+are not represented in the JSON value, and the original bytes remain the source
+for those details. XML CFPropertyList literals (`nan`, `+infinity`, `-infinity`,
+`infinity`, `inf`, `+inf`, `-inf`, case-insensitively) decode to the same known
+values. Case and aliases normalize; original archive bytes retain source text.
+
+XML numeric text whose identity the decoder cannot establish, including
+unrecognized non-finite text and finite literals that overflow the numeric
+representation, remains `{ "$plist_type": "real", "value": null }` with a
+counted limitation. The XML decoder marks these values explicitly before shared
+projection, so they cannot be confused with known binary IEEE values.
+Incomplete numeric markup retains its existing compatibility behavior described
+above. Known non-finite and unresolved numeric values have separate counts.
+Native `inspect_plist` uses these same labels for plutil's XML conversion.
## Locate a byte signature
diff --git a/src/domain/apple/plistNumbers.ts b/src/domain/apple/plistNumbers.ts
index 49a681b911..18302e5a3d 100644
--- a/src/domain/apple/plistNumbers.ts
+++ b/src/domain/apple/plistNumbers.ts
@@ -1,5 +1,8 @@
import type { JsonValue } from "../jsonValue.js";
+/** XML numeric decoding could not establish a representable value identity. */
+export class UnknownPlistReal {}
+
/** Exact decimal or signed hexadecimal XML integer text. */
export const parsePlistIntegerLiteral = (text: string): bigint | undefined => {
const match = /^([+-]?)(?:0[xX]([\da-fA-F]+)|(\d+))$/u.exec(text.trim());
@@ -134,9 +137,6 @@ export const createPlistNumberProjection = (literals: PlistNumberLiterals) => {
};
};
-/** The spellings CFPropertyList writes for reals a JSON number cannot carry. */
-export type PlistNonFiniteRealSpelling = "nan" | "+infinity" | "-infinity";
-
/**
* Read a whole `` text as one of CFPropertyList's non-finite spellings.
* Its parser compares the complete text case-insensitively and also accepts
@@ -146,19 +146,19 @@ export type PlistNonFiniteRealSpelling = "nan" | "+infinity" | "-infinity";
*/
export const parsePlistNonFiniteRealLiteral = (
text: string,
-): PlistNonFiniteRealSpelling | undefined => {
+): number | undefined => {
if (text.length < 3 || text.length > 9) return undefined;
switch (text.toLowerCase()) {
case "nan":
- return "nan";
+ return Number.NaN;
case "+infinity":
case "infinity":
case "+inf":
case "inf":
- return "+infinity";
+ return Number.POSITIVE_INFINITY;
case "-infinity":
case "-inf":
- return "-infinity";
+ return Number.NEGATIVE_INFINITY;
default:
return undefined;
}
diff --git a/src/domain/apple/plistValue.test.ts b/src/domain/apple/plistValue.test.ts
index d34175a167..91a772eafc 100644
--- a/src/domain/apple/plistValue.test.ts
+++ b/src/domain/apple/plistValue.test.ts
@@ -1,6 +1,8 @@
import { expect, it } from "vitest";
+import { parseBinary } from "plist";
-import { PlistNonFiniteReal, projectPlistValue } from "./plistValue.js";
+import { projectPlistValue } from "./plistValue.js";
+import { UnknownPlistReal } from "./plistNumbers.js";
// Binary decoders build dates from seconds; beyond the Date range they are invalid.
it("projects an unrepresentable date as unknown and counts it", () => {
@@ -128,15 +130,15 @@ it("keeps retained integer scalars distinct from lookalike dictionaries", () =>
expect(observed[0]).not.toBe(projected.value[1]);
});
-it("projects spelled non-finite reals as typed scalars and keeps unspelled ones unknown", () => {
+it("projects known non-finite numbers while retaining explicit XML unknowns", () => {
const observed: object[] = [];
const projected = projectPlistValue(
[
- new PlistNonFiniteReal("+infinity"),
- new PlistNonFiniteReal("-infinity"),
- new PlistNonFiniteReal("nan"),
- Number.NEGATIVE_INFINITY,
- Number.NaN,
+ Infinity,
+ -Infinity,
+ NaN,
+ new UnknownPlistReal(),
+ new UnknownPlistReal(),
{ $plist_type: "real", value: "nan" },
],
(wrapper) => observed.push(wrapper),
@@ -153,3 +155,78 @@ it("projects spelled non-finite reals as typed scalars and keeps unspelled ones
expect(observed).toEqual(projected.value.slice(0, 5));
expect(observed).not.toContain(projected.value[5]);
});
+
+it.each([
+ [
+ "float32",
+ [
+ "7f800000",
+ "ff800000",
+ "7fc00001",
+ "ffc00002",
+ "80000000",
+ "00000000",
+ "3fa00000",
+ ],
+ ],
+ [
+ "float64",
+ [
+ "7ff0000000000000",
+ "fff0000000000000",
+ "7ff8000000000001",
+ "fff8000000000002",
+ "8000000000000000",
+ "0000000000000000",
+ "3ff4000000000000",
+ ],
+ ],
+] as const)("retains independently authored %s IEEE values", (_, payloads) => {
+ // An array object followed by seven real objects; no plist builder oracle.
+ const start = 8 + 1 + payloads.length;
+ const width = payloads[0].length / 2;
+ const offsets = [
+ 8,
+ ...payloads.map((_, index) => start + index * (width + 1)),
+ ];
+ const table = start + payloads.length * (width + 1);
+ const trailer = Buffer.alloc(32);
+ trailer[6] = 1;
+ trailer[7] = 1;
+ trailer.writeBigUInt64BE(BigInt(offsets.length), 8);
+ trailer.writeBigUInt64BE(BigInt(table), 24);
+ const bytes = Buffer.concat([
+ Buffer.from("bplist00"),
+ Buffer.from([
+ 0xa0 | payloads.length,
+ ...payloads.map((_, index) => index + 1),
+ ]),
+ ...payloads.map((hex) =>
+ Buffer.concat([
+ Buffer.from([width === 4 ? 0x22 : 0x23]),
+ Buffer.from(hex, "hex"),
+ ]),
+ ),
+ Buffer.from(offsets),
+ trailer,
+ ]);
+ const original = Buffer.from(bytes);
+ const observed: object[] = [];
+ const projected = projectPlistValue(parseBinary(bytes), (value) =>
+ observed.push(value),
+ );
+ expect(projected.value).toEqual([
+ { $plist_type: "real", value: "+infinity" },
+ { $plist_type: "real", value: "-infinity" },
+ { $plist_type: "real", value: "nan" },
+ { $plist_type: "real", value: "nan" },
+ { $plist_type: "real", value: "-0" },
+ 0,
+ 1.25,
+ ]);
+ expect(projected.nonFiniteRealCount).toBe(4);
+ expect(projected.negativeZeroRealCount).toBe(1);
+ expect(projected.unknownRealCount).toBe(0);
+ expect(observed).toHaveLength(5);
+ expect(bytes).toEqual(original);
+});
diff --git a/src/domain/apple/plistValue.ts b/src/domain/apple/plistValue.ts
index 392810cac0..fb7b3feb2d 100644
--- a/src/domain/apple/plistValue.ts
+++ b/src/domain/apple/plistValue.ts
@@ -2,7 +2,7 @@ import { types } from "node:util";
import { z } from "zod";
import type { JsonValue } from "../jsonValue.js";
-import type { PlistNonFiniteRealSpelling } from "./plistNumbers.js";
+import { UnknownPlistReal } from "./plistNumbers.js";
/**
* Maximum plist value nesting the JSON projection supports. The XML decode
@@ -11,17 +11,12 @@ import type { PlistNonFiniteRealSpelling } from "./plistNumbers.js";
*/
export const MAX_PLIST_DEPTH = 128;
-/** A decoded non-finite real with its canonical CFPropertyList writer spelling. */
-export class PlistNonFiniteReal {
- constructor(readonly spelling: PlistNonFiniteRealSpelling) {}
-}
-
/** A decoded plist projected into JSON, with values JSON cannot express. */
export interface ProjectedPlistValue {
readonly value: JsonValue;
- /** Non-finite reals decoded with one of CFPropertyList's spellings. */
+ /** Known non-finite values, rendered with canonical numeric labels. */
readonly nonFiniteRealCount: number;
- /** Non-finite numbers without a spelling, such as NaN from unreadable text. */
+ /** XML numeric values whose identity could not be established. */
readonly unknownRealCount: number;
/** Negative-zero reals, which a JSON number would report as positive zero. */
readonly negativeZeroRealCount: number;
@@ -33,13 +28,13 @@ export interface ProjectedPlistValue {
export const unknownDateLimitation = (count: number): string =>
`${String(count)} date value(s) are reported as { "$plist_type": "date", "iso8601": null } because the decoder could not represent them, such as XML dates with years beyond 9999 or negative years, or dates outside the JavaScript date range.`;
-/** Limitation for results whose plist held spelled non-finite reals. */
+/** Limitation for known non-finite values; the labels do not imply XML provenance. */
export const nonFiniteRealLimitation = (count: number): string =>
- `${String(count)} non-finite real value(s) are reported as { "$plist_type": "real", "value": "" } with the CFPropertyList spelling nan, +infinity or -infinity because a JSON number cannot carry them.`;
+ `${String(count)} non-finite real value(s) are reported as { "$plist_type": "real", "value": "" } using canonical numeric labels nan, +infinity or -infinity because a JSON number cannot carry them.`;
-/** Limitation for results whose plist held non-finite numbers without a spelling. */
+/** Limitation for XML numeric values the decoder could not establish. */
export const unknownRealLimitation = (count: number): string =>
- `${String(count)} non-finite real value(s) are reported as { "$plist_type": "real", "value": null } because the decoder observed a non-finite number without a property-list spelling.`;
+ `${String(count)} numeric value(s) are reported as { "$plist_type": "real", "value": null } because XML numeric decoding could not establish their value identity.`;
/** The typed scalar that keeps a negative-zero real distinct from positive zero. */
export const negativeZeroReal = (): Record => ({
@@ -53,8 +48,8 @@ export const negativeZeroRealLimitation = (count: number): string =>
/**
* Project a decoded plist into JSON. Data, dates, retained big integers and
- * spelled non-finite reals become explicit `$plist_type` objects; a non-finite
- * number without a spelling becomes an explicit unknown, and a negative-zero
+ * known non-finite reals become explicit `$plist_type` objects. Unresolved XML
+ * numeric values remain explicit unknowns, and a negative-zero
* real keeps its sign, which JSON serialization would drop.
*/
export const projectPlistValue = (
@@ -82,9 +77,9 @@ export const projectPlistValue = (
$plist_type: "data",
base64: Buffer.from(item).toString("base64"),
});
- if (item instanceof PlistNonFiniteReal) {
- nonFiniteRealCount += 1;
- return scalar({ $plist_type: "real", value: item.spelling });
+ if (item instanceof UnknownPlistReal) {
+ unknownRealCount += 1;
+ return scalar({ $plist_type: "real", value: null });
}
if (item instanceof Date) {
if (!Number.isNaN(Date.prototype.getTime.call(item)))
@@ -96,8 +91,13 @@ export const projectPlistValue = (
return scalar({ $plist_type: "date", iso8601: null });
}
if (typeof item === "number" && !Number.isFinite(item)) {
- unknownRealCount += 1;
- return scalar({ $plist_type: "real", value: null });
+ nonFiniteRealCount += 1;
+ const label = Number.isNaN(item)
+ ? "nan"
+ : item > 0
+ ? "+infinity"
+ : "-infinity";
+ return scalar({ $plist_type: "real", value: label });
}
// Integer decoders never yield -0, so a signed zero here is a real.
if (typeof item === "number" && Object.is(item, -0)) {
diff --git a/src/domain/propertyListKeys.ts b/src/domain/propertyListKeys.ts
index b3379e6115..551391f468 100644
--- a/src/domain/propertyListKeys.ts
+++ b/src/domain/propertyListKeys.ts
@@ -3,11 +3,12 @@ import { parseOpenStep } from "plist";
import { decodeXmlPropertyList } from "./propertyListXml.js";
import {
+ UnknownPlistReal,
createPlistNumberCollector,
type PlistNumberLiterals,
} from "./apple/plistNumbers.js";
-import { MAX_PLIST_DEPTH, PlistNonFiniteReal } from "./apple/plistValue.js";
+import { MAX_PLIST_DEPTH } from "./apple/plistValue.js";
import { parseXmlDocument } from "./xmlDocument.js";
/** The dictionary key REA results cannot carry: validation strips it. */
@@ -42,7 +43,7 @@ export const omitPrototypeKeys = (
typeof item !== "object" ||
item instanceof Date ||
item instanceof Uint8Array ||
- item instanceof PlistNonFiniteReal
+ item instanceof UnknownPlistReal
)
return item;
return Object.fromEntries(
diff --git a/src/domain/propertyListXml.test.ts b/src/domain/propertyListXml.test.ts
index c88ed337de..586510b2c8 100644
--- a/src/domain/propertyListXml.test.ts
+++ b/src/domain/propertyListXml.test.ts
@@ -1,6 +1,7 @@
import { expect, it } from "vitest";
-import { PlistNonFiniteReal } from "./apple/plistValue.js";
+import { projectPlistValue } from "./apple/plistValue.js";
+import { UnknownPlistReal } from "./apple/plistNumbers.js";
import { parseXmlPropertyList } from "./propertyListKeys.js";
it.each([
@@ -86,41 +87,34 @@ it("retains native decoder null and nested-envelope behavior", () => {
});
it.each([
- ["nan", "nan"],
- ["NaN", "nan"],
- ["+infinity", "+infinity"],
- ["infinity", "+infinity"],
- ["INFINITY", "+infinity"],
- ["inf", "+infinity"],
- ["+inf", "+infinity"],
- ["-infinity", "-infinity"],
- ["-Infinity", "-infinity"],
- ["-inf", "-infinity"],
- ["", "-infinity"],
+ ["nan", NaN],
+ ["NaN", NaN],
+ ["+infinity", Infinity],
+ ["infinity", Infinity],
+ ["INFINITY", Infinity],
+ ["inf", Infinity],
+ ["+inf", Infinity],
+ ["-infinity", -Infinity],
+ ["-Infinity", -Infinity],
+ ["-inf", -Infinity],
+ ["", -Infinity],
] as const)(
"retains the canonical non-finite real value for %s",
- (literal, spelling) => {
+ (literal, expected) => {
const parsed = parseXmlPropertyList(
`${literal}`,
);
- expect(parsed.value).toBeInstanceOf(PlistNonFiniteReal);
- expect(parsed.value).toEqual(new PlistNonFiniteReal(spelling));
- expect(parsed.numberLiterals.reals.size).toBe(0);
+ expect(parsed.value).toBe(expected);
+ expect(parsed.numberLiterals.reals).toEqual(new Set([expected]));
expect(parsed.numberLiterals.incomplete).toBe(false);
},
);
-it("keeps non-finite reals without a property-list spelling as plain numbers", () => {
+it("keeps XML numeric overflow and unreadable text explicitly unknown", () => {
const parsed = parseXmlPropertyList(
"1e999-1e999 nan -naninfinite",
);
- expect(parsed.value).toEqual([
- Number.POSITIVE_INFINITY,
- Number.NEGATIVE_INFINITY,
- Number.NaN,
- Number.NaN,
- Number.NaN,
- ]);
+ expect(parsed.value).toEqual(Array.from({ length: 5 }, () => new UnknownPlistReal()));
expect(parsed.numberLiterals.incomplete).toBe(false);
});
@@ -128,6 +122,24 @@ it("does not infer a spelled real from incomplete numeric markup", () => {
const parsed = parseXmlPropertyList(
"-inf",
);
- expect(parsed.value).toBeNaN();
+ expect(parsed.value).toBeInstanceOf(UnknownPlistReal);
expect(parsed.numberLiterals.incomplete).toBe(true);
});
+
+it.each([
+ ["integer", "unreadable"],
+ ["integer", `${"9".repeat(400)}invalid`],
+ ["real", "1e999"],
+ ["real", "-1e999"],
+ ["real", "Infinitysuffix"],
+ ["real", "nan"],
+])("keeps unresolved <%s> numeric identity explicit", (tag, text) => {
+ const parsed = parseXmlPropertyList(
+ `<${tag}>${text}${tag}>`,
+ );
+ expect(parsed.value).toBeInstanceOf(UnknownPlistReal);
+ const projected = projectPlistValue(parsed.value);
+ expect(projected.value).toEqual({ $plist_type: "real", value: null });
+ expect(projected.unknownRealCount).toBe(1);
+ expect(projected.nonFiniteRealCount).toBe(0);
+});
diff --git a/src/domain/propertyListXml.ts b/src/domain/propertyListXml.ts
index 604911f544..f7e123b9cc 100644
--- a/src/domain/propertyListXml.ts
+++ b/src/domain/propertyListXml.ts
@@ -1,11 +1,11 @@
import type { Document, Node } from "@xmldom/xmldom";
import {
+ UnknownPlistReal,
createPlistNumberCollector,
parsePlistIntegerLiteral,
parsePlistNonFiniteRealLiteral,
} from "./apple/plistNumbers.js";
-import { PlistNonFiniteReal } from "./apple/plistValue.js";
/** Interpret an already validated, depth-bounded DOM, owning values and number facts together. */
export const decodeXmlPropertyList = (document: Document) => {
@@ -102,7 +102,11 @@ export const decodeXmlPropertyList = (document: Document) => {
numbers.markIncomplete();
const partial = Number.parseInt(first.nodeValue ?? "", 10);
// An integer has no signed zero; keep -0 from reading as a real.
- return partial === 0 ? 0 : partial;
+ return !Number.isFinite(partial)
+ ? new UnknownPlistReal()
+ : partial === 0
+ ? 0
+ : partial;
}
numbers.integer(exact);
const decoded = Number(exact);
@@ -116,11 +120,15 @@ export const decodeXmlPropertyList = (document: Document) => {
if (!literal.complete) numbers.markIncomplete();
// CFPropertyList spells NaN and the infinities; parseFloat would read
// the lowercase spellings as NaN and lose the kind and sign.
- const spelling = literal.complete
+ const known = literal.complete
? parsePlistNonFiniteRealLiteral(literal.text)
: undefined;
- if (spelling !== undefined) return new PlistNonFiniteReal(spelling);
+ if (known !== undefined) {
+ numbers.real(known);
+ return known;
+ }
const value = Number.parseFloat(literal.text);
+ if (!Number.isFinite(value)) return new UnknownPlistReal();
if (literal.complete) numbers.real(value);
return value;
}
diff --git a/src/native/parsers/plist.test.ts b/src/native/parsers/plist.test.ts
index f570d262c2..a23f87e2df 100644
--- a/src/native/parsers/plist.test.ts
+++ b/src/native/parsers/plist.test.ts
@@ -194,7 +194,7 @@ it("keeps CFPropertyList's non-finite real spellings in XML evidence", () => {
);
expect(parsed.value.limitations).toEqual([
expect.stringContaining("XML conversion"),
- '3 non-finite real value(s) are reported as { "$plist_type": "real", "value": "" } with the CFPropertyList spelling nan, +infinity or -infinity because a JSON number cannot carry them.',
- '1 non-finite real value(s) are reported as { "$plist_type": "real", "value": null } because the decoder observed a non-finite number without a property-list spelling.',
+ '3 non-finite real value(s) are reported as { "$plist_type": "real", "value": "" } using canonical numeric labels nan, +infinity or -infinity because a JSON number cannot carry them.',
+ '1 numeric value(s) are reported as { "$plist_type": "real", "value": null } because XML numeric decoding could not establish their value identity.',
]);
});
diff --git a/tests/acceptance/applications/keyedArchiveScalarKinds.test.ts b/tests/acceptance/applications/keyedArchiveScalarKinds.test.ts
index 014455d749..d2b3eb5d4f 100644
--- a/tests/acceptance/applications/keyedArchiveScalarKinds.test.ts
+++ b/tests/acceptance/applications/keyedArchiveScalarKinds.test.ts
@@ -108,11 +108,10 @@ cliTest.for(["xml", "binary"] as const)(
$plist_type: "date",
iso8601: "2001-01-01T00:00:00.000Z",
});
- // plist@5 writes JavaScript's Infinity, which CFPropertyList reads as
- // +infinity; the binary double carries no spelling.
+ // XML and binary decoding preserve the same non-finite value identity.
expect(result.objects[7]?.value).toEqual({
$plist_type: "real",
- value: format === "xml" ? "+infinity" : null,
+ value: "+infinity",
});
expect(result.objects[10]?.value).toEqual({
$plist_type: "real",
@@ -287,6 +286,65 @@ cliTest(
},
);
+cliTest("preserves binary IEEE non-finite scalar identity", async ({ cli }) => {
+ const root = await createTestTempDirectory("rea-binary-nonfinite-");
+ const path = join(root, "archive.plist");
+ const bytes = Buffer.from(
+ buildBinary({
+ $archiver: "NSKeyedArchiver",
+ $version: 100000,
+ $top: {
+ positive: { CF$UID: 1 },
+ negative: { CF$UID: 2 },
+ nan: { CF$UID: 3 },
+ },
+ $objects: [
+ "$null",
+ Infinity,
+ -Infinity,
+ NaN,
+ { $plist_type: "real", value: "nan" },
+ 1.25,
+ ],
+ }),
+ );
+ await writeFile(path, bytes);
+ const output = await cli.run({
+ arguments: ["inspect-keyed-archive", path, "--json"],
+ environment: { HOME: root, USERPROFILE: root, REA_LOG_LEVEL: "silent" },
+ });
+ expect(output.exitCode, output.stderr).toBe(0);
+ const result = keyedArchiveResultSchema.parse(
+ parseEvidence(output.json).normalized_result,
+ );
+ expect(
+ result.objects.slice(1).map(({ kind, value }) => ({ kind, value })),
+ ).toEqual([
+ { kind: "scalar", value: { $plist_type: "real", value: "+infinity" } },
+ { kind: "scalar", value: { $plist_type: "real", value: "-infinity" } },
+ { kind: "scalar", value: { $plist_type: "real", value: "nan" } },
+ { kind: "dictionary", value: { $plist_type: "real", value: "nan" } },
+ { kind: "scalar", value: 1.25 },
+ ]);
+ expect(result.archive_sha256).toBe(
+ createHash("sha256").update(bytes).digest("hex"),
+ );
+ const { call } = await connectLocalToolsMcp({
+ HOME: root,
+ USERPROFILE: root,
+ });
+ const opened = await call("open_binary", { path });
+ expect(opened.isError).not.toBe(true);
+ const mcp = await call("inspect_keyed_archive", {});
+ expect(mcp.isError).not.toBe(true);
+ expect(
+ keyedArchiveResultSchema.parse(
+ parseEvidence(mcp.structuredContent).normalized_result,
+ ),
+ ).toEqual(result);
+ expect(await readFile(path)).toEqual(bytes);
+});
+
cliTest(
"keeps CFPropertyList's non-finite real spellings in an XML keyed archive",
async ({ cli }) => {
@@ -352,7 +410,7 @@ cliTest(
'["$null",{"$plist_type":"real","value":"+infinity"},{"$plist_type":"real","value":"-infinity"},{"$plist_type":"real","value":"nan"},1.25]',
);
expect(result.limitations).toContain(
- '3 non-finite real value(s) are reported as { "$plist_type": "real", "value": "" } with the CFPropertyList spelling nan, +infinity or -infinity because a JSON number cannot carry them.',
+ '3 non-finite real value(s) are reported as { "$plist_type": "real", "value": "" } using canonical numeric labels nan, +infinity or -infinity because a JSON number cannot carry them.',
);
expect(result.limitations).not.toContainEqual(
expect.stringContaining('"value": null'),