fix(apple): keep skipped catalog entries from reading as absence
A symlink or directory named Assets.car is skipped by the non-following directory reader. When no regular catalog remains, fail with the skipped paths instead of reporting an empty inventory. When catalogs were read, list the skipped entries in the limitations. The absence limitation now says that symlinks are not followed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
79e5a3923d
commit
a079af297b
|
|
@ -1,9 +1,10 @@
|
|||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { mkdir, symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
import { ArtifactReaderFailure } from "../ArtifactReader.js";
|
||||
import {
|
||||
analyzeAppleAssetCatalogs,
|
||||
collectInterfaceBuilderResourceKeys,
|
||||
|
|
@ -68,7 +69,49 @@ describe("Apple asset catalog application workflow", () => {
|
|||
truncated: false,
|
||||
});
|
||||
expect(result.limitations).toContain(
|
||||
"No compiled Assets.car catalog was found in the app bundle; any resource keys are reported as unmatched.",
|
||||
"No compiled Assets.car catalog was found among the app bundle's regular files (symlinks are not followed); any resource keys are reported as unmatched.",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not report absence when the only catalog entry is a symlink", async () => {
|
||||
const bundle = await createTestTempDirectory("rea-asset-catalog-link-");
|
||||
const resources = join(bundle, "Contents", "Resources");
|
||||
await mkdir(resources, { recursive: true });
|
||||
await writeFile(join(bundle, "outside.car"), "catalog");
|
||||
await symlink("../../outside.car", join(resources, "Assets.car"));
|
||||
const rejected = await analyzeAppleAssetCatalogs({
|
||||
bundlePath: bundle,
|
||||
targetSha256: "a".repeat(64),
|
||||
runAssetUtil: () => Promise.resolve("[]"),
|
||||
}).catch((cause: unknown) => cause);
|
||||
expect(rejected).toBeInstanceOf(ArtifactReaderFailure);
|
||||
expect(rejected).toMatchObject({
|
||||
reason: "path",
|
||||
message: expect.stringContaining(
|
||||
"Contents/Resources/Assets.car (symlink)",
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it("discloses skipped catalog entries beside inspected catalogs", async () => {
|
||||
const bundle = await createTestTempDirectory("rea-asset-catalog-mixed-");
|
||||
const resources = join(bundle, "Contents", "Resources");
|
||||
await mkdir(join(resources, "Nested"), { recursive: true });
|
||||
await writeFile(join(resources, "Assets.car"), "catalog");
|
||||
await symlink("../Assets.car", join(resources, "Nested", "Assets.car"));
|
||||
const result = await analyzeAppleAssetCatalogs({
|
||||
bundlePath: bundle,
|
||||
targetSha256: "a".repeat(64),
|
||||
runAssetUtil: () => Promise.resolve("[]"),
|
||||
});
|
||||
expect(result.catalogs.map(({ path }) => path)).toEqual([
|
||||
"Contents/Resources/Assets.car",
|
||||
]);
|
||||
expect(result.limitations).toContainEqual(
|
||||
expect.stringContaining("Contents/Resources/Nested/Assets.car (symlink)"),
|
||||
);
|
||||
expect(result.limitations).not.toContainEqual(
|
||||
expect.stringContaining("No compiled Assets.car catalog"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -48,10 +48,15 @@ export const analyzeAppleAssetCatalogs = async (input: {
|
|||
sha256: string;
|
||||
records: ReturnType<typeof parseAppleAssetCatalogRecords>;
|
||||
}[] = [];
|
||||
const uninspected: string[] = [];
|
||||
let totalMetadataBytes = 0;
|
||||
try {
|
||||
for await (const entry of reader.entries(input.signal)) {
|
||||
if (entry.kind !== "file" || !entry.path.endsWith("Assets.car")) continue;
|
||||
if (!entry.path.endsWith("Assets.car")) continue;
|
||||
if (entry.kind !== "file") {
|
||||
uninspected.push(`${entry.path} (${entry.kind})`);
|
||||
continue;
|
||||
}
|
||||
const inspected = await inspectAssetCatalogEntry({
|
||||
reader,
|
||||
entry,
|
||||
|
|
@ -69,6 +74,13 @@ export const analyzeAppleAssetCatalogs = async (input: {
|
|||
} finally {
|
||||
await reader.close();
|
||||
}
|
||||
// Without any readable catalog, unmatched resource keys would overstate an
|
||||
// absence that a skipped catalog entry may contradict.
|
||||
if (catalogs.length === 0 && uninspected.length > 0)
|
||||
throw new ArtifactReaderFailure(
|
||||
"path",
|
||||
`Asset catalog entries are not regular files and were not inspected: ${uninspected.join(", ")}`,
|
||||
);
|
||||
const interfaceBuilder = await analyzeInterfaceBuilderBundle({
|
||||
bundlePath: input.bundlePath,
|
||||
targetSha256: input.targetSha256,
|
||||
|
|
@ -92,9 +104,14 @@ export const analyzeAppleAssetCatalogs = async (input: {
|
|||
// a missing host tool.
|
||||
...(catalogs.length === 0
|
||||
? [
|
||||
"No compiled Assets.car catalog was found in the app bundle; any resource keys are reported as unmatched.",
|
||||
"No compiled Assets.car catalog was found among the app bundle's regular files (symlinks are not followed); any resource keys are reported as unmatched.",
|
||||
]
|
||||
: []),
|
||||
...(uninspected.length === 0
|
||||
? []
|
||||
: [
|
||||
`Asset catalog entries that are not regular files were not inspected, so resource keys reported as unmatched may resolve there: ${uninspected.join(", ")}`,
|
||||
]),
|
||||
"Exact resource-key joins use only recognized image or asset key fields on decoded Interface Builder resource objects; other archive resource references are not inferred.",
|
||||
...(interfaceBuilder.graph.truncated
|
||||
? [
|
||||
|
|
|
|||
Loading…
Reference in New Issue