import { readdir, readFile, realpath, stat } from "node:fs/promises"; import path from "node:path"; import type { ComputerFileEntry } from "@rakazo/adapter-kit"; import { normalizeWorkspacePath } from "./computer-support.js"; import { isAllowedDesktopPath } from "./desktop-sandbox-paths.js"; import { isProjectWorkspacePath, projectWorkspaceRelativePath, resolveProjectHostPath, } from "./project-workspace-path.js"; export async function listProjectWorkspaceFiles( workspaceRoot: string, storedPath: string, ): Promise { if (!isProjectWorkspacePath(storedPath)) { throw new Error("Path is not under the project workspace"); } const root = await realpath(workspaceRoot); const hostDirectory = await containedHostPath( root, resolveProjectHostPath(workspaceRoot, storedPath), ); const relativeDirectory = projectWorkspaceRelativePath(storedPath); const entries = await readdir(hostDirectory, { withFileTypes: true }); const listed = await Promise.all( entries.map(async (entry) => { // A symlink that leaves the linked folder, or leads nowhere, is not part of the project. const info = await containedHostPath(root, path.join(hostDirectory, entry.name)) .then((target) => stat(target)) .catch(() => null); if (!info) return []; const childStored = normalizeWorkspacePath( relativeDirectory ? `project/${relativeDirectory}/${entry.name}` : `project/${entry.name}`, ); return [ { path: childStored, kind: info.isDirectory() ? ("dir" as const) : ("file" as const), size: info.size, ...(info.isFile() && info.mode & 0o100 ? { executable: true } : {}), }, ]; }), ); return listed.flat(); } export async function readProjectWorkspaceFile( workspaceRoot: string, storedPath: string, options?: { maxBytes?: number }, ): Promise { const hostPath = await containedHostPath( await realpath(workspaceRoot), resolveProjectHostPath(workspaceRoot, storedPath), ); const info = await stat(hostPath); if (options?.maxBytes !== undefined && info.size > options.maxBytes) { throw new Error(`computer file exceeds ${options.maxBytes} bytes`); } return new Uint8Array(await readFile(hostPath)); } /** Follows symlinks, but only to places inside the linked folder. */ async function containedHostPath(root: string, candidate: string): Promise { const target = await realpath(candidate); if (!isAllowedDesktopPath(target, [root])) { throw new Error("Path escapes the project workspace"); } return target; }