mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
4aa2b60c1d
codesign echoes the executable path and signing identifier verbatim, and an ad hoc identifier derives from the file name. A file name with line breaks added forged Authority lines, a signed file named after codesign's unsigned message was reported unsigned, and path text could supply entitlements. Parse display fields after the exact echoed path and identifier, classify unsigned output by exit status, and read requirements and entitlements from stdout. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
660 lines
21 KiB
TypeScript
660 lines
21 KiB
TypeScript
import {
|
|
NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY,
|
|
nativeMacOSCapabilities,
|
|
} from "./NativeMacOSProviderMetadata.js";
|
|
export { NATIVE_MACOS_PROVIDER_IDENTITY } from "./NativeMacOSProviderMetadata.js";
|
|
import { inspectAppleDispatchMetadata } from "./AppleDispatchMetadata.js";
|
|
import { observeNativeUi } from "./NativeUiObservation.js";
|
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
import { realpath, stat } from "node:fs/promises";
|
|
|
|
import { parse as parseXmlPlist } from "plist";
|
|
import { z } from "zod";
|
|
|
|
import {
|
|
createAnalysisExecution,
|
|
type AnalysisClient,
|
|
type AnalysisOperation,
|
|
type AnalysisProvider,
|
|
type CapabilityDescriptor,
|
|
type ProviderIdentity,
|
|
} from "../application/AnalysisProvider.js";
|
|
import {
|
|
NATIVE_TOOL_CONTRACTS,
|
|
type NativeToolName,
|
|
} from "../contracts/nativeToolContracts.js";
|
|
import type { BinaryTarget } from "../domain/binaryTarget.js";
|
|
import type { EvidenceLocation } from "../domain/evidence.js";
|
|
import {
|
|
AnalysisCancelledError,
|
|
AnalysisCapabilityUnavailableError,
|
|
AnalysisOutputError,
|
|
} from "../domain/analysisErrorCore.js";
|
|
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
|
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
|
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
|
|
import {
|
|
demangleSwiftSchema,
|
|
inspectPlistSchema,
|
|
inspectSignatureSchema,
|
|
listArchitecturesSchema,
|
|
type NativeCommandInvocation,
|
|
} from "../domain/nativeInspection.js";
|
|
import { err, ok, type Result } from "../domain/result.js";
|
|
import {
|
|
NativeCommandFailure,
|
|
XcrunCommandRunner,
|
|
type NativeCommandCapture,
|
|
type NativeCommandRunner,
|
|
} from "./CommandRunner.js";
|
|
import { parseCodeSignature } from "./parsers/codesign.js";
|
|
import { parseDemangledSymbols } from "./parsers/demangle.js";
|
|
import { parseLipoArchitectures } from "./parsers/lipo.js";
|
|
import {
|
|
parsePlistJson,
|
|
parsePlistXml,
|
|
plistJsonNeedsNumberTypes,
|
|
} from "./parsers/plist.js";
|
|
import {
|
|
architectureLocations,
|
|
inspectNativeMacho,
|
|
} from "./NativeMachoInspection.js";
|
|
|
|
/** Read-only semantic provider composed from Xcode command-line utilities. */
|
|
export class NativeMacOSProvider implements AnalysisProvider {
|
|
readonly #capabilities: readonly CapabilityDescriptor[];
|
|
|
|
constructor(
|
|
private readonly runner: NativeCommandRunner = new XcrunCommandRunner(),
|
|
platform: NodeJS.Platform = process.platform,
|
|
) {
|
|
this.#capabilities = nativeMacOSCapabilities(platform);
|
|
}
|
|
|
|
identity(): ProviderIdentity {
|
|
return IDENTITY;
|
|
}
|
|
|
|
capabilities(): readonly CapabilityDescriptor[] {
|
|
return this.#capabilities;
|
|
}
|
|
|
|
createClient(target: BinaryTarget): AnalysisClient {
|
|
return new NativeMacOSClient(target, this.runner);
|
|
}
|
|
}
|
|
|
|
class NativeMacOSClient implements AnalysisClient {
|
|
constructor(
|
|
private readonly target: BinaryTarget,
|
|
private readonly runner: NativeCommandRunner,
|
|
) {}
|
|
|
|
async execute(
|
|
operation: AnalysisOperation,
|
|
parameters: Readonly<Record<string, JsonValue>>,
|
|
options?: { readonly signal?: AbortSignal },
|
|
) {
|
|
if (options?.signal?.aborted === true)
|
|
return err(new AnalysisCancelledError(operation));
|
|
if (operation === "health")
|
|
return ok(createAnalysisExecution(null, IDENTITY));
|
|
if (operation === "inspect_native_dispatch_metadata") {
|
|
if (this.target.kind !== "executable" || this.target.format !== "mach-o")
|
|
return err(
|
|
new AnalysisCapabilityUnavailableError(
|
|
IDENTITY.id,
|
|
operation,
|
|
"Binary Apple metadata requires Mach-O; other providers can report symbol-only metadata",
|
|
),
|
|
);
|
|
try {
|
|
const maxRecords = z
|
|
.number()
|
|
.int()
|
|
.min(1)
|
|
.max(20000)
|
|
.parse(parameters.max_records ?? 5000);
|
|
const result = await inspectAppleDispatchMetadata(
|
|
this.target,
|
|
maxRecords,
|
|
options?.signal,
|
|
);
|
|
return ok(
|
|
createAnalysisExecution(result, result.provider, {
|
|
locations: [{ kind: "artifact-path", path: this.target.path }],
|
|
limitations: result.result.coverage.flatMap(({ reason }) =>
|
|
reason === null ? [] : [reason],
|
|
),
|
|
}),
|
|
);
|
|
} catch (cause) {
|
|
if (cause instanceof AnalysisError) return err(cause);
|
|
return err(
|
|
options?.signal?.aborted
|
|
? new AnalysisCancelledError(operation)
|
|
: new ProviderAdapterError(IDENTITY.id, operation, {
|
|
cause,
|
|
diagnostics: {
|
|
target_path: this.target.path,
|
|
reason:
|
|
cause instanceof Error ? cause.message : String(cause),
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
if (!isNativeOperation(operation))
|
|
return err(
|
|
new AnalysisCapabilityUnavailableError(
|
|
IDENTITY.id,
|
|
operation,
|
|
"Operation is not implemented by native macOS tools.",
|
|
),
|
|
);
|
|
if (
|
|
operation === "observe_native_ui" ||
|
|
operation === "capture_native_ui_scenario"
|
|
) {
|
|
const result = await observeNativeUi(this.target, operation, parameters, {
|
|
signal: options?.signal,
|
|
});
|
|
return result.ok
|
|
? ok(
|
|
createAnalysisExecution(result.value, IDENTITY, {
|
|
limitations: result.value.limitations,
|
|
}),
|
|
)
|
|
: result;
|
|
}
|
|
try {
|
|
const observation = await this.#dispatch(
|
|
operation,
|
|
parameters,
|
|
options?.signal,
|
|
);
|
|
return observation.ok
|
|
? ok(
|
|
createAnalysisExecution(observation.value.result, IDENTITY, {
|
|
rawResult: { provenance: observation.value.provenance },
|
|
limitations: observation.value.limitations,
|
|
locations: observation.value.locations,
|
|
}),
|
|
)
|
|
: observation;
|
|
} catch (cause: unknown) {
|
|
return err(
|
|
new AnalysisOutputError(operation, "Native output parsing failed", {
|
|
cause,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
close(): Promise<void> {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
#dispatch(
|
|
operation: NativeToolName,
|
|
parameters: Readonly<Record<string, JsonValue>>,
|
|
signal?: AbortSignal,
|
|
): Promise<Result<NativeObservation, AnalysisError>> {
|
|
switch (operation) {
|
|
case "observe_native_ui":
|
|
case "capture_native_ui_scenario":
|
|
return Promise.resolve(
|
|
err(
|
|
new AnalysisCapabilityUnavailableError(
|
|
IDENTITY.id,
|
|
operation,
|
|
"UI observation uses its explicit authority boundary",
|
|
),
|
|
),
|
|
);
|
|
case "inspect_macho":
|
|
return this.#inspectMacho(signal);
|
|
case "inspect_signature":
|
|
return this.#inspectSignature(signal);
|
|
case "inspect_plist":
|
|
return this.#inspectPlist(parameters, signal);
|
|
case "list_architectures":
|
|
return this.#listArchitectures(signal);
|
|
case "demangle_swift":
|
|
return this.#demangle(parameters, signal);
|
|
}
|
|
}
|
|
|
|
async #listArchitectures(
|
|
signal?: AbortSignal,
|
|
): Promise<Result<NativeObservation, AnalysisError>> {
|
|
const capture = await this.#run(
|
|
"list_architectures",
|
|
"lipo",
|
|
["-detailed_info", this.target.path],
|
|
{ signal },
|
|
);
|
|
if (!capture.ok) return capture;
|
|
const provenance = [invocation(capture.value, this.target.path)];
|
|
const architectures = parseLipoArchitectures(capture.value.stdout);
|
|
const result = listArchitecturesSchema.parse({
|
|
architectures: {
|
|
items: architectures,
|
|
total: architectures.length,
|
|
exhaustive: true,
|
|
limitations: [],
|
|
},
|
|
provenance,
|
|
limitations: [],
|
|
});
|
|
return ok({
|
|
result: jsonValueSchema.parse(result),
|
|
provenance,
|
|
limitations: [],
|
|
locations: architectureLocations(result.architectures.items),
|
|
});
|
|
}
|
|
|
|
async #demangle(
|
|
parameters: Readonly<Record<string, JsonValue>>,
|
|
signal?: AbortSignal,
|
|
): Promise<Result<NativeObservation, AnalysisError>> {
|
|
const symbols = z.array(z.string()).safeParse(parameters.symbols);
|
|
if (!symbols.success)
|
|
return err(
|
|
new AnalysisOutputError(
|
|
"demangle_swift",
|
|
"symbols were not parsed strings",
|
|
),
|
|
);
|
|
const capture = await this.#run(
|
|
"demangle_swift",
|
|
"swift-demangle",
|
|
["--compact", ...symbols.data],
|
|
{ signal },
|
|
);
|
|
if (!capture.ok) return capture;
|
|
const provenance = [invocation(capture.value, this.target.path)];
|
|
const result = demangleSwiftSchema.parse({
|
|
symbols: parseDemangledSymbols(symbols.data, capture.value.stdout),
|
|
provenance,
|
|
limitations: [],
|
|
});
|
|
return ok({
|
|
result: jsonValueSchema.parse(result),
|
|
provenance,
|
|
limitations: [],
|
|
locations: [],
|
|
});
|
|
}
|
|
|
|
async #inspectSignature(
|
|
signal?: AbortSignal,
|
|
): Promise<Result<NativeObservation, AnalysisError>> {
|
|
const display = await this.#run(
|
|
"inspect_signature",
|
|
"codesign",
|
|
["-d", "--verbose=4", this.target.path],
|
|
{ signal, acceptNonZero: true },
|
|
);
|
|
if (!display.ok) return display;
|
|
const unsigned = isNonzeroUnsignedObservation(display.value);
|
|
const displayFailure = codeSignCaptureFailure(display.value);
|
|
if (displayFailure !== null) return err(displayFailure);
|
|
const requirements = await this.#run(
|
|
"inspect_signature",
|
|
"codesign",
|
|
["-d", "-r-", this.target.path],
|
|
{ signal, acceptNonZero: true },
|
|
);
|
|
if (!requirements.ok) return requirements;
|
|
const requirementsFailure = codeSignCaptureFailure(requirements.value);
|
|
if (requirementsFailure !== null) return err(requirementsFailure);
|
|
const entitlements = await this.#run(
|
|
"inspect_signature",
|
|
"codesign",
|
|
["-d", "--entitlements", ":-", this.target.path],
|
|
{ signal, acceptNonZero: true },
|
|
);
|
|
if (!entitlements.ok) return entitlements;
|
|
const entitlementsFailure = codeSignCaptureFailure(entitlements.value);
|
|
if (entitlementsFailure !== null) return err(entitlementsFailure);
|
|
// codesign echoes the resolved executable path in its diagnostics.
|
|
const parsed = parseCodeSignature(display.value.stderr, unsigned, [
|
|
this.target.path,
|
|
...(await canonicalPath(this.target.path)),
|
|
]);
|
|
// The requirement is printed to stdout; stderr echoes the path.
|
|
const requirementText =
|
|
/designated\s*=>\s*(.+)$/mu.exec(requirements.value.stdout)?.[1] ?? null;
|
|
// Entitlements XML is printed to stdout; stderr echoes the path.
|
|
const entitlementValue = parseEntitlements(entitlements.value.stdout);
|
|
const captures = [display.value, requirements.value, entitlements.value];
|
|
const limitations = [...parsed.limitations];
|
|
const mixedSigning =
|
|
!unsigned &&
|
|
(isNonzeroUnsignedObservation(requirements.value) ||
|
|
isNonzeroUnsignedObservation(entitlements.value));
|
|
if (mixedSigning) {
|
|
const slices = await this.#inspectMixedSignatureSlices(
|
|
parsed.format,
|
|
requirements.value.exitCode !== 0,
|
|
entitlements.value.exitCode !== 0,
|
|
signal,
|
|
);
|
|
if (!slices.ok) return slices;
|
|
captures.push(...slices.value.captures);
|
|
limitations.push(...slices.value.limitations);
|
|
}
|
|
const provenance = captures.map((capture) =>
|
|
invocation(capture, this.target.path),
|
|
);
|
|
const result = inspectSignatureSchema.parse({
|
|
...parsed,
|
|
designated_requirement: requirementText,
|
|
entitlements: entitlementValue,
|
|
provenance,
|
|
limitations,
|
|
});
|
|
return ok({
|
|
result: jsonValueSchema.parse(result),
|
|
provenance,
|
|
limitations: result.limitations,
|
|
locations: [],
|
|
});
|
|
}
|
|
|
|
async #inspectMixedSignatureSlices(
|
|
format: string | null,
|
|
requirementsUnavailable: boolean,
|
|
entitlementsUnavailable: boolean,
|
|
signal?: AbortSignal,
|
|
): Promise<Result<MixedSignatureSlices, AnalysisError>> {
|
|
const captures: NativeCommandCapture[] = [];
|
|
const unsigned: string[] = [];
|
|
const unclassified: string[] = [];
|
|
for (const architecture of codeSignArchitectures(format, this.target)) {
|
|
const slice = await this.#run(
|
|
"inspect_signature",
|
|
"codesign",
|
|
["-d", "-a", architecture, "--verbose=4", this.target.path],
|
|
{ signal, acceptNonZero: true },
|
|
);
|
|
if (!slice.ok) return slice;
|
|
captures.push(slice.value);
|
|
if (slice.value.exitCode === 0) continue;
|
|
if (isNonzeroUnsignedObservation(slice.value))
|
|
unsigned.push(architecture);
|
|
else unclassified.push(architecture);
|
|
}
|
|
const limitations = [
|
|
unsigned.length > 0
|
|
? `Unsigned Mach-O slices: ${unsigned.join(", ")}.`
|
|
: "One or more Mach-O slices are unsigned, but their architectures could not be classified.",
|
|
];
|
|
if (unclassified.length > 0)
|
|
limitations.push(
|
|
`Signature state could not be classified for Mach-O slices: ${unclassified.join(", ")}.`,
|
|
);
|
|
if (requirementsUnavailable)
|
|
limitations.push(
|
|
"The aggregate designated requirement is unavailable because Mach-O slices have mixed signing states.",
|
|
);
|
|
if (entitlementsUnavailable)
|
|
limitations.push(
|
|
"The aggregate entitlements are unavailable because Mach-O slices have mixed signing states.",
|
|
);
|
|
return ok({ captures, limitations });
|
|
}
|
|
|
|
async #inspectPlist(
|
|
parameters: Readonly<Record<string, JsonValue>>,
|
|
signal?: AbortSignal,
|
|
): Promise<Result<NativeObservation, AnalysisError>> {
|
|
const requested = parameters.path;
|
|
if (requested !== undefined && typeof requested !== "string")
|
|
return err(
|
|
new AnalysisOutputError("inspect_plist", "path was not a string"),
|
|
);
|
|
const plist = await resolvePlistPath(this.target, requested);
|
|
if (!plist.ok) return plist;
|
|
const classified = await this.#run(
|
|
"inspect_plist",
|
|
"file",
|
|
["-b", plist.value],
|
|
{ signal },
|
|
);
|
|
if (!classified.ok) return classified;
|
|
const json = await this.#run(
|
|
"inspect_plist",
|
|
"plutil",
|
|
["-convert", "json", "-o", "-", "--", plist.value],
|
|
{ signal, acceptNonZero: true },
|
|
);
|
|
if (!json.ok) return json;
|
|
// JSON cannot express data, dates, or non-finite reals; plutil rejects
|
|
// such plists, so decode its lossless XML conversion instead. JSON also
|
|
// prints an integral <real> like an <integer>, so a number beyond the
|
|
// exact JSON range takes its element type from the XML conversion.
|
|
const jsonDecoded = json.value.exitCode === 0;
|
|
const xml =
|
|
jsonDecoded && !plistJsonNeedsNumberTypes(json.value.stdout)
|
|
? undefined
|
|
: await this.#run(
|
|
"inspect_plist",
|
|
"plutil",
|
|
["-convert", "xml1", "-o", "-", "--", plist.value],
|
|
{ signal },
|
|
);
|
|
if (xml !== undefined && !xml.ok) return xml;
|
|
const parsed =
|
|
xml === undefined
|
|
? parsePlistJson(json.value.stdout)
|
|
: jsonDecoded
|
|
? parsePlistJson(json.value.stdout, xml.value.stdout)
|
|
: parsePlistXml(xml.value.stdout);
|
|
if (!parsed.ok) return parsed;
|
|
const provenance = [
|
|
classified.value,
|
|
json.value,
|
|
...(xml === undefined ? [] : [xml.value]),
|
|
].map((item) => invocation(item, plist.value, "$PLIST"));
|
|
const result = inspectPlistSchema.parse({
|
|
format: /binary property list/iu.test(classified.value.stdout)
|
|
? "binary"
|
|
: /XML|text/iu.test(classified.value.stdout)
|
|
? "xml"
|
|
: "unknown",
|
|
value: parsed.value.value,
|
|
bundle: parsed.value.bundle,
|
|
source_path: plist.value,
|
|
provenance,
|
|
limitations: parsed.value.limitations,
|
|
});
|
|
return ok({
|
|
result: jsonValueSchema.parse(result),
|
|
provenance,
|
|
limitations: parsed.value.limitations,
|
|
locations: [{ kind: "artifact-path", path: plist.value }],
|
|
});
|
|
}
|
|
|
|
async #inspectMacho(
|
|
signal?: AbortSignal,
|
|
): Promise<Result<NativeObservation, AnalysisError>> {
|
|
if (this.target.format !== "mach-o")
|
|
return err(
|
|
new AnalysisCapabilityUnavailableError(
|
|
IDENTITY.id,
|
|
"inspect_macho",
|
|
"Active artifact is not Mach-O.",
|
|
),
|
|
);
|
|
return inspectNativeMacho({
|
|
target: this.target,
|
|
...(signal === undefined ? {} : { signal }),
|
|
run: (tool, arguments_, commandSignal) =>
|
|
this.#run("inspect_macho", tool, arguments_, {
|
|
signal: commandSignal,
|
|
}),
|
|
invocation: (capture) => invocation(capture, this.target.path),
|
|
});
|
|
}
|
|
|
|
async #run(
|
|
operation: NativeToolName,
|
|
tool: string,
|
|
arguments_: readonly string[],
|
|
options: {
|
|
readonly signal?: AbortSignal | undefined;
|
|
readonly acceptNonZero?: boolean;
|
|
} = {},
|
|
): Promise<Result<NativeCommandCapture, AnalysisError>> {
|
|
const captured = await this.runner.run(tool, arguments_, {
|
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
acceptNonZero: options.acceptNonZero ?? false,
|
|
});
|
|
if (captured.ok) return captured;
|
|
return err(translateCommandFailure(operation, captured.error));
|
|
}
|
|
}
|
|
|
|
interface NativeObservation {
|
|
readonly result: JsonValue;
|
|
readonly provenance: readonly NativeCommandInvocation[];
|
|
readonly limitations: readonly string[];
|
|
readonly locations: readonly EvidenceLocation[];
|
|
}
|
|
|
|
interface MixedSignatureSlices {
|
|
readonly captures: readonly NativeCommandCapture[];
|
|
readonly limitations: readonly string[];
|
|
}
|
|
|
|
const isNativeOperation = (
|
|
operation: AnalysisOperation,
|
|
): operation is NativeToolName =>
|
|
NATIVE_TOOL_CONTRACTS.some(({ name }) => name === operation);
|
|
|
|
const translateCommandFailure = (
|
|
operation: NativeToolName,
|
|
failure: NativeCommandFailure,
|
|
): AnalysisError => {
|
|
if (failure.reason === "unavailable")
|
|
return new AnalysisCapabilityUnavailableError(
|
|
IDENTITY.id,
|
|
operation,
|
|
`${failure.tool} is unavailable through xcrun.`,
|
|
);
|
|
if (failure.reason === "cancelled")
|
|
return new AnalysisCancelledError(operation);
|
|
return new ProviderAdapterError(IDENTITY.id, operation, { cause: failure });
|
|
};
|
|
|
|
const translateCodeSignExitFailure = (
|
|
capture: NativeCommandCapture,
|
|
): AnalysisError =>
|
|
translateCommandFailure(
|
|
"inspect_signature",
|
|
new NativeCommandFailure("codesign", "nonzero-exit", capture.exitCode),
|
|
);
|
|
|
|
const commandOutput = (capture: NativeCommandCapture): string =>
|
|
`${capture.stdout}\n${capture.stderr}`;
|
|
|
|
/**
|
|
* A signed artifact exits zero; its diagnostics can still contain this text
|
|
* through the echoed path or identifier.
|
|
*/
|
|
const isNonzeroUnsignedObservation = (capture: NativeCommandCapture): boolean =>
|
|
capture.exitCode !== null &&
|
|
capture.exitCode !== 0 &&
|
|
/not signed at all|code object is not signed/iu.test(commandOutput(capture));
|
|
|
|
const canonicalPath = async (path: string): Promise<string[]> => {
|
|
try {
|
|
return [await realpath(path)];
|
|
} catch {
|
|
return [];
|
|
}
|
|
};
|
|
|
|
const codeSignCaptureFailure = (
|
|
capture: NativeCommandCapture,
|
|
): AnalysisError | null =>
|
|
capture.exitCode === 0 || isNonzeroUnsignedObservation(capture)
|
|
? null
|
|
: translateCodeSignExitFailure(capture);
|
|
|
|
const codeSignArchitectures = (
|
|
format: string | null,
|
|
target: BinaryTarget,
|
|
): string[] => {
|
|
const universal = /^Mach-O universal \(([^)\r\n]+)\)$/u.exec(
|
|
format ?? "",
|
|
)?.[1];
|
|
if (universal !== undefined) {
|
|
const architectures = universal
|
|
.split(/\s+/u)
|
|
.filter((architecture) =>
|
|
/^(?:i386|x86_64|armv7|arm64|arm64e)$/u.test(architecture),
|
|
);
|
|
if (architectures.length > 0) return [...new Set(architectures)];
|
|
}
|
|
if (target.kind !== "executable" || target.format !== "mach-o") return [];
|
|
return target.availableArchitectures.map((architecture) =>
|
|
architecture === "x86" ? "i386" : architecture,
|
|
);
|
|
};
|
|
|
|
const invocation = (
|
|
capture: NativeCommandCapture,
|
|
artifactPath: string,
|
|
alias = "$ARTIFACT",
|
|
): NativeCommandInvocation => ({
|
|
tool: capture.tool,
|
|
command: [
|
|
capture.executable,
|
|
...capture.arguments.map((argument) =>
|
|
argument === artifactPath ? alias : argument,
|
|
),
|
|
],
|
|
tool_version: capture.toolVersion,
|
|
version_reason: capture.versionReason,
|
|
executable_sha256: capture.executableSha256,
|
|
exit: { code: capture.exitCode, signal: capture.signal },
|
|
stdout_bytes: capture.stdoutBytes,
|
|
stderr_bytes: capture.stderrBytes,
|
|
});
|
|
|
|
const parseEntitlements = (output: string): JsonValue | null => {
|
|
const start = output.indexOf("<?xml");
|
|
const end = output.lastIndexOf("</plist>");
|
|
if (start < 0 || end < start) return null;
|
|
return jsonValueSchema.parse(
|
|
parseXmlPlist(output.slice(start, end + "</plist>".length)),
|
|
);
|
|
};
|
|
|
|
const resolvePlistPath = async (
|
|
target: BinaryTarget,
|
|
requested: string | undefined,
|
|
): Promise<Result<string, AnalysisError>> => {
|
|
try {
|
|
const source = target.sourcePath ?? target.path;
|
|
if (requested !== undefined && isAbsolute(requested))
|
|
return ok(resolve(requested));
|
|
const sourceMetadata = await stat(source);
|
|
const root = sourceMetadata.isDirectory() ? source : dirname(target.path);
|
|
return ok(
|
|
requested === undefined
|
|
? resolve(root, "Contents/Info.plist")
|
|
: resolve(root, requested),
|
|
);
|
|
} catch (cause: unknown) {
|
|
return err(
|
|
new ProviderAdapterError(IDENTITY.id, "inspect_plist", { cause }),
|
|
);
|
|
}
|
|
};
|