fix(native): keep lipo path echoes out of FAT records (#2605)
* fix(native): keep lipo path echoes out of FAT records Parse FAT records only after separating the exact echoed operand path. Pass the same lipo operand at all three consumers and cover multiline paths through parser, slice-reader and compiled CLI regressions. * fix(native): use parsed lipo framing for container offsets * test(native): use explicit FAT fixtures for CLI regressions
This commit is contained in:
parent
11268fe241
commit
59d33a2733
|
|
@ -8,7 +8,7 @@ import {
|
|||
type NativeCommandRunner,
|
||||
} from "../native/CommandRunner.js";
|
||||
import {
|
||||
parseLipoArchitectures,
|
||||
parseLipoOutput,
|
||||
type LipoArchitecture,
|
||||
} from "../native/parsers/lipo.js";
|
||||
import type { MachoSlice } from "../domain/apple/dylibResolution.js";
|
||||
|
|
@ -114,7 +114,7 @@ export class MachOSliceArtifactReader implements ArtifactReader {
|
|||
};
|
||||
await this.#verifySource(source);
|
||||
const fileSize = source.initial.size;
|
||||
const architectures = parseLipoArchitectures(captured.value.stdout);
|
||||
const { architectures } = parseLipoOutput(captured.value.stdout, this.path);
|
||||
const structural = await readStructuralSlices(source.handle, fileSize);
|
||||
await this.#verifySource(source);
|
||||
if (structural.status === "malformed")
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ import {
|
|||
} from "./SignatureTargetBinding.js";
|
||||
import { parseCodeSignature } from "./parsers/codesign.js";
|
||||
import { parseDemangledSymbols } from "./parsers/demangle.js";
|
||||
import { parseLipoArchitectures } from "./parsers/lipo.js";
|
||||
import { parseLipoOutput } from "./parsers/lipo.js";
|
||||
import {
|
||||
parsePlistJson,
|
||||
parsePlistXml,
|
||||
|
|
@ -507,7 +507,10 @@ class NativeMacOSClient implements AnalysisClient {
|
|||
);
|
||||
if (!capture.ok) return capture;
|
||||
const provenance = [invocation(capture.value, this.target.path)];
|
||||
const architectures = parseLipoArchitectures(capture.value.stdout);
|
||||
const { architectures } = parseLipoOutput(
|
||||
capture.value.stdout,
|
||||
this.target.path,
|
||||
);
|
||||
const result = listArchitecturesSchema.parse({
|
||||
architectures: {
|
||||
items: architectures,
|
||||
|
|
|
|||
|
|
@ -14,10 +14,7 @@ import {
|
|||
selectMachoArchitectureName,
|
||||
} from "./MachoArchitectureSelection.js";
|
||||
import { parseDyldSymbols } from "./parsers/dyldInfo.js";
|
||||
import {
|
||||
parseLipoArchitectures,
|
||||
type LipoArchitecture,
|
||||
} from "./parsers/lipo.js";
|
||||
import { parseLipoOutput, type LipoArchitecture } from "./parsers/lipo.js";
|
||||
import { withoutEchoedPathHeader } from "./parsers/echoedPath.js";
|
||||
import { parseOtoolLoadCommands } from "./parsers/otool.js";
|
||||
import { reconcileMachoUuid } from "./parsers/machoUuid.js";
|
||||
|
|
@ -74,7 +71,10 @@ export const inspectNativeMacho = async (
|
|||
}
|
||||
const lipo = captures.find(({ tool }) => tool === "lipo");
|
||||
if (lipo === undefined) throw new TypeError("Missing lipo capture");
|
||||
const architectures = parseLipoArchitectures(lipo.stdout);
|
||||
const { format, architectures } = parseLipoOutput(
|
||||
lipo.stdout,
|
||||
context.target.path,
|
||||
);
|
||||
const selection = selectArchitecture(context.target, architectures);
|
||||
if (selection.status === "absent")
|
||||
return err(
|
||||
|
|
@ -145,7 +145,7 @@ export const inspectNativeMacho = async (
|
|||
const containerOffset =
|
||||
selectedArchitectureName === null
|
||||
? undefined
|
||||
: /^Non-fat file:/mu.test(lipo.stdout)
|
||||
: format === "thin"
|
||||
? 0
|
||||
: (architectures.find(({ name }) => name === selectedArchitectureName)
|
||||
?.file_offset ?? undefined);
|
||||
|
|
@ -185,7 +185,7 @@ const normalizeMacho = (
|
|||
toInvocation: (capture: NativeCommandCapture) => NativeCommandInvocation,
|
||||
additionalLimitations: readonly string[],
|
||||
load: ReturnType<typeof parseOtoolLoadCommands>,
|
||||
architectures: ReturnType<typeof parseLipoArchitectures>,
|
||||
architectures: readonly LipoArchitecture[],
|
||||
artifactPath: string,
|
||||
selectedArchitecture: {
|
||||
readonly name: string | null;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,28 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { parseLipoArchitectures } from "./lipo.js";
|
||||
import { parseLipoOutput } from "./lipo.js";
|
||||
|
||||
const fatRecords = `fat_magic 0xcafebabe
|
||||
nfat_arch 2
|
||||
architecture arm64
|
||||
cputype CPU_TYPE_ARM64
|
||||
cpusubtype CPU_SUBTYPE_ARM64_ALL
|
||||
capabilities 0x0
|
||||
offset 4096
|
||||
size 32
|
||||
align 2^12 (4096)
|
||||
architecture x86_64
|
||||
cputype CPU_TYPE_X86_64
|
||||
cpusubtype CPU_SUBTYPE_X86_64_ALL
|
||||
capabilities 0x0
|
||||
offset 8192
|
||||
size 32
|
||||
align 2^12 (4096)
|
||||
`;
|
||||
|
||||
describe("lipo architecture facts", () => {
|
||||
it("normalizes symbolic CPU fields and arm64e capability bits", () => {
|
||||
const architectures = parseLipoArchitectures(`Fat header in: /bin/ls
|
||||
const { format, architectures } = parseLipoOutput(`Fat header in: /bin/ls
|
||||
fat_magic 0xcafebabe
|
||||
nfat_arch 2
|
||||
architecture x86_64
|
||||
|
|
@ -22,6 +40,7 @@ architecture arm64e
|
|||
size 89088
|
||||
align 2^14 (16384)
|
||||
`);
|
||||
expect(format).toBe("fat");
|
||||
expect(architectures).toEqual([
|
||||
expect.objectContaining({
|
||||
name: "x86_64",
|
||||
|
|
@ -50,9 +69,9 @@ architecture arm64e
|
|||
|
||||
it("keeps unknown symbolic CPU values as source text without a code", () => {
|
||||
expect(
|
||||
parseLipoArchitectures(
|
||||
parseLipoOutput(
|
||||
"architecture future\n cputype CPU_TYPE_FUTURE\n cpusubtype CPU_SUBTYPE_FUTURE\n offset 4096\n size 64\n align 2^12\n",
|
||||
),
|
||||
).architectures,
|
||||
).toMatchObject([
|
||||
{
|
||||
cpu_type: "CPU_TYPE_FUTURE",
|
||||
|
|
@ -70,18 +89,18 @@ architecture arm64e
|
|||
["0x0", 3],
|
||||
])("applies printed x86_64 capabilities %s", (capabilities, code) => {
|
||||
expect(
|
||||
parseLipoArchitectures(
|
||||
parseLipoOutput(
|
||||
`architecture x86_64\n cpusubtype CPU_SUBTYPE_X86_64_ALL\n capabilities ${capabilities}\n`,
|
||||
)[0]?.cpu_subtype_code,
|
||||
).architectures[0]?.cpu_subtype_code,
|
||||
).toBe(code);
|
||||
});
|
||||
|
||||
it("leaves the subtype code unknown when lipo prints no capabilities", () => {
|
||||
// macOS 26 lipo prints this blank line for LIB64 and plain x86_64 slices.
|
||||
expect(
|
||||
parseLipoArchitectures(
|
||||
parseLipoOutput(
|
||||
"architecture x86_64\n cputype CPU_TYPE_X86_64\n cpusubtype CPU_SUBTYPE_X86_ALL\n capabilities \n offset 16384\n",
|
||||
),
|
||||
).architectures,
|
||||
).toMatchObject([
|
||||
{
|
||||
cpu_subtype: "CPU_SUBTYPE_X86_ALL",
|
||||
|
|
@ -99,9 +118,9 @@ architecture arm64e
|
|||
"retains an arm64e base subtype with capabilities '$capabilities'",
|
||||
({ capabilities, code }) => {
|
||||
expect(
|
||||
parseLipoArchitectures(
|
||||
parseLipoOutput(
|
||||
`architecture arm64e.v1\n cputype CPU_TYPE_ARM64\n cpusubtype CPU_SUBTYPE_ARM64E\n capabilities ${capabilities}\n`,
|
||||
),
|
||||
).architectures,
|
||||
).toMatchObject([
|
||||
{
|
||||
cpu_type_code: 0x0100000c,
|
||||
|
|
@ -112,3 +131,79 @@ architecture arm64e
|
|||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("lipo FAT operand framing", () => {
|
||||
it.each([
|
||||
"/fixture/\narchitecture forged\n offset 123\n size 64",
|
||||
"/fixture/\nfat_magic 0xcafebabe\nnfat_arch 1\narchitecture forged",
|
||||
"/fixture/\nNon-fat file: fake is architecture: forged",
|
||||
"/fixture/first\nsecond\nthird",
|
||||
"/fixture/first\r\narchitecture forged\r\n offset 123",
|
||||
"/fixture/[app](arm64).*+$^?\\name",
|
||||
])(
|
||||
"keeps the complete echoed operand out of FAT slice records: %j",
|
||||
(path) => {
|
||||
const output = `Fat header in: ${path}\n${fatRecords}`;
|
||||
|
||||
expect(parseLipoOutput(output, path)).toEqual(
|
||||
parseLipoOutput(fatRecords),
|
||||
);
|
||||
expect(
|
||||
parseLipoOutput(output, path).architectures.map(({ name }) => name),
|
||||
).toEqual(["arm64", "x86_64"]);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps a CRLF pathname separate from a CRLF record terminator", () => {
|
||||
const path = "/fixture/first\r\narchitecture forged\r\n size 64";
|
||||
const output = `Fat header in: ${path}\r\n${fatRecords.replaceAll("\n", "\r\n")}`;
|
||||
|
||||
expect(parseLipoOutput(output, path)).toEqual(parseLipoOutput(fatRecords));
|
||||
});
|
||||
|
||||
it("preserves ordinary FAT metadata with and without an operand", () => {
|
||||
const path = "/fixture/app";
|
||||
const output = `Fat header in: ${path}\n${fatRecords}`;
|
||||
|
||||
expect(parseLipoOutput(output, path)).toEqual(parseLipoOutput(output));
|
||||
});
|
||||
|
||||
it("does not require a FAT path header in captured fixtures", () => {
|
||||
expect(parseLipoOutput(fatRecords, "/fixture/app")).toEqual(
|
||||
parseLipoOutput(fatRecords),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not strip a header for a different operand that shares a prefix", () => {
|
||||
const path = "/fixture/app-extra\narchitecture forged";
|
||||
const output = `Fat header in: ${path}\n${fatRecords}`;
|
||||
|
||||
expect(parseLipoOutput(output, "/fixture/app")).toEqual(
|
||||
parseLipoOutput(output),
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves thin-file parsing when the operand contains FAT-like lines", () => {
|
||||
const path = "/fixture/\nFat header in: fake\narchitecture forged";
|
||||
const output = `Non-fat file: ${path} is architecture: arm64\n`;
|
||||
|
||||
expect(parseLipoOutput(output, path)).toEqual({
|
||||
format: "thin",
|
||||
architectures: [
|
||||
{
|
||||
name: "arm64",
|
||||
cpu_type: null,
|
||||
cpu_subtype: null,
|
||||
cpu_type_code: null,
|
||||
cpu_subtype_code: null,
|
||||
cpu_subtype_base: null,
|
||||
capabilities: null,
|
||||
file_offset: null,
|
||||
size: null,
|
||||
alignment: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(parseLipoOutput(output, path)).toEqual(parseLipoOutput(output));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,8 +16,26 @@ const architectureSchema = z.object({
|
|||
|
||||
export type LipoArchitecture = z.infer<typeof architectureSchema>;
|
||||
|
||||
/** Parse `lipo -detailed_info` into deterministic slice metadata. */
|
||||
export const parseLipoArchitectures = (output: string): LipoArchitecture[] => {
|
||||
interface LipoOutput {
|
||||
readonly format: "thin" | "fat";
|
||||
readonly architectures: LipoArchitecture[];
|
||||
}
|
||||
|
||||
/** Parse `lipo -detailed_info` framing and deterministic slice metadata. */
|
||||
export const parseLipoOutput = (
|
||||
output: string,
|
||||
operandPath?: string,
|
||||
): LipoOutput => {
|
||||
// The echoed FAT operand is one value, even when its pathname contains lines.
|
||||
if (operandPath !== undefined) {
|
||||
const header = `Fat header in: ${operandPath}`;
|
||||
if (output.startsWith(header)) {
|
||||
if (output.startsWith("\r\n", header.length))
|
||||
output = output.slice(header.length + 2);
|
||||
else if (output.startsWith("\n", header.length))
|
||||
output = output.slice(header.length + 1);
|
||||
}
|
||||
}
|
||||
// lipo echoes a thin file's pathname verbatim, including embedded newlines.
|
||||
// Match its final architecture field before splitting records into lines.
|
||||
const thinArchitecture =
|
||||
|
|
@ -25,20 +43,23 @@ export const parseLipoArchitectures = (output: string): LipoArchitecture[] => {
|
|||
output,
|
||||
)?.[1];
|
||||
if (thinArchitecture !== undefined)
|
||||
return [
|
||||
architectureSchema.parse({
|
||||
name: thinArchitecture,
|
||||
cpu_type: null,
|
||||
cpu_subtype: null,
|
||||
cpu_type_code: null,
|
||||
cpu_subtype_code: null,
|
||||
cpu_subtype_base: null,
|
||||
capabilities: null,
|
||||
file_offset: null,
|
||||
size: null,
|
||||
alignment: null,
|
||||
}),
|
||||
];
|
||||
return {
|
||||
format: "thin",
|
||||
architectures: [
|
||||
architectureSchema.parse({
|
||||
name: thinArchitecture,
|
||||
cpu_type: null,
|
||||
cpu_subtype: null,
|
||||
cpu_type_code: null,
|
||||
cpu_subtype_code: null,
|
||||
cpu_subtype_base: null,
|
||||
capabilities: null,
|
||||
file_offset: null,
|
||||
size: null,
|
||||
alignment: null,
|
||||
}),
|
||||
],
|
||||
};
|
||||
const architectures: LipoArchitecture[] = [];
|
||||
let current: Record<string, string> | undefined;
|
||||
const flush = (): void => {
|
||||
|
|
@ -90,7 +111,7 @@ export const parseLipoArchitectures = (output: string): LipoArchitecture[] => {
|
|||
flush();
|
||||
if (architectures.length === 0)
|
||||
throw new TypeError("lipo output contained no architectures");
|
||||
return architectures;
|
||||
return { format: "fat", architectures };
|
||||
};
|
||||
|
||||
const integer = (value: string | undefined): number | null => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import { copyFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
import { describe, expect } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
inspectMachoSchema,
|
||||
listArchitecturesSchema,
|
||||
} from "../../../src/domain/native/nativeInspection.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
|
|
@ -34,27 +38,68 @@ const resultSchema = z.object({
|
|||
describe.skipIf(process.platform !== "darwin")(
|
||||
"native Mach-O CLI with echoed path text",
|
||||
() => {
|
||||
cliTest(
|
||||
"does not read load commands or symbols from the file name",
|
||||
async ({ cli }) => {
|
||||
const directory = await createTestTempDirectory("rea-macho-echo-");
|
||||
const inspect = async (name: string) => {
|
||||
const path = join(directory, name);
|
||||
await copyFile("/usr/bin/true", path);
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-macho", path, "--json"],
|
||||
environment,
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
const { provenance: _provenance, ...facts } = resultSchema.parse(
|
||||
result.json,
|
||||
).normalized_result;
|
||||
return facts;
|
||||
};
|
||||
const forged = await inspect(FORGED_NAME);
|
||||
expect(forged).toEqual(await inspect("tool"));
|
||||
expect(JSON.stringify(forged)).not.toMatch(/forged|4242/u);
|
||||
},
|
||||
);
|
||||
const lipoName = "tool\narchitecture forged\n offset 123\n size 32";
|
||||
for (const [facts, command, filename] of [
|
||||
["load commands or symbols", "inspect-macho", FORGED_NAME],
|
||||
["FAT architecture records", "inspect-macho", lipoName],
|
||||
["FAT architecture inventory", "list-architectures", lipoName],
|
||||
["thin-file framing", "inspect-macho", "tool\nNon-fat file: forged"],
|
||||
] as const) {
|
||||
cliTest(
|
||||
`does not read ${facts} from the file name`,
|
||||
async ({ cli, processes }) => {
|
||||
const directory = await createTestTempDirectory(
|
||||
"rea-native-cli-echo-",
|
||||
);
|
||||
const fixture = join(directory, "fat-fixture");
|
||||
const compiled = await processes.run(
|
||||
"/usr/bin/xcrun",
|
||||
[
|
||||
"clang",
|
||||
"-arch",
|
||||
"arm64",
|
||||
"-arch",
|
||||
"x86_64",
|
||||
resolve("tests/conformance/c/fixture.c"),
|
||||
"-o",
|
||||
fixture,
|
||||
],
|
||||
{ timeoutMs: 30_000 },
|
||||
);
|
||||
expect(compiled.exitCode, compiled.stderr).toBe(0);
|
||||
const inspect = async (name: string) => {
|
||||
const path = join(directory, name);
|
||||
await copyFile(fixture, path);
|
||||
const result = await cli.run({
|
||||
arguments: [command, path, "--json"],
|
||||
environment,
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
const { provenance: _provenance, ...facts } = resultSchema.parse(
|
||||
result.json,
|
||||
).normalized_result;
|
||||
return facts;
|
||||
};
|
||||
const ordinary = await inspect("tool");
|
||||
// Prove this comparison exercised FAT framing, independent of host binaries.
|
||||
const architectures =
|
||||
listArchitecturesSchema.shape.architectures.parse(
|
||||
ordinary.architectures,
|
||||
);
|
||||
expect(architectures.total).toBeGreaterThan(1);
|
||||
if (command === "inspect-macho") {
|
||||
const selected =
|
||||
inspectMachoSchema.shape.selected_architecture.parse(
|
||||
ordinary.selected_architecture,
|
||||
);
|
||||
expect(selected.container_file_offset).toBeGreaterThan(0);
|
||||
}
|
||||
const forged = await inspect(filename);
|
||||
expect(forged).toEqual(ordinary);
|
||||
expect(JSON.stringify(forged)).not.toMatch(/forged|4242/u);
|
||||
},
|
||||
60_000,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { readFile } from "node:fs/promises";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it, onTestFinished } from "vitest";
|
||||
|
||||
import { MachOSliceArtifactReader } from "../../../../src/artifacts/MachOSliceArtifactReader.js";
|
||||
import { XcrunCommandRunner } from "../../../../src/native/CommandRunner.js";
|
||||
import { DarwinProcessOwnershipInspectionError } from "../../../../src/process/DarwinProcessRunTokenReader.js";
|
||||
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
|
||||
|
||||
/**
|
||||
* Read a 32-bit FAT table directly. The host's /bin/ls changes between macOS
|
||||
|
|
@ -42,6 +44,56 @@ describe("Mach-O slice reader with system lipo", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it.skipIf(process.platform !== "darwin")(
|
||||
"keeps FAT slice records separate from the echoed source pathname",
|
||||
async () => {
|
||||
const directory = await createTestTempDirectory("rea-lipo-fat-path-");
|
||||
const path = join(
|
||||
directory,
|
||||
"source\narchitecture forged\n offset 123\n size 32",
|
||||
);
|
||||
const bytes = Buffer.alloc(8224);
|
||||
bytes.writeUInt32BE(0xca_fe_ba_be, 0);
|
||||
bytes.writeUInt32BE(2, 4);
|
||||
const slices = [
|
||||
{ cpu: 0x0100_000c, subtype: 0, offset: 4096 },
|
||||
{ cpu: 0x0100_0007, subtype: 3, offset: 8192 },
|
||||
];
|
||||
slices.forEach(({ cpu, subtype, offset }, index) => {
|
||||
const entry = 8 + index * 20;
|
||||
bytes.writeUInt32BE(cpu, entry);
|
||||
bytes.writeUInt32BE(subtype, entry + 4);
|
||||
bytes.writeUInt32BE(offset, entry + 8);
|
||||
bytes.writeUInt32BE(32, entry + 12);
|
||||
bytes.writeUInt32BE(12, entry + 16);
|
||||
bytes.writeUInt32LE(0xfeed_facf, offset);
|
||||
bytes.writeUInt32LE(cpu, offset + 4);
|
||||
bytes.writeUInt32LE(subtype, offset + 8);
|
||||
bytes.writeUInt32LE(1, offset + 12);
|
||||
});
|
||||
await writeFile(path, bytes);
|
||||
const reader = new MachOSliceArtifactReader(path, {});
|
||||
try {
|
||||
const entries = [];
|
||||
for await (const entry of reader.entries()) entries.push(entry);
|
||||
expect(
|
||||
entries.map(({ byteOffset, declaredSize }) => [
|
||||
byteOffset,
|
||||
declaredSize,
|
||||
]),
|
||||
).toEqual([
|
||||
[4096, 32],
|
||||
[8192, 32],
|
||||
]);
|
||||
expect(entries.every(({ path }) => !path.includes("forged"))).toBe(
|
||||
true,
|
||||
);
|
||||
} finally {
|
||||
await reader.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform !== "darwin")(
|
||||
"checks real /bin/ls slice ranges against its FAT table",
|
||||
async () => {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
type NativeCommandRunner,
|
||||
} from "../../../../src/native/CommandRunner.js";
|
||||
import { err, ok } from "../../../../src/domain/result.js";
|
||||
import { parseLipoArchitectures } from "../../../../src/native/parsers/lipo.js";
|
||||
import { parseLipoOutput } from "../../../../src/native/parsers/lipo.js";
|
||||
import { parseCodeSignature } from "../../../../src/native/parsers/codesign.js";
|
||||
import { prepareProcessOwnershipInspection } from "../../../../src/process/ProcessOwnershipObservation.js";
|
||||
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
|
||||
|
|
@ -960,15 +960,17 @@ describe("native macOS provider failures and parsing", () => {
|
|||
});
|
||||
|
||||
it("parses thin and universal lipo fixtures", async () => {
|
||||
expect(
|
||||
parseLipoArchitectures(await fixture("lipo-thin.txt")),
|
||||
).toMatchObject([{ name: "arm64" }]);
|
||||
expect(parseLipoArchitectures(await fixture("lipo-fat.txt"))).toMatchObject(
|
||||
[
|
||||
expect(parseLipoOutput(await fixture("lipo-thin.txt"))).toMatchObject({
|
||||
format: "thin",
|
||||
architectures: [{ name: "arm64" }],
|
||||
});
|
||||
expect(parseLipoOutput(await fixture("lipo-fat.txt"))).toMatchObject({
|
||||
format: "fat",
|
||||
architectures: [
|
||||
{ name: "x86_64", file_offset: 16384, size: 4096 },
|
||||
{ name: "arm64", file_offset: 32768, size: 8192 },
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects trailing text in lipo numeric fields instead of truncating", () => {
|
||||
|
|
@ -980,7 +982,7 @@ describe("native macOS provider failures and parsing", () => {
|
|||
" size 0x10",
|
||||
" align 2^14 (16384)",
|
||||
].join("\n");
|
||||
expect(parseLipoArchitectures(output)).toMatchObject([
|
||||
expect(parseLipoOutput(output).architectures).toMatchObject([
|
||||
{ name: "arm64", file_offset: null, size: null, alignment: 16384 },
|
||||
]);
|
||||
});
|
||||
|
|
@ -993,10 +995,11 @@ describe("native macOS provider failures and parsing", () => {
|
|||
" align 2^14 (16384)",
|
||||
].join("\n");
|
||||
expect(
|
||||
parseLipoArchitectures(output.replace("(16384)", "(8192)"))[0]?.alignment,
|
||||
parseLipoOutput(output.replace("(16384)", "(8192)")).architectures[0]
|
||||
?.alignment,
|
||||
).toBeNull();
|
||||
expect(
|
||||
parseLipoArchitectures(output.replace("2^14 (16384)", "2^14"))[0]
|
||||
parseLipoOutput(output.replace("2^14 (16384)", "2^14")).architectures[0]
|
||||
?.alignment,
|
||||
).toBe(16384);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -157,6 +157,50 @@ it.each([
|
|||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["ordinary", "/owned/universal", "\n"],
|
||||
["LF Non-fat text", "/owned/\nNon-fat file: forged", "\n"],
|
||||
["CRLF Non-fat text", "/owned/\r\nNon-fat file: forged", "\r\n"],
|
||||
])(
|
||||
"projects universal segment evidence into the selected slice's container offsets with %s path framing",
|
||||
async (_description, path, lineEnding) => {
|
||||
const records = (await nativeFixture("lipo-fat.txt")).replace(
|
||||
/^Fat header in:[^\r\n]*\r?\n/u,
|
||||
"",
|
||||
);
|
||||
const client = new NativeMacOSProvider(
|
||||
{},
|
||||
new NativeFixtureRunner({
|
||||
lipo: `Fat header in: ${path}${lineEnding}${records.replaceAll("\n", lineEnding)}`,
|
||||
}),
|
||||
"darwin",
|
||||
).createClient(nativeMachoTarget(path));
|
||||
const result = await client.execute("inspect_macho", {});
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.result).toMatchObject({
|
||||
selected_architecture: { container_file_offset: 32768 },
|
||||
});
|
||||
expect(result.value.locations).toContainEqual({
|
||||
kind: "file-offset-range",
|
||||
start: 32768,
|
||||
end: 40960,
|
||||
});
|
||||
expect(result.value.locations).toContainEqual({
|
||||
kind: "file-offset",
|
||||
offset: 36864,
|
||||
});
|
||||
expect(result.value.locations).not.toContainEqual({
|
||||
kind: "file-offset-range",
|
||||
start: 0,
|
||||
end: 8192,
|
||||
});
|
||||
// Normalized tool fields keep their slice-relative meaning.
|
||||
const normalized = inspectMachoSchema.parse(result.value.result);
|
||||
expect(normalized.segments.items[0]?.file_offset).toBe(0);
|
||||
expect(normalized.entrypoints.items[0]?.file_offset).toBe(4096);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ capture: "calculator.txt", selected: "arm64e" },
|
||||
{ capture: "shortcut-droplet.txt", selected: "arm64" },
|
||||
|
|
@ -187,34 +231,6 @@ it.each([
|
|||
},
|
||||
);
|
||||
|
||||
it("projects universal segment evidence into the selected slice's container offsets", async () => {
|
||||
const client = new NativeMacOSProvider(
|
||||
{},
|
||||
new NativeFixtureRunner(),
|
||||
"darwin",
|
||||
).createClient(nativeMachoTarget("/owned/universal"));
|
||||
const result = await client.execute("inspect_macho", {});
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.locations).toContainEqual({
|
||||
kind: "file-offset-range",
|
||||
start: 32768,
|
||||
end: 40960,
|
||||
});
|
||||
expect(result.value.locations).toContainEqual({
|
||||
kind: "file-offset",
|
||||
offset: 36864,
|
||||
});
|
||||
expect(result.value.locations).not.toContainEqual({
|
||||
kind: "file-offset-range",
|
||||
start: 0,
|
||||
end: 8192,
|
||||
});
|
||||
// Normalized tool fields keep their slice-relative meaning.
|
||||
const normalized = inspectMachoSchema.parse(result.value.result);
|
||||
expect(normalized.segments.items[0]?.file_offset).toBe(0);
|
||||
expect(normalized.entrypoints.items[0]?.file_offset).toBe(4096);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
description: "thin",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { expect, it } from "vitest";
|
|||
|
||||
import { parseOtoolLoadCommands } from "../../../../src/native/parsers/otool.js";
|
||||
import { parsePlistJson } from "../../../../src/native/parsers/plist.js";
|
||||
import { parseLipoArchitectures } from "../../../../src/native/parsers/lipo.js";
|
||||
import { parseLipoOutput } from "../../../../src/native/parsers/lipo.js";
|
||||
import { nativeFixture } from "../../../fixtures/nativeCommands.js";
|
||||
|
||||
const toCrlf = (value: string): string => value.replaceAll("\n", "\r\n");
|
||||
|
|
@ -15,7 +15,10 @@ it.each([
|
|||
"retains the reported thin architecture when lipo echoes filename %j",
|
||||
(filename) => {
|
||||
const output = `input file /owned/${filename} is not a fat file\nNon-fat file: /owned/${filename} is architecture: arm64\n`;
|
||||
expect(parseLipoArchitectures(output)).toMatchObject([{ name: "arm64" }]);
|
||||
expect(parseLipoOutput(output)).toMatchObject({
|
||||
format: "thin",
|
||||
architectures: [{ name: "arm64" }],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -38,10 +41,13 @@ it("does not mistake fat slice output for a thin Non-fat file record", () => {
|
|||
" align 2^12 (4096)",
|
||||
"",
|
||||
].join("\n");
|
||||
expect(parseLipoArchitectures(output)).toMatchObject([
|
||||
{ name: "arm64", file_offset: 16384 },
|
||||
{ name: "x86_64", file_offset: 16793600 },
|
||||
]);
|
||||
expect(parseLipoOutput(output)).toMatchObject({
|
||||
format: "fat",
|
||||
architectures: [
|
||||
{ name: "arm64", file_offset: 16384 },
|
||||
{ name: "x86_64", file_offset: 16793600 },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps section blocks separate under CRLF captures", async () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue