diff --git a/src/application/BinaryTargetResolver.ts b/src/application/BinaryTargetResolver.ts index 7dfb7ce76e..cbccb04189 100644 --- a/src/application/BinaryTargetResolver.ts +++ b/src/application/BinaryTargetResolver.ts @@ -294,6 +294,9 @@ const detectArtifactFormat = async ( if (lower.endsWith(".app")) return "app-pack"; return zipPackageFormatForPath(lower) ?? "zip"; } + // Binary plists, including keyed archives, often use other extensions. + if (observed.bytesRead === 8 && magic.equals(Buffer.from("bplist00"))) + return "plist"; const named = namedArtifactFormat(lower); if (named !== undefined) return named; if ( diff --git a/tests/boundary/filesystem/binaryTarget.test.ts b/tests/boundary/filesystem/binaryTarget.test.ts index 3ab9bdd27e..0aae910e85 100644 --- a/tests/boundary/filesystem/binaryTarget.test.ts +++ b/tests/boundary/filesystem/binaryTarget.test.ts @@ -245,6 +245,17 @@ describe("binary target I/O: package classification", () => { }); }); + it("classifies a binary plist by its magic under any extension", async () => { + const directory = await createTestTempDirectory("rea-bplist-target-"); + const path = join(directory, "Model.archive"); + await writeFile(path, buildBinary({ $archiver: "NSKeyedArchiver" })); + const result = await parseBinaryTarget(path); + expect(result.ok && result.value).toMatchObject({ + kind: "artifact", + format: "plist", + }); + }); + it("does not trust a ZIP-family extension without ZIP magic", async () => { const directory = await createTestTempDirectory("rea-fake-archive-"); const path = join(directory, "fake.zip");