Merge pull request #1228 from morluto/fix/keyed-archive-host-remediation

fix(artifacts): name only host-available workflows for non-archive plists
This commit is contained in:
Kaoru0822 authored and GitHub committed 2026-10-09 01:48:51 -07:00
commit f4c99ab8a5
4 files changed
+67 -7

No files matched your search

+8 -2
View File
@@ -43,9 +43,11 @@ import { resolveArtifactIntegrityPolicy } from "./inventory/policy.js";
/** Read-only inventory and exclusively owned extraction provider. */
export class ArtifactProvider implements AnalysisProvider {
readonly #capabilities: readonly CapabilityDescriptor[];
readonly #platform: NodeJS.Platform;
constructor(platform: NodeJS.Platform = process.platform) {
this.#capabilities = artifactCapabilities(platform);
this.#platform = platform;
}
identity(): ProviderIdentity {
@@ -57,12 +59,15 @@ export class ArtifactProvider implements AnalysisProvider {
}
createClient(target: BinaryTarget): AnalysisClient {
return new ArtifactClient(target);
return new ArtifactClient(target, this.#platform);
}
}
class ArtifactClient implements AnalysisClient {
constructor(private readonly target: BinaryTarget) {}
constructor(
private readonly target: BinaryTarget,
private readonly platform: NodeJS.Platform,
) {}
async execute(
operation: AnalysisOperation,
@@ -146,6 +151,7 @@ class ArtifactClient implements AnalysisClient {
parameters: standalone
? { ...parameters, path: basename(this.target.path) }
: parameters,
platform: this.platform,
...(options?.signal === undefined ? {} : { signal: options.signal }),
});
if (standalone && result.archive_sha256 !== this.target.sha256)
+25 -2
View File
@@ -40,7 +40,7 @@ export const decodeKeyedArchiveBytes = (
if (bytes.subarray(0, 10).toString("ascii") === "NIBArchive")
throw new KeyedArchiveKindError(
"Selected file is a compiled NIBArchive, not a Foundation plist archive; decode it with decode_interface_builder",
"Decode compiled NIBArchive files with decode_interface_builder on the app bundle.",
"nib-archive",
);
const binary = bytes.subarray(0, 8).toString("ascii") === "bplist00";
const xmlText = binary ? undefined : decodeXmlPlistText(bytes);
@@ -87,12 +87,29 @@ const normalizePlist = (value: unknown): JsonValue => {
return projected.value;
};
/**
* Point to the workflow that reads the selected kind of file on this host.
* decode_interface_builder is portable; inspect_plist requires macOS.
*/
const keyedArchiveKindRemediation = (
kind: KeyedArchiveKindError["kind"],
platform: NodeJS.Platform,
): string => {
if (kind === "nib-archive")
return "Decode compiled NIBArchive files with decode_interface_builder on the app bundle.";
return platform === "darwin"
? "Inspect an ordinary property list with inspect_plist; inspect_keyed_archive decodes NSKeyedArchiver archives only."
: "Select an NSKeyedArchiver archive; inspect_keyed_archive decodes only those. inspect_plist, which reads ordinary property lists, requires a macOS host.";
};
/** Read exactly one regular, contained bundle entry without following symlinks. */
export const inspectBundleKeyedArchive = async (input: {
bundlePath: string;
targetSha256: string;
parameters: unknown;
signal?: AbortSignal;
/** Host whose available workflows the remediation names. */
platform?: NodeJS.Platform;
}) => {
const selected = keyedArchiveInputSchema.parse(input.parameters);
if (
@@ -159,7 +176,13 @@ export const inspectBundleKeyedArchive = async (input: {
"inspect_keyed_archive",
join(input.bundlePath, entry.path),
cause.message,
{ cause, remediationAction: cause.remediationAction },
{
cause,
remediationAction: keyedArchiveKindRemediation(
cause.kind,
input.platform ?? process.platform,
),
},
);
throw new ArtifactReaderFailure(
cause instanceof RangeError ? "limit" : "format",
+3 -3
View File
@@ -11,8 +11,8 @@ export class KeyedArchiveKindError extends TypeError {
constructor(
message: string,
/** The workflow that reads this kind of file. */
readonly remediationAction: string,
/** What the selected file is instead. */
readonly kind: "nib-archive" | "property-list",
) {
super(message);
}
@@ -103,7 +103,7 @@ export const projectKeyedArchive = (
if (archive?.$archiver !== "NSKeyedArchiver")
throw new KeyedArchiveKindError(
`Selected property list is not an NSKeyedArchiver archive: ${archiverMismatch(archive)}`,
"Inspect an ordinary property list with inspect_plist; inspect_keyed_archive decodes NSKeyedArchiver archives only.",
"property-list",
);
if (!Array.isArray(archive.$objects))
throw new TypeError("Expected NSKeyedArchiver with an $objects array");
@@ -4,6 +4,7 @@ import { join } from "node:path";
import { build } from "plist";
import { expect, it } from "vitest";
import { ArtifactProvider } from "../../../src/artifacts/ArtifactProvider.js";
import { ArtifactReaderFailure } from "../../../src/artifacts/ArtifactReader.js";
import { inspectBundleKeyedArchive } from "../../../src/artifacts/apple/KeyedArchiveReader.js";
import { AnalysisError } from "../../../src/domain/analysisErrorBase.js";
@@ -36,6 +37,7 @@ const inspect = (bundlePath: string, parameters: Record<string, unknown>) =>
bundlePath,
targetSha256: "a".repeat(64),
parameters,
platform: "darwin",
});
it.each([
@@ -120,6 +122,35 @@ it.each([
},
);
it.each([
["darwin", "Inspect an ordinary property list with inspect_plist"],
[
"linux",
"inspect_plist, which reads ordinary property lists, requires a macOS host",
],
] as const)(
"names the property-list workflow available on a %s host",
async (platform, action) => {
const root = await createTestTempDirectory("rea-keyed-kind-");
const path = join(root, "Defaults.plist");
await writeFile(path, Buffer.from(build({ plain: true })));
const result = await new ArtifactProvider(platform)
.createClient({
path,
sha256: "0".repeat(64),
kind: "artifact",
format: "plist",
})
.execute("inspect_keyed_archive", {});
if (result.ok) throw new Error("Expected an unsupported target");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "unsupported_target",
remediation: { action: expect.stringContaining(action) },
details: { path },
});
},
);
it("keeps a damaged NSKeyedArchiver archive a format failure", async () => {
const root = await bundle();
await writeFile(