rea/tests/boundary/providers/native/artifactProvider.archiveSaf...

179 lines
6.7 KiB
TypeScript

import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { createDirectAnalysis } from "../../../../src/composition/directAnalysis.js";
import {
ArtifactPathRegistry,
destinationCaseCollisionMessage,
ENGLISH_UNICODE_CASE_INVENTORY_LIMITATION,
normalizeArtifactPath,
} from "../../../../src/artifacts/ArtifactPaths.js";
import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js";
import { ArtifactReaderFailure } from "../../../../src/artifacts/ArtifactReader.js";
import { artifactExtractionExecutionSchema } from "../../../../src/contracts/artifactToolContracts.js";
import { artifactInventoryResultSchema } from "../../../../src/domain/artifactGraph.js";
import type { BinaryTarget } from "../../../../src/domain/binaryTargetTypes.js";
import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js";
import { parseEvidence } from "../../../../src/domain/evidence.js";
const { runProviderAnalysis } = createDirectAnalysis({});
describe("artifact archive safety", () => {
it.each([
["fixture.msix", "msix"],
["fixture.appxbundle", "appx"],
] as const)(
"reuses the hardened ZIP reader for %s package inventory",
async (name, format) => {
const root = await createTestTempDirectory("rea-windows-package-");
const path = join(root, name);
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add(
"Assets/main.js",
new TextReader("export default 'windows';"),
);
await writer.add(
"VFS/ProgramFilesX64/App/addon.node",
new TextReader("native"),
);
await writeFile(path, await writer.close());
const parsed = await parseBinaryTarget(path);
expect(parsed.ok && parsed.value).toMatchObject({
kind: "archive",
format,
});
if (!parsed.ok) return;
const result = await inventory(parsed.value);
expect(result.manifest.root_format).toBe(format);
expect(
result.occurrences.map(({ artifact_kind }) => artifact_kind),
).toEqual(expect.arrayContaining(["javascript", "native-addon"]));
expect(
result.occurrences.some(
({ logical_path: logicalPath }) => logicalPath === "Assets/main.js",
),
).toBe(true);
const output = join(root, "output");
const extracted = await new ArtifactProvider(process.env)
.createClient(parsed.value)
.execute(
"extract_artifact",
artifactExtractionExecutionSchema.parse({
output_root: output,
}),
);
expect(extracted.ok).toBe(true);
expect(await readFile(join(output, "Assets", "main.js"), "utf8")).toBe(
"export default 'windows';",
);
expect(
parseEvidence(
await runProviderAnalysis(path, "inventory_artifact", {}),
),
).toMatchObject({
operation: "inventory_artifact",
subject: { format },
});
},
);
it("rejects unsafe paths and collisions", async () => {
let unsafePathError: unknown;
try {
normalizeArtifactPath("../escape");
} catch (error: unknown) {
unsafePathError = error;
}
expect(unsafePathError).toBeInstanceOf(ArtifactReaderFailure);
expect(unsafePathError).toMatchObject({
reason: "path",
message: 'Artifact path is not normalized: "../escape"',
});
const registry = new ArtifactPathRegistry();
registry.add("A.js", "file");
expect(() => registry.add("a.js", "file")).not.toThrow();
expect(() => registry.add("A.js", "file")).toThrow(ArtifactReaderFailure);
for (const [filePath, childPath] of [
["Foo", "foo/bar"],
["root/Foo", "root/foo/bar"],
] as const) {
const casePrefixRegistry = new ArtifactPathRegistry();
casePrefixRegistry.add(filePath, "file");
expect(() => casePrefixRegistry.add(childPath, "file")).not.toThrow();
}
const exactPrefixRegistry = new ArtifactPathRegistry();
exactPrefixRegistry.add("Foo", "file");
expect(() => exactPrefixRegistry.add("Foo/bar", "file")).toThrow(
ArtifactReaderFailure,
);
const sameDirectoryRegistry = new ArtifactPathRegistry();
sameDirectoryRegistry.add("Foo/one.js", "file");
expect(() => sameDirectoryRegistry.add("Foo/two.js", "file")).not.toThrow();
const caseVariantDirectoryRegistry = new ArtifactPathRegistry();
caseVariantDirectoryRegistry.add("Foo/one.js", "file");
expect(() =>
caseVariantDirectoryRegistry.add("foo/two.js", "file"),
).not.toThrow();
});
it("names a destination case collision from the directory listing", () => {
expect(
destinationCaseCollisionMessage("res/2f.xml", ["2F.xml", "other.xml"]),
).toBe(
"Destination filesystem cannot store both res/2f.xml and res/2F.xml; inventory retains both logical names.",
);
expect(
destinationCaseCollisionMessage("res/2f.xml", ["2f.xml", "2F.xml"]),
).toBeUndefined();
});
it("extracts logical names that differ only in case", async () => {
const root = await createTestTempDirectory("rea-case-distinct-zip-");
const path = join(root, "fixture.zip");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add("Main.js", new TextReader("upper"));
await writer.add("main.js", new TextReader("lower"));
await writeFile(path, await writer.close());
const parsed = await parseBinaryTarget(path);
expect(parsed.ok).toBe(true);
if (!parsed.ok) return;
const result = await inventory(parsed.value);
expect(result.limitations).toContain(
ENGLISH_UNICODE_CASE_INVENTORY_LIMITATION,
);
expect(
result.occurrences
.filter(({ logical_path }) => logical_path !== ".")
.map(({ logical_path }) => logical_path)
.sort(),
).toEqual(["Main.js", "main.js"]);
const output = join(root, "output");
const extracted = await new ArtifactProvider(process.env)
.createClient(parsed.value)
.execute(
"extract_artifact",
artifactExtractionExecutionSchema.parse({ output_root: output }),
);
expect(extracted.ok).toBe(true);
expect(await readFile(join(output, "Main.js"), "utf8")).toBe("upper");
expect(await readFile(join(output, "main.js"), "utf8")).toBe("lower");
});
});
const inventory = async (targetValue: BinaryTarget) => {
const result = await new ArtifactProvider(process.env)
.createClient(targetValue)
.execute("inventory_artifact", {});
if (!result.ok) throw result.error;
return artifactInventoryResultSchema.parse(result.value.result);
};