import { createHash } from "node:crypto"; import { lstatSync, readdirSync, readFileSync } from "node:fs"; import { basename, join, relative } from "node:path"; export function artifactFiles(root: string): string[] { const files: string[] = []; const visit = (dir: string): void => { for (const entry of readdirSync(dir, { withFileTypes: true })) { const path = join(dir, entry.name); if (entry.isSymbolicLink()) files.push(path); else if (entry.isDirectory()) visit(path); else files.push(path); } }; visit(root); return files; } export function hashArtifacts(root: string): string { const hash = createHash("sha256"); const files = artifactFiles(root) .filter((path) => basename(path) !== ".ultra-validation.json") .sort((left, right) => relative(root, left) .replaceAll("\\", "/") .localeCompare(relative(root, right).replaceAll("\\", "/")), ); for (const file of files) { if (lstatSync(file).isSymbolicLink()) throw new Error(`symlink rejected: ${file}`); const relativePath = relative(root, file).replaceAll("\\", "/"); const bytes = readFileSync(file); hash.update(String(Buffer.byteLength(relativePath))); hash.update(":"); hash.update(relativePath); hash.update(":"); hash.update(String(bytes.length)); hash.update(":"); hash.update(bytes); } return hash.digest("hex"); }