fix(artifacts): read every ZIP package family extraction is asked to open (#2493)

extract_artifact carried its own copy of the ZIP package format list with five
of the eight members the domain owner declares, and used it both as the guard
that refuses a format without an extraction reader and as the selector for the
reader itself. A .hap, .hsp or App Pack target was therefore refused with
"Artifact format has no extraction reader" even though the next branch builds a
ZipArtifactReader for exactly those formats, while inventory_artifact classifies
the same path through the owner and reads it.

Import isZipPackageFormat from the domain owner and delete the duplicate list
and its predicate, removing the second representation instead of reconciling it.
The reader branch now narrows to ZipPackageFormat, which is what the
ZipArtifactReader constructor already requires.

Verified with 41 boundary extraction, ZIP integrity and detection tests,
check:fast and verify:test-discovery on macOS arm64. Reverting only the source
change fails exactly the three added families and leaves the zip control green.

Co-authored-by: 吴佳浩 <wujiahao@supremind.com>
This commit is contained in:
吴佳浩Alben 2026-10-13 14:39:29 +08:00 committed by GitHub
parent 141d2095f5
commit 7017c4d68c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 32 additions and 9 deletions

View File

@ -17,6 +17,7 @@ import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
import { SafeOutputTree } from "../SafeOutputTree.js";
import { SafeOutputTreeCreationFailure } from "../SafeOutputTreeCreationFailure.js";
import { ZipArtifactReader } from "../ZipArtifactReader.js";
import { isZipPackageFormat } from "../../domain/zipPackageFormat.js";
import { MachOSliceArtifactReader } from "../MachOSliceArtifactReader.js";
import {
artifactExtractionResultSchema,
@ -400,13 +401,6 @@ interface ExtractionInventory {
readonly integrityContradictions: readonly IntegrityContradiction[];
}
const ZIP_FORMATS = ["ipa", "apk", "msix", "appx", "zip"] as const;
const isZipFormat = (
format: BinaryTarget["format"],
): format is (typeof ZIP_FORMATS)[number] =>
(ZIP_FORMATS as readonly string[]).includes(format);
/**
* Refuse a format without an extraction reader before inventory work starts.
* The target kind, not the host, is unsupported by extraction. The error keeps
@ -422,7 +416,7 @@ const requireExtractableFormat = async (
!directory &&
format !== "asar" &&
!isMachOFormat(format) &&
!isZipFormat(format)
!isZipPackageFormat(format)
)
throw new AnalysisUnsupportedTargetError(
"extract_artifact",
@ -443,7 +437,7 @@ const createReader = async (
if (await requireExtractableFormat(path, input))
return new DirectoryArtifactReader(path);
if (format === "asar") return new AsarArtifactReader(path);
if (isZipFormat(format)) return new ZipArtifactReader(path, format);
if (isZipPackageFormat(format)) return new ZipArtifactReader(path, format);
return new MachOSliceArtifactReader(path, input.environment);
};

View File

@ -3,10 +3,12 @@ import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { writeOrderedZip } from "../../../fixtures/artifactEntryOrder.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js";
import { artifactExtractionExecutionSchema } from "../../../../src/contracts/artifactToolContracts.js";
import { artifactExtractionResultSchema } from "../../../../src/domain/artifactGraph.js";
import type { BinaryTarget } from "../../../../src/domain/binaryTargetTypes.js";
describe("artifact extraction format support", () => {
@ -42,6 +44,33 @@ describe("artifact extraction format support", () => {
});
await expect(access(output)).rejects.toThrow();
});
it.each(["zip", "hap", "hsp", "app-pack"] as const)(
"extracts the ZIP-backed %s package family that inventory already supports",
async (format) => {
const root = await createTestTempDirectory("rea-extract-zip-family-");
const archive = join(
root,
`Payload.${format === "app-pack" ? "app" : format}`,
);
await writeOrderedZip(archive, ["payload.txt"]);
const result = await new ArtifactProvider(process.env)
.createClient(archiveTarget(archive, format))
.execute(
"extract_artifact",
artifactExtractionExecutionSchema.parse({
output_root: join(root, "output"),
}),
);
if (!result.ok) throw result.error;
const extracted = artifactExtractionResultSchema.parse(
result.value.result,
);
expect(
extracted.artifacts.map(({ relative_path }) => relative_path),
).toEqual(["payload.txt"]);
},
);
});
const archiveTarget = (