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:
4 files changed
+67
-7
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user