UNPKG

@pgxsinkit/pgwasm

Version:
228 lines • 14.4 kB
/** * A WASI preview1 FILESYSTEM adapter over `RepackedSyncClient`. * * ## Why this file exists * * The broker gives a futex-parked thread synchronous access to ONE repacked store. A wasm engine does * not speak that API — it speaks `wasi_snapshot_preview1`, thirty-odd i32-returning imports over its * own linear memory. This module is the seam: every WASI file call a guest makes is translated into a * broker request, and the errno the broker already speaks (the protocol's numbers ARE WASI preview1 * errnos) is handed straight back. Nothing here knows about pgwasm, OPFS, or any particular engine — * it needs a client, a way to reach the guest's memory, and nothing else. * * ## What it owns and what it refuses to touch * * The adapter owns fd `preopenFd` (3 by default — the "/" preopen) and every fd at or above `fdBase` * (4 by default). It NEVER touches fds 0–2: a host keeps its own stdin/stdout/stderr, and * {@link WasiPreview1Fs.compose} builds the merged import object that routes each call to whichever * side owns the fd. `owns()` is public so a host can make that decision itself. * * ## The fd table * * One entry per open descriptor, keyed by the WASI fd the guest sees: * * { fd, path, isDir, clientFd, offset, fdflags, rightsBase, rightsInheriting, readable, writable } * * - `path` is the normalized absolute path the fd was opened with. It is kept for the whole life of * the descriptor because the store resizes by PATH, not by descriptor: `fd_filestat_set_size` and * `fd_allocate` resolve through it. * - `clientFd` is the broker's descriptor for a FILE. A DIRECTORY has none: the store cannot open a * directory at all (its `open` answers `EISDIR`), so a directory fd is adapter-local — a remembered * path plus a listing snapshot — and is still usable for `fd_readdir`, `fd_filestat_get`, `fd_sync` * and as a dirfd for every path operation. * - `offset` is tracked HERE, not in the store. Every read and write the adapter issues carries an * explicit position (the store's `pread`/`pwrite` form), so the store's own cursor is never used and * `fd_seek`/`fd_tell` are exact even though several threads share one store. * * ## The mapping, decision by decision * * - **rights → access.** `RIGHTS_FD_READ` grants read, `RIGHTS_FD_WRITE` grants write; a request that * asks for neither is read-only. That access is enforced by the BROKER (per its own fd) and again * here, because a POSIX `open(O_RDONLY|O_CREAT)` has to open the store descriptor wider than the * guest asked for — see below. * - **oflags → POSIX bits.** `CREAT`→`O_CREAT`, `EXCL`→`O_EXCL`, `TRUNC`→`O_TRUNC`, and the access * mode from the rights. `planOpen` (server side) rejects `O_CREAT`/`O_TRUNC` without write access, * so a create/truncate request always adds write to the POSIX access mode while the adapter keeps * the NARROWER access the rights asked for and rejects a later `fd_write` with `ENOTCAPABLE`. * - **`O_DIRECTORY` and directories.** With `DIRECTORY` set the adapter never calls `open` (that would * create a file); it stats and builds a directory fd. Without it, `open` is attempted and an * `EISDIR` answer is recognised as "this is a directory" and turned into a directory fd too — which * costs nothing on the common file path. * - **`O_APPEND` is emulated here, never passed to the broker.** An append write resolves end-of-file * with `fstat` and then `pwrite`s there, so the adapter's own offset stays exact and * `fd_fdstat_set_flags` can turn `APPEND` on and off on a descriptor that was not opened with it. * - **`fd_seek`.** `SET`/`CUR` are pure arithmetic on the adapter's offset; `END` resolves the size * with `fstat` on the broker fd (never `size(path)`, which would race a rename). * - **`fd_readdir` cookies.** A cookie is an index into a listing SNAPSHOT taken on cookie 0, exactly * as a POSIX `readdir` may. The snapshot is built out of `readdirPage` calls, and it exists for a * protocol reason as much as a semantic one: the broker treats a cursor past the end of the listing * as a protocol violation and DETACHES the client, so a stale cookie from a directory that shrank * must never reach it. A cookie past the snapshot's end reports zero bytes used — end of directory. * - **truncate by path.** `fd_filestat_set_size` and `fd_allocate` call `truncate(path, size)` with * the descriptor's remembered path. The store has no hard links, so nothing is lost. * - **`fd_sync`/`fd_datasync`.** Both map to the broker's `fsync`, whose durability is STORE-WIDE: on * success every byte written through the broker by ANY client before the call returned is * recoverable. That is stronger than `fd_sync` promises, never weaker. * - **symlinks.** The store HAS them, and `SYMLINK_FOLLOW` therefore means something everywhere it * appears: `path_filestat_get` picks `stat` or `lstat`, and a `path_open` without it sends * `O_NOFOLLOW` so a final component that IS a link answers `ELOOP` instead of opening its target. * `path_unlink_file` removes the LINK, never the target — that is the core's `unlink`, not * something added here. `path_symlink` writes a link and `path_readlink` reads one back, both * verbatim: the store takes ABSOLUTE targets only (see `validateSymlinkTarget`), so a relative * target is `EINVAL` rather than being silently reinterpreted, and an absolute one is a STORE * path — which is the same thing as a guest path whenever the preopen is the store root, the * default and the only arrangement a datadir uses. `path_link` remains `ENOTSUP`: the store has * no hard links. * - **`fd_advise`** is a no-op success. **`fd_filestat_set_times`/`path_filestat_set_times`** answer * `ENOTSUP`: the broker exposes no `utimes` opcode, and inventing an adapter-local timestamp would * make two threads on ONE store disagree about a file's mtime, which is exactly what this whole * arrangement exists to prevent. * * ## Failure discipline * * Every exported function is wrapped so that a JS exception can never leave it. A throw out of a WASI * import unwinds through the guest's nounwind frames and surfaces as a bare `RuntimeError: * unreachable` with no attribution at all; instead the wrapper reports `EIO` and hands the cause to * `onError` with the call name. A transport failure (the coordinator died, the client was detached) is * therefore an `EIO` the guest can act on rather than an abort it cannot. */ import type { RepackedSyncClient } from "../broker/client"; /** * WASI preview1 errno numbers. A superset of the store's own `FS_ERRNO` (whose values already ARE * these numbers); the extra codes are ones a filesystem ADAPTER has to answer and the store core * never produces, so they live here rather than widening the core's error vocabulary. */ export declare const WASI_ERRNO: { readonly SUCCESS: 0; readonly ACCES: 2; readonly BADF: 8; readonly EXIST: 20; readonly INVAL: 28; readonly IO: 29; readonly ISDIR: 31; readonly LOOP: 32; readonly NOENT: 44; readonly NOSYS: 52; readonly NOTDIR: 54; readonly NOTEMPTY: 55; readonly NOTSUP: 58; readonly OVERFLOW: 61; readonly PERM: 63; readonly SPIPE: 70; readonly NOTCAPABLE: 76; }; /** WASI preview1 filetypes. */ export declare const WASI_FILETYPE: { readonly UNKNOWN: 0; readonly BLOCK_DEVICE: 1; readonly CHARACTER_DEVICE: 2; readonly DIRECTORY: 3; readonly REGULAR_FILE: 4; readonly SOCKET_DGRAM: 5; readonly SOCKET_STREAM: 6; readonly SYMBOLIC_LINK: 7; }; /** `path_open` oflags. */ export declare const OFLAGS_CREAT = 1; export declare const OFLAGS_DIRECTORY = 2; export declare const OFLAGS_EXCL = 4; export declare const OFLAGS_TRUNC = 8; /** `fdflags`, on `path_open` and `fd_fdstat_set_flags`. */ export declare const FDFLAGS_APPEND = 1; export declare const FDFLAGS_DSYNC = 2; export declare const FDFLAGS_NONBLOCK = 4; export declare const FDFLAGS_RSYNC = 8; export declare const FDFLAGS_SYNC = 16; /** `lookupflags`, on every path operation that could follow a symlink. */ export declare const LOOKUPFLAGS_SYMLINK_FOLLOW = 1; /** The two rights the adapter reads; the rest are carried through untouched. */ export declare const RIGHTS_FD_READ: bigint; export declare const RIGHTS_FD_WRITE: bigint; /** What a preopen advertises when the host asked for nothing narrower. */ export declare const RIGHTS_ALL = 18446744073709551615n; /** `fd_seek` whence values. */ export declare const WHENCE_SET = 0; export declare const WHENCE_CUR = 1; export declare const WHENCE_END = 2; export interface WasiPreview1FsOptions { /** The synchronous broker client this adapter turns WASI calls into. */ readonly client: RepackedSyncClient; /** * The guest's linear memory, resolved on EVERY call. A shared wasm memory grows underneath the host * and any cached `Uint8Array`/`DataView` goes stale (or detaches outright, for a non-shared one), so * nothing here holds a view across a call. */ readonly memory: () => ArrayBuffer | SharedArrayBuffer; /** The fd the guest sees the preopened directory as. Defaults to 3, the WASI convention. */ readonly preopenFd?: number; /** The directory that preopen names. Defaults to the store root. */ readonly preopenPath?: string; /** The first fd the adapter hands out, and the bottom of the range it claims. Defaults to 4. */ readonly fdBase?: number; /** The adapter's clock, in milliseconds. Defaults to the wall clock; used for diagnostics. */ readonly now?: () => bigint; /** Where a JS exception that escaped a WASI call is reported. Defaults to `console.error`. */ readonly onError?: (call: string, cause: unknown) => void; } /** The `wasi_snapshot_preview1` filesystem surface, with the ABI's exact signatures. */ export interface WasiPreview1FsFunctions { fd_prestat_get(fd: number, resultPtr: number): number; fd_prestat_dir_name(fd: number, pathPtr: number, pathLen: number): number; fd_close(fd: number): number; fd_read(fd: number, iovsPtr: number, iovsLen: number, nreadPtr: number): number; fd_pread(fd: number, iovsPtr: number, iovsLen: number, offset: bigint, nreadPtr: number): number; fd_write(fd: number, iovsPtr: number, iovsLen: number, nwrittenPtr: number): number; fd_pwrite(fd: number, iovsPtr: number, iovsLen: number, offset: bigint, nwrittenPtr: number): number; fd_seek(fd: number, offset: bigint, whence: number, resultPtr: number): number; fd_tell(fd: number, resultPtr: number): number; fd_fdstat_get(fd: number, resultPtr: number): number; fd_fdstat_set_flags(fd: number, fdflags: number): number; fd_fdstat_set_rights(fd: number, rightsBase: bigint, rightsInheriting: bigint): number; fd_filestat_get(fd: number, resultPtr: number): number; fd_filestat_set_size(fd: number, size: bigint): number; fd_filestat_set_times(fd: number, atim: bigint, mtim: bigint, fstflags: number): number; fd_readdir(fd: number, bufPtr: number, bufLen: number, cookie: bigint, bufusedPtr: number): number; fd_sync(fd: number): number; fd_datasync(fd: number): number; fd_allocate(fd: number, offset: bigint, length: bigint): number; fd_advise(fd: number, offset: bigint, length: bigint, advice: number): number; path_open(dirfd: number, dirflags: number, pathPtr: number, pathLen: number, oflags: number, rightsBase: bigint, rightsInheriting: bigint, fdflags: number, resultPtr: number): number; path_filestat_get(dirfd: number, flags: number, pathPtr: number, pathLen: number, resultPtr: number): number; path_filestat_set_times(dirfd: number, flags: number, pathPtr: number, pathLen: number, atim: bigint, mtim: bigint, fstflags: number): number; path_create_directory(dirfd: number, pathPtr: number, pathLen: number): number; path_remove_directory(dirfd: number, pathPtr: number, pathLen: number): number; path_unlink_file(dirfd: number, pathPtr: number, pathLen: number): number; path_rename(dirfd: number, oldPtr: number, oldLen: number, newDirfd: number, newPtr: number, newLen: number): number; path_readlink(dirfd: number, pathPtr: number, pathLen: number, bufPtr: number, bufLen: number, bufusedPtr: number): number; path_symlink(oldPtr: number, oldLen: number, dirfd: number, newPtr: number, newLen: number): number; path_link(oldDirfd: number, oldFlags: number, oldPtr: number, oldLen: number, newDirfd: number, newPtr: number, newLen: number): number; } /** The adapter: the WASI surface plus the three composition/lifecycle helpers a host needs. */ export interface WasiPreview1Fs extends WasiPreview1FsFunctions { /** Whether this adapter answers for `fd`: the preopen, or anything at or above `fdBase`. */ owns(fd: number): boolean; /** How many descriptors the adapter currently holds, preopen excluded. */ openFdCount(): number; /** * A merged `wasi_snapshot_preview1` object: every filesystem call goes to the adapter when the fd * (or the dirfd, for a path operation) is adapter-owned and to `base` otherwise. Everything in * `base` that is not a filesystem call — `args_get`, `clock_time_get`, `poll_oneoff`, `proc_exit`, * `random_get`, `sched_yield`, the socket stubs — is carried through untouched, as are `base`'s own * fd 0/1/2 handlers. */ compose(base: Readonly<Record<string, unknown>>): Record<string, unknown>; /** * Close every descriptor, returning how many were released. The call a thread makes on its way out: * without it the coordinator holds the thread's store descriptors until the whole channel detaches. */ closeAll(): number; } /** * Canonicalize a guest path the way the store demands: absolute, no `.`/`..`, no empty or repeated * separator, no trailing slash. Deliberately lenient in the same places a WASI host has to be — * wasi-libc hands preopen-RELATIVE paths, but an absolute one is accepted too, and a trailing NUL from * a fixed-size buffer is stripped rather than rejected. */ export declare function normalizeWasiPath(path: string): string; export declare function createWasiPreview1Fs(options: WasiPreview1FsOptions): WasiPreview1Fs; //# sourceMappingURL=preview1.d.ts.map