fix(targets): cancel resolution before provider admission

This commit is contained in:
morluto 2026-10-10 00:16:04 +08:00
parent 44c92f88c5
commit f3c4a332f7
9 changed files with 244 additions and 77 deletions

View File

@ -51,11 +51,10 @@ export async function verifyLegacyGhidraReferenceSnapshot(
legacyParameters,
);
assert.notEqual(legacyProfile.digest, observed.analysis_profile.digest);
const parsed = await parseBinaryTarget(
target.path,
process.cwd(),
process.arch,
);
const parsed = await parseBinaryTarget(target.path, {
cwd: process.cwd(),
hostArchitecture: process.arch,
});
if (!parsed.ok) throw parsed.error;
const nativeTarget = parsed.value;
const result = structuredClone(observed.normalized_result);

View File

@ -10,6 +10,7 @@ import {
import { extname, isAbsolute, resolve } from "node:path";
import { BinaryTargetError } from "../domain/configurationErrors.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { err, ok, type Result } from "../domain/result.js";
import {
@ -32,6 +33,15 @@ import {
type ExecutableMetadata,
} from "../domain/binaryTarget.js";
/** Options for resolving a local binary target and its stable identity. */
export interface BinaryTargetResolverOptions {
readonly cwd?: string;
readonly hostArchitecture?: NodeJS.Architecture;
readonly targetKind?: BinaryTarget["kind"];
readonly formatHint?: ExecutableFormatHint;
readonly signal?: AbortSignal;
}
/**
* Resolve and classify a readable local target before a provider is selected.
* FAT Mach-O inputs select only a host-compatible architecture so setup remains
@ -39,24 +49,32 @@ import {
*/
export const parseBinaryTarget = async (
input: string,
cwd = process.cwd(),
hostArchitecture: NodeJS.Architecture = process.arch,
targetKind?: BinaryTarget["kind"],
formatHint?: ExecutableFormatHint,
): Promise<Result<BinaryTarget, BinaryTargetError>> => {
options: BinaryTargetResolverOptions = {},
): Promise<
Result<BinaryTarget, BinaryTargetError | AnalysisCancelledError>
> => {
const cwd = options.cwd ?? process.cwd();
const hostArchitecture = options.hostArchitecture ?? process.arch;
const { targetKind, formatHint, signal } = options;
const candidate = isAbsolute(input) ? input : resolve(cwd, input);
try {
throwIfTargetResolutionCancelled(signal);
await access(candidate, constants.R_OK);
throwIfTargetResolutionCancelled(signal);
const canonical = await realpath(candidate);
throwIfTargetResolutionCancelled(signal);
const resolved = await resolveAppBundle(canonical);
throwIfTargetResolutionCancelled(signal);
if (!resolved.ok) return err(resolved.error);
const { executable: path, infoPlist } = resolved.value;
const bundle =
infoPlist === undefined ? {} : { bundleInfoPlist: infoPlist };
const handle = await open(path, "r");
try {
throwIfTargetResolutionCancelled(signal);
if (!(await handle.stat()).isFile())
return err(new BinaryTargetError(path, "target is not a regular file"));
throwIfTargetResolutionCancelled(signal);
if (formatHint === "dos-com") {
if (targetKind !== undefined && targetKind !== "executable")
return err(
@ -70,7 +88,7 @@ export const parseBinaryTarget = async (
return ok({
path,
sourcePath: canonical,
sha256: await sha256Handle(handle),
sha256: await sha256Handle(handle, signal),
kind: "executable",
format: "dos-com",
architecture: "x86",
@ -84,28 +102,32 @@ export const parseBinaryTarget = async (
return ok({
path,
sourcePath: process.platform === "win32" ? candidate : canonical,
sha256: await sha256Handle(handle),
sha256: await sha256Handle(handle, signal),
kind: "database",
format: "analysis-database",
});
const artifactFormat = await detectArtifactFormat(path, handle);
const artifactFormat = await detectArtifactFormat(path, handle, signal);
if (artifactFormat !== undefined) {
const identity = {
path,
sourcePath: process.platform === "win32" ? candidate : canonical,
sha256: await sha256Handle(handle),
sha256: await sha256Handle(handle, signal),
};
return isArchiveFormat(artifactFormat)
? ok({ ...identity, kind: "archive", format: artifactFormat })
: ok({ ...identity, kind: "artifact", format: artifactFormat });
}
const detected = await readExecutableMetadata(handle, hostArchitecture);
const detected = await readExecutableMetadata(
handle,
hostArchitecture,
signal,
);
if (!detected.ok) return err(new BinaryTargetError(path, detected.error));
return ok({
path,
sourcePath: process.platform === "win32" ? candidate : canonical,
...bundle,
sha256: await sha256Handle(handle),
sha256: await sha256Handle(handle, signal),
kind: "executable",
...detected.value,
});
@ -113,6 +135,7 @@ export const parseBinaryTarget = async (
await handle.close();
}
} catch (cause: unknown) {
if (cause instanceof AnalysisCancelledError) return err(cause);
if (!(cause instanceof Error)) throw cause;
const code = filesystemErrorCode(cause);
if (code === undefined) throw cause;
@ -146,9 +169,15 @@ const targetFailureReason = (code: string, detail: string): string => {
}
};
const throwIfTargetResolutionCancelled = (signal?: AbortSignal): void => {
if (signal?.aborted === true)
throw new AnalysisCancelledError("resolve_binary_target");
};
const detectArtifactFormat = async (
path: string,
handle: FileHandle,
signal?: AbortSignal,
): Promise<
| Exclude<
BinaryTarget["format"],
@ -156,9 +185,11 @@ const detectArtifactFormat = async (
>
| undefined
> => {
throwIfTargetResolutionCancelled(signal);
const lower = path.toLowerCase();
const magic = Buffer.alloc(8);
const observed = await handle.read(magic, 0, magic.length, 0);
throwIfTargetResolutionCancelled(signal);
if (hasZipSignature(magic.subarray(0, observed.bytesRead))) {
return zipPackageFormatForPath(lower) ?? "zip";
}
@ -171,10 +202,13 @@ const detectArtifactFormat = async (
)
return "pkg";
if (lower.endsWith(".dmg")) {
throwIfTargetResolutionCancelled(signal);
const size = (await handle.stat()).size;
throwIfTargetResolutionCancelled(signal);
if (size >= 512) {
const trailer = Buffer.alloc(4);
const read = await handle.read(trailer, 0, trailer.length, size - 512);
throwIfTargetResolutionCancelled(signal);
if (read.bytesRead === 4 && trailer.toString("ascii") === "koly")
return "dmg";
}
@ -199,12 +233,17 @@ const isArchiveFormat = (
): format is Extract<BinaryTarget, { kind: "archive" }>["format"] =>
["zip", "ipa", "apk", "msix", "appx", "asar", "dmg", "pkg"].includes(format);
const sha256Handle = async (handle: FileHandle): Promise<string> => {
const sha256Handle = async (
handle: FileHandle,
signal?: AbortSignal,
): Promise<string> => {
const hash = createHash("sha256");
const chunk = Buffer.allocUnsafe(64 * 1024);
let position = 0;
while (true) {
throwIfTargetResolutionCancelled(signal);
const observed = await handle.read(chunk, 0, chunk.length, position);
throwIfTargetResolutionCancelled(signal);
if (observed.bytesRead === 0) break;
hash.update(chunk.subarray(0, observed.bytesRead));
position += observed.bytesRead;
@ -233,19 +272,23 @@ const resolveAppBundle = async (
const readExecutableMetadata = async (
handle: FileHandle,
hostArchitecture: NodeJS.Architecture,
signal?: AbortSignal,
): Promise<Result<ExecutableMetadata, string>> => {
throwIfTargetResolutionCancelled(signal);
const prefix = Buffer.alloc(4096);
const prefixRead = await handle.read(prefix, 0, prefix.length, 0);
throwIfTargetResolutionCancelled(signal);
const bytes = prefix.subarray(0, prefixRead.bytesRead);
// Header commitments such as Mach-O load commands and FAT slices are
// checked against the whole file, not the probed prefix.
const fileSize = (await handle.stat()).size;
throwIfTargetResolutionCancelled(signal);
if (bytes.length >= 2 && bytes[0] === 0x4d && bytes[1] === 0x5a) {
const offset = mzWindowsHeaderOffset(bytes);
if (offset !== null) {
if (offset < 64)
return err("invalid Windows new-header offset in MZ image");
return readPeMetadata(handle, offset);
return readPeMetadata(handle, offset, signal);
}
// MZ relocation records may extend past the initial probe; their format
// bounds are at most 65535 records inside a 65535-paragraph header.
@ -261,6 +304,7 @@ const readExecutableMetadata = async (
) {
const table = Buffer.alloc(tableEnd);
const observed = await handle.read(table, 0, table.length, 0);
throwIfTargetResolutionCancelled(signal);
return parseExecutableHeader(
table.subarray(0, observed.bytesRead),
hostArchitecture,
@ -278,6 +322,7 @@ const readExecutableMetadata = async (
if (count <= 128 && required > bytes.length) {
const header = Buffer.alloc(required);
const headerRead = await handle.read(header, 0, header.length, 0);
throwIfTargetResolutionCancelled(signal);
return parseExecutableHeader(
header.subarray(0, headerRead.bytesRead),
hostArchitecture,
@ -292,7 +337,9 @@ const readExecutableMetadata = async (
const readPeMetadata = async (
handle: FileHandle,
offset: number,
signal?: AbortSignal,
): Promise<Result<ExecutableMetadata, string>> => {
throwIfTargetResolutionCancelled(signal);
const fileHeader = Buffer.alloc(24);
const fileHeaderRead = await handle.read(
fileHeader,
@ -300,6 +347,7 @@ const readPeMetadata = async (
fileHeader.length,
offset,
);
throwIfTargetResolutionCancelled(signal);
if (fileHeaderRead.bytesRead !== fileHeader.length)
return err("invalid or truncated PE header");
const signature = fileHeader.toString("ascii", 0, 2);
@ -309,6 +357,7 @@ const readPeMetadata = async (
if (optionalHeaderSize > 4096) return err("invalid PE optional header size");
const record = Buffer.alloc(24 + optionalHeaderSize);
const recordRead = await handle.read(record, 0, record.length, offset);
throwIfTargetResolutionCancelled(signal);
return recordRead.bytesRead === record.length
? parsePeRecord(record)
: err("invalid or truncated PE header");

View File

@ -201,6 +201,7 @@ const runAnalysis = async (
const prepared = await prepareSnapshot({
path,
snapshotPath,
signal,
...(options.formatHint === undefined
? {}
: { formatHint: options.formatHint }),
@ -434,6 +435,7 @@ const prepareSnapshot = async (options: {
readonly path: string;
readonly formatHint?: ExecutableFormatHint;
readonly snapshotPath: string | undefined;
readonly signal: AbortSignal;
}): Promise<
Result<
{ readonly snapshot?: AnalysisSnapshot; readonly target?: BinaryTarget },
@ -445,13 +447,12 @@ const prepareSnapshot = async (options: {
return ok({});
const loaded = await readAnalysisSnapshot(snapshotPath);
if (!loaded.ok) return loaded;
const target = await parseBinaryTarget(
path,
process.cwd(),
process.arch,
undefined,
options.formatHint,
);
const target = await parseBinaryTarget(path, {
signal: options.signal,
...(options.formatHint === undefined
? {}
: { formatHint: options.formatHint }),
});
if (!target.ok) return target;
if (!snapshotMatchesTarget(loaded.value.target, target.value))
return err(

View File

@ -4,6 +4,7 @@ import { homeDirectoryFromEnvironment } from "../config/homeDirectory.js";
import { join } from "node:path";
import { analysisErrorRemediationAction } from "../domain/analysisErrorPresentation.js";
import { BinaryTargetError } from "../domain/configurationErrors.js";
import { parseBinaryTarget } from "./BinaryTargetResolver.js";
import { execFileOutput } from "../process/ExecFileOutput.js";
import type { JsonValue } from "../domain/jsonValue.js";
@ -324,10 +325,18 @@ export const systemDoctorHost = (
readMacosVersion(hostExecFileOutput, commandEnvironment),
linuxDistribution: readLinuxDistribution,
async validTarget(path) {
return (await parseBinaryTarget(path, process.cwd(), architecture)).ok;
return (
await parseBinaryTarget(path, {
cwd: process.cwd(),
hostArchitecture: architecture,
})
).ok;
},
async inspectTarget(path) {
const result = await parseBinaryTarget(path, process.cwd(), architecture);
const result = await parseBinaryTarget(path, {
cwd: process.cwd(),
hostArchitecture: architecture,
});
if (result.ok)
return {
name: "target",
@ -336,19 +345,21 @@ export const systemDoctorHost = (
detail: path,
};
const error = result.error;
const targetError =
error instanceof BinaryTargetError ? error : undefined;
return {
name: "target",
ok: false,
classification:
error.constraint === "directory_requires_file"
targetError?.constraint === "directory_requires_file"
? "unsupported_target"
: "config_drift",
detail: path,
details: {
reason: error.reason,
...(error.constraint === undefined
reason: targetError?.reason ?? error.message,
...(targetError?.constraint === undefined
? {}
: { constraint: error.constraint }),
: { constraint: targetError.constraint }),
},
remediation: analysisErrorRemediationAction(error),
};

View File

@ -29,7 +29,9 @@ export class AndroidAnalysisService {
const request = androidRequestSchema.safeParse({ operation, input });
if (!request.success)
return err(new AnalysisInputError(operation, { cause: request.error }));
const target = await parseBinaryTarget(request.data.input.path);
const target = await parseBinaryTarget(request.data.input.path, {
...(options?.signal === undefined ? {} : { signal: options.signal }),
});
if (!target.ok) return target;
const executed = await this.provider.execute(
target.value,

View File

@ -65,13 +65,15 @@ export const resolveSessionOpen = async (
input: ResolveSessionOpenInput,
): Promise<Result<ResolvedSessionOpen, AnalysisError>> => {
const { path, options } = input;
const parsed = await parseBinaryTarget(
path,
process.cwd(),
process.arch,
options.targetKind,
options.formatHint,
);
const parsed = await parseBinaryTarget(path, {
...(options.targetKind === undefined
? {}
: { targetKind: options.targetKind }),
...(options.formatHint === undefined
? {}
: { formatHint: options.formatHint }),
...(options.signal === undefined ? {} : { signal: options.signal }),
});
if (!parsed.ok) return parsed;
return resolveSessionTarget({ ...input, target: parsed.value });
};

View File

@ -226,12 +226,16 @@ describe("binary target I/O: filesystem and app bundle target resolution", () =>
const directory = await createTestTempDirectory("rea-target-");
await writeFile(join(directory, "sample.hop"), "database");
await writeFile(join(directory, "text"), "hello");
const database = await parseBinaryTarget("sample.hop", directory);
const database = await parseBinaryTarget("sample.hop", { cwd: directory });
expect(database.ok && database.value.sha256).toBe(
"3549b0028b75d981cdda2e573e9cb49dedc200185876df299f912b79f69dabd8",
);
expect((await parseBinaryTarget("text", directory)).ok).toBe(false);
expect((await parseBinaryTarget("missing", directory)).ok).toBe(false);
expect((await parseBinaryTarget("text", { cwd: directory })).ok).toBe(
false,
);
expect((await parseBinaryTarget("missing", { cwd: directory })).ok).toBe(
false,
);
});
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
@ -242,7 +246,10 @@ describe("binary target I/O: filesystem and app bundle target resolution", () =>
await writeFile(path, thinMach(0xcffaedfe, 0x0100000c));
await chmod(path, 0);
try {
const result = await parseBinaryTarget(path, directory, "arm64");
const result = await parseBinaryTarget(path, {
cwd: directory,
hostArchitecture: "arm64",
});
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error).toMatchObject({ path, systemCode: "EACCES" });
@ -275,7 +282,10 @@ describe("binary target I/O: filesystem and app bundle target resolution", () =>
join(programs, "Example & Tool"),
thinMach(0xfeedfacf, 0x0100000c),
);
const result = await parseBinaryTarget(app, directory, "arm64");
const result = await parseBinaryTarget(app, {
cwd: directory,
hostArchitecture: "arm64",
});
expect(result.ok && result.value).toMatchObject({
path: await realpath(join(programs, "Example & Tool")),
format: "mach-o",
@ -300,7 +310,14 @@ describe("binary target I/O: filesystem and app bundle target resolution", () =>
await mkdir(join(contents, "MacOS"), { recursive: true });
if (plist !== undefined)
await writeFile(join(contents, "Info.plist"), plist);
expect((await parseBinaryTarget(app, directory, "arm64")).ok).toBe(false);
expect(
(
await parseBinaryTarget(app, {
cwd: directory,
hostArchitecture: "arm64",
})
).ok,
).toBe(false);
});
it.skipIf(process.platform === "win32")(
@ -318,7 +335,14 @@ describe("binary target I/O: filesystem and app bundle target resolution", () =>
"<plist><dict><key>CFBundleExecutable</key><string>Escaping</string></dict></plist>",
);
await symlink(outside, join(programs, "Escaping"));
expect((await parseBinaryTarget(app, directory, "arm64")).ok).toBe(false);
expect(
(
await parseBinaryTarget(app, {
cwd: directory,
hostArchitecture: "arm64",
})
).ok,
).toBe(false);
},
);
});
@ -327,12 +351,11 @@ describe("binary target I/O: explicit kinds and executable headers", () => {
it("honors an explicit database kind without relying on the file suffix", async () => {
const directory = await createTestTempDirectory("rea-target-");
await writeFile(join(directory, "saved-analysis"), "database");
const result = await parseBinaryTarget(
"saved-analysis",
directory,
"arm64",
"database",
);
const result = await parseBinaryTarget("saved-analysis", {
cwd: directory,
hostArchitecture: "arm64",
targetKind: "database",
});
expect(result.ok && result.value).toMatchObject({
kind: "database",
format: "analysis-database",
@ -343,7 +366,10 @@ describe("binary target I/O: explicit kinds and executable headers", () => {
const directory = await createTestTempDirectory("rea-target-");
const path = join(directory, "delayed.exe");
await writeFile(path, pe(0x8664, 8192));
const result = await parseBinaryTarget(path, directory, "x64");
const result = await parseBinaryTarget(path, {
cwd: directory,
hostArchitecture: "x64",
});
expect(result.ok && result.value).toMatchObject({
format: "pe",
architecture: "x86_64",
@ -354,7 +380,10 @@ describe("binary target I/O: explicit kinds and executable headers", () => {
const directory = await createTestTempDirectory("rea-target-");
const path = join(directory, "truncated");
await writeFile(path, thinMach(0xcffaedfe, 0x0100000c).subarray(0, 12));
const result = await parseBinaryTarget(path, directory, "arm64");
const result = await parseBinaryTarget(path, {
cwd: directory,
hostArchitecture: "arm64",
});
expect(result.ok).toBe(false);
expect(result.ok ? undefined : result.error).toMatchObject({
path,
@ -369,7 +398,10 @@ describe("binary target I/O: explicit kinds and executable headers", () => {
const header = thinMach(0xcffaedfe, 0x0100000c);
header.writeUInt32LE(8192, 20);
await writeFile(path, Buffer.concat([header, Buffer.alloc(8192)]));
const result = await parseBinaryTarget(path, directory, "arm64");
const result = await parseBinaryTarget(path, {
cwd: directory,
hostArchitecture: "arm64",
});
expect(result.ok && result.value).toMatchObject({
format: "mach-o",
architecture: "arm64",
@ -391,7 +423,10 @@ describe("app executable filename fidelity", () => {
`<plist><dict><key>CFBundleExecutable</key><string>${name}</string></dict></plist>`,
);
await writeFile(executable, thinMach(0xfeedfacf, 0x0100000c));
const result = await parseBinaryTarget(app, directory, "arm64");
const result = await parseBinaryTarget(app, {
cwd: directory,
hostArchitecture: "arm64",
});
expect(result.ok && result.value).toMatchObject({
path: await realpath(executable),
format: "mach-o",
@ -416,7 +451,10 @@ describe("app executable filename fidelity", () => {
`<plist><dict><key>CFBundleExecutable</key><string>${encoded}</string></dict></plist>`,
);
await writeFile(executable, thinMach(0xfeedfacf, 0x0100000c));
const result = await parseBinaryTarget(app, directory, "arm64");
const result = await parseBinaryTarget(app, {
cwd: directory,
hostArchitecture: "arm64",
});
expect(result.ok && result.value).toMatchObject({
path: await realpath(executable),
format: "mach-o",
@ -449,7 +487,10 @@ describe("app executable filename fidelity", () => {
join(contents, "MacOS", "Inner"),
thinMach(0xfeedfacf, 0x0100000c),
);
const result = await parseBinaryTarget(app, directory, "arm64");
const result = await parseBinaryTarget(app, {
cwd: directory,
hostArchitecture: "arm64",
});
expect(result.ok && result.value).toMatchObject({
path: await realpath(executable),
});
@ -469,7 +510,10 @@ describe("app executable filename fidelity", () => {
`<plist><dict>${entry}<key>CFBundleExecutable</key><string>App</string></dict></plist>`,
);
await writeFile(executable, thinMach(0xfeedfacf, 0x0100000c));
const result = await parseBinaryTarget(app, directory, "arm64");
const result = await parseBinaryTarget(app, {
cwd: directory,
hostArchitecture: "arm64",
});
expect(result.ok && result.value).toMatchObject({
path: await realpath(executable),
});
@ -495,7 +539,10 @@ describe("app executable filename fidelity", () => {
plist,
]);
await writeFile(executable, thinMach(0xfeedfacf, 0x0100000c));
const result = await parseBinaryTarget(app, directory, "arm64");
const result = await parseBinaryTarget(app, {
cwd: directory,
hostArchitecture: "arm64",
});
expect(result.ok && result.value).toMatchObject({
path: await realpath(executable),
format: "mach-o",
@ -515,7 +562,10 @@ describe("iOS-style app bundle targets", () => {
await mkdir(app);
await writeFile(join(app, "Info.plist"), plist("Flat"));
await writeFile(join(app, "Flat"), thinMach(0xfeedfacf, 0x0100000c));
const result = await parseBinaryTarget(app, directory, "arm64");
const result = await parseBinaryTarget(app, {
cwd: directory,
hostArchitecture: "arm64",
});
expect(result.ok && result.value).toMatchObject({
path: await realpath(join(app, "Flat")),
bundleInfoPlist: join(await realpath(app), "Info.plist"),
@ -535,7 +585,10 @@ describe("iOS-style app bundle targets", () => {
thinMach(0xfeedfacf, 0x0100000c),
);
await symlink("bin/real", join(app, "Linked"));
const result = await parseBinaryTarget(app, directory, "arm64");
const result = await parseBinaryTarget(app, {
cwd: directory,
hostArchitecture: "arm64",
});
expect(result.ok && result.value).toMatchObject({
path: await realpath(join(app, "bin", "real")),
bundleInfoPlist: join(await realpath(app), "Info.plist"),
@ -556,7 +609,10 @@ describe("iOS-style app bundle targets", () => {
join(contents, "MacOS", "Linked"),
thinMach(0xfeedfacf, 0x0100000c),
);
const result = await parseBinaryTarget(app, directory, "arm64");
const result = await parseBinaryTarget(app, {
cwd: directory,
hostArchitecture: "arm64",
});
expect(result.ok && result.value).toMatchObject({
path: await realpath(join(contents, "MacOS", "Linked")),
});
@ -572,7 +628,10 @@ describe("iOS-style app bundle targets", () => {
await mkdir(join(wrapper, "Locked.app"), { recursive: true });
await chmod(wrapper, 0o000);
try {
const result = await parseBinaryTarget(app, directory, "arm64");
const result = await parseBinaryTarget(app, {
cwd: directory,
hostArchitecture: "arm64",
});
if (result.ok) throw new Error("Expected a permission denial");
expect(result.error.message).toContain("permission denied");
} finally {
@ -591,14 +650,24 @@ describe("iOS-style app bundle targets", () => {
await writeFile(join(wrapped, "Info.plist"), plist("Phone"));
await writeFile(join(wrapped, "Phone"), thinMach(0xfeedfacf, 0x0100000c));
await symlink("Wrapper/Phone.app", join(app, "WrappedBundle"));
const result = await parseBinaryTarget(app, directory, "arm64");
const result = await parseBinaryTarget(app, {
cwd: directory,
hostArchitecture: "arm64",
});
expect(result.ok && result.value).toMatchObject({
path: await realpath(join(wrapped, "Phone")),
format: "mach-o",
});
await mkdir(join(app, "Wrapper", "Second.app"));
expect((await parseBinaryTarget(app, directory, "arm64")).ok).toBe(false);
expect(
(
await parseBinaryTarget(app, {
cwd: directory,
hostArchitecture: "arm64",
})
).ok,
).toBe(false);
},
);
@ -612,7 +681,10 @@ describe("iOS-style app bundle targets", () => {
await writeFile(outside, thinMach(0xfeedfacf, 0x0100000c));
await writeFile(join(app, "Info.plist"), plist("Escaping"));
await symlink(outside, join(app, "Escaping"));
const result = await parseBinaryTarget(app, directory, "arm64");
const result = await parseBinaryTarget(app, {
cwd: directory,
hostArchitecture: "arm64",
});
if (result.ok) throw new Error("Expected an escaping program file");
expect(result.error.message).toContain("leaves the bundle root");
},

View File

@ -11,13 +11,11 @@ describe("DOS COM admission", () => {
const path = join(directory, "fixture.js");
const bytes = Buffer.from("b83412c3", "hex");
await writeFile(path, bytes);
const target = await parseBinaryTarget(
path,
directory,
"arm64",
undefined,
"dos-com",
);
const target = await parseBinaryTarget(path, {
cwd: directory,
hostArchitecture: "arm64",
formatHint: "dos-com",
});
expect(target).toMatchObject({
ok: true,
value: {
@ -38,7 +36,11 @@ describe("DOS COM admission", () => {
const path = join(directory, "fixture.com");
await writeFile(path, Buffer.alloc(length));
expect(
await parseBinaryTarget(path, directory, "x64", undefined, "dos-com"),
await parseBinaryTarget(path, {
cwd: directory,
hostArchitecture: "x64",
formatHint: "dos-com",
}),
).toMatchObject({ ok: false, error: { _tag: "BinaryTargetError" } });
});
it("rejects contradictory target kind", async () => {
@ -46,7 +48,12 @@ describe("DOS COM admission", () => {
const path = join(directory, "fixture.com");
await writeFile(path, Buffer.from("c3", "hex"));
expect(
await parseBinaryTarget(path, directory, "x64", "database", "dos-com"),
await parseBinaryTarget(path, {
cwd: directory,
hostArchitecture: "x64",
targetKind: "database",
formatHint: "dos-com",
}),
).toMatchObject({ ok: false, error: { _tag: "BinaryTargetError" } });
});
});

View File

@ -1,5 +1,5 @@
import { expect, it as test, vi } from "vitest";
import { access, readFile, writeFile } from "node:fs/promises";
import { access, readFile, truncate, writeFile } from "node:fs/promises";
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
import { AndroidAnalysisService } from "../../../src/application/android/AndroidAnalysisService.js";
import { JadxProvider } from "../../../src/android/JadxProvider.js";
@ -17,6 +17,30 @@ import { writeJadxJarInventory } from "../../fixtures/android/jadxJar.js";
const it = test.skipIf(process.platform === "win32");
it("cancels Android admission while hashing the selected APK", async () => {
const { service, apk, launches, provider } = await setup();
await writeFile(apk, Buffer.from([0x50, 0x4b, 0x03, 0x04]));
await truncate(apk, 1024 * 1024 * 1024);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 100);
try {
const result = await service.execute(
"inspect_android_package",
{ path: apk },
{ signal: controller.signal },
);
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError" },
});
expect(launches).toHaveLength(0);
} finally {
clearTimeout(timer);
await provider.close();
await verifyCleanup(launches);
}
});
it("rejects a copied JAR that differs from its admitted session identity", async () => {
const { jar } = await setup();
const admitted = await hashAndroidFile(jar);