Skip to content

Commit bbdd764

Browse files
trivikrrichardlau
authored andcommitted
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: #64104Fixes: #64103 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent f4e7bf1 commit bbdd764

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
constfs=require('fs');
911
constpath=require('path');
1012
const{ VirtualProvider }=require('internal/vfs/provider');
@@ -17,6 +19,8 @@ const {
1719
createENOENT,
1820
}=require('internal/vfs/errors');
1921

22+
constkReadFileUnknownBufferLength=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+
constencoding=typeofoptions==='string' ? options : options?.encoding;
44+
if(encoding&&encoding!=='buffer'){
45+
buffer=buffer.toString(encoding);
46+
}
47+
returnbuffer;
48+
}
49+
50+
#readFileUnknownSizeResult(buffers,totalRead,options){
51+
returnthis.#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-
returnfs.readFileSync(this.#realPath,options);
100+
constsize=fs.fstatSync(this.#fd).size;
101+
if(size===0){
102+
constbuffers=[];
103+
lettotalRead=0;
104+
105+
while(true){
106+
constbuffer=Buffer.allocUnsafe(kReadFileUnknownBufferLength);
107+
constread=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+
returnthis.#readFileUnknownSizeResult(buffers,totalRead,options);
115+
}
116+
117+
constbuffer=Buffer.allocUnsafe(size);
118+
letbytesRead=0;
119+
while(bytesRead<buffer.byteLength){
120+
constread=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+
returnthis.#readFileResult(buffer,bytesRead,options);
83132
}
84133

85134
asyncreadFile(options){
86135
this.#checkClosed('read');
87-
returnfs.promises.readFile(this.#realPath,options);
136+
constsize=(awaitthis.stat()).size;
137+
if(size===0){
138+
constbuffers=[];
139+
lettotalRead=0;
140+
141+
while(true){
142+
constbuffer=Buffer.allocUnsafe(kReadFileUnknownBufferLength);
143+
const{bytesRead: read}=awaitthis.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+
returnthis.#readFileUnknownSizeResult(buffers,totalRead,options);
155+
}
156+
157+
constbuffer=Buffer.allocUnsafe(size);
158+
letbytesRead=0;
159+
while(bytesRead<buffer.byteLength){
160+
const{bytesRead: read}=awaitthis.read(
161+
buffer,
162+
bytesRead,
163+
buffer.byteLength-bytesRead,
164+
bytesRead,
165+
);
166+
if(read===0)break;
167+
bytesRead+=read;
168+
}
169+
170+
returnthis.#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+
constroot=path.join('/tmp','vfs-real-readFileSync-'+process.pid);
51+
constrealMountPoint=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+
constrealVfs=vfs
58+
.create(newvfs.RealFSProvider(root),{emitExperimentalWarning: false})
59+
.mount(realMountPoint);
60+
try{
61+
fs.writeFileSync(path.join(root,'a.txt'),'still readable');
62+
constfd=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
awaithandle.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+
constsyncHandle=awaitmyVfs.provider.open('/rename-read.txt','r');
82+
constasyncHandle=awaitmyVfs.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(awaitasyncHandle.readFile('utf8'),'still readable');
88+
}finally{
89+
awaitsyncHandle.close();
90+
awaitasyncHandle.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+
constcontent='a'.repeat(8192)+'trailing data';
98+
fs.writeFileSync(path.join(root,'zero-stat.txt'),content);
99+
constsyncHandle=awaitmyVfs.provider.open('/zero-stat.txt','r');
100+
constasyncHandle=awaitmyVfs.provider.open('/zero-stat.txt','r');
101+
constoriginalFstatSync=fs.fstatSync;
102+
constoriginalFstat=fs.fstat;
103+
104+
fs.fstatSync=common.mustCall(functionfstatSync(...args){
105+
conststats=originalFstatSync.apply(this,args);
106+
stats.size=0;
107+
returnstats;
108+
});
109+
fs.fstat=common.mustCall(functionfstat(fd,options,callback){
110+
returnoriginalFstat.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(awaitasyncHandle.readFile('utf8'),content);
119+
}finally{
120+
fs.fstatSync=originalFstatSync;
121+
fs.fstat=originalFstat;
122+
awaitsyncHandle.close();
123+
awaitasyncHandle.close();
124+
}
125+
}
126+
78127
// ===== EBADF after close =====
79128
{
80129
awaitmyVfs.promises.writeFile('/h.txt','hello');

0 commit comments

Comments
 (0)