mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
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.
This commit is contained in:
1 parent
69324b0f86
commit
f2b7bbb312
9 files changed
+243
-81
No files matched your search
@@ -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 `<integer>-0</integer>` literal, remain the number `0`.
|
||||
|
||||
An XML `<real>` 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
|
||||
|
||||
|
||||
@@ -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 `<real>` 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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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": "<spelling>" } 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": "<spelling>" } 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<string, JsonValue> => ({
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"],
|
||||
["<![CDATA[-inf]]>", "-infinity"],
|
||||
["nan", NaN],
|
||||
["NaN", NaN],
|
||||
["+infinity", Infinity],
|
||||
["infinity", Infinity],
|
||||
["INFINITY", Infinity],
|
||||
["inf", Infinity],
|
||||
["+inf", Infinity],
|
||||
["-infinity", -Infinity],
|
||||
["-Infinity", -Infinity],
|
||||
["-inf", -Infinity],
|
||||
["<![CDATA[-inf]]>", -Infinity],
|
||||
] as const)(
|
||||
"retains the canonical non-finite real value for %s",
|
||||
(literal, spelling) => {
|
||||
(literal, expected) => {
|
||||
const parsed = parseXmlPropertyList(
|
||||
`<plist><real>${literal}</real></plist>`,
|
||||
);
|
||||
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(
|
||||
"<plist><array><real>1e999</real><real>-1e999</real><real> nan </real><real>-nan</real><real>infinite</real></array></plist>",
|
||||
);
|
||||
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(
|
||||
"<plist><real>-inf<!-- unsupported --></real></plist>",
|
||||
);
|
||||
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<!-- unsupported -->"],
|
||||
])("keeps unresolved <%s> numeric identity explicit", (tag, text) => {
|
||||
const parsed = parseXmlPropertyList(
|
||||
`<plist><${tag}>${text}</${tag}></plist>`,
|
||||
);
|
||||
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);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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": "<spelling>" } 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": "<spelling>" } 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.',
|
||||
]);
|
||||
});
|
||||
@@ -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": "<spelling>" } 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": "<spelling>" } using canonical numeric labels nan, +infinity or -infinity because a JSON number cannot carry them.',
|
||||
);
|
||||
expect(result.limitations).not.toContainEqual(
|
||||
expect.stringContaining('"value": null'),
|
||||
|
||||
Reference in new issue
Block a user