* fix(native): derive Mach-O word size and byte order from the selected slice inspect_macho read endian and word_size from `file -b` for the whole file. macOS `file` never prints a Mach-O byte order, so endian was null for every target. For a universal binary, word_size matched "64-bit" anywhere in the summary instead of describing the selected slice, and it was null for a thin 32-bit slice. Both now come from the selected slice's otool header: the magic gives the word size and the CPU family gives the byte order (x86 and ARM little, PowerPC big, other families unknown). The file capture is no longer needed by inspect_macho, so it is no longer run there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(native): select a 32-bit slice beside a 64-bit slice through inspect_macho Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
144 lines
4.6 KiB
TypeScript
144 lines
4.6 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { readFile } from "node:fs/promises";
|
|
import type { BinaryTarget } from "../../src/domain/binaryTargetTypes.js";
|
|
import { ok } from "../../src/domain/result.js";
|
|
import type {
|
|
NativeCommandCapture,
|
|
NativeCommandRunner,
|
|
} from "../../src/native/CommandRunner.js";
|
|
|
|
/** Replay captured native command outputs through the production runner seam. */
|
|
export class NativeFixtureRunner implements NativeCommandRunner {
|
|
constructor(
|
|
private readonly overrides: Readonly<Record<string, string>> = {},
|
|
private readonly exitCode = 0,
|
|
) {}
|
|
|
|
async run(tool: string, arguments_: readonly string[]) {
|
|
const output =
|
|
this.overrides[`${tool}:${arguments_[0] ?? ""}`] ??
|
|
this.overrides[tool] ??
|
|
(await outputFor(tool, arguments_));
|
|
return ok({
|
|
...capture(tool, arguments_, output),
|
|
exitCode: this.exitCode,
|
|
});
|
|
}
|
|
}
|
|
|
|
const outputFor = async (
|
|
tool: string,
|
|
arguments_: readonly string[],
|
|
): Promise<string> => {
|
|
if (tool === "lipo") return nativeFixture("lipo-fat.txt");
|
|
if (tool === "otool") return nativeFixture("otool-load.txt");
|
|
if (tool === "nm") return "_main\n_$s4Test3fooyyF\n";
|
|
if (tool === "dyld_info")
|
|
return nativeFixture(
|
|
arguments_[0] === "-imports" ? "dyld-imports.txt" : "dyld-exports.txt",
|
|
);
|
|
if (tool === "dwarfdump")
|
|
return `UUID: 01234567-89AB-CDEF-0123-456789ABCDEF (arm64) ${arguments_.at(-1)}\n`;
|
|
if (tool === "file")
|
|
return arguments_.at(-1)?.endsWith(".plist") === true
|
|
? "XML 1.0 document text\n"
|
|
: "Mach-O 64-bit executable arm64\n";
|
|
if (tool === "vtool") return "Load command 3 LC_BUILD_VERSION\n";
|
|
if (tool === "swift-demangle") return nativeFixture("demangle.txt");
|
|
if (tool === "plutil") return nativeFixture("plist.json");
|
|
if (tool === "codesign") {
|
|
if (arguments_.includes("--entitlements"))
|
|
return nativeFixture("entitlements.xml");
|
|
if (arguments_.includes("-r-"))
|
|
return "designated => identifier com.example.fixture\n";
|
|
return nativeFixture("codesign.txt");
|
|
}
|
|
throw new Error(`Unexpected fixture tool ${tool}`);
|
|
};
|
|
|
|
/**
|
|
* codesign prints display diagnostics to stderr and requirements or
|
|
* entitlements to stdout.
|
|
*/
|
|
const fixtureStreams = (
|
|
tool: string,
|
|
arguments_: readonly string[],
|
|
output: string,
|
|
) => {
|
|
const stderr =
|
|
tool === "codesign" &&
|
|
!arguments_.includes("-r-") &&
|
|
!arguments_.includes("--entitlements");
|
|
return {
|
|
stdout: stderr ? "" : output,
|
|
stderr: stderr ? output : "",
|
|
stdoutBytes: Buffer.byteLength(stderr ? "" : output),
|
|
stderrBytes: Buffer.byteLength(stderr ? output : ""),
|
|
};
|
|
};
|
|
|
|
const capture = (
|
|
tool: string,
|
|
arguments_: readonly string[],
|
|
output: string,
|
|
): NativeCommandCapture => ({
|
|
tool,
|
|
executable: `/usr/bin/${tool}`,
|
|
executableSha256: "a".repeat(64),
|
|
toolVersion: null,
|
|
versionReason: "fixture",
|
|
arguments: [...arguments_],
|
|
...fixtureStreams(tool, arguments_, output),
|
|
exitCode: 0,
|
|
signal: null,
|
|
});
|
|
|
|
/** Build the Mach-O target identity used by native boundary fixtures. */
|
|
export const nativeMachoTarget = (
|
|
path: string,
|
|
sourcePath?: string,
|
|
): BinaryTarget & {
|
|
readonly kind: "executable";
|
|
readonly format: "mach-o";
|
|
} => ({
|
|
path,
|
|
...(sourcePath === undefined ? {} : { sourcePath }),
|
|
sha256: "0".repeat(64),
|
|
kind: "executable",
|
|
format: "mach-o",
|
|
architecture: "arm64",
|
|
availableArchitectures: ["x86_64", "arm64"],
|
|
});
|
|
|
|
/** Register the actual bytes of a filesystem-backed native test target. */
|
|
export const nativeMachoTargetForFile = async (
|
|
path: string,
|
|
sourcePath?: string,
|
|
): Promise<ReturnType<typeof nativeMachoTarget>> => ({
|
|
...nativeMachoTarget(path, sourcePath),
|
|
sha256: createHash("sha256")
|
|
.update(await readFile(path))
|
|
.digest("hex"),
|
|
});
|
|
|
|
/** Load one captured native tool output. */
|
|
export const nativeFixture = (name: string): Promise<string> =>
|
|
readFile(new URL(`./native-macos/${name}`, import.meta.url), "utf8");
|
|
|
|
/** Add consistent Mach command-count metadata to a command-only test capture. */
|
|
export const withOtoolMachHeader = (output: string): string => {
|
|
const count = [...output.matchAll(/^Load command (\d+)$/gmu)].length;
|
|
const sizes = [...output.matchAll(/^\s*cmdsize\s+(\d+)\s*$/gmu)].map(
|
|
([, size]) => Number(size),
|
|
);
|
|
if (sizes.length !== count)
|
|
throw new TypeError("command-only fixture must provide every cmdsize");
|
|
const bytes = sizes.reduce((sum, size) => sum + size, 0);
|
|
return [
|
|
"Mach header",
|
|
" magic cputype cpusubtype caps filetype ncmds sizeofcmds flags",
|
|
` 0xfeedfacf 16777228 0 0x00 2 ${count} ${bytes} 0x00000000`,
|
|
output,
|
|
].join("\n");
|
|
};
|