#!/usr/bin/env node import { createHash } from "node:crypto"; import { lstat, readFile, readdir, realpath } from "node:fs/promises"; import { relative, resolve, sep } from "node:path"; import { pathToFileURL } from "node:url"; import { canonicalJsonDigest, canonicalValueBytes, } from "../../src/security/canonical-digest.ts"; const validClass = /^(?:git-object|git-ref|lfs|source|build|precompressed|release-archive)$/u; const byteCompare = (left, right) => Buffer.from(left).compare(Buffer.from(right)); function normalizedRelativePath(root, path) { const value = relative(root, path).split(sep).join("/"); if ( value === "" || value.startsWith("../") || value.startsWith("/") || value.includes("/../") || value !== value.normalize("NFC") ) { throw new Error( "Closure member path is outside the root or non-canonical.", ); } return value; } async function walk(root, current, members, memberClass) { const entries = await readdir(current, { withFileTypes: true }); entries.sort((a, b) => byteCompare(a.name, b.name)); for (const entry of entries) { if ( entry.name !== entry.name.normalize("NFC") || entry.name === "." || entry.name === ".." ) throw new Error("Closure path is non-canonical."); const path = resolve(current, entry.name); const info = await lstat(path); if (info.isSymbolicLink()) throw new Error("Closure cannot contain a symlink."); if (info.isDirectory()) { await walk(root, path, members, memberClass); continue; } if (!info.isFile()) throw new Error( "Closure cannot contain device, socket, FIFO, or other special entries.", ); if (info.nlink !== 1) throw new Error("Closure cannot contain hard-link aliases."); const bytes = await readFile(path); members.push({ class: memberClass, path: normalizedRelativePath(root, path), bytes: bytes.byteLength, sha256: createHash("sha256").update(bytes).digest("hex"), }); } } export async function enumerateClosure(inputs) { if (!Array.isArray(inputs) || inputs.length === 0) throw new Error("At least one explicit closure root is required."); const members = []; const roots = new Set(); for (const input of inputs) { if ( input === null || typeof input !== "object" || !validClass.test(input.class) || typeof input.root !== "string" ) { throw new Error("Closure root descriptor is invalid."); } const root = await realpath(input.root); if (roots.has(`${input.class}\0${root}`)) throw new Error("Duplicate closure root."); roots.add(`${input.class}\0${root}`); const info = await lstat(root); if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("Closure root must be a real directory."); await walk(root, root, members, input.class); } members.sort((a, b) => byteCompare(`${a.class}\0${a.path}`, `${b.class}\0${b.path}`), ); const identities = new Set(); const folded = new Set(); for (const member of members) { const identity = `${member.class}\0${member.path}`; const fold = identity.toLocaleLowerCase("en-US"); if (identities.has(identity) || folded.has(fold)) throw new Error("Closure contains a duplicate or case-fold collision."); identities.add(identity); folded.add(fold); } const value = { schema: "portfolio-publication-closure/v1", members }; return { ...value, digest: canonicalJsonDigest("portfolio-publication-closure/v1", value), }; } async function main() { const descriptors = process.argv.slice(2).map((argument) => { const separator = argument.indexOf("="); if (separator < 1) throw new Error("Usage: enumerate-closure.mjs = [...]"); return { class: argument.slice(0, separator), root: argument.slice(separator + 1), }; }); process.stdout.write( Buffer.from(canonicalValueBytes(await enumerateClosure(descriptors))), ); process.stdout.write("\n"); } if ( process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href ) { main().catch((error) => { process.stderr.write( `Closure enumeration failed: ${error instanceof Error ? error.message : "unknown error"}\n`, ); process.exitCode = 1; }); }