89 lines
4.3 KiB
JavaScript
89 lines
4.3 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import test from "node:test";
|
|
import ts from "typescript";
|
|
|
|
const root = path.resolve(import.meta.dirname, "../../..");
|
|
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-link-safety-unit-"));
|
|
const sourcePath = path.join(root, "web/protocol/archive-link-safety.ts");
|
|
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
|
|
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
|
|
fileName: sourcePath,
|
|
reportDiagnostics: true,
|
|
});
|
|
assert.deepEqual(transpiled.diagnostics, []);
|
|
fs.writeFileSync(path.join(temporary, "archive-link-safety.mjs"), transpiled.outputText);
|
|
const safety = await import(pathToFileURL(path.join(temporary, "archive-link-safety.mjs")));
|
|
const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04D/manifest.json"), "utf8"));
|
|
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex");
|
|
|
|
const valid = {
|
|
schemaVersion: 1,
|
|
temporaryRootId: "staging:archive-1",
|
|
entries: [
|
|
{ path: "payload/data.bin", type: "FILE", target: null },
|
|
{ path: "payload/data-alias.bin", type: "SYMLINK", target: "./data.bin" },
|
|
{ path: "payload/data-hard.bin", type: "HARDLINK", target: "payload/data.bin" },
|
|
{ path: "payload", type: "DIRECTORY", target: null },
|
|
{ path: "alias-dir", type: "SYMLINK", target: "payload" },
|
|
],
|
|
};
|
|
|
|
test("M12-04D binds the archive link gate and evidence artifacts", () => {
|
|
assert.equal(manifest.task, "M12-04D");
|
|
assert.equal(manifest.parentTask, "M12-04C");
|
|
assert.equal(manifest.nextTask, "M12-04E");
|
|
for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
|
|
});
|
|
|
|
test("M12-04D resolves symlinks relative to their parent and hardlinks from the archive root", () => {
|
|
const result = safety.resolveArchiveLinkEntries(valid);
|
|
assert.equal(result.status, "READY");
|
|
assert.equal(result.entries.find((entry) => entry.path === "payload/data-alias.bin").resolvedPath, "payload/data.bin");
|
|
assert.equal(result.entries.find((entry) => entry.path === "payload/data-hard.bin").resolvedPath, "payload/data.bin");
|
|
assert.equal(result.entries.find((entry) => entry.path === "alias-dir").resolvedPath, "payload");
|
|
assert.ok(result.entries.every((entry) => entry.withinTemporaryRoot === true));
|
|
});
|
|
|
|
test("M12-04D rejects absolute, drive, URI and traversal targets before writing", () => {
|
|
for (const target of ["/outside", "\\\\server\\share", "C:/outside", "https://evil.example/a", "../../outside", "payload/../../outside"]) {
|
|
assert.throws(() => safety.resolveArchiveLinkEntries({
|
|
...valid,
|
|
entries: [{ path: "payload/link", type: "SYMLINK", target }, { path: "payload", type: "DIRECTORY", target: null }],
|
|
}), { code: "IO_ARCHIVE_UNSAFE" });
|
|
}
|
|
});
|
|
|
|
test("M12-04D rejects missing targets, cycles and hardlinks to directories", () => {
|
|
assert.throws(() => safety.resolveArchiveLinkEntries({
|
|
...valid,
|
|
entries: [{ path: "link", type: "SYMLINK", target: "missing.bin" }],
|
|
}), { code: "IO_ARCHIVE_UNSAFE" });
|
|
assert.throws(() => safety.resolveArchiveLinkEntries({
|
|
...valid,
|
|
entries: [
|
|
{ path: "a", type: "SYMLINK", target: "b" },
|
|
{ path: "b", type: "HARDLINK", target: "a" },
|
|
],
|
|
}), { code: "IO_ARCHIVE_UNSAFE" });
|
|
assert.throws(() => safety.resolveArchiveLinkEntries({
|
|
...valid,
|
|
entries: [
|
|
{ path: "dir", type: "DIRECTORY", target: null },
|
|
{ path: "dir-hard", type: "HARDLINK", target: "dir" },
|
|
],
|
|
}), { code: "IO_ARCHIVE_UNSAFE" });
|
|
});
|
|
|
|
test("M12-04D rejects duplicate members and undeclared fields", () => {
|
|
assert.throws(() => safety.parseArchiveLinkRequest({ ...valid, entries: [{ ...valid.entries[0], target: null }, { ...valid.entries[0], target: null }] }), { code: "IO_ARCHIVE_UNSAFE" });
|
|
assert.throws(() => safety.parseArchiveLinkRequest({ ...valid, future: true }), { code: "IO_ARCHIVE_UNSAFE" });
|
|
assert.throws(() => safety.parseArchiveLinkRequest({ ...valid, entries: [{ ...valid.entries[0], mode: 0o644 }] }), { code: "IO_ARCHIVE_UNSAFE" });
|
|
});
|
|
|
|
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));
|