mirror of
https://github.com/morluto/rea.git
synced 2026-11-02 21:06:04 +00:00
* fix(native): preserve mixed-slice signature observations * fix(native): retain signature facts when slice inspection fails * fix(native): accumulate known slice observations before failure
756 lines
23 KiB
TypeScript
756 lines
23 KiB
TypeScript
import { inspectSignatureSchema } from "../../../../src/domain/native/nativeInspection.js";
|
||
import { realpath, writeFile } from "node:fs/promises";
|
||
import { join } from "node:path";
|
||
import { expect, it } from "vitest";
|
||
import {
|
||
CPU,
|
||
fatImage,
|
||
machoImage,
|
||
} from "../../../../src/artifacts/apple/MachoImage.fixture.js";
|
||
import { err, ok } from "../../../../src/domain/result.js";
|
||
import {
|
||
NativeCommandFailure,
|
||
type NativeCommandCapture,
|
||
type NativeCommandOptions,
|
||
type NativeCommandRunner,
|
||
} from "../../../../src/native/CommandRunner.js";
|
||
import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js";
|
||
import { parseOtoolLoadCommands } from "../../../../src/native/parsers/otool.js";
|
||
import { parseDyldSymbols } from "../../../../src/native/parsers/dyldInfo.js";
|
||
import {
|
||
NativeFixtureRunner as FixtureRunner,
|
||
nativeFixture as fixture,
|
||
nativeMachoTarget as machoTarget,
|
||
nativeMachoTargetForFile,
|
||
withOtoolMachHeader,
|
||
} from "../../../fixtures/nativeCommands.js";
|
||
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
|
||
|
||
const signatureTarget = async (
|
||
signedArchitecture: "arm64" | "arm64e" = "arm64",
|
||
) => {
|
||
const path = join(
|
||
await createTestTempDirectory("rea-native-inventory-"),
|
||
"fixture",
|
||
);
|
||
await writeFile(
|
||
path,
|
||
fatImage(
|
||
[CPU.x86_64, CPU[signedArchitecture]].map((cpu) => ({
|
||
cpu,
|
||
bytes: machoImage({ cpu }),
|
||
})),
|
||
),
|
||
);
|
||
return nativeMachoTargetForFile(path);
|
||
};
|
||
|
||
it("distinguishes a codesign execution failure from an observed unsigned artifact", async () => {
|
||
for (const kind of ["missing", "unsigned"] as const) {
|
||
const fixtureOutput = await fixture(`dyld-inventory/codesign-${kind}.txt`);
|
||
const client = new NativeMacOSProvider(
|
||
{},
|
||
{
|
||
async run(tool, arguments_) {
|
||
const output =
|
||
kind === "unsigned"
|
||
? `${arguments_.at(-1)}: code object is not signed at all\n`
|
||
: fixtureOutput;
|
||
const capture = await new FixtureRunner({ codesign: output }, 1).run(
|
||
tool,
|
||
arguments_,
|
||
);
|
||
if (!capture.ok || kind !== "unsigned") return capture;
|
||
return ok({
|
||
...capture.value,
|
||
stdout: "",
|
||
stderr: output,
|
||
stdoutBytes: 0,
|
||
stderrBytes: Buffer.byteLength(output),
|
||
});
|
||
},
|
||
},
|
||
"darwin",
|
||
).createClient(await signatureTarget());
|
||
const execution = await client.execute("inspect_signature", {});
|
||
if (kind === "missing")
|
||
expect(execution).toMatchObject({
|
||
ok: false,
|
||
error: { _tag: "ProviderAdapterError" },
|
||
});
|
||
else {
|
||
expect(execution.ok).toBe(true);
|
||
if (execution.ok)
|
||
expect(execution.value.result).toMatchObject({ signed: false });
|
||
}
|
||
}
|
||
});
|
||
|
||
it("reports unsigned slices when a universal Mach-O has mixed signatures", async () => {
|
||
const signedFixture = await fixture("codesign.txt");
|
||
const unsigned = await fixture("dyld-inventory/codesign-unsigned.txt");
|
||
const entitlements = await fixture("entitlements.xml");
|
||
for (const signedArchitecture of ["arm64", "arm64e"] as const) {
|
||
const signed = signedFixture.replace(
|
||
/^Format=.*$/mu,
|
||
`Format=Mach-O universal (x86_64 ${signedArchitecture})`,
|
||
);
|
||
const client = new NativeMacOSProvider(
|
||
{},
|
||
signatureRunner({ signed, unsigned, entitlements, unsignedHeader: true }),
|
||
"darwin",
|
||
).createClient(await signatureTarget(signedArchitecture));
|
||
|
||
const execution = await client.execute("inspect_signature", {});
|
||
|
||
expect(execution.ok).toBe(true);
|
||
if (!execution.ok) continue;
|
||
expect(execution.value.result).toMatchObject({
|
||
signed: true,
|
||
identifier: "com.example.fixture",
|
||
designated_requirement: null,
|
||
entitlements: { "com.apple.security.app-sandbox": true },
|
||
limitations: expect.arrayContaining([
|
||
"Partial signature coverage: aggregate signing metadata describes codesign’s default slice; Mach-O slices have mixed signing states.",
|
||
"Unsigned Mach-O slices: x86_64.",
|
||
"The aggregate designated requirement is unavailable because Mach-O slices have mixed signing states.",
|
||
]),
|
||
provenance: expect.arrayContaining([
|
||
expect.objectContaining({
|
||
command: [
|
||
"/usr/bin/codesign",
|
||
"-d",
|
||
"-a",
|
||
"x86_64",
|
||
"--verbose=4",
|
||
"$ARTIFACT",
|
||
],
|
||
exit: { code: 1, signal: null },
|
||
}),
|
||
expect.objectContaining({
|
||
command: expect.arrayContaining(["-a", signedArchitecture]),
|
||
exit: { code: 0, signal: null },
|
||
}),
|
||
]),
|
||
});
|
||
}
|
||
});
|
||
|
||
it("rejects nonzero supplemental codesign output that is not an unsigned observation", async () => {
|
||
const signed = await fixture("codesign.txt");
|
||
const entitlements = await fixture("entitlements.xml");
|
||
const client = new NativeMacOSProvider(
|
||
{},
|
||
signatureRunner({
|
||
signed,
|
||
unsigned: "/private/fixture: invalid or unsupported format\n",
|
||
entitlements,
|
||
}),
|
||
"darwin",
|
||
).createClient(await signatureTarget());
|
||
|
||
const execution = await client.execute("inspect_signature", {});
|
||
|
||
expect(execution).toMatchObject({
|
||
ok: false,
|
||
error: { _tag: "ProviderAdapterError" },
|
||
});
|
||
});
|
||
|
||
it.each([
|
||
"extra diagnostic",
|
||
"foreign executable",
|
||
"slice diagnostic",
|
||
"later slice diagnostic",
|
||
])("rejects an unsigned response with %s", async (kind) => {
|
||
const signed = (await fixture("codesign.txt")).replace(
|
||
/^Format=.*$/mu,
|
||
"Format=Mach-O universal (x86_64 arm64)",
|
||
);
|
||
const replay = signatureRunner({
|
||
signed,
|
||
unsigned: "code object is not signed at all",
|
||
entitlements: await fixture("entitlements.xml"),
|
||
unsignedHeader: true,
|
||
});
|
||
const runner: NativeCommandRunner = {
|
||
async run(tool, args, options) {
|
||
const result = await replay.run(tool, args, options);
|
||
if (!result.ok) return result;
|
||
if (kind === "later slice diagnostic") {
|
||
if (!args.includes("arm64")) return result;
|
||
const stderr = "later architecture probe failed\n";
|
||
return ok({
|
||
...result.value,
|
||
exitCode: 1,
|
||
stderr,
|
||
stderrBytes: Buffer.byteLength(stderr),
|
||
});
|
||
}
|
||
if (result.value.exitCode === 0) return result;
|
||
const isSlice = args.includes("-a");
|
||
if ((kind === "slice diagnostic") !== isSlice) return result;
|
||
const stderr =
|
||
kind === "foreign executable"
|
||
? result.value.stderr.replace(
|
||
/^Executable=.*\n/u,
|
||
"Executable=/foreign/program\n",
|
||
)
|
||
: `${result.value.stderr}additional codesign failure\n`;
|
||
return ok({
|
||
...result.value,
|
||
stderr,
|
||
stderrBytes: Buffer.byteLength(stderr),
|
||
});
|
||
},
|
||
};
|
||
const result = await new NativeMacOSProvider({}, runner, "darwin")
|
||
.createClient(await signatureTarget())
|
||
.execute("inspect_signature", {});
|
||
expect(result).toMatchObject({
|
||
ok: false,
|
||
error: { _tag: "ProviderAdapterError" },
|
||
});
|
||
if (kind.endsWith("slice diagnostic") && !result.ok) {
|
||
const partial = inspectSignatureSchema.parse(
|
||
result.error.partialObservation,
|
||
);
|
||
expect(partial).toMatchObject({
|
||
signed: true,
|
||
identifier: "com.example.fixture",
|
||
entitlements: { "com.apple.security.app-sandbox": true },
|
||
designated_requirement: null,
|
||
limitations: expect.arrayContaining([
|
||
expect.stringContaining("Partial signature coverage"),
|
||
expect.stringContaining("did not complete"),
|
||
]),
|
||
});
|
||
expect(partial.provenance).toHaveLength(
|
||
kind === "later slice diagnostic" ? 5 : 4,
|
||
);
|
||
expect(partial.provenance[0]?.command).toContain("--verbose=4");
|
||
expect(partial.provenance[1]?.command).toContain("-r-");
|
||
expect(partial.provenance[2]?.command).toContain("--entitlements");
|
||
if (kind === "later slice diagnostic") {
|
||
expect(partial.limitations).toEqual(
|
||
expect.arrayContaining([
|
||
"Unsigned Mach-O slices: x86_64.",
|
||
"The aggregate designated requirement is unavailable because Mach-O slices have mixed signing states.",
|
||
]),
|
||
);
|
||
expect(partial.provenance[4]).toMatchObject({
|
||
command: expect.arrayContaining(["-a", "arm64"]),
|
||
exit: { code: 1, signal: null },
|
||
});
|
||
}
|
||
expect(partial.provenance[3]).toMatchObject({
|
||
command: expect.arrayContaining(["-a", "x86_64"]),
|
||
exit: { code: 1, signal: null },
|
||
});
|
||
}
|
||
});
|
||
|
||
it("retains native inventory facts from captured Apple tool output", async () => {
|
||
const outputs: Record<string, string> = {};
|
||
for (const tool of ["lipo", "otool", "nm", "dwarfdump", "vtool"])
|
||
outputs[tool] = await fixture(`dyld-inventory/${tool}.txt`);
|
||
outputs.otool = withOtoolMachHeader(outputs.otool ?? "");
|
||
outputs["dyld_info:-imports"] = await fixture("dyld-inventory/imports.txt");
|
||
const exportsOutput = await fixture("dyld-inventory/exports.txt");
|
||
outputs["dyld_info:-exports"] = exportsOutput;
|
||
const client = new NativeMacOSProvider(
|
||
{},
|
||
new FixtureRunner(outputs),
|
||
"darwin",
|
||
).createClient(machoTarget("/private/fixture"));
|
||
const execution = await client.execute("inspect_macho", {});
|
||
if (!execution.ok) throw execution.error;
|
||
expect(execution.value.result).toMatchObject({
|
||
imports: {
|
||
items: [
|
||
{ name: "_puts", address: null, source: "libSystem" },
|
||
{
|
||
name: "_fixture_weak",
|
||
address: null,
|
||
source: "<weak-def-coalesce>",
|
||
},
|
||
],
|
||
},
|
||
exports: {
|
||
items: expect.arrayContaining([
|
||
expect.objectContaining({
|
||
name: "_fixture_export",
|
||
address: "0x100000468",
|
||
}),
|
||
expect.objectContaining({
|
||
name: "_fixture_weak",
|
||
address: "0x100000460",
|
||
weak: true,
|
||
}),
|
||
expect.objectContaining({
|
||
name: "_fixture_absolute",
|
||
address: "0x42",
|
||
}),
|
||
]),
|
||
},
|
||
});
|
||
expect(
|
||
parseDyldSymbols(await fixture("dyld-imports.txt"), "imports"),
|
||
).toEqual([
|
||
{
|
||
name: "__Block_copy",
|
||
address: null,
|
||
weak: null,
|
||
reexport: null,
|
||
source: "libSystem",
|
||
},
|
||
{
|
||
name: "_objc_bp_assist_cfg_np",
|
||
address: null,
|
||
weak: true,
|
||
reexport: null,
|
||
source: "libSystem",
|
||
},
|
||
]);
|
||
expect(
|
||
parseDyldSymbols(
|
||
await fixture("dyld-inventory/library-reexports.txt"),
|
||
"exports",
|
||
),
|
||
).toEqual([
|
||
{
|
||
name: "__ZNKSt10bad_typeid4whatEv",
|
||
address: null,
|
||
weak: null,
|
||
reexport: true,
|
||
source: "libc++abi",
|
||
},
|
||
]);
|
||
expect(parseDyldSymbols(exportsOutput, "exports")).toContainEqual({
|
||
name: "_fixture_export",
|
||
address: null,
|
||
weak: null,
|
||
reexport: false,
|
||
source: null,
|
||
});
|
||
});
|
||
|
||
it("reports a renamed re-export once under its exported name", async () => {
|
||
const client = new NativeMacOSProvider(
|
||
{},
|
||
new FixtureRunner({
|
||
"dyld_info:-exports": await fixture(
|
||
"dyld-inventory/renamed-reexports.txt",
|
||
),
|
||
nm: "_foo_renamed\n_own\n",
|
||
}),
|
||
"darwin",
|
||
).createClient(machoTarget("/private/fixture"));
|
||
const execution = await client.execute("inspect_macho", {});
|
||
if (!execution.ok) throw execution.error;
|
||
expect(execution.value.result).toMatchObject({
|
||
exports: {
|
||
items: [
|
||
{
|
||
name: "_foo_renamed",
|
||
address: null,
|
||
weak: null,
|
||
reexport: true,
|
||
source: "libfoo",
|
||
},
|
||
{ name: "_own", reexport: false, source: null },
|
||
],
|
||
limitations: [
|
||
"Merged nm/dyld_info results may be incomplete for stripped binaries.",
|
||
"Renamed re-exports are listed under their exported name; the source library's symbol name is not reported.",
|
||
],
|
||
},
|
||
});
|
||
});
|
||
|
||
it("splits re-export rows at the source library", () => {
|
||
// dyld_info -exports rows for synthetic dylibs: a source symbol named
|
||
// `operator()`, and a library whose install name contains ` from `.
|
||
expect(
|
||
parseDyldSymbols(
|
||
[
|
||
"[re-export] _call_renamed (operator() from libfoo)",
|
||
"[re-export] _g (from lib from foo)",
|
||
"[re-export] _nested (from lib (from foo))",
|
||
].join("\n"),
|
||
"exports",
|
||
null,
|
||
new Set(["_call_renamed", "_g", "_nested"]),
|
||
),
|
||
).toEqual([
|
||
{
|
||
name: "_call_renamed",
|
||
address: null,
|
||
weak: null,
|
||
reexport: true,
|
||
source: "libfoo",
|
||
sourceSymbol: "operator()",
|
||
},
|
||
{
|
||
name: "_g",
|
||
address: null,
|
||
weak: null,
|
||
reexport: true,
|
||
source: "lib from foo",
|
||
},
|
||
{
|
||
name: "_nested",
|
||
address: null,
|
||
weak: null,
|
||
reexport: true,
|
||
source: "lib (from foo)",
|
||
},
|
||
]);
|
||
});
|
||
|
||
it("retains symbol names containing spaces in dyld inventory rows", () => {
|
||
expect(
|
||
parseDyldSymbols(" 0x0000 _entry with space (from fixture)", "imports"),
|
||
).toEqual([
|
||
{
|
||
name: "_entry with space",
|
||
address: null,
|
||
weak: null,
|
||
reexport: null,
|
||
source: "fixture",
|
||
},
|
||
]);
|
||
expect(
|
||
parseDyldSymbols(
|
||
" offset symbol\n 0x00000348 _entry with space",
|
||
"exports",
|
||
"0x0",
|
||
),
|
||
).toEqual([
|
||
{
|
||
name: "_entry with space",
|
||
address: "0x348",
|
||
weak: null,
|
||
reexport: false,
|
||
source: null,
|
||
},
|
||
]);
|
||
expect(
|
||
parseDyldSymbols("[re-export] _entry with space (from fixture)", "exports"),
|
||
).toEqual([
|
||
{
|
||
name: "_entry with space",
|
||
address: null,
|
||
weak: null,
|
||
reexport: true,
|
||
source: "fixture",
|
||
},
|
||
]);
|
||
expect(
|
||
parseDyldSymbols("0x123 _weak with space [weak-def]", "exports"),
|
||
).toEqual([
|
||
{
|
||
name: "_weak with space",
|
||
address: "0x123",
|
||
weak: true,
|
||
reexport: false,
|
||
source: null,
|
||
},
|
||
]);
|
||
});
|
||
|
||
it("preserves numeric-looking segment and section identifiers from otool", () => {
|
||
const parsed = parseOtoolLoadCommands(
|
||
withOtoolMachHeader(`Load command 0
|
||
cmd LC_SEGMENT_64
|
||
cmdsize 152
|
||
segname 0001
|
||
vmaddr 0x0000000000004000
|
||
vmsize 0x0000000000004000
|
||
fileoff 16384
|
||
filesize 16384
|
||
maxprot 0x00000003
|
||
initprot 0x00000003
|
||
nsects 1
|
||
flags 0x4
|
||
Section
|
||
sectname 0002
|
||
segname 0001
|
||
addr 0x0000000000004000
|
||
size 0x0000000000000008
|
||
offset 16384
|
||
align 2^0 (1)
|
||
reloff 0
|
||
nreloc 0
|
||
flags 0x00000000
|
||
reserved1 0
|
||
reserved2 0
|
||
`),
|
||
);
|
||
expect(parsed.segments[0]).toMatchObject({
|
||
name: "0001",
|
||
file_offset: 16384,
|
||
sections: [{ segment: "0001", name: "0002", size: 8 }],
|
||
});
|
||
});
|
||
|
||
const signatureRunner = (outputs: {
|
||
readonly signed: string;
|
||
readonly unsigned: string;
|
||
readonly entitlements: string;
|
||
readonly unsignedHeader?: boolean;
|
||
}): NativeCommandRunner => ({
|
||
async run(
|
||
tool: string,
|
||
arguments_: readonly string[],
|
||
options: NativeCommandOptions,
|
||
) {
|
||
const architectureIndex = arguments_.indexOf("-a");
|
||
const architecture =
|
||
architectureIndex < 0 ? null : arguments_[architectureIndex + 1];
|
||
const selectedResponse = arguments_.includes("-r-")
|
||
? { output: outputs.unsigned, exitCode: 1 }
|
||
: arguments_.includes("--entitlements")
|
||
? { output: outputs.entitlements, exitCode: 0 }
|
||
: architecture === "x86_64"
|
||
? { output: outputs.unsigned, exitCode: 1 }
|
||
: { output: outputs.signed, exitCode: 0 };
|
||
const response =
|
||
selectedResponse.exitCode !== 0 &&
|
||
selectedResponse.output.includes("code object is not signed at all")
|
||
? {
|
||
...selectedResponse,
|
||
output: `${outputs.unsignedHeader ? `Executable=${await realpath(arguments_.at(-1)!)}\n` : ""}${arguments_.at(-1)}: code object is not signed at all\n`,
|
||
}
|
||
: selectedResponse;
|
||
const capture: NativeCommandCapture = {
|
||
tool,
|
||
executable: `/usr/bin/${tool}`,
|
||
executableSha256: "a".repeat(64),
|
||
toolVersion: null,
|
||
versionReason: "fixture",
|
||
arguments: [...arguments_],
|
||
// Entitlements XML is printed to stdout; diagnostics to stderr.
|
||
...(response.exitCode === 0 && arguments_.includes("--entitlements")
|
||
? {
|
||
stdout: response.output,
|
||
stderr: "",
|
||
stdoutBytes: Buffer.byteLength(response.output),
|
||
stderrBytes: 0,
|
||
}
|
||
: {
|
||
stdout: "",
|
||
stderr: response.output,
|
||
stdoutBytes: 0,
|
||
stderrBytes: Buffer.byteLength(response.output),
|
||
}),
|
||
exitCode: response.exitCode,
|
||
signal: null,
|
||
};
|
||
return Promise.resolve(
|
||
response.exitCode === 0 || options.acceptNonZero === true
|
||
? ok(capture)
|
||
: err(
|
||
new NativeCommandFailure(tool, "nonzero-exit", response.exitCode),
|
||
),
|
||
);
|
||
},
|
||
});
|
||
|
||
it.each([
|
||
"/tmp/Load command 12/plugin.dylib",
|
||
"@rpath/plugin-Load command 34.dylib",
|
||
])(
|
||
"keeps load-command-looking text inside native install names: %s",
|
||
(installName) => {
|
||
const parsed = parseOtoolLoadCommands(
|
||
withOtoolMachHeader(`Load command 0
|
||
cmd LC_ID_DYLIB
|
||
cmdsize 80
|
||
name ${installName} (offset 24)
|
||
time stamp 2 Thu Jan 1 00:00:02 1970
|
||
current version 3.2.1
|
||
compatibility version 1.0.0
|
||
Load command 1
|
||
cmd LC_UUID
|
||
cmdsize 24
|
||
uuid 01234567-89AB-CDEF-0123-456789ABCDEF
|
||
`),
|
||
);
|
||
expect(parsed.commands.map(({ index, kind }) => ({ index, kind }))).toEqual(
|
||
[
|
||
{ index: 0, kind: "LC_ID_DYLIB" },
|
||
{ index: 1, kind: "LC_UUID" },
|
||
],
|
||
);
|
||
expect(parsed.commands[0]?.fields.name).toBe(`${installName} (offset 24)`);
|
||
expect(parsed.dependencies).toEqual([]);
|
||
expect(parsed.uuid).toBe("01234567-89AB-CDEF-0123-456789ABCDEF");
|
||
},
|
||
);
|
||
it.each(["weak-def", "absolute", "literal suffix"])(
|
||
"preserves an nm-proven export name ending in [%s]",
|
||
(suffix) => {
|
||
const name = `symbol [${suffix}]`;
|
||
expect(
|
||
parseDyldSymbols(
|
||
`offset symbol\n0x120 ${name}`,
|
||
"exports",
|
||
"0x1000",
|
||
new Set([name]),
|
||
),
|
||
).toEqual([
|
||
{ name, address: "0x1120", weak: null, reexport: false, source: null },
|
||
]);
|
||
},
|
||
);
|
||
|
||
it("keeps true weak annotations when nm establishes the unannotated name", () => {
|
||
expect(
|
||
parseDyldSymbols(
|
||
"0x120 _weak [weak-def]",
|
||
"exports",
|
||
null,
|
||
new Set(["_weak"]),
|
||
),
|
||
).toEqual([
|
||
{
|
||
name: "_weak",
|
||
address: "0x120",
|
||
weak: true,
|
||
reexport: false,
|
||
source: null,
|
||
},
|
||
]);
|
||
});
|
||
|
||
it("leaves an ambiguous short and literal name to the caller's nm inventory", () => {
|
||
expect(
|
||
parseDyldSymbols(
|
||
"0x120 symbol [weak-def]",
|
||
"exports",
|
||
null,
|
||
new Set(["symbol", "symbol [weak-def]"]),
|
||
),
|
||
).toEqual([]);
|
||
});
|
||
|
||
it("retains unmatched re-export rows when the known export inventory is incomplete", () => {
|
||
expect(
|
||
parseDyldSymbols(
|
||
"[re-export] _g (from libfoo)",
|
||
"exports",
|
||
null,
|
||
new Set(["_own"]),
|
||
),
|
||
).toEqual([
|
||
{
|
||
name: "_g",
|
||
address: null,
|
||
weak: null,
|
||
reexport: true,
|
||
source: "libfoo",
|
||
},
|
||
]);
|
||
});
|
||
|
||
it("retains annotation parsing without nm name evidence", () => {
|
||
expect(parseDyldSymbols("0x120 _weak [weak-def]", "exports")).toEqual([
|
||
{
|
||
name: "_weak",
|
||
address: "0x120",
|
||
weak: true,
|
||
reexport: false,
|
||
source: null,
|
||
},
|
||
]);
|
||
});
|
||
|
||
/** Replay fixtures, answering nm by whether its dyld-info pass is disabled. */
|
||
const symbolTableRunner = (
|
||
outputs: Readonly<Record<string, string>>,
|
||
nm: { readonly symbolTable: string; readonly withDyldInfo: string },
|
||
) => {
|
||
const nmArguments: (readonly string[])[] = [];
|
||
const replay = (output: string) =>
|
||
new FixtureRunner({ ...outputs, nm: output });
|
||
const runner: NativeCommandRunner = {
|
||
run: (tool, arguments_) => {
|
||
if (tool !== "nm") return replay("").run(tool, arguments_);
|
||
nmArguments.push(arguments_);
|
||
return replay(
|
||
arguments_.includes("--no-dyldinfo") ? nm.symbolTable : nm.withDyldInfo,
|
||
).run(tool, arguments_);
|
||
},
|
||
};
|
||
return { runner, nmArguments };
|
||
};
|
||
|
||
/** nm arguments in provenance, which records the artifact operand as a placeholder. */
|
||
const nmCommands = (result: unknown) =>
|
||
(result as { provenance: { tool: string; command: string[] }[] }).provenance
|
||
.filter(({ tool }) => tool === "nm")
|
||
.map(({ command }) => [...command.slice(1, -1), "/private/fixture"]);
|
||
|
||
it("reads exports from the symbol table without llvm-nm's dyld-info pass", async () => {
|
||
const names = await fixture("dyld-inventory/nm.txt");
|
||
const { runner, nmArguments } = symbolTableRunner(
|
||
{ "dyld_info:-exports": await fixture("dyld-inventory/exports.txt") },
|
||
{ symbolTable: names, withDyldInfo: names },
|
||
);
|
||
const execution = await new NativeMacOSProvider({}, runner, "darwin")
|
||
.createClient(machoTarget("/private/fixture"))
|
||
.execute("inspect_macho", {});
|
||
if (!execution.ok) throw execution.error;
|
||
expect(nmArguments).toEqual([
|
||
["-gjU", "--no-dyldinfo", "-arch", "arm64", "/private/fixture"],
|
||
]);
|
||
expect(nmCommands(execution.value.result)).toEqual(nmArguments);
|
||
});
|
||
|
||
it("reruns nm with its dyld-info pass when a stripped symbol table omits trie exports", async () => {
|
||
const trieNames = await fixture("dyld-inventory/stripped-swift-nm.txt");
|
||
const { runner, nmArguments } = symbolTableRunner(
|
||
{
|
||
"dyld_info:-exports": await fixture(
|
||
"dyld-inventory/stripped-swift-exports.txt",
|
||
),
|
||
},
|
||
{ symbolTable: "", withDyldInfo: trieNames },
|
||
);
|
||
const execution = await new NativeMacOSProvider({}, runner, "darwin")
|
||
.createClient(machoTarget("/private/fixture"))
|
||
.execute("inspect_macho", {});
|
||
if (!execution.ok) throw execution.error;
|
||
const rerun = ["-gjU", "-arch", "arm64", "/private/fixture"];
|
||
expect(nmArguments).toEqual([
|
||
["-gjU", "--no-dyldinfo", "-arch", "arm64", "/private/fixture"],
|
||
rerun,
|
||
]);
|
||
// Provenance names the nm run whose output the exports came from.
|
||
expect(nmCommands(execution.value.result)).toEqual([rerun]);
|
||
const exported = (
|
||
execution.value.result as { exports: { items: { name: string }[] } }
|
||
).exports.items.map(({ name }) => name);
|
||
expect(exported).toEqual(trieNames.trim().split("\n").sort());
|
||
});
|
||
|
||
it("reruns nm with its dyld-info pass when a dyld_info export row is unreadable", async () => {
|
||
const names = await fixture("dyld-inventory/nm.txt");
|
||
const exportsOutput = await fixture("dyld-inventory/exports.txt");
|
||
const { runner, nmArguments } = symbolTableRunner(
|
||
{
|
||
"dyld_info:-exports": `${exportsOutput.trimEnd()}\n <unrecognized export row>\n`,
|
||
},
|
||
{ symbolTable: names, withDyldInfo: names },
|
||
);
|
||
const execution = await new NativeMacOSProvider({}, runner, "darwin")
|
||
.createClient(machoTarget("/private/fixture"))
|
||
.execute("inspect_macho", {});
|
||
if (!execution.ok) throw execution.error;
|
||
expect(nmArguments.map((arguments_) => arguments_.slice(0, 2))).toEqual([
|
||
["-gjU", "--no-dyldinfo"],
|
||
["-gjU", "-arch"],
|
||
]);
|
||
});
|