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}`, + ); + 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'),