Integrate the numeric precision correction from #1135 with complete supplemental metadata handling. Keep decoded numbers with an explicit limitation for mixed XML content or unclassified integer text, rather than assigning another literal's exact decimal. Project nonnumeric UID and CF$UID dictionary values recursively while preserving numeric references, graph identity and original byte digests. Add filesystem, built CLI and MCP regressions for the review findings, adjacent XML representations and numeric reference controls. Retain the original native Foundation qualification fixtures and verifier. Verification: 84 focused tests; npm run check; npm run docs:check; npm run verify:test-discovery; npm run verify:keyed-archive (real Foundation, CLI and stdio MCP). No real Hopper or Ghidra verification was needed or run. Co-authored-by: Rudy Mizrahi Celekli <47457359+rudycelekli@users.noreply.github.com> Signed-off-by: morluto <76467478+morluto@users.noreply.github.com>
155 lines
4.8 KiB
TypeScript
155 lines
4.8 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { build } from "plist";
|
|
import { expect } from "vitest";
|
|
|
|
import { keyedArchiveResultSchema } from "../../src/domain/apple/keyedArchive.js";
|
|
import type { JsonValue } from "../../src/domain/jsonValue.js";
|
|
import { createTestTempDirectory } from "./temporaryDirectory.js";
|
|
|
|
const EXACT_INTEGER = { $plist_type: "integer", decimal: "9007199254740993" };
|
|
const ROUNDED = 9007199254740992;
|
|
|
|
/** Accepted decoder representations whose supplemental metadata needs care. */
|
|
export const mixedArchiveNumberElements = [
|
|
"<real>9007199254740992<![CDATA[0]]></real>",
|
|
"<real><![CDATA[1]]>9007199254740992</real>",
|
|
"<integer>9007199254740992<!-- split -->3</integer>",
|
|
"<integer>9007199254740992<![CDATA[3]]></integer>",
|
|
"<integer><![CDATA[9007199254740992]]>3</integer>",
|
|
"<integer><![CDATA[90071992547409923]]></integer>",
|
|
"<integer>9007199254740992<?rea split?>3</integer>",
|
|
"<integer>9007199254740992<string>3</string></integer>",
|
|
"<integer>9007199254740992x</integer>",
|
|
] as const;
|
|
|
|
/** Ordinary dictionaries and malformed numeric references at the same boundary. */
|
|
export const archiveNumberMetadataCases = [
|
|
...mixedArchiveNumberElements.map((element) => ({
|
|
name: element,
|
|
source: { first: "__INTEGER__", other: "__OTHER__" },
|
|
element,
|
|
expected: { first: ROUNDED, other: ROUNDED },
|
|
incomplete: true,
|
|
malformedReference: false,
|
|
})),
|
|
...(["UID", "CF$UID"] as const).flatMap((key) => [
|
|
{
|
|
name: `${key} dictionary data`,
|
|
source: { [key]: { amount: "__INTEGER__" } },
|
|
element: "",
|
|
expected: { [key]: { amount: EXACT_INTEGER } },
|
|
incomplete: false,
|
|
malformedReference: false,
|
|
},
|
|
{
|
|
name: `${key} array data`,
|
|
source: { [key]: ["__INTEGER__"] },
|
|
element: "",
|
|
expected: { [key]: [EXACT_INTEGER] },
|
|
incomplete: false,
|
|
malformedReference: false,
|
|
},
|
|
{
|
|
name: `${key} malformed numeric reference`,
|
|
source: { [key]: "__INTEGER__" },
|
|
element: "",
|
|
expected: { [key]: ROUNDED },
|
|
incomplete: false,
|
|
malformedReference: true,
|
|
},
|
|
]),
|
|
{
|
|
name: "entity-encoded integer text",
|
|
source: { amount: "__INTEGER__" },
|
|
element: "",
|
|
expected: { amount: EXACT_INTEGER },
|
|
incomplete: false,
|
|
malformedReference: false,
|
|
integerElement: "<integer>9007199254740993</integer>",
|
|
},
|
|
] satisfies readonly {
|
|
name: string;
|
|
source: JsonValue;
|
|
element: string;
|
|
expected: JsonValue;
|
|
incomplete: boolean;
|
|
malformedReference: boolean;
|
|
integerElement?: string;
|
|
}[];
|
|
|
|
type MetadataCase = (typeof archiveNumberMetadataCases)[number];
|
|
|
|
/** Write one inert archive and retain the original byte digest for assertions. */
|
|
export const keyedArchiveNumberMetadataFixture = async (item: MetadataCase) => {
|
|
const root = await createTestTempDirectory("rea-keyed-number-metadata-");
|
|
const xml = build({
|
|
$archiver: "NSKeyedArchiver",
|
|
$version: 100000,
|
|
$objects: ["$null", item.source],
|
|
$top: { root: { CF$UID: 1 } },
|
|
})
|
|
.replace(
|
|
"<string>__INTEGER__</string>",
|
|
"integerElement" in item
|
|
? item.integerElement
|
|
: "<integer>9007199254740993</integer>",
|
|
)
|
|
.replace("<string>__OTHER__</string>", item.element);
|
|
const bytes = Buffer.from(xml);
|
|
const path = join(root, "archive.plist");
|
|
await writeFile(path, bytes);
|
|
return {
|
|
root,
|
|
path,
|
|
digest: createHash("sha256").update(bytes).digest("hex"),
|
|
};
|
|
};
|
|
|
|
/** Require exact ordinary values and honest limitations for incomplete metadata. */
|
|
export const expectKeyedArchiveNumberMetadata = (
|
|
value: unknown,
|
|
item: MetadataCase,
|
|
digest: string,
|
|
) => {
|
|
const result = keyedArchiveResultSchema.parse(value);
|
|
expect(result.archive_sha256).toBe(digest);
|
|
expect(result.roots).toEqual({ root: { CF$UID: 1 } });
|
|
expect(result.objects).toHaveLength(2);
|
|
expect(result.objects[1]).toMatchObject({
|
|
id: 1,
|
|
kind: "dictionary",
|
|
value: item.expected,
|
|
});
|
|
expect(result.references).toContainEqual({
|
|
source: null,
|
|
path: ["root"],
|
|
target: 1,
|
|
status: "resolved",
|
|
raw: { CF$UID: 1 },
|
|
});
|
|
if (item.incomplete) {
|
|
expect(result.limitations).toContainEqual(
|
|
expect.stringMatching(/metadata.*completely observed.*precision/u),
|
|
);
|
|
expect(
|
|
result.limitations.some((note) => note.includes("unambiguous integer")),
|
|
).toBe(false);
|
|
} else if (item.malformedReference) {
|
|
expect(result.references).toContainEqual({
|
|
source: 1,
|
|
path: [],
|
|
target: null,
|
|
status: "malformed",
|
|
raw: item.expected,
|
|
});
|
|
} else {
|
|
expect(result.references).toHaveLength(1);
|
|
expect(result.limitations).toContainEqual(
|
|
expect.stringMatching(/^1 unambiguous integer value/u),
|
|
);
|
|
}
|
|
};
|