import { randomUUID } from "node:crypto"; import { constants } from "node:fs"; import { chmod, lstat, mkdir, open, rename, rm, symlink, } from "node:fs/promises"; import { basename, dirname, isAbsolute, join, relative, resolve, } from "node:path"; import { BUILD_PROTOCOL_VERSION, assertSha256Digest, type Sha256Digest, type WorkspaceFile, type WorkspaceManifest, } from "../shared/build-protocol"; export interface MaterializeCas { get(digest: Sha256Digest): Promise; has(digest: Sha256Digest): Promise; } function safePath(path: string): boolean { return ( path.length > 0 && !isAbsolute(path) && !path.includes("\\") && !path.includes("\0") && path .split("/") .every((part) => part !== "" && part !== "." && part !== "..") ); } export function parseWorkspaceManifest(data: Uint8Array): WorkspaceManifest { let value: unknown; try { value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(data)); } catch { throw new Error("Workspace manifest is not valid UTF-8 JSON"); } const manifest = value as Partial; if ( manifest.version !== BUILD_PROTOCOL_VERSION || !Array.isArray(manifest.files) ) { throw new Error("Unsupported workspace manifest"); } const paths = new Set(); for (const file of manifest.files as WorkspaceFile[]) { if ( !file || !safePath(file.path) || (file.type !== "file" && file.type !== "symlink") || !Number.isSafeInteger(file.size) || file.size < 0 || ![0o644, 0o755, 0o777].includes(file.mode) ) { throw new Error("Workspace manifest contains an invalid file"); } assertSha256Digest(file.digest); if (paths.has(file.path)) throw new Error(`Duplicate workspace path: ${file.path}`); for (const parent of dirname(file.path).split("/")) { if (parent && paths.has(parent)) { throw new Error(`Workspace path conflicts with a file: ${file.path}`); } } paths.add(file.path); } for (const path of paths) { if ([...paths].some((other) => other.startsWith(`${path}/`))) { throw new Error(`Workspace path conflicts with a directory: ${path}`); } } return manifest as WorkspaceManifest; } function safeSymlinkTarget(filePath: string, target: string): boolean { if ( !target || isAbsolute(target) || target.includes("\\") || target.includes("\0") ) return false; const resolved = resolve("/workspace", dirname(filePath), target); return resolved === "/workspace" || resolved.startsWith("/workspace/"); } export async function materializeWorkspace( cas: MaterializeCas, manifestDigest: Sha256Digest, destination: string, ): Promise { assertSha256Digest(manifestDigest); const manifest = parseWorkspaceManifest(await cas.get(manifestDigest)); const missing: Sha256Digest[] = []; for (const file of manifest.files) if (!(await cas.has(file.digest))) missing.push(file.digest); if (missing.length) throw new Error(`Workspace blobs are missing: ${missing.join(", ")}`); await mkdir(dirname(destination), { recursive: true }); try { await lstat(destination); throw new Error(`Workspace destination already exists: ${destination}`); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; } const temporary = join( dirname(destination), `.${basename(destination)}.${process.pid}.${randomUUID()}.tmp`, ); await mkdir(temporary, { mode: 0o755 }); try { for (const file of manifest.files) { const target = join(temporary, file.path); if (relative(temporary, target).startsWith("..")) throw new Error("Unsafe workspace path"); await mkdir(dirname(target), { recursive: true, mode: 0o755 }); const data = await cas.get(file.digest); if (data.byteLength !== file.size) { throw new Error(`Workspace blob size mismatch for ${file.path}`); } if (file.type === "symlink") { const link = new TextDecoder("utf-8", { fatal: true }).decode(data); if (!safeSymlinkTarget(file.path, link)) throw new Error(`Unsafe symlink target for ${file.path}`); await symlink(link, target); } else { const handle = await open( target, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, file.mode, ); try { await handle.writeFile(data); } finally { await handle.close(); } await chmod(target, file.mode); } } await rename(temporary, destination); } catch (error) { await rm(temporary, { recursive: true, force: true }); throw error; } return manifest; }