Skip to content

fix: patch the fd family (open/read/close/fstat) in installFsPatches - #70

Merged
mcollina merged 1 commit into
platformatic:mainfrom
robertsLando:feat/fd-family-fs-patches-upstream
Sep 23, 2026
Merged

mcollina merged 1 commit into
platformatic:mainfrom
robertsLando:feat/fd-family-fs-patches-upstream

Conversation

@robertsLando

Copy link
Copy Markdown
Contributor

installFsPatches() never patched the descriptor family, so fs.openSync on a path inside a mounted VFS throws ENOENT even though fs.readFileSync on the same path works:

const fs = require('node:fs');
const { create } = require('@platformatic/vfs');

const vfs = create();
vfs.writeFileSync('/a.txt', 'hello');
vfs.mount('/mnt');

fs.readFileSync('/mnt/a.txt', 'utf8');  // 'hello'
fs.openSync('/mnt/a.txt');              // ENOENT: no such file or directory

fs.openSync isn't exotic — plenty of libraries reach for it, and none of them know to call the VFS instance directly. Reported downstream as yao-pkg/pkg#302, hit while porting the Grain compiler to a bundled-binary setup built on this VFS.

What was already there

This is a gap in the patch layer only:

  • lib/fd.js already allocates virtual fds from 10000+ specifically so they cannot collide with real ones, and exposes openVirtualFd / getVirtualFd / closeVirtualFd. That is the routing mechanism.
  • lib/file_system.js already implements openSync, closeSync, readSync, fstatSync and async open, close, read, fstat.
  • lib/streams.js already drives vfs.openSync for createReadStream, so the handle path is exercised today.

Changes

lib/fd.js — now owns the fd-keyed operations (closeFdSync, readFdSync, fstatFdSync, closeFd, readFd, fstatFd). A handle carries its own content and position, and the existing VirtualFileSystem methods never touched this, so which VFS opened an fd is irrelevant. Both VirtualFileSystem and the node:fs patches delegate here rather than duplicating the logic.

It also collapses fs.read/readSync's overloads — (fd, buffer, options), (fd, options, cb), (fd, cb) — which VirtualFileSystem.readSync did not accept either.

lib/file_system.js — the six fd methods become thin delegations. Net −52 lines.

lib/module_hooks.js — patches openSync/open (resolved by path) and readSync/read, closeSync/close, fstatSync/fstat (routed via getVirtualFd(fd)), each falling through to the original when the path or fd isn't ours. findVFSForWatch is renamed findVFSForExistingPath, since the open patches need exactly the same "which VFS owns this path, honouring overlay" lookup; it was module-private, so nothing external moves.

Deliberately out of scope

  • fs.promises.open. It has to return a real FileHandle, and VirtualFileHandle has no .fd, createReadStream, readLines or chmod. Leaving it unpatched keeps today's behaviour (falls through, throws ENOENT) instead of handing callers a half-shaped object. Happy to open a separate issue if you'd like it tracked.
  • Write flags on an overlay mount keep falling through to the real fs, matching how readFileSync and createReadStream already treat overlays.

Verification

11 new tests in test/fs-hooks.test.js: the sync path, the callback path, the options-object overload, sequential reads advancing position, EBADF on a stale fd, real-fs descriptors still working while a VFS is mounted, and overlay fall-through.

npm test → 244/244 passing. npm run lint clean.

Commit is DCO signed off. Happy to adjust naming or split the fd.js extraction out if you'd rather review it separately.

installFsPatches never patched the descriptor family, so fs.openSync on a
path inside a mounted VFS threw ENOENT even though fs.readFileSync on the
same path worked:

    vfs.writeFileSync('/a.txt', 'hello');
    vfs.mount('/mnt');
    fs.readFileSync('/mnt/a.txt', 'utf8');  // 'hello'
    fs.openSync('/mnt/a.txt');              // ENOENT

VirtualFileSystem already implemented the whole family, and fd.js already
allocated virtual fds from 10000+ so they cannot shadow real ones. Only the
patch layer was missing.

- fd.js gains the fd-keyed operations. A handle carries its own content and
  position, so which VFS opened it is irrelevant; VirtualFileSystem and the
  node:fs patches now both delegate here instead of duplicating.
- fd.js collapses fs.read/readSync's options-object and short-form overloads,
  which VirtualFileSystem.readSync did not accept either.
- findVFSForWatch becomes findVFSForExistingPath: the open patches need the
  same "which VFS owns this path, honouring overlay" lookup.

Write flags on an overlay mount keep falling through to the real fs, matching
how readFileSync and createReadStream already treat overlays.

fs.promises.open stays unpatched: it must return a real FileHandle, and
VirtualFileHandle has no .fd, createReadStream, readLines or chmod. Falling
through preserves today's behaviour rather than handing back a half-shaped
object.

Reported downstream as yao-pkg/pkg#302.

Signed-off-by: robertsLando <daniel.sorridi@gmail.com>
@mcollina
mcollina merged commit d8f5d4e into platformatic:main Sep 23, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants