fix(artifacts): select extraction readers from scanned format

This commit is contained in:
64579525
2026-10-12 23:03:51 +08:00
parent 681f34879e
commit 2b29105bf9
2 changed files with 84 additions and 10 deletions
+13 -9
View File
@@ -413,7 +413,7 @@ interface ExtractionInventory {
const ZIP_FORMATS = ["ipa", "apk", "msix", "appx", "zip"] as const;
const isZipFormat = (
format: BinaryTarget["format"],
format: BinaryTarget["format"] | ArtifactGraphManifest["root_format"],
): format is (typeof ZIP_FORMATS)[number] =>
(ZIP_FORMATS as readonly string[]).includes(format);
@@ -447,15 +447,14 @@ const createReader = async (
path: string,
input: Pick<
ArtifactExtractionInput,
"inputPath" | "inputFormat" | "environment" | "resourceScope"
"inputPath" | "environment" | "resourceScope"
>,
manifest: ArtifactGraphManifest,
signal?: AbortSignal,
): Promise<ArtifactReader> => {
const format = input.inputFormat;
// The scanned container determines admission; a replaced path cannot select
// a different reader. Bind the reopened descriptor to its observed bytes.
if (manifest.root_format === "ines") {
// The current scan owns format selection; the opened target may be stale.
const format = manifest.root_format;
if (format === "ines") {
const owner = await openRootArtifact(
path,
input.resourceScope,
@@ -497,11 +496,16 @@ const createReader = async (
throw cause;
}
}
if (await requireExtractableFormat(path, input))
return new DirectoryArtifactReader(path);
if (format === "directory") return new DirectoryArtifactReader(path);
if (format === "asar") return new AsarArtifactReader(path);
if (isZipFormat(format)) return new ZipArtifactReader(path, format);
return new MachOSliceArtifactReader(path, input.environment);
if (format === "mach-o" || format === "mach-o-universal")
return new MachOSliceArtifactReader(path, input.environment);
throw new AnalysisUnsupportedTargetError(
"extract_artifact",
input.inputPath,
`Artifact format has no extraction reader: ${format}`,
);
};
const preflight = (entry: ArtifactEntry): void => {
@@ -1,15 +1,85 @@
import { access, symlink, writeFile } from "node:fs/promises";
import { createPackage } from "@electron/asar";
import { access, mkdir, readFile, symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js";
import { inesFixture } from "../../../fixtures/ines.js";
import { writeOrderedZip } from "../../../fixtures/artifactEntryOrder.js";
import { artifactExtractionResultSchema } from "../../../../src/domain/artifactGraph.js";
import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js";
import { artifactExtractionExecutionSchema } from "../../../../src/contracts/artifactToolContracts.js";
import type { BinaryTarget } from "../../../../src/domain/binaryTargetTypes.js";
describe("artifact extraction format support", () => {
it.each(["zip", "asar"] as const)(
"extracts the scanned %s after an active iNES path is replaced",
async (format) => {
const root = await createTestTempDirectory("rea-extract-replaced-");
const path = join(root, `source.${format}`);
await writeFile(path, inesFixture());
const target = await parseBinaryTarget(path);
if (!target.ok) throw target.error;
expect(target.value.format).toBe("ines");
const client = new ArtifactProvider(process.env).createClient(
target.value,
);
if (format === "zip") {
await writeOrderedZip(path, ["payload.txt"]);
} else {
const source = join(root, "source");
await mkdir(source);
await writeFile(join(source, "payload.txt"), "evidence\n");
await createPackage(source, path);
}
const output = join(root, "output");
const result = await client.execute(
"extract_artifact",
artifactExtractionExecutionSchema.parse({ output_root: output }),
);
if (!result.ok) throw result.error;
const extracted = artifactExtractionResultSchema.parse(
result.value.result,
);
expect(extracted.manifest.root_format).toBe(format);
expect(
extracted.artifacts.map(({ relative_path }) => relative_path),
).toEqual(["payload.txt"]);
expect(await readFile(join(output, "payload.txt"), "utf8")).toBe(
"evidence\n",
);
},
);
it("refuses a scanned non-container instead of selecting a stale reader", async () => {
const root = await createTestTempDirectory("rea-extract-replaced-file-");
const path = join(root, "source.bin");
await writeFile(path, inesFixture());
const target = await parseBinaryTarget(path);
if (!target.ok) throw target.error;
const client = new ArtifactProvider(process.env).createClient(target.value);
await writeFile(path, "ordinary replacement bytes");
const output = join(root, "output");
expect(
await client.execute(
"extract_artifact",
artifactExtractionExecutionSchema.parse({ output_root: output }),
),
).toMatchObject({
ok: false,
error: {
_tag: "AnalysisUnsupportedTargetError",
reason: expect.stringContaining(
"Artifact format has no extraction reader:",
),
},
});
await expect(access(output)).rejects.toThrow();
});
it("refuses unsupported formats before inventory or output", async () => {
const root = await createTestTempDirectory("rea-extract-format-");
const image = join(root, "Image.dmg");