Skip to content

Commit 8a749d7

Browse files
committed
vfs: read RealFSProvider files from open fd
Read RealFileHandle contents through the open file descriptor instead of reopening the original real path. This keeps already-open VFS file descriptors usable after the backing file is renamed. Use positioned reads so readFileSync() and readFile() preserve the handle's current offset. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5
1 parent 9f23e70 commit 8a749d7

3 files changed

Lines changed: 91 additions & 2 deletions

File tree

lib/internal/vfs/providers/real.js

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const {
55
StringPrototypeStartsWith,
66
} = primordials;
77

8+
const { Buffer } = require('buffer');
89
const fs = require('fs');
910
const path = require('path');
1011
const { VirtualProvider } = require('internal/vfs/provider');
@@ -34,6 +35,19 @@ class RealFileHandle extends VirtualFileHandle {
3435
}
3536
}
3637

38+
#readFileBuffer(size) {
39+
return Buffer.allocUnsafe(size || 8192);
40+
}
41+
42+
#readFileResult(buffer, bytesRead, options) {
43+
buffer = buffer.subarray(0, bytesRead);
44+
const encoding = typeof options === 'string' ? options : options?.encoding;
45+
if (encoding && encoding !== 'buffer') {
46+
buffer = buffer.toString(encoding);
47+
}
48+
return buffer;
49+
}
50+
3751
/**
3852
* @param {string} path The VFS path
3953
* @param {string} flags The open flags
@@ -79,12 +93,41 @@ class RealFileHandle extends VirtualFileHandle {
7993

8094
readFileSync(options) {
8195
this.#checkClosed('read');
82-
return fs.readFileSync(this.#realPath, options);
96+
const size = fs.fstatSync(this.#fd).size;
97+
const buffer = this.#readFileBuffer(size);
98+
let bytesRead = 0;
99+
while (bytesRead < buffer.byteLength) {
100+
const read = fs.readSync(
101+
this.#fd,
102+
buffer,
103+
bytesRead,
104+
buffer.byteLength - bytesRead,
105+
bytesRead,
106+
);
107+
if (read === 0) break;
108+
bytesRead += read;
109+
}
110+
111+
return this.#readFileResult(buffer, bytesRead, options);
83112
}
84113

85114
async readFile(options) {
86115
this.#checkClosed('read');
87-
return fs.promises.readFile(this.#realPath, options);
116+
const size = (await this.stat()).size;
117+
const buffer = this.#readFileBuffer(size);
118+
let bytesRead = 0;
119+
while (bytesRead < buffer.byteLength) {
120+
const { bytesRead: read } = await this.read(
121+
buffer,
122+
bytesRead,
123+
buffer.byteLength - bytesRead,
124+
bytesRead,
125+
);
126+
if (read === 0) break;
127+
bytesRead += read;
128+
}
129+
130+
return this.#readFileResult(buffer, bytesRead, options);
88131
}
89132

90133
writeFileSync(data, options) {

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,32 @@ assert.strictEqual(
4343
}
4444

4545
myVfs.unmount();
46+
47+
// readFileSync via a RealFSProvider fd remains usable after the backing path
48+
// is renamed.
49+
{
50+
const root = path.join('/tmp', 'vfs-real-readFileSync-' + process.pid);
51+
const realMountPoint = path.join('/tmp', 'vfs-real-readFileSync-mount-' + process.pid);
52+
fs.rmSync(root, { recursive: true, force: true });
53+
fs.rmSync(realMountPoint, { recursive: true, force: true });
54+
fs.mkdirSync(root, { recursive: true });
55+
fs.mkdirSync(realMountPoint, { recursive: true });
56+
57+
const realVfs = vfs
58+
.create(new vfs.RealFSProvider(root), { emitExperimentalWarning: false })
59+
.mount(realMountPoint);
60+
try {
61+
fs.writeFileSync(path.join(root, 'a.txt'), 'still readable');
62+
const fd = fs.openSync(path.join(realMountPoint, 'a.txt'), 'r');
63+
try {
64+
fs.renameSync(path.join(root, 'a.txt'), path.join(root, 'b.txt'));
65+
assert.strictEqual(fs.readFileSync(fd, 'utf8'), 'still readable');
66+
} finally {
67+
fs.closeSync(fd);
68+
}
69+
} finally {
70+
realVfs.unmount();
71+
fs.rmSync(root, { recursive: true, force: true });
72+
fs.rmSync(realMountPoint, { recursive: true, force: true });
73+
}
74+
}

test/parallel/test-vfs-real-provider-handle.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,23 @@ const myVfs = vfs.create(new vfs.RealFSProvider(root));
7575
await handle.close();
7676
}
7777

78+
// ===== readFile through an open real fd survives backing path rename =====
79+
{
80+
fs.writeFileSync(path.join(root, 'rename-read.txt'), 'still readable');
81+
const syncHandle = await myVfs.provider.open('/rename-read.txt', 'r');
82+
const asyncHandle = await myVfs.provider.open('/rename-read.txt', 'r');
83+
fs.renameSync(path.join(root, 'rename-read.txt'),
84+
path.join(root, 'rename-read-renamed.txt'));
85+
try {
86+
assert.strictEqual(syncHandle.readFileSync('utf8'), 'still readable');
87+
assert.strictEqual(await asyncHandle.readFile('utf8'), 'still readable');
88+
} finally {
89+
await syncHandle.close();
90+
await asyncHandle.close();
91+
fs.unlinkSync(path.join(root, 'rename-read-renamed.txt'));
92+
}
93+
}
94+
7895
// ===== EBADF after close =====
7996
{
8097
await myVfs.promises.writeFile('/h.txt', 'hello');

0 commit comments

Comments
 (0)