fix(artifacts): read keyed archive dictionaries keyed UID as dictionaries
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>
This commit is contained in:
parent
676959ecb5
commit
35c82b93eb
|
|
@ -117,8 +117,41 @@ try {
|
|||
).length < 2
|
||||
)
|
||||
throw new Error("Real shared object identity was not preserved");
|
||||
const uidRoot = join(root, "uid-root.plist");
|
||||
await promisify(execFile)("/usr/bin/xcrun", [
|
||||
"swift",
|
||||
"-module-cache-path",
|
||||
join(root, "modules"),
|
||||
fileURLToPath(
|
||||
new URL(
|
||||
"../tests/conformance/native/keyed-archive-uid-root.swift",
|
||||
import.meta.url,
|
||||
),
|
||||
),
|
||||
uidRoot,
|
||||
]);
|
||||
const uidRootXml = join(root, "uid-root.xml.plist");
|
||||
await promisify(execFile)("/usr/bin/plutil", [
|
||||
"-convert",
|
||||
"xml1",
|
||||
"-o",
|
||||
uidRootXml,
|
||||
uidRoot,
|
||||
]);
|
||||
for (const path of [uidRoot, uidRootXml]) {
|
||||
const uidGraph = await artifactCli("inspect-keyed-archive", path);
|
||||
assert.deepEqual(
|
||||
uidGraph.references.map(({ source, path, target, status }) => ({
|
||||
source,
|
||||
path,
|
||||
target,
|
||||
status,
|
||||
})),
|
||||
[{ source: null, path: ["UID"], target: 1, status: "resolved" }],
|
||||
);
|
||||
}
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ok: true, mocked: false, cli: true, stdio_mcp: true, xml_golden: true, format: graph.archive_format, objects: graph.total_objects, references: graph.total_references, shared_identity: true, cyclic_identity: true, target_classes_instantiated_by_reader: false })}\n`,
|
||||
`${JSON.stringify({ ok: true, mocked: false, cli: true, stdio_mcp: true, xml_golden: true, uid_named_root: true, format: graph.archive_format, objects: graph.total_objects, references: graph.total_references, shared_identity: true, cyclic_identity: true, target_classes_instantiated_by_reader: false })}\n`,
|
||||
);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
|
|
|
|||
|
|
@ -51,6 +51,41 @@ describe("inert keyed archive decoding", () => {
|
|||
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"), {
|
||||
|
|
|
|||
|
|
@ -54,11 +54,17 @@ const record = (
|
|||
value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? value
|
||||
: undefined;
|
||||
const uid = (value: JsonValue | undefined): JsonValue | undefined => {
|
||||
/**
|
||||
* A UID decodes as a one-key `UID` (binary) or `CF$UID` (XML) dictionary with
|
||||
* a numeric value. Any other value means an ordinary dictionary, such as a
|
||||
* `$top` that holds a root encoded under the key `UID`.
|
||||
*/
|
||||
const uid = (value: JsonValue | undefined): number | undefined => {
|
||||
const object = record(value);
|
||||
if (object === undefined || Object.keys(object).length !== 1)
|
||||
return undefined;
|
||||
return Object.hasOwn(object, "UID") ? object.UID : object["CF$UID"];
|
||||
const marker = Object.hasOwn(object, "UID") ? object.UID : object["CF$UID"];
|
||||
return typeof marker === "number" ? marker : undefined;
|
||||
};
|
||||
|
||||
/** Interpret serialized Foundation references without instantiating archive classes. */
|
||||
|
|
@ -99,11 +105,8 @@ export const projectKeyedArchive = (
|
|||
);
|
||||
const target = uid(item.value);
|
||||
if (target !== undefined) {
|
||||
const valid =
|
||||
typeof target === "number" &&
|
||||
Number.isSafeInteger(target) &&
|
||||
target >= 0;
|
||||
const index = valid ? target : null;
|
||||
const index =
|
||||
Number.isSafeInteger(target) && target >= 0 ? target : null;
|
||||
references.push({
|
||||
source,
|
||||
path: item.path,
|
||||
|
|
@ -143,9 +146,7 @@ export const projectKeyedArchive = (
|
|||
const fields = record(value);
|
||||
const classUid = uid(fields?.$class);
|
||||
const classId =
|
||||
typeof classUid === "number" &&
|
||||
Number.isSafeInteger(classUid) &&
|
||||
classUid >= 0
|
||||
classUid !== undefined && Number.isSafeInteger(classUid) && classUid >= 0
|
||||
? classUid
|
||||
: null;
|
||||
const descriptor = classId === null ? undefined : record(table[classId]);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
// Encode the root under a key that matches the UID marker spelling.
|
||||
let archiver = NSKeyedArchiver(requiringSecureCoding: false)
|
||||
archiver.encode("payload" as NSString, forKey: "UID")
|
||||
archiver.finishEncoding()
|
||||
try archiver.encodedData.write(to: URL(fileURLWithPath: CommandLine.arguments[1]))
|
||||
Loading…
Reference in New Issue