132 lines
4.9 KiB
TypeScript
132 lines
4.9 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { createHash } from "node:crypto";
|
|
import { cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { writeEngineChecksums } from "./checksummed-engine-pack.mjs";
|
|
import { DOTNET_LOCK, verifyDotNetPack } from "./dotnet-engine-pack.mjs";
|
|
|
|
const temporaryDirectories: string[] = [];
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
temporaryDirectories
|
|
.splice(0)
|
|
.map((directory) => rm(directory, { recursive: true, force: true })),
|
|
);
|
|
});
|
|
|
|
describe(".NET engine-pack gate", () => {
|
|
it("verifies the installed exact runtime, bridge sources and closed checksums", async () => {
|
|
await expect(
|
|
verifyDotNetPack(path.resolve("public", "engines", "dotnet")),
|
|
).resolves.toEqual(
|
|
expect.objectContaining({
|
|
engineVersion: ".NET 10.0.10 System.Text.RegularExpressions",
|
|
source: expect.objectContaining({
|
|
runtimeVersion: "10.0.10",
|
|
runtimeCommit: "f7d90799ce",
|
|
}),
|
|
toolchain: expect.objectContaining({
|
|
sdkVersion: "10.0.302",
|
|
sdkCommit: "35b593bebf",
|
|
workload: "wasm-tools",
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("pins the compiler/runtime identity used by the checked-in pack", () => {
|
|
expect(DOTNET_LOCK).toEqual(
|
|
expect.objectContaining({
|
|
sdkVersion: "10.0.302",
|
|
runtimeVersion: "10.0.10",
|
|
bridgeAbi: 1,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("rejects modified framework assets", async () => {
|
|
const directory = await mkdtemp(path.join(tmpdir(), "regex-dotnet-pack-"));
|
|
temporaryDirectories.push(directory);
|
|
const pack = path.join(directory, "dotnet");
|
|
await cp(path.resolve("public", "engines", "dotnet"), pack, {
|
|
recursive: true,
|
|
});
|
|
const metadata = JSON.parse(
|
|
await readFile(path.join(pack, "engine-metadata.json"), "utf8"),
|
|
) as {
|
|
files: readonly { path: string }[];
|
|
};
|
|
const runtime = metadata.files.find(({ path: name }) =>
|
|
/^_framework\/System\.Text\.RegularExpressions\..+\.wasm$/u.test(name),
|
|
);
|
|
expect(runtime).toBeDefined();
|
|
await writeFile(path.join(pack, runtime?.path ?? ""), "modified");
|
|
|
|
await expect(verifyDotNetPack(pack)).rejects.toThrow(/SHA-256 mismatch/u);
|
|
});
|
|
|
|
it("rejects substituted notices even when self-reported checksums are regenerated", async () => {
|
|
const directory = await mkdtemp(path.join(tmpdir(), "regex-dotnet-pack-"));
|
|
temporaryDirectories.push(directory);
|
|
const pack = path.join(directory, "dotnet");
|
|
await cp(path.resolve("public", "engines", "dotnet"), pack, {
|
|
recursive: true,
|
|
});
|
|
const replacement = Buffer.from("substituted notices\n");
|
|
await writeFile(path.join(pack, "THIRD-PARTY-NOTICES.txt"), replacement);
|
|
const metadataFile = path.join(pack, "engine-metadata.json");
|
|
const metadata = JSON.parse(await readFile(metadataFile, "utf8")) as {
|
|
files: { path: string; sha256: string; bytes: number }[];
|
|
};
|
|
const record = metadata.files.find(
|
|
({ path: name }) => name === "THIRD-PARTY-NOTICES.txt",
|
|
);
|
|
expect(record).toBeDefined();
|
|
if (!record) return;
|
|
record.sha256 = createHash("sha256").update(replacement).digest("hex");
|
|
record.bytes = replacement.byteLength;
|
|
await writeFile(metadataFile, `${JSON.stringify(metadata, null, 2)}\n`);
|
|
await writeEngineChecksums(pack);
|
|
|
|
await expect(verifyDotNetPack(pack)).rejects.toThrow(
|
|
/pinned runtime contract/u,
|
|
);
|
|
});
|
|
|
|
it("rejects a build-host path hidden in WebAssembly after checksums are regenerated", async () => {
|
|
const directory = await mkdtemp(path.join(tmpdir(), "regex-dotnet-pack-"));
|
|
temporaryDirectories.push(directory);
|
|
const pack = path.join(directory, "dotnet");
|
|
await cp(path.resolve("public", "engines", "dotnet"), pack, {
|
|
recursive: true,
|
|
});
|
|
const metadataFile = path.join(pack, "engine-metadata.json");
|
|
const metadata = JSON.parse(await readFile(metadataFile, "utf8")) as {
|
|
files: { path: string; sha256: string; bytes: number }[];
|
|
};
|
|
const record = metadata.files.find(({ path: name }) =>
|
|
/^_framework\/RegexTools\.DotNet\..+\.wasm$/u.test(name),
|
|
);
|
|
expect(record).toBeDefined();
|
|
if (!record) return;
|
|
const assetFile = path.join(pack, record.path);
|
|
const replacement = Buffer.concat([
|
|
await readFile(assetFile),
|
|
Buffer.from("/mnt/private/regex-tools/obj/linked/RegexTools.DotNet.pdb"),
|
|
]);
|
|
await writeFile(assetFile, replacement);
|
|
record.sha256 = createHash("sha256").update(replacement).digest("hex");
|
|
record.bytes = replacement.byteLength;
|
|
await writeFile(metadataFile, `${JSON.stringify(metadata, null, 2)}\n`);
|
|
await writeEngineChecksums(pack);
|
|
|
|
await expect(verifyDotNetPack(pack)).rejects.toThrow(
|
|
/Local absolute path/u,
|
|
);
|
|
});
|
|
});
|