import { randomUUID } from "node:crypto"; import { constants, type Stats } from "node:fs"; import { lstat, mkdir, open, realpath, rename, rm, } from "node:fs/promises"; import path from "node:path"; import { normalizeRepositoryRelativePath } from "./repository-file-inventory.ts"; type WritableHandle = Readonly<{ writeFile(data: string): Promise; sync(): Promise; close(): Promise; }>; type DirectoryHandle = Readonly<{ sync(): Promise; close(): Promise; }>; export type RiskCoverageArtifactFileSystem = Readonly<{ openFile(target: string, flags: number, mode: number): Promise; openDirectory(target: string): Promise; rename(source: string, destination: string): Promise; rm(target: string, options: Readonly<{ force: true }>): Promise; }>; type WriterDependencies = Readonly<{ createNonce?: () => string; fileSystem?: RiskCoverageArtifactFileSystem; }>; type ReadableHandle = Readonly<{ stat(): Promise; readFile(encoding: "utf8"): Promise; close(): Promise; }>; type InputDependencies = Readonly<{ lstatPath?: (target: string) => Promise; realpathPath?: (target: string) => Promise; openFile?: (target: string, flags: number) => Promise; }>; const defaultFileSystem: RiskCoverageArtifactFileSystem = Object.freeze({ openFile: async (target, flags, mode) => { const handle = await open(target, flags, mode); return { writeFile: async (data) => handle.writeFile(data, "utf8"), sync: async () => handle.sync(), close: async () => handle.close(), }; }, openDirectory: async (target) => { const handle = await open(target, constants.O_RDONLY); return { sync: async () => handle.sync(), close: async () => handle.close(), }; }, rename, rm, }); function hasErrorCode(error: unknown, code: string): boolean { return ( typeof error === "object" && error !== null && "code" in error && error.code === code ); } function isWithin(root: string, target: string): boolean { const relative = path.relative(root, target); return ( relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)) ); } function hasStableIdentity(metadata: Stats): boolean { return ( Number.isSafeInteger(metadata.dev) && Number.isSafeInteger(metadata.ino) && metadata.dev > 0 && metadata.ino > 0 ); } function sameFileIdentity(before: Stats, after: Stats): boolean { if (!hasStableIdentity(before) || !hasStableIdentity(after)) { throw new TypeError("stable file identity unavailable"); } return before.dev === after.dev && before.ino === after.ino; } async function rejectSymlinkAncestors( repositoryRoot: string, relativePath: string, label: string, lstatPath: (target: string) => Promise, ): Promise { let current = repositoryRoot; let currentRelative = ""; const directory = path.posix.dirname(relativePath); if (directory === ".") return; for (const segment of directory.split("/")) { current = path.join(current, segment); currentRelative = currentRelative ? `${currentRelative}/${segment}` : segment; const metadata = await lstatPath(current); if (metadata.isSymbolicLink()) { throw new TypeError(`${label} ancestor is a symlink: ${currentRelative}`); } if (!metadata.isDirectory()) { throw new TypeError(`${label} ancestor is not a directory: ${currentRelative}`); } } } function assertDirectory(metadata: Stats, relativePath: string): void { if (metadata.isSymbolicLink()) { throw new TypeError(`artifact ancestor is a symlink: ${relativePath}`); } if (!metadata.isDirectory()) { throw new TypeError(`artifact ancestor is not a directory: ${relativePath}`); } } export async function readRiskCoverageInput(input: Readonly<{ repositoryRoot: string; relativePath: string; label: string; }>, dependencies: InputDependencies = {}): Promise> { const lstatPath = dependencies.lstatPath ?? lstat; const realpathPath = dependencies.realpathPath ?? realpath; const openFile = dependencies.openFile ?? ((target: string, flags: number) => open(target, flags)); const relativePath = normalizeRepositoryRelativePath( input.relativePath, `${input.label} path`, ); const repositoryRoot = path.resolve(input.repositoryRoot); const repositoryRealpath = await realpathPath(repositoryRoot); const absolutePath = path.resolve(repositoryRoot, relativePath); await rejectSymlinkAncestors( repositoryRoot, relativePath, input.label, lstatPath, ); const metadata = await lstatPath(absolutePath); if (metadata.isSymbolicLink()) { throw new TypeError(`${input.label} path is a symlink: ${relativePath}`); } if (!metadata.isFile()) { throw new TypeError(`${input.label} path is not a regular file: ${relativePath}`); } const resolvedPath = await realpathPath(absolutePath); if (!isWithin(repositoryRealpath, resolvedPath)) { throw new TypeError(`${input.label} path is outside repository: ${relativePath}`); } const handle = await openFile( absolutePath, constants.O_RDONLY | constants.O_NOFOLLOW, ); try { const openedMetadata = await handle.stat(); if (!openedMetadata.isFile()) { throw new TypeError(`${input.label} path is not a regular file: ${relativePath}`); } if (!sameFileIdentity(metadata, openedMetadata)) { throw new TypeError(`${input.label} path changed during validation: ${relativePath}`); } const text = await handle.readFile("utf8"); return Object.freeze({ relativePath, absolutePath, text }); } finally { await handle.close(); } } export async function resolveRiskCoverageArtifactPath(input: Readonly<{ repositoryRoot: string; relativePath: string; inputPaths: readonly string[]; }>): Promise { const relativePath = normalizeRepositoryRelativePath( input.relativePath, "artifact path", ); if (!relativePath.startsWith("artifacts/quality/")) { throw new TypeError("artifact path must be below artifacts/quality"); } const normalizedInputs = input.inputPaths.map((inputPath) => normalizeRepositoryRelativePath(inputPath, "input path"), ); if (normalizedInputs.includes(relativePath)) { throw new TypeError(`artifact path must not overwrite an input: ${relativePath}`); } const repositoryRoot = path.resolve(input.repositoryRoot); const repositoryRealpath = await realpath(repositoryRoot); const relativeDirectory = path.posix.dirname(relativePath); let currentDirectory = repositoryRoot; let currentRelative = ""; for (const segment of relativeDirectory.split("/")) { currentDirectory = path.join(currentDirectory, segment); currentRelative = currentRelative ? `${currentRelative}/${segment}` : segment; let metadata: Stats; try { metadata = await lstat(currentDirectory); } catch (error) { if (!hasErrorCode(error, "ENOENT")) throw error; try { await mkdir(currentDirectory); } catch (mkdirError) { if (!hasErrorCode(mkdirError, "EEXIST")) throw mkdirError; } metadata = await lstat(currentDirectory); } assertDirectory(metadata, currentRelative); const resolvedDirectory = await realpath(currentDirectory); if (!isWithin(repositoryRealpath, resolvedDirectory)) { throw new TypeError(`artifact ancestor is outside repository: ${currentRelative}`); } } const absolutePath = path.resolve(repositoryRoot, relativePath); let destinationMetadata: Stats | undefined; try { destinationMetadata = await lstat(absolutePath); if (destinationMetadata.isSymbolicLink()) { throw new TypeError(`artifact path is a symlink: ${relativePath}`); } if (!destinationMetadata.isFile()) { throw new TypeError(`artifact path is not a regular file: ${relativePath}`); } } catch (error) { if (!hasErrorCode(error, "ENOENT")) throw error; } if (destinationMetadata) { if (!hasStableIdentity(destinationMetadata)) { throw new TypeError("artifact stable file identity unavailable"); } const destinationRealpath = await realpath(absolutePath); for (const inputPath of normalizedInputs) { const inputAbsolutePath = path.resolve(repositoryRoot, inputPath); const inputRealpath = await realpath(inputAbsolutePath); const inputMetadata = await lstat(inputAbsolutePath); if (!hasStableIdentity(inputMetadata)) { throw new TypeError(`input stable file identity unavailable: ${inputPath}`); } if ( inputRealpath === destinationRealpath || (inputMetadata.dev === destinationMetadata.dev && inputMetadata.ino === destinationMetadata.ino) ) { throw new TypeError(`artifact path is the same file as an input: ${inputPath}`); } } } return absolutePath; } export async function writeRiskCoverageArtifactAtomic( input: Readonly<{ repositoryRoot: string; relativePath: string; inputPaths: readonly string[]; value: unknown; }>, dependencies: WriterDependencies = {}, ): Promise { const serialized = JSON.stringify(input.value, null, 2); if (serialized === undefined) { throw new TypeError("risk coverage artifact is not JSON serializable"); } const destination = await resolveRiskCoverageArtifactPath(input); const temporaryPath = path.join( path.dirname(destination), `.${path.basename(destination)}.${(dependencies.createNonce ?? randomUUID)()}.tmp`, ); const fileSystem = dependencies.fileSystem ?? defaultFileSystem; let ownsTemporaryFile = false; try { const handle = await fileSystem.openFile( temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600, ); ownsTemporaryFile = true; let primaryFailure: unknown; try { await handle.writeFile(`${serialized}\n`); await handle.sync(); } catch (error) { primaryFailure = error; } try { await handle.close(); } catch (error) { primaryFailure ??= error; } if (primaryFailure !== undefined) throw primaryFailure; await fileSystem.rename(temporaryPath, destination); ownsTemporaryFile = false; const directoryHandle = await fileSystem.openDirectory(path.dirname(destination)); try { try { await directoryHandle.sync(); } catch (error) { // Windows and some filesystems do not support fsync on directory handles. if (!hasErrorCode(error, "EINVAL") && !hasErrorCode(error, "ENOTSUP")) { throw error; } } } finally { await directoryHandle.close(); } } catch (error) { if (ownsTemporaryFile) { try { await fileSystem.rm(temporaryPath, { force: true }); } catch { // Preserve the publication failure and clean only our nonce-owned path. } } throw error; } }