const fdPath = (fd:number): string => `${process.platform === "linux" ? "/proc/self/fd" : "/dev/fd"}/${fd}`; import { createRequire } from "node:module"; import { closeSync, openSync, readSync, writeSync, fsyncSync, fstatSync, readdirSync, renameSync, unlinkSync } from "node:fs"; import { join } from "node:path"; import { spawn } from "node:child_process"; import type { WorkspaceFsAtBindingV1, NativeWorkspaceFsAtHandleV1, NativeWorkspaceFsAtStatV1, NativeWorkspaceFsAtComponentV1 } from "../native/workspace-fs-at-binding.js"; const require = createRequire(import.meta.url); const binding = require("../../native/workspace-fs-at/build/Release/workspace_fs_at.node") as WorkspaceFsAtBindingV1; export interface WorkspaceFsAtStatV1 extends NativeWorkspaceFsAtStatV1 {} export type LockFileName = "writer.lock" | "session-readers.lock"; export type WorkspaceFlockKindV1 = "shared" | "exclusive"; export type WorkspaceFlockWaitV1 = "blocking" | "nonblocking"; const component = (value: string): NativeWorkspaceFsAtComponentV1 => { if (typeof value !== "string" || value.length === 0 || Buffer.byteLength(value, "utf8") > 255 || value !== value.trim() || value === "." || value === ".." || value.includes("/") || value.includes("\\") || value.includes("\0")) throw new Error("invalid path component"); return value as NativeWorkspaceFsAtComponentV1; }; const normalizeError = (e: unknown): Error => e instanceof Error ? e : new Error(String(e)); const rawHandles = new WeakMap(); const borrowing = new WeakMap(); const live = new WeakMap(); const rawOf = (value: object): NativeWorkspaceFsAtHandleV1 => { if (!rawHandles.has(value) || live.get(value) !== true) throw Object.assign(new Error("workspace descriptor is closed"), { code: "ERR_WORKSPACE_FS_AT_HANDLE_CLOSED" }); return rawHandles.get(value)!; }; abstract class Owned { constructor(raw: NativeWorkspaceFsAtHandleV1, readonly opened: WorkspaceFsAtStatV1, readonly path: string) { rawHandles.set(this, raw); borrowing.set(this, 0); live.set(this, true); } stat(): WorkspaceFsAtStatV1 { if (live.get(this) !== true) throw Object.assign(new Error("workspace descriptor is closed"), { code: "ERR_WORKSPACE_FS_AT_HANDLE_CLOSED" }); return this.opened; } close(): void { if (live.get(this) !== true) return; if ((borrowing.get(this) ?? 0) !== 0) throw Object.assign(new Error("workspace descriptor is borrowed"), { code: "ERR_WORKSPACE_FS_AT_BORROWED" }); live.set(this, false); binding.close(rawHandles.get(this)!); } } export class OwnedWorkspaceFsAtDirectory extends Owned {} export class OwnedWorkspaceFsAtRegularFile extends Owned {} const wrapDirectory = (result: {handle: NativeWorkspaceFsAtHandleV1; openedStat: WorkspaceFsAtStatV1}, path: string): OwnedWorkspaceFsAtDirectory => { try { if ((result.openedStat.mode & 0o170000) !== 0o040000) throw new Error("not a directory"); return new OwnedWorkspaceFsAtDirectory(result.handle, result.openedStat, path); } catch (e) { try { binding.close(result.handle); } catch {} throw e; } }; const wrapFile = (result: {handle: NativeWorkspaceFsAtHandleV1; openedStat: WorkspaceFsAtStatV1}, path = ""): OwnedWorkspaceFsAtRegularFile => { try { const st=result.openedStat; if ((st.mode&0o170000)!==0o100000 || (st.mode&0o7777)!==0o600 || st.uid!==(process.getuid?.()??st.uid) || st.nlink!==1n) throw new Error("invalid regular file"); return new OwnedWorkspaceFsAtRegularFile(result.handle,st,path); } catch (e) { try { binding.close(result.handle); } catch {} throw e; } }; const wrapLock = wrapFile; const withBorrowedFd = (value: object, action: (fd:number)=>T): T => { const raw=rawOf(value), n=borrowing.get(value)??0; borrowing.set(value,n+1); try { return action(binding.fdNumberForSynchronousBorrow(raw)); } finally { borrowing.set(value,n); } }; export class WorkspaceFsAtV1 { openRoot(): OwnedWorkspaceFsAtDirectory { return wrapDirectory(binding.openat({parent:null,name:"/",kind:"directory",createMode:0}), "/"); } openDirectoryAt(parent: OwnedWorkspaceFsAtDirectory, name: string): OwnedWorkspaceFsAtDirectory { return wrapDirectory(binding.openat({parent:rawOf(parent),name:component(name),kind:"directory",createMode:0}), join(parent.path, name)); } openOrCreateLockAt(parent: OwnedWorkspaceFsAtDirectory, name: LockFileName, mode: 0o600): OwnedWorkspaceFsAtRegularFile { if ((name!=="writer.lock"&&name!=="session-readers.lock")||mode!==0o600) throw new Error("invalid lock"); return wrapLock(binding.openat({parent:rawOf(parent),name:component(name),kind:"regular_lock",createMode:0o600})); } mkdirAt(parent: OwnedWorkspaceFsAtDirectory, name:string, mode:0o700):void { binding.mkdirat(rawOf(parent),component(name),mode); } statAtNoFollow(parent:OwnedWorkspaceFsAtDirectory,name:string):WorkspaceFsAtStatV1 { return binding.fstatat(rawOf(parent),component(name)); } fsyncDirectory(directory:OwnedWorkspaceFsAtDirectory):void { binding.fsyncDirectory(rawOf(directory)); } flockOwnedLock(owned:OwnedWorkspaceFsAtRegularFile,kind:WorkspaceFlockKindV1,wait:WorkspaceFlockWaitV1):void { const ext:{flockSync(fd:number,operation:string):void}=require("fs-ext"); withBorrowedFd(owned,fd=>ext.flockSync(fd,kind==="shared"?(wait==="blocking"?"sh":"shnb"):(wait==="blocking"?"ex":"exnb"))); } } export const fsAtInternal = { raw: (v: object) => rawOf(v), withFd: withBorrowedFd, openDirectory(parent: OwnedWorkspaceFsAtDirectory,name:string):OwnedWorkspaceFsAtDirectory { return wrapDirectory(binding.openat({parent:rawOf(parent),name:component(name),kind:"directory",createMode:0}), join(parent.path, name)); }, mkdir(parent:OwnedWorkspaceFsAtDirectory,name:string):void { binding.mkdirat(rawOf(parent),component(name),0o700); }, openFile(parent:OwnedWorkspaceFsAtDirectory,name:string,access:"read"|"create"):OwnedWorkspaceFsAtRegularFile { // regular_lock is the sole native regular-file operation. Every state file is 0600, // and the no-follow open still occurs relative to the retained directory handle. if(access==="create") { try { binding.fstatat(rawOf(parent),component(name)); throw Object.assign(new Error("exists"),{code:"EEXIST"}); } catch(e) { if((e as NodeJS.ErrnoException).code!=="ENOENT") throw e; } } else binding.fstatat(rawOf(parent), component(name)); return wrapFile(binding.openat({parent:rawOf(parent),name:component(name),kind:"regular_lock",createMode:0o600})); }, readFile(file:OwnedWorkspaceFsAtRegularFile,max:number):Uint8Array { return withBorrowedFd(file,fd=>{const st=fstatSync(fd);if(st.size>max)throw new Error("file too large");const out=Buffer.alloc(st.size);let off=0;while(off{let off=0;while(offfsyncSync(fd)); }, rename(parent:OwnedWorkspaceFsAtDirectory,from:string,to:string,replace:boolean):void { if(!replace){ try{ withBorrowedFd(parent,fd=>{ readdirSync(fdPath(fd)); }); }catch{} } withBorrowedFd(parent,fd=>renameSync(`${fdPath(fd)}/${component(from)}`,`${fdPath(fd)}/${component(to)}`)); }, unlink(parent:OwnedWorkspaceFsAtDirectory,name:string):void { withBorrowedFd(parent,fd=>unlinkSync(`${fdPath(fd)}/${component(name)}`)); }, listDirectory(directory:OwnedWorkspaceFsAtDirectory):readonly string[] { return readdirSync(directory.path); }, fsyncDirectory(directory:OwnedWorkspaceFsAtDirectory):void { binding.fsyncDirectory(rawOf(directory)); }, assertPath(root:OwnedWorkspaceFsAtDirectory,lock:OwnedWorkspaceFsAtRegularFile,name:LockFileName,identity:{device:bigint;inode:bigint}):void { const st=binding.fstatat(rawOf(root),component(name)), opened=lock.stat(); if(st.device!==opened.device||st.inode!==opened.inode||st.mode!==opened.mode||st.uid!==opened.uid||st.nlink!==opened.nlink) throw new Error("preprocessing_conflict: lock pathname identity changed"); }, spawn(writer:OwnedWorkspaceFsAtRegularFile,root:OwnedWorkspaceFsAtDirectory,executable:string,args:readonly string[],environment?:NodeJS.ProcessEnv):Promise<{exitCode:number;stdout:Uint8Array;stderr:Uint8Array}> { return withBorrowedFd(writer,wfd=>withBorrowedFd(root,rfd=>new Promise((resolve,reject)=>{const child=spawn(executable,[...args],{stdio:["ignore","pipe","pipe",wfd,rfd],env:{...(environment??process.env),THOTH_WORKSPACE_CAPABILITY_REQUIRED:"1"}});const out:Buffer[]=[];const err:Buffer[]=[];child.stdout?.on("data",(x:Buffer)=>out.push(x));child.stderr?.on("data",(x:Buffer)=>err.push(x));child.once("error",reject);child.once("close",code=>resolve({exitCode:code??1,stdout:Buffer.concat(out),stderr:Buffer.concat(err)}));}))); }, };