Skip to content

Commit a6d3e93

Browse files
authored
vfs: support writeFileSync with virtual fds
Route fs.writeFileSync() and fs.appendFileSync() calls with VFS-owned file descriptors through the virtual file handle instead of falling back to the native fs binding. This preserves descriptor semantics for both memory-backed VFS files and RealFSProvider handles. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5 PR-URL: #64165 Fixes: #64164 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 615749b commit a6d3e93

2 files changed

Lines changed: 93 additions & 4 deletions

File tree

lib/internal/vfs/setup.js

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const {
99
} = primordials;
1010

1111
const { Buffer } = require('buffer');
12+
const { isArrayBufferView } = require('internal/util/types');
1213
const { resolve, sep } = require('path');
1314
const { fileURLToPath, URL } = require('internal/url');
1415
const { kEmptyObject } = require('internal/util');
@@ -46,6 +47,31 @@ function noopFd(fd) {
4647
return undefined;
4748
}
4849

50+
function toWriteBuffer(data, options) {
51+
if (Buffer.isBuffer(data)) return data;
52+
if (isArrayBufferView(data)) {
53+
return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
54+
}
55+
const encoding = typeof options === 'string' ? options : options?.encoding;
56+
return Buffer.from(data, encoding || 'utf8');
57+
}
58+
59+
function writeFileSyncFd(fd, data, options) {
60+
const vfd = getVirtualFd(fd);
61+
if (vfd === undefined) return undefined;
62+
63+
const buffer = toWriteBuffer(data, options);
64+
let offset = 0;
65+
let length = buffer.byteLength;
66+
while (length > 0) {
67+
const written = vfd.entry.writeSync(buffer, offset, length, null);
68+
offset += written;
69+
length -= written;
70+
}
71+
72+
return true;
73+
}
74+
4975
// Registry of active VFS instances.
5076
const activeVFSList = [];
5177

@@ -288,10 +314,14 @@ function createVfsHandlers() {
288314

289315
// ==================== Sync path-based write ops ====================
290316

291-
writeFileSync: (path, data, options) =>
292-
vfsOpVoid(path, (vfs, n) => vfs.writeFileSync(n, data, options)),
293-
appendFileSync: (path, data, options) =>
294-
vfsOpVoid(path, (vfs, n) => vfs.appendFileSync(n, data, options)),
317+
writeFileSync(path, data, options) {
318+
if (typeof path === 'number') return writeFileSyncFd(path, data, options);
319+
return vfsOpVoid(path, (vfs, n) => vfs.writeFileSync(n, data, options));
320+
},
321+
appendFileSync(path, data, options) {
322+
if (typeof path === 'number') return writeFileSyncFd(path, data, options);
323+
return vfsOpVoid(path, (vfs, n) => vfs.appendFileSync(n, data, options));
324+
},
295325
mkdirSync: (path, options) =>
296326
vfsOp(path, (vfs, n) => ({ result: vfs.mkdirSync(n, options) })),
297327
rmdirSync: (path) => vfsOpVoid(path, (vfs, n) => vfs.rmdirSync(n)),

test/parallel/test-vfs-fs-writeFileSync.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,63 @@ assert.strictEqual(fs.readFileSync(target, 'utf8'), 'fresh more');
2626
fs.writeFileSync(target, Buffer.from('binary'));
2727
assert.strictEqual(fs.readFileSync(target, 'utf8'), 'binary');
2828

29+
// writeFileSync via a VFS fd writes through the open descriptor.
30+
{
31+
const fdTarget = path.join(mountPoint, 'src/fd.txt');
32+
const fd = fs.openSync(fdTarget, 'w+');
33+
try {
34+
fs.writeFileSync(fd, 'hello');
35+
fs.writeFileSync(fd, new Uint8Array(Buffer.from(' world')));
36+
assert.strictEqual(fs.readFileSync(fdTarget, 'utf8'), 'hello world');
37+
} finally {
38+
fs.closeSync(fd);
39+
}
40+
}
41+
42+
// appendFileSync via a VFS fd follows normal fd write semantics.
43+
{
44+
const fdTarget = path.join(mountPoint, 'src/append-fd.txt');
45+
fs.writeFileSync(fdTarget, 'start');
46+
const fd = fs.openSync(fdTarget, 'a');
47+
try {
48+
fs.appendFileSync(fd, ' end');
49+
assert.strictEqual(fs.readFileSync(fdTarget, 'utf8'), 'start end');
50+
} finally {
51+
fs.closeSync(fd);
52+
}
53+
}
54+
2955
myVfs.unmount();
56+
57+
// writeFileSync via a RealFSProvider fd remains tied to the open descriptor
58+
// after the backing path is renamed.
59+
{
60+
const root = path.join('/tmp', 'vfs-real-writeFileSync-' + process.pid);
61+
const realMountPoint = path.join('/tmp', 'vfs-real-writeFileSync-mount-' + process.pid);
62+
fs.rmSync(root, { recursive: true, force: true });
63+
fs.rmSync(realMountPoint, { recursive: true, force: true });
64+
fs.mkdirSync(root, { recursive: true });
65+
fs.mkdirSync(realMountPoint, { recursive: true });
66+
67+
const realVfs = vfs
68+
.create(new vfs.RealFSProvider(root), { emitExperimentalWarning: false })
69+
.mount(realMountPoint);
70+
try {
71+
const mountedFile = path.join(realMountPoint, 'a.txt');
72+
fs.writeFileSync(path.join(root, 'a.txt'), 'old');
73+
const fd = fs.openSync(mountedFile, 'r+');
74+
try {
75+
fs.renameSync(path.join(root, 'a.txt'), path.join(root, 'b.txt'));
76+
fs.writeFileSync(path.join(root, 'a.txt'), 'new');
77+
fs.writeFileSync(fd, 'updated');
78+
assert.strictEqual(fs.readFileSync(path.join(root, 'b.txt'), 'utf8'), 'updated');
79+
assert.strictEqual(fs.readFileSync(path.join(root, 'a.txt'), 'utf8'), 'new');
80+
} finally {
81+
fs.closeSync(fd);
82+
}
83+
} finally {
84+
realVfs.unmount();
85+
fs.rmSync(root, { recursive: true, force: true });
86+
fs.rmSync(realMountPoint, { recursive: true, force: true });
87+
}
88+
}

0 commit comments

Comments
 (0)