import { createHash } from "node:crypto"; import { createReadStream } from "node:fs"; import { lstat, realpath, stat } from "node:fs/promises"; import { dirname, isAbsolute, relative, resolve, sep } from "node:path"; import type { FileRef } from "./contracts.ts"; function isWithin(root: string, candidate: string): boolean { const pathFromRoot = relative(root, candidate); return pathFromRoot === "" || (!pathFromRoot.startsWith(`..${sep}`) && pathFromRoot !== ".." && !isAbsolute(pathFromRoot)); } async function nearestExistingAncestor(path: string): Promise { let current = path; while (true) { try { await stat(current); return current; } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; const parent = dirname(current); if (parent === current) throw error; current = parent; } } } export async function workspaceRoot(cwd: string): Promise { return await realpath(cwd); } export async function resolveExistingWorkspaceFile(cwd: string, inputPath: string): Promise { const root = await workspaceRoot(cwd); const lexical = resolve(root, inputPath); if (!isWithin(root, lexical)) throw new Error(`Path is outside the workspace: ${inputPath}`); if ((await lstat(lexical)).isSymbolicLink()) throw new Error(`Unsafe workspace-file symlink: ${inputPath}`); const canonical = await realpath(lexical); if (!isWithin(root, canonical)) throw new Error(`Path resolves outside the workspace: ${inputPath}`); if (!(await stat(canonical)).isFile()) throw new Error(`Path is not a file: ${inputPath}`); return canonical; } export async function resolveExistingWorkspaceDirectory(cwd: string, inputPath: string): Promise { const root = await workspaceRoot(cwd); const lexical = resolve(root, inputPath); if (!isWithin(root, lexical)) throw new Error(`Path is outside the workspace: ${inputPath}`); if ((await lstat(lexical)).isSymbolicLink()) throw new Error(`Unsafe workspace-directory symlink: ${inputPath}`); const canonical = await realpath(lexical); if (!isWithin(root, canonical)) throw new Error(`Path resolves outside the workspace: ${inputPath}`); if (!(await stat(canonical)).isDirectory()) throw new Error(`Path is not a directory: ${inputPath}`); return canonical; } export async function resolveWorkspacePath(cwd: string, inputPath: string): Promise { const root = await workspaceRoot(cwd); const lexical = resolve(root, inputPath); if (lexical === root || !isWithin(root, lexical)) throw new Error(`Path is outside the workspace: ${inputPath}`); const ancestor = await nearestExistingAncestor(dirname(lexical)); const canonicalAncestor = await realpath(ancestor); if (!isWithin(root, canonicalAncestor)) throw new Error(`Path resolves outside the workspace: ${inputPath}`); return lexical; } export async function workspaceRelativePath(cwd: string, absolutePath: string): Promise { const root = await workspaceRoot(cwd); if (!isWithin(root, absolutePath)) throw new Error(`Path is outside the workspace: ${absolutePath}`); return relative(root, absolutePath).split(sep).join("/"); } async function sha256File(path: string, signal?: AbortSignal): Promise { signal?.throwIfAborted(); const hash = createHash("sha256"); for await (const chunk of createReadStream(path, signal === undefined ? {} : { signal })) hash.update(chunk); signal?.throwIfAborted(); return hash.digest("hex"); } export async function snapshotFile(cwd: string, inputPath: string, signal?: AbortSignal): Promise { signal?.throwIfAborted(); const absolute = await resolveExistingWorkspaceFile(cwd, inputPath); return { path: await workspaceRelativePath(cwd, absolute), bytes: (await stat(absolute)).size, sha256: await sha256File(absolute, signal), }; }