fix(artifacts): bind operations to admitted sources

This commit is contained in:
morluto
2026-10-27 07:03:02 +08:00
parent ace4dbf36e
commit 2dd2ffcd45
20 changed files with 704 additions and 238 deletions
@@ -158,6 +158,7 @@ const isArtifactBundleSubject = (
subject: EvidenceSubjectTarget,
target: BinaryTarget,
): boolean =>
target.bundleInfoPlist !== undefined &&
(operation === "inventory_artifact" ||
operation === "inspect_artifact" ||
operation === "extract_artifact") &&
+8
View File
@@ -311,6 +311,10 @@ class ArtifactClient implements AnalysisClient {
const result = await extractArtifact(
{
inputPath: this.target.sourcePath ?? this.target.path,
expectedRootSha256:
this.target.bundleInfoPlist === undefined
? this.target.sha256
: undefined,
selectedPath: this.target.artifactSelectedPath,
inputFormat: this.target.format,
outputRoot: parsed.output_root,
@@ -506,6 +510,10 @@ class ArtifactClient implements AnalysisClient {
options?: ExecutionOptions,
) {
return inventoryArtifact(this.target.sourcePath ?? this.target.path, {
expectedRootSha256:
this.target.bundleInfoPlist === undefined
? this.target.sha256
: undefined,
selectedPath: this.target.artifactSelectedPath,
resourceScope: this.#resourceScope,
environment: this.environment,
+33
View File
@@ -0,0 +1,33 @@
import { realpath } from "node:fs/promises";
import { ArtifactReaderFailure } from "./ArtifactReader.js";
/** Verify selection spelling independently of canonical descriptor ownership. */
export const assertArtifactSelectedSource = async (
canonicalPath: string,
selectedPath: string | undefined,
): Promise<void> => {
if (selectedPath === undefined) return;
const changed = (cause?: unknown) =>
new ArtifactReaderFailure(
"integrity",
`Selected artifact no longer resolves to its admitted source: ${selectedPath}`,
{
userMessage:
"The selected artifact path no longer names the opened file. Reopen the selected artifact, then retry.",
...(cause === undefined ? {} : { cause }),
},
);
let current: string;
try {
current = await realpath(selectedPath);
} catch (cause: unknown) {
if (
cause instanceof Error &&
"code" in cause &&
["ENOENT", "ENOTDIR", "ELOOP"].includes(String(cause.code))
)
throw changed(cause);
throw cause;
}
if (current !== canonicalPath) throw changed();
};
+2 -6
View File
@@ -1,3 +1,4 @@
import { assertArtifactSelectedSource } from "./ArtifactSelectedSource.js";
import { appendCallerPath } from "../filesystem/callerPath.js";
import type { AsarHeaderAdmission } from "../config/asarHeaderAdmission.js";
import { artifactIntegrityDetails } from "../domain/artifactIntegrity.js";
@@ -437,12 +438,7 @@ export class AsarArtifactReader implements ArtifactReader {
// The packed pathname is fixed for this reader; member checks only need
// to re-observe the caller's independently selected alias.
this.#selectedCanonicalSource ??= await realpath(this.path);
const current = await realpath(selected);
if (this.#selectedCanonicalSource !== current)
throw new ArtifactReaderFailure(
"integrity",
`Selected ASAR archive no longer resolves to its admitted source: ${selected}`,
);
await assertArtifactSelectedSource(this.#selectedCanonicalSource, selected);
}
async #openUnpackedStream(
+31 -5
View File
@@ -1,3 +1,6 @@
import { assertAdmittedRootDigest } from "../inventory/classify.js";
import { assertArtifactSelectedSource } from "../ArtifactSelectedSource.js";
import { hashStableRootArtifactHandle } from "../inventory/hashStableRootArtifact.js";
import { absoluteCallerPath } from "../../filesystem/callerPath.js";
import type { AsarHeaderAdmission } from "../../config/asarHeaderAdmission.js";
import {
@@ -56,7 +59,8 @@ import type { ArtifactIntegrityPolicyName } from "../../domain/artifactIntegrity
export interface ArtifactExtractionInput {
readonly asarHeaderAdmission?: AsarHeaderAdmission;
readonly inputPath: string;
/** Selected ASAR spelling; packed I/O remains on inputPath. */
readonly expectedRootSha256?: string | undefined;
/** Selected artifact spelling; packed I/O remains on inputPath. */
readonly selectedPath?: string | undefined;
readonly inputFormat: BinaryTarget["format"];
readonly outputRoot: string;
@@ -79,6 +83,8 @@ const extractArtifactInScope = async (
// Cancellation wins over every later refusal, including unsupported formats.
abortIfNeeded(signal);
const sourcePath = await realpath(input.inputPath);
if (input.expectedRootSha256 !== undefined)
await assertArtifactSelectedSource(sourcePath, input.selectedPath);
const admission = await requireExtractableFormat(sourcePath, input, signal);
const { snapshot, zipSource } =
await scanArtifactInventoryForExtractionInScope(
@@ -86,6 +92,7 @@ const extractArtifactInScope = async (
{
resourceScope: input.resourceScope,
selectedPath: input.selectedPath,
expectedRootSha256: input.expectedRootSha256,
signal,
environment: input.environment,
asarHeaderAdmission:
@@ -176,7 +183,7 @@ export const materializeArtifactInventoryInScope = async (
)
throw new AnalysisUnsupportedTargetError(
"extract_artifact",
input.inputPath,
input.selectedPath ?? input.inputPath,
`Godot PCK directory is ${containerMetadata.directory_status}; its complete contents cannot be extracted`,
);
const selectedOccurrences = snapshot.occurrences.filter(
@@ -218,7 +225,7 @@ export const materializeArtifactInventoryInScope = async (
if (occurrence.encrypted)
throw new AnalysisUnsupportedTargetError(
"extract_artifact",
input.inputPath,
input.selectedPath ?? input.inputPath,
`Archive entry ${occurrence.logical_path} is encrypted; REA does not decrypt archive entries, so the archive cannot be extracted completely`,
);
// Inventory retains unavailable source occurrences, but extraction cannot
@@ -377,6 +384,8 @@ const materializeSelection = async ({
traversal.snapshot(),
reader.provenance(),
);
if (input.expectedRootSha256 !== undefined)
await assertArtifactSelectedSource(sourcePath, input.selectedPath);
await output.commit(signal);
return completed;
} catch (cause: unknown) {
@@ -550,7 +559,11 @@ const requireExtractableFormat = async (
path: string,
input: Pick<
ArtifactExtractionInput,
"inputPath" | "inputFormat" | "resourceScope"
| "inputPath"
| "selectedPath"
| "inputFormat"
| "resourceScope"
| "expectedRootSha256"
>,
signal?: AbortSignal,
previous?: ArtifactEmbeddedFormatProbeFacts,
@@ -591,13 +604,26 @@ const requireExtractableFormat = async (
((format === "elf" || format === "pe" || format === "mach-o") &&
((await probes.nodeSea()) !== undefined ||
(await probes.pyInstaller())));
if (pack && input.expectedRootSha256 !== undefined) {
const digest = await hashStableRootArtifactHandle(
path,
source.handle,
source.initial,
signal,
);
assertAdmittedRootDigest(
path,
input.expectedRootSha256,
digest.sha256,
);
}
return { pack, probeFacts: probes.completedFacts() };
},
);
if (!detected.pack)
throw new AnalysisUnsupportedTargetError(
"extract_artifact",
input.inputPath,
input.selectedPath ?? input.inputPath,
`Artifact format has no extraction reader: ${format}`,
);
return { directory, probeFacts: detected.probeFacts };
+65 -13
View File
@@ -1,3 +1,4 @@
import { assertArtifactSelectedSource } from "../ArtifactSelectedSource.js";
import { artifactNameForClassification } from "../../domain/artifactNameSelection.js";
import type { NodeSeaAdmission } from "../nodeSea/NodeSeaAdmission.js";
import type { AsarHeaderAdmission } from "../../config/asarHeaderAdmission.js";
@@ -65,7 +66,11 @@ export const classifyAndHashRootForInventory = async (
expectedMetadata: Stats,
options: Pick<
ArtifactInventoryOptions,
"resourceScope" | "signal" | "asarHeaderAdmission" | "selectedPath"
| "resourceScope"
| "signal"
| "asarHeaderAdmission"
| "selectedPath"
| "expectedRootSha256"
>,
probeFacts?: ArtifactEmbeddedFormatProbeFacts,
): Promise<RootInventoryClassification> => {
@@ -76,6 +81,15 @@ export const classifyAndHashRootForInventory = async (
`Root artifact changed before inventory: ${path}`,
);
}
if (directory && options.expectedRootSha256 !== undefined)
throw new ArtifactReaderFailure(
"integrity",
`Artifact root changed after target admission: expected an admitted file at ${path}, observed a directory`,
{
userMessage:
"The opened file was replaced by a directory. Select the intended artifact and reopen it before retrying.",
},
);
if (directory)
return {
format: "directory",
@@ -83,6 +97,8 @@ export const classifyAndHashRootForInventory = async (
rootSource: undefined,
readerAdmission: undefined,
};
if (options.expectedRootSha256 !== undefined)
await assertArtifactSelectedSource(path, options.selectedPath);
const source = await openRootArtifact(
path,
options.resourceScope,
@@ -101,27 +117,46 @@ export const classifyAndHashRootForInventory = async (
"integrity",
`Root artifact changed before inventory: ${path}`,
);
// Prior probe state can prove replacement without interpreting any bytes.
const probes = new ArtifactEmbeddedFormatProbes(
path,
source,
options.signal,
probeFacts,
);
const admittedDigest =
options.expectedRootSha256 === undefined
? undefined
: await hashStableRootArtifactHandle(
path,
handle,
initial,
options.signal,
);
if (admittedDigest !== undefined)
assertAdmittedRootDigest(
path,
options.expectedRootSha256,
admittedDigest.sha256,
);
const classified = await classifyRootFormat(
path,
source,
new ArtifactEmbeddedFormatProbes(
path,
source,
options.signal,
probeFacts,
),
probes,
options.signal,
options.asarHeaderAdmission,
options.selectedPath,
);
const format =
typeof classified === "string" ? classified : classified.kind;
const digest = await hashStableRootArtifactHandle(
path,
handle,
initial,
options.signal,
);
const digest =
admittedDigest ??
(await hashStableRootArtifactHandle(
path,
handle,
initial,
options.signal,
));
const retainSource =
isZipPackageFormat(format) ||
format === "mach-o-universal" ||
@@ -357,3 +392,20 @@ const readRootMzWindowsHeaderSignature = async (
if (available === 0) return undefined;
return { offset, bytes: window.subarray(0, available) };
};
/** Compare physically completed bytes before interpreting an admitted file root. */
export const assertAdmittedRootDigest = (
path: string,
expected: string | undefined,
observed: string,
): void => {
if (expected !== undefined && observed !== expected)
throw new ArtifactReaderFailure(
"integrity",
`Artifact root changed after target admission at ${path}: expected ${expected}, observed ${observed}`,
{
userMessage:
"The artifact changed after it was opened. Reopen the selected artifact, then retry.",
},
);
};
+2
View File
@@ -13,6 +13,8 @@ export const STRICT_INTEGRITY_POLICY: ArtifactIntegrityPolicy = {
/** Options shared by artifact inventory scans. */
export interface ArtifactInventoryOptions {
readonly resourceScope: ArtifactResourceScope;
/** Identity of a previously admitted file root; directory roots have no file digest. */
readonly expectedRootSha256?: string | undefined;
/** Independently selected root spelling used only for ASAR companion lookup. */
readonly selectedPath?: string | undefined;
readonly signal?: AbortSignal | undefined;
@@ -0,0 +1,66 @@
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
import { expect, it } from "vitest";
import { artifactExtractionResultSchema } from "../../src/domain/artifactGraph.js";
import { parseEvidence } from "../../src/domain/evidence.js";
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
it("refuses changed admitted archives over stdio MCP before publishing extraction and accepts an explicit reopen", async () => {
const root = await createTestTempDirectory("rea-admitted-archive-mcp-");
const temporary = join(root, "temporary");
await mkdir(temporary);
const path = join(root, "selected.zip");
const writeArchive = async (value: string) => {
const zip = new ZipWriter(new Uint8ArrayWriter());
await zip.add("payload.txt", new TextReader(value));
await writeFile(path, await zip.close());
};
await writeArchive("admitted version one");
const { call } = await connectLocalToolsMcp({
TMPDIR: temporary,
TEMP: temporary,
TMP: temporary,
REA_MCP_TOOL_FAMILIES: "session,artifact",
});
expect((await call("open_binary", { path })).isError).not.toBe(true);
const openedTemporaryBaseline = (await readdir(temporary)).sort();
await writeArchive("changed version two");
for (const operation of ["inspect_artifact", "extract_artifact"]) {
const refused = parseMcpToolError(await call(operation, {}));
expect(refused.error).toMatchObject({
code: "artifact_operation_failed",
details: { reason: "integrity" },
message: expect.stringContaining("Reopen the selected artifact"),
});
expect((await readdir(temporary)).sort()).toEqual(openedTemporaryBaseline);
}
expect((await call("open_binary", { path })).isError).not.toBe(true);
const extracted = await call("extract_artifact", {});
expect(extracted.isError, JSON.stringify(extracted)).not.toBe(true);
const result = artifactExtractionResultSchema.parse(
parseEvidence(extracted.structuredContent).normalized_result,
);
try {
expect(dirname(result.output_root)).toBe(temporary);
expect(
await readFile(join(result.output_root, "payload.txt"), "utf8"),
).toBe("changed version two");
} finally {
await rm(result.output_root, { recursive: true, force: true });
}
const reopenedTemporaryBaseline = (await readdir(temporary)).sort();
await writeFile(path, "not an archive");
for (const operation of ["inspect_artifact", "extract_artifact"]) {
const refused = parseMcpToolError(await call(operation, {}));
expect(refused.error).toMatchObject({
code: "artifact_operation_failed",
details: { reason: "integrity" },
});
expect((await readdir(temporary)).sort()).toEqual(
reopenedTemporaryBaseline,
);
}
});
@@ -0,0 +1,272 @@
import { build } from "plist";
import { elf } from "../../../src/domain/binaryTarget.fixture.js";
import {
machoImage,
CPU,
} from "../../../src/artifacts/apple/MachoImage.fixture.js";
import { buildGodotPck, pckU64 } from "../../fixtures/godotPck.js";
import {
access,
mkdir,
readFile,
writeFile,
unlink,
symlink,
} from "node:fs/promises";
import { join } from "node:path";
import { createPackage } from "@electron/asar";
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
import { expect, it } from "vitest";
import { ArtifactProvider } from "../../../src/artifacts/ArtifactProvider.js";
import {
artifactExtractionResultSchema,
artifactInventoryResultSchema,
} from "../../../src/domain/artifactGraph.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
it.each([
{ format: "zip", replacement: "valid" },
{ format: "apk", replacement: "valid" },
{ format: "asar", replacement: "valid" },
{ format: "zip", replacement: "malformed" },
{ format: "asar", replacement: "malformed" },
])(
"binds $format $replacement archive operations to the admitted source before output creation",
async ({ format, replacement }) => {
const root = await createTestTempDirectory(
"rea-archive-admitted-identity-",
);
const path = join(root, `selected.${format}`);
const source = join(root, "source");
await mkdir(source);
const writeArchive = async (value: string) => {
if (format === "asar") {
await writeFile(join(source, "payload.txt"), value);
await createPackage(source, path);
} else {
const zip = new ZipWriter(new Uint8ArrayWriter());
await zip.add("payload.txt", new TextReader(value));
await writeFile(path, await zip.close());
}
};
await writeArchive("admitted version one");
const session = createTestBinarySession(new ArtifactProvider({}));
try {
const opened = await session.open(path);
if (!opened.ok) throw opened.error;
const initial = await session.execute("inventory_artifact", {});
if (!initial.ok) throw initial.error;
if (replacement === "malformed") await writeFile(path, "not an archive");
else await writeArchive("changed version two");
for (const operation of [
"inventory_artifact",
"inspect_artifact",
"extract_artifact",
] as const) {
const output = join(root, `refused-${operation}`);
const result = await session.execute(
operation,
operation === "extract_artifact"
? { output_root: output, integrity_policy: "fail" }
: {},
);
expect(result).toMatchObject({
ok: false,
error: {
reason: "integrity",
detail: expect.stringContaining("changed after target admission"),
},
});
await expect(access(output)).rejects.toMatchObject({ code: "ENOENT" });
}
if (replacement === "valid") {
const reopened = await session.open(path);
if (!reopened.ok) throw reopened.error;
const inventory = await session.execute("inventory_artifact", {});
if (!inventory.ok) throw inventory.error;
expect(
artifactInventoryResultSchema.parse(inventory.value.result).manifest
.root_sha256,
).not.toBe(
artifactInventoryResultSchema.parse(initial.value.result).manifest
.root_sha256,
);
const extracted = await session.execute("extract_artifact", {
output_root: join(root, "reopened-output"),
integrity_policy: "fail",
});
if (!extracted.ok) throw extracted.error;
const result = artifactExtractionResultSchema.parse(
extracted.value.result,
);
expect(
await readFile(join(result.output_root, "payload.txt"), "utf8"),
).toBe("changed version two");
}
} finally {
expect(await session.close()).toEqual({ ok: true, value: null });
}
},
);
it.skipIf(process.platform === "win32")(
"rejects retargeted ZIP selections without attributing or publishing the old packed bytes",
async () => {
const root = await createTestTempDirectory("rea-admitted-zip-alias-");
const stored = join(root, "stored.zip");
const other = join(root, "other.zip");
const selected = join(root, "selected.zip");
const zip = new ZipWriter(new Uint8ArrayWriter());
await zip.add("payload.txt", new TextReader("same bytes"));
const bytes = await zip.close();
await writeFile(stored, bytes);
await writeFile(other, bytes);
await symlink(stored, selected);
const session = createTestBinarySession(new ArtifactProvider({}));
try {
const opened = await session.open(selected);
if (!opened.ok) throw opened.error;
await unlink(selected);
await symlink(other, selected);
for (const operation of [
"inventory_artifact",
"extract_artifact",
] as const) {
const output = join(root, operation);
const result = await session.execute(
operation,
operation === "extract_artifact" ? { output_root: output } : {},
);
expect(result).toMatchObject({
ok: false,
error: {
reason: "integrity",
detail: expect.stringContaining(
"no longer resolves to its admitted source",
),
},
});
await expect(access(output)).rejects.toMatchObject({ code: "ENOENT" });
}
} finally {
await session.close();
}
},
);
it("rejects an admitted archive replaced by a directory", async () => {
const root = await createTestTempDirectory("rea-admitted-file-directory-");
const path = join(root, "selected.zip");
const zip = new ZipWriter(new Uint8ArrayWriter());
await zip.add("payload.txt", new TextReader("original"));
await writeFile(path, await zip.close());
const session = createTestBinarySession(new ArtifactProvider({}));
try {
const opened = await session.open(path);
if (!opened.ok) throw opened.error;
await unlink(path);
await mkdir(path);
await writeFile(join(path, "payload.txt"), "replacement directory");
const output = join(root, "output");
expect(
await session.execute("extract_artifact", { output_root: output }),
).toMatchObject({ ok: false, error: { reason: "integrity" } });
await expect(access(output)).rejects.toMatchObject({ code: "ENOENT" });
} finally {
await session.close();
}
});
it("keeps app bundle aggregate inventories live when resources change", async () => {
const root = await createTestTempDirectory("rea-admitted-bundle-resources-");
const app = join(root, "Fixture.app");
await mkdir(join(app, "Contents", "MacOS"), { recursive: true });
await writeFile(
join(app, "Contents", "MacOS", "fixture"),
machoImage({ cpu: CPU.arm64e }),
);
await writeFile(
join(app, "Contents", "Info.plist"),
build({ CFBundlePackageType: "APPL", CFBundleExecutable: "fixture" }),
);
const payload = join(app, "Contents", "payload.txt");
await writeFile(payload, "first resource");
const session = createTestBinarySession(new ArtifactProvider({}));
try {
const opened = await session.open(app);
if (!opened.ok) throw opened.error;
const initial = await session.execute("inventory_artifact", {});
if (!initial.ok) throw initial.error;
await writeFile(payload, "second resource");
const current = await session.execute("inventory_artifact", {});
if (!current.ok) throw current.error;
expect(
artifactInventoryResultSchema.parse(current.value.result).manifest
.root_sha256,
).not.toBe(
artifactInventoryResultSchema.parse(initial.value.result).manifest
.root_sha256,
);
const extracted = await session.execute("extract_artifact", {
output_root: join(root, "output"),
});
if (!extracted.ok) throw extracted.error;
const result = artifactExtractionResultSchema.parse(extracted.value.result);
expect(
await readFile(
join(result.output_root, "Contents", "payload.txt"),
"utf8",
),
).toBe("second resource");
} finally {
await session.close();
}
});
it("checks embedded native packs before acceptance while preserving unsupported fast refusal", async () => {
const root = await createTestTempDirectory("rea-admitted-native-preflight-");
const path = join(root, "game");
const writePack = async (text: string) => {
const pack = buildGodotPck(2, [
{ path: "res://payload.txt", data: Buffer.from(text) },
]);
await writeFile(
path,
Buffer.concat([
elf(2, 1, 62),
pack,
pckU64(pack.length),
Buffer.from("GDPC"),
]),
);
};
await writePack("first");
const session = createTestBinarySession(new ArtifactProvider({}));
try {
const opened = await session.open(path);
if (!opened.ok) throw opened.error;
await writePack("second");
const output = join(root, "output");
expect(
await session.execute("extract_artifact", { output_root: output }),
).toMatchObject({
ok: false,
error: {
reason: "integrity",
detail: expect.stringContaining("changed after target admission"),
},
});
await expect(access(output)).rejects.toMatchObject({ code: "ENOENT" });
await writeFile(path, "not a native pack");
expect(
await session.execute("extract_artifact", { output_root: output }),
).toMatchObject({
ok: false,
error: { _tag: "AnalysisUnsupportedTargetError" },
});
await expect(access(output)).rejects.toMatchObject({ code: "ENOENT" });
} finally {
await session.close();
}
});
@@ -98,7 +98,7 @@ describe("artifact archive inventory", () => {
await writeFile(join(source, "main.js"), "console.log('ok');\n");
const asarPath = join(root, "app.asar");
await createPackage(source, asarPath);
const asarResult = await inventory(target(asarPath, "asar"));
const asarResult = await inventory(await target(asarPath));
expect(asarResult.occurrences).toEqual(
expect.arrayContaining([
expect.objectContaining({ logical_path: "main.js" }),
@@ -107,7 +107,7 @@ describe("artifact archive inventory", () => {
const unpackedPath = join(root, "unpacked.asar");
await createPackageWithOptions(source, unpackedPath, { unpack: "*.js" });
const unpackedResult = await inventory(target(unpackedPath, "asar"));
const unpackedResult = await inventory(await target(unpackedPath));
expect(unpackedResult.occurrences).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -121,7 +121,7 @@ describe("artifact archive inventory", () => {
"changed();\n",
);
const corrupted = await new ArtifactProvider(process.env)
.createClient(target(unpackedPath, "asar"))
.createClient(await target(unpackedPath))
.execute("inventory_artifact", {});
expect(corrupted).toMatchObject({
ok: false,
@@ -135,7 +135,7 @@ describe("artifact archive inventory", () => {
},
});
const failedInspection = await new ArtifactProvider(process.env)
.createClient(target(unpackedPath, "asar"))
.createClient(await target(unpackedPath))
.execute("inspect_artifact", {});
expect(failedInspection).toMatchObject({
ok: false,
@@ -151,13 +151,8 @@ const inventory = async (targetValue: BinaryTarget) => {
return artifactInventoryResultSchema.parse(result.value.result);
};
const target = (
path: string,
format: Extract<BinaryTarget, { kind: "archive" }>["format"] | "directory",
): BinaryTarget => ({
path,
sourcePath: path,
sha256: "0".repeat(64),
kind: "archive",
format: format === "directory" ? "asar" : format,
});
const target = async (path: string): Promise<BinaryTarget> => {
const parsed = await parseBinaryTarget(path);
if (!parsed.ok) throw parsed.error;
return parsed.value;
};
@@ -3,6 +3,7 @@ import { join } from "node:path";
import { expect, it } from "vitest";
import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js";
import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js";
import {
cArchiveExecutable,
pyzArchive,
@@ -28,17 +29,9 @@ it.each([1, 129])(
})),
});
await writeFile(path, bytes);
const client = new ArtifactProvider({}).createClient({
path,
sourcePath: path,
sha256: "0".repeat(64),
kind: "executable",
format: "pe",
executableRole: "application",
managed: false,
architecture: "x86_64",
availableArchitectures: ["x86_64"],
});
const target = await parseBinaryTarget(path);
if (!target.ok) throw target.error;
const client = new ArtifactProvider({}).createClient(target.value);
try {
const result = await client.execute("inventory_artifact", {});
if (count === 1) {
@@ -5,6 +5,7 @@ import { createPackage } from "@electron/asar";
import { expect, it, onTestFinished, vi } from "vitest";
import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js";
import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js";
import { ArtifactReaderFailure } from "../../../../src/artifacts/ArtifactReader.js";
import { AsarArtifactReader } from "../../../../src/artifacts/AsarArtifactReader.js";
import { SafeOutputTree } from "../../../../src/artifacts/SafeOutputTree.js";
@@ -29,19 +30,16 @@ const createArchive = async () => {
return { root, archive };
};
const createClient = (path: string) =>
new ArtifactProvider(process.env).createClient({
path,
sourcePath: path,
sha256: "0".repeat(64),
kind: "archive",
format: "asar",
});
const createClient = async (path: string) => {
const target = await parseBinaryTarget(path);
if (!target.ok) throw target.error;
return new ArtifactProvider(process.env).createClient(target.value);
};
it("retains a failed reader close, blocks new work, and retries the same reader", async () => {
const { root, archive } = await createArchive();
const output = join(root, "output");
const client = createClient(archive);
const client = await createClient(archive);
const originalClose = AsarArtifactReader.prototype.close;
const closeOwners: AsarArtifactReader[] = [];
let readerCloseBlocked = true;
@@ -109,7 +107,7 @@ it("retains a failed reader close, blocks new work, and retries the same reader"
it("retains a failed rollback owner and preserves the materialization failure", async () => {
const { root, archive } = await createArchive();
const output = join(root, "output");
const client = createClient(archive);
const client = await createClient(archive);
const originalWrite = SafeOutputTree.prototype.write;
const originalRollback = SafeOutputTree.prototype.rollback;
const rollbackOwners: SafeOutputTree[] = [];
@@ -173,7 +171,7 @@ it("retains a tree whose setup failed after root identity was captured", async (
const { root, archive } = await createArchive();
const output = join(root, "output");
const blocker = join(output, "unowned.txt");
const client = createClient(archive);
const client = await createClient(archive);
const prototypeHandle = await open(join(root, "prototype-handle"), "w");
const handlePrototype = Object.getPrototypeOf(prototypeHandle) as Pick<
typeof prototypeHandle,
@@ -213,7 +211,7 @@ it("retains a tree whose setup failed after root identity was captured", async (
it("waits for admitted extraction before close and rejects later work", async () => {
const { root, archive } = await createArchive();
const output = join(root, "output");
const client = createClient(archive);
const client = await createClient(archive);
const originalWrite = SafeOutputTree.prototype.write;
const started = deferred();
const finish = deferred();
@@ -247,7 +245,13 @@ it("waits for admitted extraction before close and rejects later work", async ()
const extraction = client.execute("extract_artifact", {
output_root: output,
});
await started.promise;
await Promise.race([
started.promise,
extraction.then((result) => {
if (!result.ok) throw result.error;
throw new Error("Extraction completed before reaching its write stage");
}),
]);
const closing = client.close().then((result) => {
closeFinished = true;
return result;
@@ -15,53 +15,55 @@ describe("artifact directory diagnostics", () => {
"omits inaccessible directory descendants during inventory but refuses incomplete extraction",
async () => {
const root = await createTestTempDirectory("rea-directory-policy-");
const source = join(root, "source");
const source = join(root, "Fixture.app");
const blocked = join(source, "blocked");
const output = join(root, "output");
await mkdir(blocked, { recursive: true });
await mkdir(join(source, "Contents", "MacOS"), { recursive: true });
await writeFile(
join(source, "Contents", "Info.plist"),
"<plist><dict><key>CFBundleExecutable</key><string>Fixture</string></dict></plist>",
);
await writeFile(
join(source, "Contents", "MacOS", "Fixture"),
thinMach(0xfeedfacf, 0x0100000c),
);
await writeFile(join(source, "readable.txt"), "retained bytes\n");
await writeFile(join(blocked, "hidden.txt"), "unexamined bytes\n");
await chmod(blocked, 0);
try {
await withArtifactClient(
{
path: source,
sourcePath: source,
sha256: "0".repeat(64),
kind: "archive",
format: "asar",
},
async (client) => {
const inventoried = await client.execute("inventory_artifact", {});
if (!inventoried.ok) throw inventoried.error;
const inventory = artifactInventoryResultSchema.parse(
inventoried.value.result,
);
expect(inventory.coverage.status).toBe("unknown");
expect(inventory.occurrences).toContainEqual(
expect.objectContaining({
logical_path: "readable.txt",
hash_status: "verified",
}),
);
expect(inventory.limitations).toContainEqual(
expect.stringContaining("EACCES"),
);
const extracted = await client.execute("extract_artifact", {
output_root: output,
});
expect(extracted).toMatchObject({
ok: false,
error: {
_tag: "ArtifactOperationError",
detail: expect.stringContaining("EACCES"),
},
});
await expect(access(output)).rejects.toMatchObject({
code: "ENOENT",
});
},
);
const target = await parseBinaryTarget(source);
if (!target.ok) throw target.error;
await withArtifactClient(target.value, async (client) => {
const inventoried = await client.execute("inventory_artifact", {});
if (!inventoried.ok) throw inventoried.error;
const inventory = artifactInventoryResultSchema.parse(
inventoried.value.result,
);
expect(inventory.coverage.status).toBe("unknown");
expect(inventory.occurrences).toContainEqual(
expect.objectContaining({
logical_path: "readable.txt",
hash_status: "verified",
}),
);
expect(inventory.limitations).toContainEqual(
expect.stringContaining("EACCES"),
);
const extracted = await client.execute("extract_artifact", {
output_root: output,
});
expect(extracted).toMatchObject({
ok: false,
error: {
_tag: "ArtifactOperationError",
detail: expect.stringContaining("EACCES"),
},
});
await expect(access(output)).rejects.toMatchObject({
code: "ENOENT",
});
});
} finally {
await chmod(blocked, 0o700);
}
@@ -5,6 +5,11 @@ import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js";
import {
CPU,
machoImage,
} from "../../../../src/artifacts/apple/MachoImage.fixture.js";
import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js";
import { artifactInventoryResultSchema } from "../../../../src/domain/artifactGraph.js";
import type { BinaryTarget } from "../../../../src/domain/binaryTargetTypes.js";
@@ -18,7 +23,11 @@ describe("artifact directory inventory", () => {
await mkdir(join(app, "Contents", "Frameworks", "Fixture.framework"), {
recursive: true,
});
const machO = Buffer.from([0xcf, 0xfa, 0xed, 0xfe, 0x0c, 0, 0, 1]);
const machO = machoImage({ cpu: CPU.arm64 });
await writeFile(
join(app, "Contents/Info.plist"),
'<plist version="1.0"><dict><key>CFBundleExecutable</key><string>Fixture</string></dict></plist>',
);
await writeFile(join(app, "Contents", "MacOS", "Fixture"), machO, {
mode: 0o755,
});
@@ -30,7 +39,7 @@ describe("artifact directory inventory", () => {
await writeFile(join(app, "Contents", "Resources", "main.js.MAP"), "{}\n");
await symlink("/etc/passwd", join(app, "Contents", "Resources", "outside"));
const client = new ArtifactProvider(process.env).createClient(
target(app, "directory"),
await target(app),
);
const input = {};
const first = await client.execute("inventory_artifact", input);
@@ -72,13 +81,9 @@ describe("artifact directory inventory", () => {
});
});
const target = (
path: string,
format: Extract<BinaryTarget, { kind: "archive" }>["format"] | "directory",
): BinaryTarget => ({
path,
sourcePath: path,
sha256: "0".repeat(64),
kind: "archive",
format: format === "directory" ? "asar" : format,
});
const target = async (path: string): Promise<BinaryTarget> => {
const parsed = await parseBinaryTarget(path);
if (!parsed.ok) throw parsed.error;
expect(parsed.value.bundleInfoPlist).toBe(join(path, "Contents/Info.plist"));
return parsed.value;
};
@@ -5,6 +5,11 @@ import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js";
import {
CPU,
machoImage,
} from "../../../../src/artifacts/apple/MachoImage.fixture.js";
import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js";
import { artifactExtractionExecutionSchema } from "../../../../src/contracts/artifactToolContracts.js";
import {
@@ -19,13 +24,13 @@ describe("artifact extraction", () => {
"refuses an invalid parent (%s) before acquiring output and permits a corrected request",
async (parent) => {
const root = await createTestTempDirectory("rea-extract-invalid-parent-");
const source = join(root, "source");
const source = join(root, "source.app");
await mkdir(source);
await writeFile(join(source, "payload.txt"), "selected payload\n");
await writeFile(join(root, "file"), "parent is a file");
const output = join(root, "out");
await withArtifactClient(target(source, "directory"), async (client) => {
await withArtifactClient(await target(source), async (client) => {
const refused = await client.execute("extract_artifact", {
output_root: `${root}/${parent}/out`,
});
@@ -52,12 +57,12 @@ describe("artifact extraction", () => {
it("keeps committed files after the owning client closes", async () => {
const root = await createTestTempDirectory("rea-extract-client-close-");
const source = join(root, "source");
const source = join(root, "source.app");
await mkdir(source);
await writeFile(join(source, "payload.txt"), "committed\n");
const output = join(root, "output");
const client = new ArtifactProvider(process.env).createClient(
target(source, "directory"),
await target(source),
);
const result = await client.execute("extract_artifact", {
@@ -72,11 +77,11 @@ describe("artifact extraction", () => {
it("extracts all regular occurrences through an exclusively owned output tree", async () => {
const root = await createTestTempDirectory("rea-extract-");
const source = join(root, "source");
const source = join(root, "source.app");
await mkdir(join(source, "assets"), { recursive: true });
await writeFile(join(source, "assets", "selected.js"), "selected();\n");
await writeFile(join(source, "assets", "ignored.js"), "ignored();\n");
const targetValue = target(source, "directory");
const targetValue = await target(source);
const graph = await inventory(targetValue);
expect(
graph.occurrences.some(
@@ -160,7 +165,7 @@ describe("artifact extraction", () => {
expect((await readdir(root)).sort()).toEqual([
"output",
"relocated-output",
"source",
"source.app",
]);
});
});
@@ -172,13 +177,15 @@ const inventory = async (targetValue: BinaryTarget) => {
return artifactInventoryResultSchema.parse(result.value.result);
};
const target = (
path: string,
format: Extract<BinaryTarget, { kind: "archive" }>["format"] | "directory",
): BinaryTarget => ({
path,
sourcePath: path,
sha256: "0".repeat(64),
kind: "archive",
format: format === "directory" ? "asar" : format,
});
/** A real bundle target has an executable identity distinct from its resource tree. */
const target = async (path: string): Promise<BinaryTarget> => {
await writeFile(
join(path, "Info.plist"),
'<plist version="1.0"><dict><key>CFBundleExecutable</key><string>Main</string></dict></plist>',
);
await writeFile(join(path, "Main"), machoImage({ cpu: CPU.arm64 }));
const parsed = await parseBinaryTarget(path);
if (!parsed.ok) throw parsed.error;
expect(parsed.value.bundleInfoPlist).toBe(join(path, "Info.plist"));
return parsed.value;
};
@@ -1,7 +1,9 @@
import { createHash } from "node:crypto";
import {
access,
appendFile,
mkdir,
readFile,
rename,
rm,
writeFile,
@@ -33,7 +35,9 @@ it("refuses an unsupported source without hashing its bytes", async () => {
{
path: source,
sourcePath: source,
sha256: "0".repeat(64),
sha256: createHash("sha256")
.update(await readFile(source))
.digest("hex"),
kind: "archive",
format: "dmg",
},
@@ -73,7 +77,9 @@ it("preserves cancellation arriving during the detection descriptor's final clos
{
path: source,
sourcePath: source,
sha256: "0".repeat(64),
sha256: createHash("sha256")
.update(await readFile(source))
.digest("hex"),
kind: "archive",
format: "dmg",
},
@@ -118,7 +124,9 @@ it("keeps detection I/O failure distinct from unsupported format and closes its
{
path: source,
sourcePath: source,
sha256: "0".repeat(64),
sha256: createHash("sha256")
.update(await readFile(source))
.digest("hex"),
kind: "archive",
format: "dmg",
},
@@ -150,7 +158,7 @@ it.each([
"changed same inode",
"directory replacement",
] as const)(
"refuses to reuse completed detection facts after %s before hashing",
"refuses to reuse completed detection facts after %s before inventory output",
async (change) => {
const root = await createTestTempDirectory(
"rea-extraction-admission-identity-",
@@ -161,11 +169,13 @@ it.each([
buildGodotPck(3, [{ path: "res://main.gd", data: "extends Node\n" }]),
);
await writeFile(source, bytes);
let hashesAtMutation: number | undefined;
const close = OwnedFileHandle.prototype.close;
const closeSpy = vi
.spyOn(OwnedFileHandle.prototype, "close")
.mockImplementationOnce(async function (this: OwnedFileHandle) {
await close.call(this);
hashesAtMutation = hash.mock.calls.length;
if (change === "changed same inode")
await appendFile(source, "changed");
else if (change === "directory replacement") {
@@ -183,7 +193,9 @@ it.each([
{
path: source,
sourcePath: source,
sha256: "0".repeat(64),
sha256: createHash("sha256")
.update(await readFile(source))
.digest("hex"),
kind: "executable",
format: "elf",
architecture: "x86_64",
@@ -197,7 +209,9 @@ it.each([
ok: false,
error: { _tag: "ArtifactOperationError", reason: "integrity" },
});
expect(hash).not.toHaveBeenCalled();
expect(hashesAtMutation).toBeDefined();
// Initial admission may hash; facts known stale after close must not.
expect(hash).toHaveBeenCalledTimes(hashesAtMutation!);
await expect(access(output)).rejects.toMatchObject({
code: "ENOENT",
});
@@ -275,7 +289,9 @@ it.each([
{
path: source,
sourcePath: source,
sha256: "0".repeat(64),
sha256: createHash("sha256")
.update(await readFile(source))
.digest("hex"),
kind: "executable",
format: "pe",
executableRole: "application",
@@ -5,12 +5,12 @@ import { join } from "node:path";
import { createPackage, createPackageWithOptions } from "@electron/asar";
import { expect, it, vi } from "vitest";
import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js";
import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js";
import {
materializeArtifactInventory,
scanArtifactInventory,
} from "../../../fixtures/artifactInventory.js";
import { artifactExtractionResultSchema } from "../../../../src/domain/artifactGraph.js";
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { DirectoryArtifactReader } from "../../../../src/artifacts/DirectoryArtifactReader.js";
@@ -29,9 +29,7 @@ it.for([false, true])(
"unrepresented",
);
const snapshot = await scanArtifactInventory(source);
const result = await extract(source, join(root, "output"));
if (!result.ok) throw result.error;
const extracted = artifactExtractionResultSchema.parse(result.value.result);
const extracted = await extractDirectory(source, join(root, "output"));
expect(extracted.coverage).toEqual(snapshot.coverage);
expect(extracted.coverage.status).toBe(
invalidName ? "unknown" : "complete",
@@ -317,9 +315,7 @@ it("preserves every equal-content occurrence and copies a nested ASAR as its con
await writeFile(join(source, "a.txt"), "same bytes\n");
await writeFile(join(source, "b.txt"), "same bytes\n");
const output = join(root, "output");
const result = await extract(source, output);
if (!result.ok) throw result.error;
const extracted = artifactExtractionResultSchema.parse(result.value.result);
const extracted = await extractDirectory(source, output);
expect(extracted.artifacts.map(({ relative_path }) => relative_path)).toEqual(
["a.txt", "app.asar", "b.txt"],
);
@@ -350,11 +346,7 @@ it("ignores unavailable members of a nested ASAR but refuses them as active entr
await rm(join(`${archive}.unpacked`, "native", "addon.node"));
const nestedOutput = join(root, "nested-output");
const nestedResult = await extract(source, nestedOutput);
if (!nestedResult.ok) throw nestedResult.error;
const nestedExtraction = artifactExtractionResultSchema.parse(
nestedResult.value.result,
);
const nestedExtraction = await extractDirectory(source, nestedOutput);
expect(
nestedExtraction.extraction_manifest.selected_occurrence_ids,
).toHaveLength(1);
@@ -369,7 +361,7 @@ it("ignores unavailable members of a nested ASAR but refuses them as active entr
).toBe(0);
const activeOutput = join(root, "active-output");
const activeResult = await extract(archive, activeOutput);
const activeResult = await extractArchive(archive, activeOutput);
if (activeResult.ok)
throw new Error("Unavailable active ASAR member must fail extraction");
expect(projectAnalysisError(activeResult.error)).toMatchObject({
@@ -396,13 +388,16 @@ const inventoriedExtraction = async (source: string, output: string) => {
);
};
const extract = (source: string, output: string) =>
new ArtifactProvider(process.env)
.createClient({
path: source,
sourcePath: source,
sha256: "0".repeat(64),
kind: "archive",
format: "asar",
})
/** Raw directory completeness belongs to inventory/materialization, not file target admission. */
const extractDirectory = async (source: string, output: string) => {
const materialize = await inventoriedExtraction(source, output);
return materialize();
};
const extractArchive = async (source: string, output: string) => {
const target = await parseBinaryTarget(source);
if (!target.ok) throw target.error;
return new ArtifactProvider(process.env)
.createClient(target.value)
.execute("extract_artifact", { output_root: output });
};
@@ -3,11 +3,13 @@ import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { elfExportImage } from "../../../fixtures/binaryDiagnostics/nativeExports.js";
import { writeOrderedZip } from "../../../fixtures/artifactEntryOrder.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { buildGodotPck, embedGodotPck } from "../../../fixtures/godotPck.js";
import { buildGodotPck, pckU64 } from "../../../fixtures/godotPck.js";
import { withArtifactClient } from "../../../support/artifactClientScope.js";
import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js";
import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js";
import {
CPU,
@@ -22,13 +24,17 @@ describe("artifact extraction format support", () => {
it("refuses unsupported formats before inventory or output", async () => {
const root = await createTestTempDirectory("rea-extract-format-");
const image = join(root, "Image.dmg");
await writeFile(image, "not mounted");
const udif = Buffer.alloc(512);
udif.write("koly");
await writeFile(image, udif);
const alias = join(root, "Alias.dmg");
await symlink(image, alias);
const output = join(root, "output");
const selected = await admittedTarget(alias);
expect(selected.format).toBe("dmg");
const extract = (signal?: AbortSignal) =>
new ArtifactProvider(process.env)
.createClient(archiveTarget(alias, "dmg"))
.createClient(selected)
.execute(
"extract_artifact",
artifactExtractionExecutionSchema.parse({ output_root: output }),
@@ -59,7 +65,7 @@ describe("artifact extraction format support", () => {
const output = join(root, "output");
expect(
await new ArtifactProvider(process.env)
.createClient(thinMachOTarget(binary))
.createClient(await admittedTarget(binary))
.execute(
"extract_artifact",
artifactExtractionExecutionSchema.parse({ output_root: output }),
@@ -99,7 +105,7 @@ describe("artifact extraction format support", () => {
]),
);
const result = await new ArtifactProvider(process.env)
.createClient(thinMachOTarget(binary))
.createClient(await admittedTarget(binary))
.execute(
"extract_artifact",
artifactExtractionExecutionSchema.parse({
@@ -119,39 +125,40 @@ describe("artifact extraction reader selection", () => {
const root = await createTestTempDirectory("rea-extract-embedded-pck-");
const binary = join(root, "game");
const output = join(root, "output");
const pack = buildGodotPck(3, [
{ path: "res://main.gd", data: "extends Node\n" },
]);
await writeFile(
binary,
embedGodotPck(
buildGodotPck(3, [{ path: "res://main.gd", data: "extends Node\n" }]),
),
);
await withArtifactClient(
{
path: binary,
sourcePath: binary,
sha256: "0".repeat(64),
kind: "executable",
format: "elf",
architecture: "x86_64",
availableArchitectures: ["x86_64"],
},
async (client) => {
const result = await client.execute("extract_artifact", {
output_root: output,
});
if (!result.ok) throw result.error;
const extracted = artifactExtractionResultSchema.parse(
result.value.result,
);
expect(extracted.manifest.root_format).toBe("godot-pck");
expect(
extracted.artifacts.map(({ relative_path }) => relative_path),
).toEqual(["main.gd"]);
expect(await readFile(join(output, "main.gd"), "utf8")).toBe(
"extends Node\n",
);
},
Buffer.concat([
elfExportImage({ symbols: [] }),
pack,
pckU64(pack.length),
Buffer.from("GDPC"),
]),
);
const selected = await admittedTarget(binary);
expect(selected).toMatchObject({
kind: "executable",
format: "elf",
architecture: "x86_64",
});
await withArtifactClient(await admittedTarget(binary), async (client) => {
const result = await client.execute("extract_artifact", {
output_root: output,
});
if (!result.ok) throw result.error;
const extracted = artifactExtractionResultSchema.parse(
result.value.result,
);
expect(extracted.manifest.root_format).toBe("godot-pck");
expect(
extracted.artifacts.map(({ relative_path }) => relative_path),
).toEqual(["main.gd"]);
expect(await readFile(join(output, "main.gd"), "utf8")).toBe(
"extends Node\n",
);
});
});
it.each(["zip", "hap", "hsp", "app-pack"] as const)(
@@ -164,7 +171,7 @@ describe("artifact extraction reader selection", () => {
);
await writeOrderedZip(archive, ["payload.txt"]);
const result = await new ArtifactProvider(process.env)
.createClient(archiveTarget(archive, format))
.createClient(await admittedTarget(archive))
.execute(
"extract_artifact",
artifactExtractionExecutionSchema.parse({
@@ -182,23 +189,8 @@ describe("artifact extraction reader selection", () => {
);
});
const archiveTarget = (
path: string,
format: Extract<BinaryTarget, { kind: "archive" }>["format"],
): BinaryTarget => ({
path,
sourcePath: path,
sha256: "0".repeat(64),
kind: "archive",
format,
});
const thinMachOTarget = (path: string): BinaryTarget => ({
path,
sourcePath: path,
sha256: "0".repeat(64),
kind: "executable",
format: "mach-o",
architecture: "arm64",
availableArchitectures: ["arm64"],
});
const admittedTarget = async (path: string): Promise<BinaryTarget> => {
const parsed = await parseBinaryTarget(path);
if (!parsed.ok) throw parsed.error;
return parsed.value;
};
@@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest";
import { writeOrderedZip } from "../../../fixtures/artifactEntryOrder.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js";
import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js";
import { artifactExtractionExecutionSchema } from "../../../../src/contracts/artifactToolContracts.js";
import { artifactExtractionResultSchema } from "../../../../src/domain/artifactGraph.js";
@@ -16,7 +17,7 @@ describe("artifact extraction identity", () => {
const source = join(root, "source.zip");
await writeOrderedZip(source, ["ä.js", "a.js", "Z.js"]);
const result = await new ArtifactProvider(process.env)
.createClient(archiveTarget(source))
.createClient(await archiveTarget(source))
.execute(
"extract_artifact",
artifactExtractionExecutionSchema.parse({
@@ -34,10 +35,8 @@ describe("artifact extraction identity", () => {
});
});
const archiveTarget = (path: string): BinaryTarget => ({
path,
sourcePath: path,
sha256: "0".repeat(64),
kind: "archive",
format: "zip",
});
const archiveTarget = async (path: string): Promise<BinaryTarget> => {
const parsed = await parseBinaryTarget(path);
if (!parsed.ok) throw parsed.error;
return parsed.value;
};
@@ -4,11 +4,13 @@ import { join } from "node:path";
import { expect, it, vi } from "vitest";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { CPU_ARM64, universalMacho } from "../../../fixtures/universalMacho.js";
import { thinMach } from "../../../../src/domain/binaryTarget.fixture.js";
import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js";
import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js";
import { ArtifactReaderFailure } from "../../../../src/artifacts/ArtifactReader.js";
import { artifactExtractionExecutionSchema } from "../../../../src/contracts/artifactToolContracts.js";
import type { BinaryTarget } from "../../../../src/domain/binaryTargetTypes.js";
const sliceReader = vi.hoisted(() => ({
reached: vi.fn(),
@@ -41,10 +43,21 @@ it("routes universal Mach-O extraction through slice inventory", async () => {
);
const root = await createTestTempDirectory("rea-extract-universal-");
const image = join(root, "Universal");
await writeFile(image, Buffer.from([0xca, 0xfe, 0xba, 0xbe, 0, 0, 0, 0]));
await writeFile(
image,
universalMacho([
{
bytes: thinMach(0xfeedfacf, CPU_ARM64),
cpuType: CPU_ARM64,
cpuSubtype: 0,
},
]).bytes,
);
const target = await parseBinaryTarget(image, { hostArchitecture: "arm64" });
if (!target.ok) throw target.error;
const output = join(root, "output");
const result = await new ArtifactProvider(process.env)
.createClient(universalTarget(image))
.createClient(target.value)
.execute(
"extract_artifact",
artifactExtractionExecutionSchema.parse({ output_root: output }),
@@ -61,14 +74,3 @@ it("routes universal Mach-O extraction through slice inventory", async () => {
expect(sliceReader.reached).toHaveBeenCalledOnce();
await expect(access(output)).rejects.toThrow();
});
const universalTarget = (path: string): BinaryTarget => ({
path,
sourcePath: path,
sha256: "0".repeat(64),
kind: "executable",
format: "mach-o-universal",
architecture: "arm64",
availableArchitectures: ["arm64"],
universalSlice: { offset: 0, size: 32 },
});