diff --git a/docs/native-investigation.md b/docs/native-investigation.md
index dd749f80e5..fed3529bf5 100644
--- a/docs/native-investigation.md
+++ b/docs/native-investigation.md
@@ -634,6 +634,18 @@ 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.
+
## Locate a byte signature
Use `search_bytes` after opening a target with Ghidra to locate a known constant
diff --git a/src/artifacts/apple/KeyedArchiveReader.ts b/src/artifacts/apple/KeyedArchiveReader.ts
index 71fd628f2b..79bb6ecc58 100644
--- a/src/artifacts/apple/KeyedArchiveReader.ts
+++ b/src/artifacts/apple/KeyedArchiveReader.ts
@@ -8,7 +8,9 @@ import {
} from "../../domain/analysisErrorCore.js";
import {
negativeZeroRealLimitation,
+ nonFiniteRealLimitation,
unknownDateLimitation,
+ unknownRealLimitation,
} from "../../domain/apple/plistValue.js";
import { createPlistNumberProjection } from "../../domain/apple/plistNumbers.js";
import {
@@ -86,6 +88,7 @@ export const decodeKeyedArchiveBytes = (
}
: parseXmlPropertyList(xmlText ?? "");
const {
+ nonFiniteRealCount,
unknownRealCount,
negativeZeroRealCount,
unknownDateCount,
@@ -115,11 +118,12 @@ export const decodeKeyedArchiveBytes = (
...(parsed.omittedPrototypeKeys === 0
? []
: [omittedPrototypeKeysLimitation(parsed.omittedPrototypeKeys)]),
+ ...(nonFiniteRealCount === 0
+ ? []
+ : [nonFiniteRealLimitation(nonFiniteRealCount)]),
...(unknownRealCount === 0
? []
- : [
- `${String(unknownRealCount)} non-finite real value(s) are reported as { "$plist_type": "real", "value": null } because JSON evidence cannot preserve NaN vs infinity.`,
- ]),
+ : [unknownRealLimitation(unknownRealCount)]),
...(negativeZeroRealCount === 0
? []
: [negativeZeroRealLimitation(negativeZeroRealCount)]),
diff --git a/src/domain/apple/keyedArchive.ts b/src/domain/apple/keyedArchive.ts
index 24c56db62a..5ab26c85fb 100644
--- a/src/domain/apple/keyedArchive.ts
+++ b/src/domain/apple/keyedArchive.ts
@@ -247,6 +247,7 @@ export const projectKeyedArchive = (
const page = objects.slice(selection.offset, end);
const ids = new Set(page.map(({ id }) => id));
return {
+ nonFiniteRealCount: projected.nonFiniteRealCount,
unknownRealCount: projected.unknownRealCount,
negativeZeroRealCount: projected.negativeZeroRealCount,
unknownDateCount: projected.unknownDateCount,
diff --git a/src/domain/apple/plistNumbers.ts b/src/domain/apple/plistNumbers.ts
index 04ca01af29..49a681b911 100644
--- a/src/domain/apple/plistNumbers.ts
+++ b/src/domain/apple/plistNumbers.ts
@@ -133,3 +133,33 @@ 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
+ * `infinity`, `inf`, `+inf` and `-inf`; its writer emits `nan`, `+infinity`
+ * and `-infinity`. Any other text, including surrounding whitespace, is not
+ * a spelled non-finite real.
+ */
+export const parsePlistNonFiniteRealLiteral = (
+ text: string,
+): PlistNonFiniteRealSpelling | undefined => {
+ if (text.length < 3 || text.length > 9) return undefined;
+ switch (text.toLowerCase()) {
+ case "nan":
+ return "nan";
+ case "+infinity":
+ case "infinity":
+ case "+inf":
+ case "inf":
+ return "+infinity";
+ case "-infinity":
+ case "-inf":
+ return "-infinity";
+ default:
+ return undefined;
+ }
+};
diff --git a/src/domain/apple/plistValue.test.ts b/src/domain/apple/plistValue.test.ts
index fae48252b1..d34175a167 100644
--- a/src/domain/apple/plistValue.test.ts
+++ b/src/domain/apple/plistValue.test.ts
@@ -1,6 +1,6 @@
import { expect, it } from "vitest";
-import { projectPlistValue } from "./plistValue.js";
+import { PlistNonFiniteReal, projectPlistValue } from "./plistValue.js";
// Binary decoders build dates from seconds; beyond the Date range they are invalid.
it("projects an unrepresentable date as unknown and counts it", () => {
@@ -11,6 +11,7 @@ it("projects an unrepresentable date as unknown and counts it", () => {
far: { $plist_type: "date", iso8601: null },
near: { $plist_type: "date", iso8601: "1970-01-01T00:00:00.000Z" },
},
+ nonFiniteRealCount: 0,
unknownRealCount: 0,
negativeZeroRealCount: 0,
unknownDateCount: 1,
@@ -126,3 +127,29 @@ it("keeps retained integer scalars distinct from lookalike dictionaries", () =>
expect(observed[0]).toBe(projected.value[0]);
expect(observed[0]).not.toBe(projected.value[1]);
});
+
+it("projects spelled non-finite reals as typed scalars and keeps unspelled ones unknown", () => {
+ const observed: object[] = [];
+ const projected = projectPlistValue(
+ [
+ new PlistNonFiniteReal("+infinity"),
+ new PlistNonFiniteReal("-infinity"),
+ new PlistNonFiniteReal("nan"),
+ Number.NEGATIVE_INFINITY,
+ Number.NaN,
+ { $plist_type: "real", value: "nan" },
+ ],
+ (wrapper) => observed.push(wrapper),
+ );
+ expect(projected.nonFiniteRealCount).toBe(3);
+ expect(projected.unknownRealCount).toBe(2);
+ expect(projected.negativeZeroRealCount).toBe(0);
+ expect(JSON.stringify(projected.value)).toBe(
+ '[{"$plist_type":"real","value":"+infinity"},{"$plist_type":"real","value":"-infinity"},{"$plist_type":"real","value":"nan"},{"$plist_type":"real","value":null},{"$plist_type":"real","value":null},{"$plist_type":"real","value":"nan"}]',
+ );
+ expect(Array.isArray(projected.value)).toBe(true);
+ if (!Array.isArray(projected.value)) throw new Error("Expected array");
+ // Only generated wrappers are reported; the lookalike stays a dictionary.
+ expect(observed).toEqual(projected.value.slice(0, 5));
+ expect(observed).not.toContain(projected.value[5]);
+});
diff --git a/src/domain/apple/plistValue.ts b/src/domain/apple/plistValue.ts
index 28ebe08a1d..392810cac0 100644
--- a/src/domain/apple/plistValue.ts
+++ b/src/domain/apple/plistValue.ts
@@ -2,6 +2,7 @@ import { types } from "node:util";
import { z } from "zod";
import type { JsonValue } from "../jsonValue.js";
+import type { PlistNonFiniteRealSpelling } from "./plistNumbers.js";
/**
* Maximum plist value nesting the JSON projection supports. The XML decode
@@ -10,10 +11,17 @@ import type { JsonValue } from "../jsonValue.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;
- /** Reals decoded as NaN, which the decoder also uses for ±infinity. */
+ /** Non-finite reals decoded with one of CFPropertyList's spellings. */
+ readonly nonFiniteRealCount: number;
+ /** Non-finite numbers without a spelling, such as NaN from unreadable text. */
readonly unknownRealCount: number;
/** Negative-zero reals, which a JSON number would report as positive zero. */
readonly negativeZeroRealCount: number;
@@ -25,6 +33,14 @@ 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. */
+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.`;
+
+/** Limitation for results whose plist held non-finite numbers without a spelling. */
+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.`;
+
/** The typed scalar that keeps a negative-zero real distinct from positive zero. */
export const negativeZeroReal = (): Record => ({
$plist_type: "real",
@@ -36,16 +52,17 @@ export const negativeZeroRealLimitation = (count: number): string =>
`${String(count)} negative-zero real value(s) are reported as { "$plist_type": "real", "value": "-0" } because a JSON number cannot carry the sign of zero.`;
/**
- * Project a decoded plist into JSON. Data, dates and retained big integers become explicit
- * `$plist_type` objects; a non-finite real becomes an explicit unknown
- * because the XML decoder reports NaN and ±infinity alike, and a
- * negative-zero real keeps its sign, which JSON serialization would drop.
+ * 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
+ * real keeps its sign, which JSON serialization would drop.
*/
export const projectPlistValue = (
value: unknown,
/** Observe generated wrappers before consumers classify the JSON value tree. */
onWrappedScalar?: (value: Record) => void,
): ProjectedPlistValue => {
+ let nonFiniteRealCount = 0;
let unknownRealCount = 0;
let negativeZeroRealCount = 0;
let unknownDateCount = 0;
@@ -65,6 +82,10 @@ 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 Date) {
if (!Number.isNaN(Date.prototype.getTime.call(item)))
return scalar({
@@ -115,6 +136,7 @@ export const projectPlistValue = (
const projected = project(value, 0);
return {
value: projected,
+ nonFiniteRealCount,
unknownRealCount,
negativeZeroRealCount,
unknownDateCount,
diff --git a/src/domain/propertyListKeys.ts b/src/domain/propertyListKeys.ts
index 24804ea8a3..b3379e6115 100644
--- a/src/domain/propertyListKeys.ts
+++ b/src/domain/propertyListKeys.ts
@@ -7,7 +7,7 @@ import {
type PlistNumberLiterals,
} from "./apple/plistNumbers.js";
-import { MAX_PLIST_DEPTH } from "./apple/plistValue.js";
+import { MAX_PLIST_DEPTH, PlistNonFiniteReal } from "./apple/plistValue.js";
import { parseXmlDocument } from "./xmlDocument.js";
/** The dictionary key REA results cannot carry: validation strips it. */
@@ -41,7 +41,8 @@ export const omitPrototypeKeys = (
item === null ||
typeof item !== "object" ||
item instanceof Date ||
- item instanceof Uint8Array
+ item instanceof Uint8Array ||
+ item instanceof PlistNonFiniteReal
)
return item;
return Object.fromEntries(
diff --git a/src/domain/propertyListXml.test.ts b/src/domain/propertyListXml.test.ts
index 155178a12a..c88ed337de 100644
--- a/src/domain/propertyListXml.test.ts
+++ b/src/domain/propertyListXml.test.ts
@@ -1,5 +1,6 @@
import { expect, it } from "vitest";
+import { PlistNonFiniteReal } from "./apple/plistValue.js";
import { parseXmlPropertyList } from "./propertyListKeys.js";
it.each([
@@ -83,3 +84,50 @@ it("retains native decoder null and nested-envelope behavior", () => {
).value,
).toEqual([{ value: null }, [null]]);
});
+
+it.each([
+ ["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) => {
+ 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.numberLiterals.incomplete).toBe(false);
+ },
+);
+
+it("keeps non-finite reals without a property-list spelling as plain numbers", () => {
+ 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.numberLiterals.incomplete).toBe(false);
+});
+
+it("does not infer a spelled real from incomplete numeric markup", () => {
+ const parsed = parseXmlPropertyList(
+ "-inf",
+ );
+ expect(parsed.value).toBeNaN();
+ expect(parsed.numberLiterals.incomplete).toBe(true);
+});
diff --git a/src/domain/propertyListXml.ts b/src/domain/propertyListXml.ts
index 558d407d4c..604911f544 100644
--- a/src/domain/propertyListXml.ts
+++ b/src/domain/propertyListXml.ts
@@ -3,7 +3,9 @@ import type { Document, Node } from "@xmldom/xmldom";
import {
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) => {
@@ -111,9 +113,15 @@ export const decodeXmlPropertyList = (document: Document) => {
case "real": {
requireContent(node);
const literal = content(node, characterNode);
+ 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
+ ? parsePlistNonFiniteRealLiteral(literal.text)
+ : undefined;
+ if (spelling !== undefined) return new PlistNonFiniteReal(spelling);
const value = Number.parseFloat(literal.text);
if (literal.complete) numbers.real(value);
- else numbers.markIncomplete();
return value;
}
case "data": {
diff --git a/src/native/parsers/plist.test.ts b/src/native/parsers/plist.test.ts
index 7459f9c767..f570d262c2 100644
--- a/src/native/parsers/plist.test.ts
+++ b/src/native/parsers/plist.test.ts
@@ -183,3 +183,18 @@ it("reports XML numbers whose integer or real origin is ambiguous", () => {
expect.stringContaining("2 number(s)"),
]);
});
+
+it("keeps CFPropertyList's non-finite real spellings in XML evidence", () => {
+ const parsed = parsePlistXml(
+ "Up+infinityDown-INFINITYMissingnanOverflow1e999Ratio1.25",
+ );
+ if (!parsed.ok) throw parsed.error;
+ expect(JSON.stringify(parsed.value.value)).toBe(
+ '{"Up":{"$plist_type":"real","value":"+infinity"},"Down":{"$plist_type":"real","value":"-infinity"},"Missing":{"$plist_type":"real","value":"nan"},"Overflow":{"$plist_type":"real","value":null},"Ratio":1.25}',
+ );
+ 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.',
+ ]);
+});
diff --git a/src/native/parsers/plist.ts b/src/native/parsers/plist.ts
index 5c79f6ade7..0b4fddea4d 100644
--- a/src/native/parsers/plist.ts
+++ b/src/native/parsers/plist.ts
@@ -5,8 +5,10 @@ import type { JsonValue } from "../../domain/jsonValue.js";
import {
negativeZeroReal,
negativeZeroRealLimitation,
+ nonFiniteRealLimitation,
projectPlistValue,
unknownDateLimitation,
+ unknownRealLimitation,
type ProjectedPlistValue,
} from "../../domain/apple/plistValue.js";
import {
@@ -202,11 +204,12 @@ export const parsePlistXml = (
bundle: projectPlistBundle(value),
limitations: [
'plutil cannot express this plist as JSON, so it was decoded from plutil\'s XML conversion; data and date values are typed objects such as { "$plist_type": "data", "base64": ... }.',
+ ...(projected.nonFiniteRealCount === 0
+ ? []
+ : [nonFiniteRealLimitation(projected.nonFiniteRealCount)]),
...(projected.unknownRealCount === 0
? []
- : [
- `${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.`,
- ]),
+ : [unknownRealLimitation(projected.unknownRealCount)]),
...negativeZeroLimitations(projected.negativeZeroRealCount),
...(projected.unknownDateCount === 0
? []
diff --git a/tests/acceptance/applications/keyedArchiveScalarKinds.test.ts b/tests/acceptance/applications/keyedArchiveScalarKinds.test.ts
index e709663f66..014455d749 100644
--- a/tests/acceptance/applications/keyedArchiveScalarKinds.test.ts
+++ b/tests/acceptance/applications/keyedArchiveScalarKinds.test.ts
@@ -108,9 +108,11 @@ 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.
expect(result.objects[7]?.value).toEqual({
$plist_type: "real",
- value: null,
+ value: format === "xml" ? "+infinity" : null,
});
expect(result.objects[10]?.value).toEqual({
$plist_type: "real",
@@ -284,3 +286,77 @@ cliTest(
}
},
);
+
+cliTest(
+ "keeps CFPropertyList's non-finite real spellings in an XML keyed archive",
+ async ({ cli }) => {
+ const root = await createTestTempDirectory("rea-keyed-non-finite-");
+ const archive = {
+ $archiver: "NSKeyedArchiver",
+ $version: 100000,
+ $top: {
+ up: { CF$UID: 1 },
+ down: { CF$UID: 2 },
+ missing: { CF$UID: 3 },
+ control: { CF$UID: 4 },
+ },
+ $objects: ["$null", 1.5, 0.25, 1.75, 1.25],
+ };
+ // plist@5 writes its own spellings for non-finite reals, so encode
+ // placeholder reals and patch them to the text CFPropertyList writes.
+ let xml = build(archive);
+ for (const [placeholder, spelling] of [
+ ["1.5", "+infinity"],
+ ["0.25", "-INFINITY"],
+ ["1.75", "nan"],
+ ] as const) {
+ expect(xml).toContain(`${placeholder}`);
+ xml = xml.replace(
+ `${placeholder}`,
+ `${spelling}`,
+ );
+ }
+ const bytes = Buffer.from(xml);
+ const path = join(root, "archive.plist");
+ await writeFile(path, bytes);
+ const output = await cli.run({
+ arguments: ["inspect-keyed-archive", path, "--json"],
+ environment: {
+ HOME: root,
+ USERPROFILE: root,
+ REA_LOG_LEVEL: "silent",
+ REA_ANALYSIS_PROVIDER: "auto",
+ },
+ });
+ expect(output.exitCode, output.stderr).toBe(0);
+ const result = keyedArchiveResultSchema.parse(
+ parseEvidence(output.json).normalized_result,
+ );
+ const { call } = await connectLocalToolsMcp({
+ HOME: root,
+ USERPROFILE: root,
+ });
+ const opened = await call("open_binary", { path });
+ expect(opened.isError, JSON.stringify(opened)).not.toBe(true);
+ const mcp = await call("inspect_keyed_archive", {});
+ expect(mcp.isError, JSON.stringify(mcp)).not.toBe(true);
+ expect(
+ keyedArchiveResultSchema.parse(
+ parseEvidence(mcp.structuredContent).normalized_result,
+ ),
+ ).toEqual(result);
+ for (const object of result.objects.slice(1)) {
+ expect(object).toMatchObject({ kind: "scalar", status: "observed" });
+ }
+ expect(JSON.stringify(result.objects.map(({ value }) => value))).toBe(
+ '["$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.',
+ );
+ expect(result.limitations).not.toContainEqual(
+ expect.stringContaining('"value": null'),
+ );
+ expect(await readFile(path)).toEqual(bytes);
+ },
+);
diff --git a/tests/boundary/cli/nativePlistCli.test.ts b/tests/boundary/cli/nativePlistCli.test.ts
index d63fdbffd7..88952029a4 100644
--- a/tests/boundary/cli/nativePlistCli.test.ts
+++ b/tests/boundary/cli/nativePlistCli.test.ts
@@ -1,5 +1,5 @@
import { execFile } from "node:child_process";
-import { mkdir, writeFile } from "node:fs/promises";
+import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
@@ -81,6 +81,42 @@ describe.skipIf(process.platform !== "darwin")(
describe.skipIf(process.platform !== "darwin")(
"native plist CLI source formats",
() => {
+ cliTest(
+ "retains non-finite values through real plutil XML conversion",
+ async ({ cli }) => {
+ const directory = await createTestTempDirectory("rea-plist-reals-");
+ const path = join(directory, "selected.plist");
+ const source = Buffer.from(`
+positive+infinity
+negative-INFINITY
+nannancontrol1.25
+`);
+ await writeFile(path, source);
+ const result = await cli.run({
+ arguments: ["inspect-plist", path, "--json"],
+ environment: {
+ REA_LOG_LEVEL: "silent",
+ REA_ANALYSIS_PROVIDER: "auto",
+ },
+ });
+ expect(result.exitCode, result.stderr).toBe(0);
+ expect(result.json).toMatchObject({
+ normalized_result: {
+ value: {
+ positive: { $plist_type: "real", value: "+infinity" },
+ negative: { $plist_type: "real", value: "-infinity" },
+ nan: { $plist_type: "real", value: "nan" },
+ control: 1.25,
+ },
+ limitations: expect.arrayContaining([
+ expect.stringContaining("3 non-finite real value(s)"),
+ ]),
+ },
+ });
+ expect(await readFile(path)).toEqual(source);
+ },
+ );
+
cliTest.for([
["openstep", "unknown"],
["openstep-data", "unknown"],
@@ -230,11 +266,11 @@ describe.skipIf(process.platform !== "darwin")(
iso8601: "2020-01-01T00:00:00.000Z",
},
Far: { $plist_type: "date", iso8601: null },
- Ratio: { $plist_type: "real", value: null },
+ Ratio: { $plist_type: "real", value: "nan" },
Nested: [
{
Ratio: 0.5,
- Unbounded: { $plist_type: "real", value: null },
+ Unbounded: { $plist_type: "real", value: "-infinity" },
"": "empty key",
},
],