mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(apple): preserve XML plist UID reference identity
This commit is contained in:
1 parent
2886cc1efe
commit
91c28e2735
10 files changed
+199
-105
No files matched your search
@@ -549,9 +549,11 @@ complete observation.
|
||||
malformed/unresolved/nil references. Missing fields stay absent; unknown
|
||||
classes are preserved. Select `root`, `offset` and `limit` explicitly.
|
||||
Binary references retain their serialized type-8 UID identity; ordinary
|
||||
dictionaries with `UID` or `CF$UID` keys remain authored data. XML has no UID
|
||||
scalar, so its dictionary UID convention remains ambiguous with authored
|
||||
dictionaries of the same shape. Object `kind` describes the decoded archive value: data, dates and numbers
|
||||
dictionaries with `UID` or `CF$UID` keys remain authored data. XML uses
|
||||
Foundation's single numeric `CF$UID` dictionary convention for references;
|
||||
a numeric `UID` dictionary remains authored data. Reference identity follows
|
||||
the parsed format, rather than the displayed key alone. Object `kind`
|
||||
describes the decoded archive value: data, dates and numbers
|
||||
remain `scalar` when their JSON value uses a `$plist_type` wrapper. An actual
|
||||
dictionary with the same fields remains `dictionary`.
|
||||
XML must be well-formed. Data literals use
|
||||
|
||||
@@ -837,3 +837,88 @@ it("distinguishes binary UID records from authored UID-shaped dictionaries", ()
|
||||
{ path: ["shared", 1], target: 2 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps XML authored UID dictionaries as data alongside CF$UID references", () => {
|
||||
const graph = decodeKeyedArchiveBytes(
|
||||
Buffer.from(
|
||||
build({
|
||||
$archiver: "NSKeyedArchiver",
|
||||
$version: 100000,
|
||||
$top: { root: { CF$UID: 1 } },
|
||||
$objects: [
|
||||
"$null",
|
||||
{ $class: { CF$UID: 2 }, authoredRecord: { UID: 3 } },
|
||||
{ $classname: "T", $classes: ["T", "NSObject"] },
|
||||
"sharedString",
|
||||
],
|
||||
}),
|
||||
),
|
||||
{ offset: 0, limit: 10 },
|
||||
);
|
||||
expect(graph.objects[1]?.value).toMatchObject({ authoredRecord: { UID: 3 } });
|
||||
expect(graph.total_references).toBe(2);
|
||||
expect(
|
||||
graph.references.map(({ source, path, target, status }) => ({
|
||||
source,
|
||||
path,
|
||||
target,
|
||||
status,
|
||||
})),
|
||||
).toEqual([
|
||||
{ source: null, path: ["root"], target: 1, status: "resolved" },
|
||||
{ source: 1, path: ["$class"], target: 2, status: "resolved" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves exact integers in XML authored UID dictionaries", () => {
|
||||
const xml = build({
|
||||
$archiver: "NSKeyedArchiver",
|
||||
$objects: ["$null", { UID: 9007199254740992 }],
|
||||
$top: { root: { CF$UID: 1 } },
|
||||
}).replace("9007199254740992", "9007199254740993");
|
||||
const graph = decodeKeyedArchiveBytes(Buffer.from(xml), {
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
});
|
||||
expect(graph.objects[1]?.value).toEqual({
|
||||
UID: { $plist_type: "integer", decimal: "9007199254740993" },
|
||||
});
|
||||
expect(graph.total_references).toBe(1);
|
||||
});
|
||||
|
||||
it("preserves XML dictionary identity when prototype-key omission leaves a CF$UID field", () => {
|
||||
const xml = build({
|
||||
$archiver: "NSKeyedArchiver",
|
||||
$objects: [
|
||||
"$null",
|
||||
{ authoredRecord: { CF$UID: 2, prototypeKey: "authored" } },
|
||||
"target",
|
||||
],
|
||||
$top: { root: { CF$UID: 1 }, genuine: { CF$UID: 2 } },
|
||||
}).replace("<key>prototypeKey</key>", "<key>__proto__</key>");
|
||||
const graph = decodeKeyedArchiveBytes(Buffer.from(xml), {
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
});
|
||||
expect(graph.objects[1]?.value).toEqual({ authoredRecord: { CF$UID: 2 } });
|
||||
expect(graph.total_references).toBe(2);
|
||||
expect(graph.references).toEqual([
|
||||
expect.objectContaining({
|
||||
source: null,
|
||||
path: ["genuine"],
|
||||
target: 2,
|
||||
status: "resolved",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
source: null,
|
||||
path: ["root"],
|
||||
target: 1,
|
||||
status: "resolved",
|
||||
}),
|
||||
]);
|
||||
expect(graph.limitations).toContainEqual(
|
||||
expect.stringMatching(
|
||||
/\b1 dictionary entry\b.*\b__proto__\b.*\bomitted\b/u,
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { markBinaryPlistDictionary } from "../../domain/apple/plistUid.js";
|
||||
import { markPlistDictionary } from "../../domain/apple/plistUid.js";
|
||||
import {
|
||||
createPlistNumberCollector,
|
||||
type PlistNumberLiterals,
|
||||
@@ -408,7 +408,7 @@ export const retainBinaryPlistUidIdentity = (
|
||||
!(item instanceof Date) &&
|
||||
!(item instanceof Uint8Array)
|
||||
) {
|
||||
markBinaryPlistDictionary(item);
|
||||
markPlistDictionary(item);
|
||||
for (const child of Object.values(item)) pending.push(child);
|
||||
}
|
||||
}
|
||||
@@ -428,7 +428,7 @@ export const retainBinaryPlistUidIdentity = (
|
||||
}
|
||||
if (item !== null && typeof item === "object") {
|
||||
const uid = Object.getOwnPropertyDescriptor(item, "UID")?.value;
|
||||
if (typeof uid === "number") markBinaryPlistDictionary(item, uid);
|
||||
if (typeof uid === "number") markPlistDictionary(item, uid);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildBinary, parseBinary } from "plist";
|
||||
|
||||
import {
|
||||
buildInterfaceBuilderAnalysis,
|
||||
@@ -158,20 +157,19 @@ describe("compiled Interface Builder graph projection", () => {
|
||||
});
|
||||
|
||||
it("reads recognized objects from a compiled NSKeyedArchiver object table", () => {
|
||||
const bytes = buildBinary({
|
||||
const parsed = parseInterfaceBuilderRecords({
|
||||
$archiver: "NSKeyedArchiver",
|
||||
$version: 100000,
|
||||
$objects: [
|
||||
"$null",
|
||||
{ $class: { UID: 2 }, title: "Build", identifier: "build-button" },
|
||||
{ $class: { CF$UID: 2 }, title: "Build", identifier: "build-button" },
|
||||
{
|
||||
$classname: "UIButton",
|
||||
$classes: ["UIButton", "UIControl", "UIView", "NSObject"],
|
||||
},
|
||||
],
|
||||
$top: { root: { UID: 1 } },
|
||||
$top: { root: { CF$UID: 1 } },
|
||||
});
|
||||
const parsed = parseInterfaceBuilderRecords(parseBinary(bytes));
|
||||
expect(parsed.objects).toMatchObject([
|
||||
{
|
||||
id: "1",
|
||||
@@ -190,12 +188,12 @@ describe("keyed archive objects with unresolved classes", () => {
|
||||
$version: 100000,
|
||||
$objects: [
|
||||
"$null",
|
||||
{ $class: { UID: 99 }, label: "Unresolved object" },
|
||||
{ $class: { CF$UID: 99 }, label: "Unresolved object" },
|
||||
{ $classname: "UIView", $classes: ["UIView", "NSObject"] },
|
||||
{ $class: { UID: 2 }, "NS.string": "ok" },
|
||||
{ $class: { CF$UID: 2 }, "NS.string": "ok" },
|
||||
"plain scalar",
|
||||
],
|
||||
$top: { root: { UID: 3 } },
|
||||
$top: { root: { CF$UID: 3 } },
|
||||
});
|
||||
// The class descriptor and the scalar are not archived objects.
|
||||
expect(parsed.objects).toMatchObject([
|
||||
@@ -204,7 +202,7 @@ describe("keyed archive objects with unresolved classes", () => {
|
||||
kind: "other",
|
||||
class_name: null,
|
||||
name: "Unresolved object",
|
||||
attributes: { label: "Unresolved object", $class: { UID: 99 } },
|
||||
attributes: { label: "Unresolved object", $class: { CF$UID: 99 } },
|
||||
},
|
||||
{ id: "3", kind: "view", class_name: "UIView" },
|
||||
]);
|
||||
@@ -226,11 +224,11 @@ describe("keyed archive objects with unresolved classes", () => {
|
||||
$version: 100000,
|
||||
$objects: [
|
||||
"$null",
|
||||
{ $class: { UID: 99 }, label: "Unresolved object" },
|
||||
{ $class: { CF$UID: 99 }, label: "Unresolved object" },
|
||||
{ $classname: "UIView", $classes: ["UIView", "NSObject"] },
|
||||
{ $class: { UID: 2 }, "NS.string": "ok" },
|
||||
{ $class: { CF$UID: 2 }, "NS.string": "ok" },
|
||||
],
|
||||
$top: { root: { UID: 1 } },
|
||||
$top: { root: { CF$UID: 1 } },
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -313,13 +311,13 @@ describe("keyed archive identity decoding", () => {
|
||||
$version: 100000,
|
||||
$objects: [
|
||||
"$null",
|
||||
{ archiveUID: 2, title: "First", $class: { UID: 4 } },
|
||||
{ title: "Second", $class: { UID: 4 } },
|
||||
{ archiveUID: 2, title: "First", $class: { CF$UID: 4 } },
|
||||
{ title: "Second", $class: { CF$UID: 4 } },
|
||||
{
|
||||
NSSource: { UID: 2 },
|
||||
NSDestination: { UID: 1 },
|
||||
NSSource: { CF$UID: 2 },
|
||||
NSDestination: { CF$UID: 1 },
|
||||
NSLabel: "peer",
|
||||
$class: { UID: 5 },
|
||||
$class: { CF$UID: 5 },
|
||||
},
|
||||
{
|
||||
$classname: "ArchiveProbeView",
|
||||
@@ -330,7 +328,7 @@ describe("keyed archive identity decoding", () => {
|
||||
$classes: ["NSNibOutletConnector", "NSObject"],
|
||||
},
|
||||
],
|
||||
$top: { root: { UID: 1 } },
|
||||
$top: { root: { CF$UID: 1 } },
|
||||
});
|
||||
|
||||
expect(parsed.objects.map(({ id }) => id)).toEqual(["1", "2", "3"]);
|
||||
@@ -352,16 +350,16 @@ describe("keyed archive identity decoding", () => {
|
||||
$objects: [
|
||||
"$null",
|
||||
{
|
||||
subviews: [{ UID: 2 }],
|
||||
$class: { UID: 5 },
|
||||
subviews: [{ CF$UID: 2 }],
|
||||
$class: { CF$UID: 5 },
|
||||
},
|
||||
{ $class: { UID: 6 } },
|
||||
{ $class: { UID: 7 } },
|
||||
{ $class: { CF$UID: 6 } },
|
||||
{ $class: { CF$UID: 7 } },
|
||||
{
|
||||
source: { UID: 3 },
|
||||
destination: { UID: 2 },
|
||||
source: { CF$UID: 3 },
|
||||
destination: { CF$UID: 2 },
|
||||
label: "runAction:",
|
||||
$class: { UID: 8 },
|
||||
$class: { CF$UID: 8 },
|
||||
},
|
||||
{ $classname: "UIView", $classes: ["UIView", "NSObject"] },
|
||||
{ $classname: "UIButton", $classes: ["UIButton", "UIView"] },
|
||||
@@ -373,9 +371,9 @@ describe("keyed archive identity decoding", () => {
|
||||
$classname: "NSNibControlConnector",
|
||||
$classes: ["NSNibControlConnector", "NSObject"],
|
||||
},
|
||||
[{ UID: 1 }],
|
||||
[{ CF$UID: 1 }],
|
||||
],
|
||||
$top: { root: { UID: 9 } },
|
||||
$top: { root: { CF$UID: 9 } },
|
||||
});
|
||||
|
||||
expect(parsed.objects.map(({ id }) => id)).toContain("2");
|
||||
@@ -694,24 +692,24 @@ describe("keyed archive object references", () => {
|
||||
// inlining references would expand the table into an exponential tree.
|
||||
const viewCount = 40;
|
||||
const views = Array.from({ length: viewCount }, (_, index) => ({
|
||||
$class: { UID: 1 },
|
||||
NSWindow: { UID: 2 },
|
||||
NSSuperview: { UID: index === 0 ? 2 : 3 + index - 1 },
|
||||
NSNextKeyView: { UID: 3 + ((index + 1) % viewCount) },
|
||||
NSSubviews: index + 1 < viewCount ? [{ UID: 3 + index + 1 }] : [],
|
||||
NSTitle: { UID: 3 + viewCount },
|
||||
$class: { CF$UID: 1 },
|
||||
NSWindow: { CF$UID: 2 },
|
||||
NSSuperview: { CF$UID: index === 0 ? 2 : 3 + index - 1 },
|
||||
NSNextKeyView: { CF$UID: 3 + ((index + 1) % viewCount) },
|
||||
NSSubviews: index + 1 < viewCount ? [{ CF$UID: 3 + index + 1 }] : [],
|
||||
NSTitle: { CF$UID: 3 + viewCount },
|
||||
}));
|
||||
const parsed = parseInterfaceBuilderRecords({
|
||||
$archiver: "NSKeyedArchiver",
|
||||
$objects: [
|
||||
"$null",
|
||||
{ $classname: "NSView", $classes: ["NSView", "NSObject"] },
|
||||
{ $class: { UID: 3 + viewCount + 1 }, NSContentView: { UID: 3 } },
|
||||
{ $class: { CF$UID: 3 + viewCount + 1 }, NSContentView: { CF$UID: 3 } },
|
||||
...views,
|
||||
"Title",
|
||||
{ $classname: "NSWindow", $classes: ["NSWindow", "NSObject"] },
|
||||
],
|
||||
$top: { root: { UID: 2 } },
|
||||
$top: { root: { CF$UID: 2 } },
|
||||
});
|
||||
|
||||
expect(parsed.objects).toHaveLength(viewCount + 1);
|
||||
@@ -779,19 +777,19 @@ describe("AppKit keyed archive connectors", () => {
|
||||
$archiver: "NSKeyedArchiver",
|
||||
$objects: [
|
||||
"$null",
|
||||
{ $class: { UID: 6 } },
|
||||
{ $class: { UID: 7 }, identifier: "unrelated-identifier" },
|
||||
{ $class: { CF$UID: 6 } },
|
||||
{ $class: { CF$UID: 7 }, identifier: "unrelated-identifier" },
|
||||
{
|
||||
$class: { UID: 8 },
|
||||
NSSource: { UID: 1 },
|
||||
NSDestination: { UID: 2 },
|
||||
NSLabel: { UID: 5 },
|
||||
$class: { CF$UID: 8 },
|
||||
NSSource: { CF$UID: 1 },
|
||||
NSDestination: { CF$UID: 2 },
|
||||
NSLabel: { CF$UID: 5 },
|
||||
},
|
||||
{
|
||||
$class: { UID: 9 },
|
||||
NSSource: { UID: 2 },
|
||||
NSDestination: { UID: 1 },
|
||||
NSLabel: { UID: 10 },
|
||||
$class: { CF$UID: 9 },
|
||||
NSSource: { CF$UID: 2 },
|
||||
NSDestination: { CF$UID: 1 },
|
||||
NSLabel: { CF$UID: 10 },
|
||||
},
|
||||
"o_button",
|
||||
{
|
||||
@@ -835,24 +833,24 @@ describe("unsupported keyed archive connections", () => {
|
||||
$archiver: "NSKeyedArchiver",
|
||||
$objects: [
|
||||
"$null",
|
||||
{ $class: { UID: 6 } },
|
||||
{ $class: { UID: 6 } },
|
||||
{ $class: { CF$UID: 6 } },
|
||||
{ $class: { CF$UID: 6 } },
|
||||
{
|
||||
$class: { UID: 7 },
|
||||
NSSource: { UID: 1 },
|
||||
NSDestination: { UID: 2 },
|
||||
$class: { CF$UID: 7 },
|
||||
NSSource: { CF$UID: 1 },
|
||||
NSDestination: { CF$UID: 2 },
|
||||
NSLabel: "outlet",
|
||||
},
|
||||
{
|
||||
$class: { UID: 8 },
|
||||
NSSource: { UID: 1 },
|
||||
NSDestination: { UID: 2 },
|
||||
$class: { CF$UID: 8 },
|
||||
NSSource: { CF$UID: 1 },
|
||||
NSDestination: { CF$UID: 2 },
|
||||
NSLabel: "performAction:",
|
||||
},
|
||||
{
|
||||
$class: { UID: 9 },
|
||||
NSSource: { UID: 1 },
|
||||
NSDestination: { UID: 2 },
|
||||
$class: { CF$UID: 9 },
|
||||
NSSource: { CF$UID: 1 },
|
||||
NSDestination: { CF$UID: 2 },
|
||||
NSLabel: "value",
|
||||
},
|
||||
{ $classname: "NSView", $classes: ["NSView", "NSObject"] },
|
||||
@@ -921,9 +919,9 @@ describe("keyed archive class identity", () => {
|
||||
$objects: [
|
||||
"$null",
|
||||
{
|
||||
$class: { UID: 2 },
|
||||
NSSource: { UID: 1 },
|
||||
NSDestination: { UID: 1 },
|
||||
$class: { CF$UID: 2 },
|
||||
NSSource: { CF$UID: 1 },
|
||||
NSDestination: { CF$UID: 1 },
|
||||
},
|
||||
{
|
||||
$classname: "TelemetryConnectorState",
|
||||
@@ -947,9 +945,9 @@ describe("keyed archive class identity", () => {
|
||||
$objects: [
|
||||
"$null",
|
||||
{
|
||||
$class: { UID: 3 },
|
||||
NSSource: { UID: 1 },
|
||||
NSDestination: { UID: 1 },
|
||||
$class: { CF$UID: 3 },
|
||||
NSSource: { CF$UID: 1 },
|
||||
NSDestination: { CF$UID: 1 },
|
||||
},
|
||||
{ $classname: "NSNibConnector", $classes: ["NSNibConnector"] },
|
||||
{
|
||||
@@ -970,9 +968,9 @@ describe("keyed archive class identity", () => {
|
||||
$objects: [
|
||||
"$null",
|
||||
{
|
||||
$class: { UID: 99 },
|
||||
NSSource: { UID: 1 },
|
||||
NSDestination: { UID: 1 },
|
||||
$class: { CF$UID: 99 },
|
||||
NSSource: { CF$UID: 1 },
|
||||
NSDestination: { CF$UID: 1 },
|
||||
},
|
||||
],
|
||||
$top: {},
|
||||
@@ -982,7 +980,7 @@ describe("keyed archive class identity", () => {
|
||||
expect.objectContaining({
|
||||
class_name: null,
|
||||
id: "1",
|
||||
attributes: expect.objectContaining({ $class: { UID: 99 } }),
|
||||
attributes: expect.objectContaining({ $class: { CF$UID: 99 } }),
|
||||
}),
|
||||
);
|
||||
expect(parsed.connections).toEqual([]);
|
||||
@@ -1004,12 +1002,12 @@ describe("AppKit control connectors", () => {
|
||||
$archiver: "NSKeyedArchiver",
|
||||
$objects: [
|
||||
"$null",
|
||||
{ $class: { UID: 4 } },
|
||||
{ $class: { UID: 5 } },
|
||||
{ $class: { CF$UID: 4 } },
|
||||
{ $class: { CF$UID: 5 } },
|
||||
{
|
||||
$class: { UID: 6 },
|
||||
NSSource: { UID: 2 },
|
||||
NSDestination: { UID: 1 },
|
||||
$class: { CF$UID: 6 },
|
||||
NSSource: { CF$UID: 2 },
|
||||
NSDestination: { CF$UID: 1 },
|
||||
NSLabel: "buttonClicked:",
|
||||
},
|
||||
{ $classname: "BuildDelegate", $classes: ["NSObject"] },
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "./interfaceBuilderGraph.js";
|
||||
import { parseInterfaceBuilderRecords } from "./interfaceBuilderKeyedArchive.js";
|
||||
|
||||
const uid = (value: number) => ({ UID: value });
|
||||
const uid = (value: number) => ({ CF$UID: value });
|
||||
|
||||
const archive = (
|
||||
fields: Record<string, unknown> = {},
|
||||
@@ -151,8 +151,8 @@ it.each([
|
||||
{ id: "2" },
|
||||
{ archiveUID: 2 },
|
||||
{ archiveUID: 2, className: "NSView" },
|
||||
{ UID: 2, archiveUID: 2 },
|
||||
{ UID: "2" },
|
||||
{ CF$UID: 2, archiveUID: 2 },
|
||||
{ CF$UID: "2" },
|
||||
uid(-1),
|
||||
uid(99),
|
||||
uid(0),
|
||||
|
||||
@@ -49,7 +49,7 @@ describe("projectKeyedArchive root reference projection", () => {
|
||||
$objects: ["$null", "target"],
|
||||
$top: {
|
||||
UID: { CF$UID: 1 },
|
||||
CF$UID: { UID: 1 },
|
||||
CF$UID: { CF$UID: 1 },
|
||||
},
|
||||
},
|
||||
{ offset: 0, limit: 2 },
|
||||
@@ -60,7 +60,7 @@ describe("projectKeyedArchive root reference projection", () => {
|
||||
source: null,
|
||||
path: ["CF$UID"],
|
||||
target: 1,
|
||||
raw: { UID: 1 },
|
||||
raw: { CF$UID: 1 },
|
||||
status: "resolved",
|
||||
},
|
||||
{
|
||||
@@ -106,7 +106,7 @@ it("keeps generated unknown scalar wrappers scalar and lookalike dictionaries or
|
||||
reason: "unsupported-markup",
|
||||
},
|
||||
],
|
||||
$top: { root: { UID: 1 } },
|
||||
$top: { root: { CF$UID: 1 } },
|
||||
},
|
||||
{ offset: 0, limit: 3 },
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { plistUidValue } from "./plistUid.js";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
|
||||
/** Source element types needed to distinguish large integers from integral reals. */
|
||||
@@ -75,12 +76,8 @@ export const createPlistNumberProjection = (literals: PlistNumberLiterals) => {
|
||||
if (typeof item === "number") return mapNumber(item);
|
||||
if (Array.isArray(item)) return item.map(map);
|
||||
if (item !== null && typeof item === "object") {
|
||||
const keys = Object.keys(item);
|
||||
const marker = item[keys[0] ?? ""];
|
||||
const reference =
|
||||
keys.length === 1 &&
|
||||
(keys[0] === "UID" || keys[0] === "CF$UID") &&
|
||||
typeof marker === "number";
|
||||
const marker = plistUidValue(item);
|
||||
const reference = marker !== undefined;
|
||||
if (reference) {
|
||||
// Keep malformed numeric reference markers recognizable by the graph reader.
|
||||
if (
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
/** Serialized binary UID identity is separate from an ordinary dictionary's keys. */
|
||||
const binaryUidValues = new WeakMap<object, number | undefined>();
|
||||
/** Serialized plist UID identity is separate from an ordinary dictionary's keys. */
|
||||
const plistUidValues = new WeakMap<object, number | undefined>();
|
||||
|
||||
/** The binary decoder marks every dictionary; only type-8 records carry a UID. */
|
||||
export const markBinaryPlistDictionary = (
|
||||
value: object,
|
||||
uid?: number,
|
||||
): void => {
|
||||
binaryUidValues.set(value, uid);
|
||||
/** Decoders mark every dictionary with its format-defined reference identity. */
|
||||
export const markPlistDictionary = (value: object, uid?: number): void => {
|
||||
plistUidValues.set(value, uid);
|
||||
};
|
||||
|
||||
/** Preserve source identity through the single JSON projection owner. */
|
||||
/** Preserve source identity when a projection rebuilds dictionaries. */
|
||||
export const copyPlistUidIdentity = (
|
||||
source: object,
|
||||
projected: object,
|
||||
): void => {
|
||||
if (binaryUidValues.has(source))
|
||||
binaryUidValues.set(projected, binaryUidValues.get(source));
|
||||
if (plistUidValues.has(source))
|
||||
plistUidValues.set(projected, plistUidValues.get(source));
|
||||
};
|
||||
|
||||
/** XML/untyped decoded archives use Foundation's dictionary UID convention. */
|
||||
/** Source identity takes precedence over Foundation's XML CF$UID convention. */
|
||||
export const plistUidValue = (value: unknown): number | undefined => {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value))
|
||||
return undefined;
|
||||
if (binaryUidValues.has(value)) return binaryUidValues.get(value);
|
||||
if (plistUidValues.has(value)) return plistUidValues.get(value);
|
||||
const fields = Object.keys(value);
|
||||
if (fields.length !== 1) return undefined;
|
||||
const key = fields[0];
|
||||
if (key !== "UID" && key !== "CF$UID") return undefined;
|
||||
if (key !== "CF$UID") return undefined;
|
||||
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
||||
return typeof descriptor?.value === "number" ? descriptor.value : undefined;
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { copyPlistUidIdentity } from "./apple/plistUid.js";
|
||||
import { parseOpenStep } from "plist";
|
||||
|
||||
import { decodeXmlPropertyList } from "./propertyListXml.js";
|
||||
@@ -47,13 +48,15 @@ export const omitPrototypeKeys = (
|
||||
item instanceof UnknownPlistScalar
|
||||
)
|
||||
return item;
|
||||
return Object.fromEntries(
|
||||
const projected = Object.fromEntries(
|
||||
Object.entries(item).flatMap(([name, entry]) => {
|
||||
if (name !== PROTOTYPE_KEY) return [[name, strip(entry)]];
|
||||
omitted += 1;
|
||||
return [];
|
||||
}),
|
||||
);
|
||||
copyPlistUidIdentity(item, projected);
|
||||
return projected;
|
||||
};
|
||||
const stripped = strip(value);
|
||||
return { value: stripped, omittedPrototypeKeys: omitted };
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { markPlistDictionary } from "./apple/plistUid.js";
|
||||
import type { Node } from "@xmldom/xmldom";
|
||||
|
||||
import { readXmlPlistSource } from "./propertyListXmlSource.js";
|
||||
@@ -88,7 +89,18 @@ export const decodeXmlPropertyList = (text: string) => {
|
||||
if (key !== undefined)
|
||||
throw new TypeError("Missing value for key while parsing <dict/>");
|
||||
// Define own properties safely, including __proto__; the caller owns omission accounting.
|
||||
return Object.fromEntries(entries);
|
||||
const value = Object.fromEntries(entries);
|
||||
// XML archives use Foundation's CF$UID convention; a UID key is data.
|
||||
const keys = Object.keys(value);
|
||||
markPlistDictionary(
|
||||
value,
|
||||
keys.length === 1 &&
|
||||
keys[0] === "CF$UID" &&
|
||||
typeof value["CF$UID"] === "number"
|
||||
? value["CF$UID"]
|
||||
: undefined,
|
||||
);
|
||||
return value;
|
||||
}
|
||||
case "key": {
|
||||
// The pinned decoder also accepted key elements as scalars. Keep their
|
||||
|
||||
Reference in new issue
Block a user