Skip to content

Commit 8cb8312

Browse files
authored
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 PR-URL: #64104 Fixes: #64103 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 62a3b1b commit 8cb8312

3 files changed

Lines changed: 163 additions & 2 deletions

File tree

lib/internal/vfs/providers/real.js

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
'use strict';
22

33
const {
4+
ArrayPrototypePush,
45
Promise,
56
StringPrototypeStartsWith,
67
} = primordials;
78

9+
const { Buffer } = require('buffer');
810
const fs = require('fs');
911
const path = require('path');
1012
const { VirtualProvider } = require('internal/vfs/provider');
@@ -17,6 +19,8 @@ const {
1719
createENOENT,
1820
} = require('internal/vfs/errors');
1921

22+
const kReadFileUnknownBufferLength = 8192;
23+
2024
/**
2125
* A file handle that wraps a real file descriptor.
2226
*/
@@ -34,6 +38,20 @@ class RealFileHandle extends VirtualFileHandle {
3438
}
3539
}
3640

41+
#readFileResult(buffer, bytesRead, options) {
42+
buffer = buffer.subarray(0, bytesRead);
43+
const encoding = typeof options === 'string' ? options : options?.encoding;
44+
if (encoding && encoding !== 'buffer') {
45+
buffer = buffer.toString(encoding);
46+
}
47+
return buffer;
48+
}
49+
50+
#readFileUnknownSizeResult(buffers, totalRead, options) {
51+
return this.#readFileResult(
52+
Buffer.concat(buffers, totalRead), totalRead, options);
53+
}
54+
3755
/**
3856
* @param {string} path The VFS path
3957
* @param {string} flags The open flags
@@ -79,12 +97,77 @@ class RealFileHandle extends VirtualFileHandle {
7997

8098
readFileSync(options) {
8199
this.#checkClosed('read');
82-
return fs.readFileSync(this.#realPath, options);
100+
const size = fs.fstatSync(this.#fd).size;
101+
if (size === 0) {
102+
const buffers = [];
103+
let totalRead = 0;
104+
105+
while (true) {
106+
const buffer = Buffer.allocUnsafe(kReadFileUnknownBufferLength);
107+
const read = fs.readSync(
108+
this.#fd, buffer, 0, buffer.byteLength, totalRead);
109+
if (read === 0) break;
110+
ArrayPrototypePush(buffers, buffer.subarray(0, read));
111+
totalRead += read;
112+
}
113+
114+
return this.#readFileUnknownSizeResult(buffers, totalRead, options);
115+
}
116+
117+
const buffer = Buffer.allocUnsafe(size);
118+
let bytesRead = 0;
119+
while (bytesRead < buffer.byteLength) {
120+
const read = fs.readSync(
121+
this.#fd,
122+
buffer,
123+
bytesRead,
124+
buffer.byteLength - bytesRead,
125+
bytesRead,
126+
);
127+
if (read === 0) break;
128+
bytesRead += read;
129+
}
130+
131+
return this.#readFileResult(buffer, bytesRead, options);
83132
}
84133

85134
async readFile(options) {
86135
this.#checkClosed('read');
87-
return fs.promises.readFile(this.#realPath, options);
136+
const size = (await this.stat()).size;
137+
if (size === 0) {
138+
const buffers = [];
139+
let totalRead = 0;
140+
141+
while (true) {
142+
const buffer = Buffer.allocUnsafe(kReadFileUnknownBufferLength);
143+
const { bytesRead: read } = await this.read(
144+
buffer,
145+
0,
146+
buffer.byteLength,
147+
totalRead,
148+
);
149+
if (read === 0) break;
150+
ArrayPrototypePush(buffers, buffer.subarray(0, read));
151+
totalRead += read;
152+
}
153+
154+
return this.#readFileUnknownSizeResult(buffers, totalRead, options);
155+
}
156+
157+
const buffer = Buffer.allocUnsafe(size);
158+
let bytesRead = 0;
159+
while (bytesRead < buffer.byteLength) {
160+
const { bytesRead: read } = await this.read(
161+
buffer,
162+
bytesRead,
163+
buffer.byteLength - bytesRead,
164+
bytesRead,
165+
);
166+
if (read === 0) break;
167+
bytesRead += read;
168+
}
169+
170+
return this.#readFileResult(buffer, bytesRead, options);
88171
}
89172

90173
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: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,55 @@ 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+
95+
// ===== readFile reads past the fallback chunk when fstat reports size 0 =====
96+
{
97+
const content = 'a'.repeat(8192) + 'trailing data';
98+
fs.writeFileSync(path.join(root, 'zero-stat.txt'), content);
99+
const syncHandle = await myVfs.provider.open('/zero-stat.txt', 'r');
100+
const asyncHandle = await myVfs.provider.open('/zero-stat.txt', 'r');
101+
const originalFstatSync = fs.fstatSync;
102+
const originalFstat = fs.fstat;
103+
104+
fs.fstatSync = common.mustCall(function fstatSync(...args) {
105+
const stats = originalFstatSync.apply(this, args);
106+
stats.size = 0;
107+
return stats;
108+
});
109+
fs.fstat = common.mustCall(function fstat(fd, options, callback) {
110+
return originalFstat.call(this, fd, options, (err, stats) => {
111+
if (stats) stats.size = 0;
112+
callback(err, stats);
113+
});
114+
});
115+
116+
try {
117+
assert.strictEqual(syncHandle.readFileSync('utf8'), content);
118+
assert.strictEqual(await asyncHandle.readFile('utf8'), content);
119+
} finally {
120+
fs.fstatSync = originalFstatSync;
121+
fs.fstat = originalFstat;
122+
await syncHandle.close();
123+
await asyncHandle.close();
124+
}
125+
}
126+
78127
// ===== EBADF after close =====
79128
{
80129
await myVfs.promises.writeFile('/h.txt', 'hello');

0 commit comments

Comments
 (0)