fix(apple): attribute only application-root components in macOS inventories
Directory, ZIP, and DMG inventories can hold installers, disk-image extras, and AppleDouble sidecars beside the app. Those components were classified as the application's frameworks, libraries, JavaScript, and metadata, and fed runtime families and bridge candidates. For these formats, components outside every application root are now left out, with a limitation that counts them. IPA projection is unchanged. A selected directory whose name ends in .app is now its own root by identity, so an empty or partial inventory still reports it. iOS is claimed only for shallow roots that have an Info.plist, so an empty root reports no platform. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b956bd589f
commit
55249704c5
|
|
@ -45,8 +45,17 @@ macOS mounting adapter. When a DMG has only its root identity, the projection is
|
|||
`partial` and says that bundle absence is unknown. Other inventory formats, such
|
||||
as APK or ASAR, are rejected as input errors.
|
||||
|
||||
`platforms` lists `macos` for deep `Contents/` application roots and `ios` for
|
||||
shallow IPA application roots.
|
||||
An inventoried directory whose name ends in `.app` is its own root even when its
|
||||
inventory is partial or lacks `Contents/`. For directory, ZIP, and DMG
|
||||
inventories, components outside every application root are not attributed to
|
||||
the application. This covers installers, disk-image extras, and AppleDouble
|
||||
sidecars, which therefore stay out of runtime families and bridge candidates. A
|
||||
limitation reports how many inventoried entries were left out. IPA projection
|
||||
still lists every archive component, including `SwiftSupport/`.
|
||||
|
||||
`platforms` lists `macos` for application roots with `Contents/`. It lists `ios`
|
||||
for shallow application roots that have an `Info.plist`. An application root
|
||||
with neither, such as an empty inventory, has no platform.
|
||||
|
||||
## macOS bundle anatomy
|
||||
|
||||
|
|
|
|||
|
|
@ -350,9 +350,14 @@ describe("Apple bundle layouts and containers", () => {
|
|||
macho("Payload/Fixture.app/Fixture"),
|
||||
{ path: "Payload/Fixture.app/embedded.mobileprovision" },
|
||||
macho("Payload/Fixture.app/PlugIns/Share.appex/Share"),
|
||||
macho("SwiftSupport/iphoneos/libswiftCore.dylib"),
|
||||
]),
|
||||
);
|
||||
expect(result.platforms).toEqual(["ios"]);
|
||||
// IPA projection still lists archive components outside Payload/.
|
||||
expect(result.components.native_libraries.map(({ path }) => path)).toEqual([
|
||||
"SwiftSupport/iphoneos/libswiftCore.dylib",
|
||||
]);
|
||||
expect(
|
||||
result.bundles.map(({ path, role, layout }) => [path, role, layout]),
|
||||
).toEqual([
|
||||
|
|
@ -370,3 +375,66 @@ describe("Apple bundle layouts and containers", () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("macOS application scope", () => {
|
||||
it("attributes only components inside the application roots", () => {
|
||||
const result = project(
|
||||
inventoryEvidence("zip", "Distribution.zip", [
|
||||
macho("Fixture.app/Contents/MacOS/Fixture"),
|
||||
macho("Fixture.app/Contents/Frameworks/Core.framework/Versions/A/Core"),
|
||||
{
|
||||
path: "Fixture.app/Contents/Resources/app.js",
|
||||
format: "javascript-bundle",
|
||||
},
|
||||
macho("Extras/Plugin.framework/Versions/A/Plugin"),
|
||||
macho("Extras/libextra.dylib"),
|
||||
{ path: "Extras/installer.js", format: "javascript-bundle" },
|
||||
{ path: "Extras/Info.plist" },
|
||||
]),
|
||||
);
|
||||
const paths = (components: readonly { readonly path: string }[]) =>
|
||||
components.map(({ path }) => path);
|
||||
expect(paths(result.components.frameworks)).toEqual([
|
||||
"Fixture.app/Contents/Frameworks/Core.framework/Versions/A/Core",
|
||||
]);
|
||||
expect(paths(result.components.native_libraries)).toEqual([]);
|
||||
expect(paths(result.components.javascript)).toEqual([
|
||||
"Fixture.app/Contents/Resources/app.js",
|
||||
]);
|
||||
expect(paths(result.components.bundle_metadata)).toEqual([]);
|
||||
expect(
|
||||
result.bridge_candidates.every(
|
||||
({ source_path: source, native_path: target }) =>
|
||||
[source, target].every((path) => path.startsWith("Fixture.app/")),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(result.limitations).toContain(
|
||||
"4 inventoried entries outside every application root are not attributed to the application.",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps a selected .app directory as the root when its contents are missing", () => {
|
||||
const empty = project(inventoryEvidence("directory", "Fixture.app", []));
|
||||
expect(empty).toMatchObject({
|
||||
application_roots: ["."],
|
||||
platforms: [],
|
||||
bundles: [
|
||||
{
|
||||
path: ".",
|
||||
role: "application",
|
||||
layout: "shallow",
|
||||
info_plist_path: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const partial = project(
|
||||
inventoryEvidence("directory", "Fixture.app", [
|
||||
{ path: "Contents/Resources/Localizable.strings" },
|
||||
]),
|
||||
);
|
||||
expect(partial).toMatchObject({
|
||||
application_roots: ["."],
|
||||
platforms: ["macos"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -128,11 +128,18 @@ export const projectAppleApplication = (
|
|||
},
|
||||
};
|
||||
});
|
||||
const all = entries.flatMap(({ component }) =>
|
||||
const inventoried = entries.flatMap(({ component }) =>
|
||||
component === undefined ? [] : [component],
|
||||
);
|
||||
const subjectName = evidence[0]?.subject?.name ?? "";
|
||||
const roots = applicationRoots(entries, subjectName);
|
||||
const roots = applicationRoots(entries, {
|
||||
name: evidence[0]?.subject?.name ?? "",
|
||||
format: rootFormat,
|
||||
});
|
||||
const { all, unattributed } = attributeComponents(
|
||||
inventoried,
|
||||
roots,
|
||||
rootFormat,
|
||||
);
|
||||
const bundles = detectBundles(entries, roots);
|
||||
const classified = classifyComponents(entries, all, roots);
|
||||
const runtimeFamilies = identifyRuntimeFamilies(all);
|
||||
|
|
@ -153,6 +160,7 @@ export const projectAppleApplication = (
|
|||
bundles,
|
||||
sidecars: entries.some(({ path }) => isSidecar(path)),
|
||||
symlinks: entries.some(({ kind }) => kind === "symlink"),
|
||||
unattributed,
|
||||
});
|
||||
const withoutId = {
|
||||
root_sha256: inventory.manifest.root_sha256,
|
||||
|
|
@ -184,6 +192,24 @@ export const projectAppleApplication = (
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* IPA projection keeps every archive component, as before. Directory, ZIP,
|
||||
* and DMG inventories can hold installers and other siblings, so only
|
||||
* components inside an application root are attributed to it.
|
||||
*/
|
||||
const attributeComponents = (
|
||||
inventoried: readonly Component[],
|
||||
roots: readonly string[],
|
||||
rootFormat: RootFormat,
|
||||
): { readonly all: Component[]; readonly unattributed: number } => {
|
||||
if (rootFormat === "ipa") return { all: [...inventoried], unattributed: 0 };
|
||||
const visible = inventoried.filter(({ path }) => !isSidecar(path));
|
||||
const all = visible.filter(({ path }) =>
|
||||
roots.some((root) => isWithin(path, root)),
|
||||
);
|
||||
return { all, unattributed: visible.length - all.length };
|
||||
};
|
||||
|
||||
const appleRootFormat = (format: string): RootFormat => {
|
||||
const root = APPLE_ROOT_FORMATS.find((candidate) => candidate === format);
|
||||
if (root === undefined)
|
||||
|
|
@ -314,6 +340,7 @@ const projectionLimitations = (facts: {
|
|||
readonly bundles: readonly z.infer<typeof appleBundleSchema>[];
|
||||
readonly sidecars: boolean;
|
||||
readonly symlinks: boolean;
|
||||
readonly unattributed: number;
|
||||
}): string[] => [
|
||||
...(!facts.complete
|
||||
? ["Source inventory pages are incomplete; absence is unknown."]
|
||||
|
|
@ -341,6 +368,11 @@ const projectionLimitations = (facts: {
|
|||
"Symlinks are reported by path only; their targets are not inventoried, so bundles are reported at their real paths.",
|
||||
]
|
||||
: []),
|
||||
...(facts.unattributed > 0
|
||||
? [
|
||||
`${facts.unattributed} inventoried entries outside every application root are not attributed to the application.`,
|
||||
]
|
||||
: []),
|
||||
...(facts.sidecars
|
||||
? [
|
||||
"AppleDouble sidecar entries (._* files and __MACOSX/) describe neighbouring files and are excluded from bundle roles.",
|
||||
|
|
|
|||
|
|
@ -73,8 +73,12 @@ export const isWithin = (path: string, root: string): boolean =>
|
|||
*/
|
||||
export const applicationRoots = (
|
||||
entries: readonly Entry[],
|
||||
subjectName: string,
|
||||
subject: { readonly name: string; readonly format: string },
|
||||
): string[] => {
|
||||
// A selected `.app` directory is the application, even when its inventory
|
||||
// is partial or lacks Contents/; coverage and limitations describe that.
|
||||
if (subject.format === "directory" && /\.app$/iu.test(subject.name))
|
||||
return ["."];
|
||||
const found = new Set<string>();
|
||||
for (const { path } of entries) {
|
||||
if (isSidecar(path)) continue;
|
||||
|
|
@ -83,13 +87,6 @@ export const applicationRoots = (
|
|||
found.add(ios[1]);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
/\.app$/iu.test(subjectName) &&
|
||||
/^Contents\/(?:Info\.plist|MacOS\/[^/]+)$/u.test(path)
|
||||
) {
|
||||
found.add(".");
|
||||
continue;
|
||||
}
|
||||
const segments = path.split("/");
|
||||
const index = segments.findIndex(
|
||||
(segment, position) =>
|
||||
|
|
@ -280,13 +277,15 @@ const signingPaths = (
|
|||
.filter((path) => tree.files.has(path))
|
||||
.sort(compare);
|
||||
|
||||
/** Platforms of application roots whose layout was observed, not assumed. */
|
||||
export const platformsOf = (bundles: readonly Bundle[]): ("ios" | "macos")[] =>
|
||||
[
|
||||
...new Set(
|
||||
bundles.flatMap(({ role, layout }) => {
|
||||
bundles.flatMap(({ role, layout, info_plist_path: plist }) => {
|
||||
if (role !== "application") return [];
|
||||
if (layout === "macos-deep") return ["macos" as const];
|
||||
if (layout === "shallow") return ["ios" as const];
|
||||
// An empty or partial root has no Contents/ either; require Info.plist.
|
||||
if (layout === "shallow" && plist !== null) return ["ios" as const];
|
||||
return [];
|
||||
}),
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in New Issue