fix(artifacts): select extraction readers from scanned format
This commit is contained in:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user