mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 07:16:13 +00:00
Foundation encodes `encode(_:forKey: "UID")` as a `$top` dictionary whose single key is `UID`. The keyed archive projection treated every one-key `UID`/`CF$UID` dictionary as a reference marker, so that root became a malformed reference and the real link to its object was lost. Treat a one-key dictionary as a UID only when its value is a number, matching the binary decoder's UID shape and Foundation's XML `CF$UID` rule; otherwise descend into it as an ordinary dictionary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
3.5 KiB
TypeScript
117 lines
3.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { build, buildBinary } from "plist";
|
|
import { decodeKeyedArchiveBytes } from "./KeyedArchiveReader.js";
|
|
|
|
const archive = {
|
|
$archiver: "NSKeyedArchiver",
|
|
$version: 100000,
|
|
$objects: [
|
|
"$null",
|
|
{
|
|
$class: { UID: 4 },
|
|
child: { UID: 2 },
|
|
shared: { UID: 2 },
|
|
self: { UID: 1 },
|
|
conditional: { UID: 0 },
|
|
broken: { UID: 90 },
|
|
},
|
|
{ "NS.objects": [{ UID: 1 }, { UID: 3 }] },
|
|
"value",
|
|
{ $classname: "UnknownModel", $classes: ["UnknownModel", "NSObject"] },
|
|
],
|
|
$top: { root: { UID: 1 }, other: { UID: 2 } },
|
|
};
|
|
describe("inert keyed archive decoding", () => {
|
|
it("preserves CF$UID, malformed references and original pagination identities", () => {
|
|
const bytes = Buffer.from(
|
|
build({
|
|
...archive,
|
|
$objects: [
|
|
"$null",
|
|
{ bad: { CF$UID: -1 }, valid: { CF$UID: 2 }, broken: { CF$UID: 90 } },
|
|
"value",
|
|
],
|
|
}),
|
|
);
|
|
const graph = decodeKeyedArchiveBytes(bytes, {
|
|
root: "root",
|
|
offset: 1,
|
|
limit: 1,
|
|
});
|
|
expect(graph.roots).toEqual({ root: { UID: 1 } });
|
|
expect(graph.objects.map(({ id }) => id)).toEqual([1]);
|
|
expect(graph.next_offset).toBe(2);
|
|
expect(graph.references).toContainEqual(
|
|
expect.objectContaining({ source: 1, target: 90, status: "unresolved" }),
|
|
);
|
|
expect(graph.references).toContainEqual(
|
|
expect.objectContaining({ source: 1, target: null, status: "malformed" }),
|
|
);
|
|
expect(graph.references).toContainEqual(
|
|
expect.objectContaining({ source: 1, target: 2, status: "resolved" }),
|
|
);
|
|
});
|
|
it.each([
|
|
["binary", { UID: 1 }],
|
|
["XML", { CF$UID: 1 }],
|
|
] as const)(
|
|
"resolves a %s root encoded under a UID-named key",
|
|
(format, reference) => {
|
|
const named = {
|
|
...archive,
|
|
$objects: ["$null", "payload"],
|
|
$top: { UID: reference, CF$UID: reference },
|
|
};
|
|
const graph = decodeKeyedArchiveBytes(
|
|
Buffer.from(format === "binary" ? buildBinary(named) : build(named)),
|
|
{ offset: 0, limit: 2 },
|
|
);
|
|
expect(
|
|
graph.references.map(({ source, path, target, status }) => ({
|
|
source,
|
|
path,
|
|
target,
|
|
status,
|
|
})),
|
|
).toEqual([
|
|
{ source: null, path: ["CF$UID"], target: 1, status: "resolved" },
|
|
{ source: null, path: ["UID"], target: 1, status: "resolved" },
|
|
]);
|
|
const selected = decodeKeyedArchiveBytes(
|
|
Buffer.from(format === "binary" ? buildBinary(named) : build(named)),
|
|
{ root: "UID", offset: 0, limit: 2 },
|
|
);
|
|
expect(selected.references).toEqual([
|
|
expect.objectContaining({ path: ["UID"], target: 1 }),
|
|
]);
|
|
},
|
|
);
|
|
it("rejects missing roots, malformed plist, and non-keyed archives", () => {
|
|
expect(() =>
|
|
decodeKeyedArchiveBytes(Buffer.from("bplist00bad"), {
|
|
offset: 0,
|
|
limit: 1,
|
|
}),
|
|
).toThrow();
|
|
expect(() =>
|
|
decodeKeyedArchiveBytes(Buffer.from(build({ ...archive, $top: {} })), {
|
|
offset: 0,
|
|
limit: 1,
|
|
}),
|
|
).toThrow("roots");
|
|
expect(() =>
|
|
decodeKeyedArchiveBytes(Buffer.from(build({ plain: true })), {
|
|
offset: 0,
|
|
limit: 1,
|
|
}),
|
|
).toThrow("NSKeyedArchiver");
|
|
expect(() =>
|
|
decodeKeyedArchiveBytes(Buffer.from(buildBinary(archive)), {
|
|
root: "missing",
|
|
offset: 0,
|
|
limit: 1,
|
|
}),
|
|
).toThrow("does not exist");
|
|
});
|
|
});
|