Skip to content

Commit be92138

Browse files
committed
vfs: fix abort signal, readFile flag, realpath.native, and metadata ops
- Check AbortSignal before VFS fast path in readFile/writeFile/appendFile - Honor options.flag in readFile/readFileSync on VFS paths - Route realpath.native/realpathSync.native through VFS handlers - Implement chmod/chown/utimes/lutimes in MemoryProvider instead of no-ops - Pass bigint option through VFSStatWatcher to statSync/createZeroStats - Coerce BigInt positions to Number in MemoryFileHandle read/write - Track nlink on MemoryEntry, increment on link, decrement on unlink - Update ctime alongside mtime on content mutations (write/truncate)
1 parent c68909d commit be92138

17 files changed

Lines changed: 705 additions & 44 deletions

lib/fs.js

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ function readFile(path, options, callback) {
395395
const h = vfsState.handlers;
396396
if (h !== null) {
397397
const opts = typeof options === 'function' ? undefined : options;
398+
if (checkAborted(opts?.signal, callback)) return;
398399
try {
399400
const result = h.readFileSync(path, opts);
400401
if (result !== undefined) {
@@ -2611,7 +2612,7 @@ function lchown(path, uid, gid, callback) {
26112612

26122613
const h = vfsState.handlers;
26132614
if (h !== null) {
2614-
const result = h.lchownSync(path);
2615+
const result = h.lchownSync(path, uid, gid);
26152616
if (result !== undefined) {
26162617
process.nextTick(callback, null);
26172618
return;
@@ -2637,7 +2638,7 @@ function lchownSync(path, uid, gid) {
26372638

26382639
const h = vfsState.handlers;
26392640
if (h !== null) {
2640-
const result = h.lchownSync(path);
2641+
const result = h.lchownSync(path, uid, gid);
26412642
if (result !== undefined) return;
26422643
}
26432644

@@ -2717,7 +2718,7 @@ function chown(path, uid, gid, callback) {
27172718

27182719
const h = vfsState.handlers;
27192720
if (h !== null) {
2720-
const result = h.chownSync(path);
2721+
const result = h.chownSync(path, uid, gid);
27212722
if (result !== undefined) {
27222723
process.nextTick(callback, null);
27232724
return;
@@ -2744,7 +2745,7 @@ function chownSync(path, uid, gid) {
27442745

27452746
const h = vfsState.handlers;
27462747
if (h !== null) {
2747-
const result = h.chownSync(path);
2748+
const result = h.chownSync(path, uid, gid);
27482749
if (result !== undefined) return;
27492750
}
27502751

@@ -2882,7 +2883,7 @@ function lutimes(path, atime, mtime, callback) {
28822883

28832884
const h = vfsState.handlers;
28842885
if (h !== null) {
2885-
const result = h.lutimesSync(path);
2886+
const result = h.lutimesSync(path, atime, mtime);
28862887
if (result !== undefined) {
28872888
process.nextTick(callback, null);
28882889
return;
@@ -2912,7 +2913,7 @@ function lutimesSync(path, atime, mtime) {
29122913

29132914
const h = vfsState.handlers;
29142915
if (h !== null) {
2915-
const result = h.lutimesSync(path);
2916+
const result = h.lutimesSync(path, atime, mtime);
29162917
if (result !== undefined) return;
29172918
}
29182919

@@ -2998,6 +2999,7 @@ function writeFile(path, data, options, callback) {
29982999
const h = vfsState.handlers;
29993000
if (h !== null) {
30003001
const opts = typeof options === 'function' ? undefined : options;
3002+
if (checkAborted(opts?.signal, callback)) return;
30013003
try {
30023004
const result = h.writeFileSync(path, data, opts);
30033005
if (result !== undefined) {
@@ -3137,6 +3139,7 @@ function appendFile(path, data, options, callback) {
31373139
const h = vfsState.handlers;
31383140
if (h !== null) {
31393141
const opts = typeof options === 'function' ? undefined : options;
3142+
if (checkAborted(opts?.signal, callback)) return;
31403143
try {
31413144
const result = h.appendFileSync(path, data, opts);
31423145
if (result !== undefined) {
@@ -3547,6 +3550,11 @@ function realpathSync(p, options) {
35473550
* @returns {string | Buffer}
35483551
*/
35493552
realpathSync.native = (path, options) => {
3553+
const h = vfsState.handlers;
3554+
if (h !== null) {
3555+
const result = h.realpathSync(path, options);
3556+
if (result !== undefined) return result;
3557+
}
35503558
options = getOptions(options);
35513559
return binding.realpath(
35523560
getValidatedPath(path),
@@ -3725,6 +3733,19 @@ function realpath(p, options, callback) {
37253733
*/
37263734
realpath.native = (path, options, callback) => {
37273735
callback = makeCallback(callback || options);
3736+
const h = vfsState.handlers;
3737+
if (h !== null) {
3738+
try {
3739+
const result = h.realpathSync(path, options);
3740+
if (result !== undefined) {
3741+
process.nextTick(callback, null, result);
3742+
return;
3743+
}
3744+
} catch (err) {
3745+
process.nextTick(callback, err);
3746+
return;
3747+
}
3748+
}
37283749
options = getOptions(options);
37293750
path = getValidatedPath(path);
37303751
const req = new FSReqCallback();

lib/internal/fs/promises.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,7 +1186,7 @@ async function lchmod(path, mode) {
11861186
async function lchown(path, uid, gid) {
11871187
const h = vfsState.handlers;
11881188
if (h !== null) {
1189-
const result = h.lchownSync(path);
1189+
const result = h.lchownSync(path, uid, gid);
11901190
if (result !== undefined) return;
11911191
}
11921192

@@ -1213,7 +1213,7 @@ async function fchown(handle, uid, gid) {
12131213
async function chown(path, uid, gid) {
12141214
const h = vfsState.handlers;
12151215
if (h !== null) {
1216-
const result = h.chownSync(path);
1216+
const result = h.chownSync(path, uid, gid);
12171217
if (result !== undefined) return;
12181218
}
12191219

@@ -1261,7 +1261,7 @@ async function futimes(handle, atime, mtime) {
12611261
async function lutimes(path, atime, mtime) {
12621262
const h = vfsState.handlers;
12631263
if (h !== null) {
1264-
const result = h.lutimesSync(path);
1264+
const result = h.lutimesSync(path, atime, mtime);
12651265
if (result !== undefined) return;
12661266
}
12671267

@@ -1346,6 +1346,7 @@ async function mkdtempDisposable(prefix, options) {
13461346
async function writeFile(path, data, options) {
13471347
const h = vfsState.handlers;
13481348
if (h !== null) {
1349+
checkAborted(options?.signal);
13491350
const vfsResult = await h.writeFile(path, data, options);
13501351
if (vfsResult !== undefined) return;
13511352
}
@@ -1388,6 +1389,7 @@ function isCustomIterable(obj) {
13881389
async function appendFile(path, data, options) {
13891390
const h = vfsState.handlers;
13901391
if (h !== null) {
1392+
checkAborted(options?.signal);
13911393
const vfsResult = await h.appendFile(path, data, options);
13921394
if (vfsResult !== undefined) return;
13931395
}
@@ -1400,6 +1402,7 @@ async function appendFile(path, data, options) {
14001402
async function readFile(path, options) {
14011403
const h = vfsState.handlers;
14021404
if (h !== null) {
1405+
checkAborted(options?.signal);
14031406
const vfsResult = await h.readFile(path, options);
14041407
if (vfsResult !== undefined) return vfsResult;
14051408
}

lib/internal/vfs/file_handle.js

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const {
44
DateNow,
55
MathMax,
66
MathMin,
7+
Number,
78
Symbol,
89
} = primordials;
910

@@ -363,7 +364,8 @@ class MemoryFileHandle extends VirtualFileHandle {
363364

364365
// Get content (resolves dynamic content providers)
365366
const content = this.content;
366-
const readPos = position !== null && position !== undefined ? position : this.position;
367+
const readPos = position !== null && position !== undefined ?
368+
Number(position) : this.position;
367369
const available = content.length - readPos;
368370

369371
if (available <= 0) {
@@ -409,7 +411,8 @@ class MemoryFileHandle extends VirtualFileHandle {
409411
// In append mode, always write at the end
410412
const writePos = this.#isAppend() ?
411413
this.#size :
412-
(position !== null && position !== undefined ? position : this.position);
414+
(position !== null && position !== undefined ?
415+
Number(position) : this.position);
413416
const data = buffer.subarray(offset, offset + length);
414417

415418
// Expand buffer if needed (geometric doubling for amortized O(1) appends)
@@ -429,10 +432,12 @@ class MemoryFileHandle extends VirtualFileHandle {
429432
this.#size = neededSize;
430433
}
431434

432-
// Update the entry's content and mtime
435+
// Update the entry's content, mtime, and ctime
433436
if (this.#entry) {
437+
const now = DateNow();
434438
this.#entry.content = this.#content.subarray(0, this.#size);
435-
this.#entry.mtime = DateNow();
439+
this.#entry.mtime = now;
440+
this.#entry.ctime = now;
436441
}
437442

438443
// Update position if not using explicit position
@@ -520,10 +525,12 @@ class MemoryFileHandle extends VirtualFileHandle {
520525
this.#size = buffer.length;
521526
}
522527

523-
// Update the entry's content and mtime
528+
// Update the entry's content, mtime, and ctime
524529
if (this.#entry) {
530+
const now = DateNow();
525531
this.#entry.content = this.#content.subarray(0, this.#size);
526-
this.#entry.mtime = DateNow();
532+
this.#entry.mtime = now;
533+
this.#entry.ctime = now;
527534
}
528535

529536
this.position = this.#size;
@@ -585,10 +592,12 @@ class MemoryFileHandle extends VirtualFileHandle {
585592
this.#size = len;
586593
}
587594

588-
// Update the entry's content and mtime
595+
// Update the entry's content, mtime, and ctime
589596
if (this.#entry) {
597+
const now = DateNow();
590598
this.#entry.content = this.#content.subarray(0, this.#size);
591-
this.#entry.mtime = DateNow();
599+
this.#entry.mtime = now;
600+
this.#entry.ctime = now;
592601
}
593602
}
594603

lib/internal/vfs/file_system.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,26 @@ class VirtualFileSystem {
640640
this[kProvider].linkSync(existingProviderPath, newProviderPath);
641641
}
642642

643+
chmodSync(filePath, mode) {
644+
const providerPath = this.#toProviderPath(filePath);
645+
this[kProvider].chmodSync(providerPath, mode);
646+
}
647+
648+
chownSync(filePath, uid, gid) {
649+
const providerPath = this.#toProviderPath(filePath);
650+
this[kProvider].chownSync(providerPath, uid, gid);
651+
}
652+
653+
utimesSync(filePath, atime, mtime) {
654+
const providerPath = this.#toProviderPath(filePath);
655+
this[kProvider].utimesSync(providerPath, atime, mtime);
656+
}
657+
658+
lutimesSync(filePath, atime, mtime) {
659+
const providerPath = this.#toProviderPath(filePath);
660+
this[kProvider].lutimesSync(providerPath, atime, mtime);
661+
}
662+
643663
/**
644664
* Creates a unique temporary directory synchronously.
645665
* @param {string} prefix The prefix for the temp directory

lib/internal/vfs/provider.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,9 @@ class VirtualProvider {
244244
* @returns {Promise<Buffer|string>}
245245
*/
246246
async readFile(path, options) {
247-
const handle = await this.open(path, 'r');
247+
const flag = (typeof options === 'object' && options !== null) ?
248+
(options.flag ?? 'r') : 'r';
249+
const handle = await this.open(path, flag);
248250
try {
249251
return await handle.readFile(options);
250252
} finally {
@@ -259,7 +261,9 @@ class VirtualProvider {
259261
* @returns {Buffer|string}
260262
*/
261263
readFileSync(path, options) {
262-
const handle = this.openSync(path, 'r');
264+
const flag = (typeof options === 'object' && options !== null) ?
265+
(options.flag ?? 'r') : 'r';
266+
const handle = this.openSync(path, flag);
263267
try {
264268
return handle.readFileSync(options);
265269
} finally {

lib/internal/vfs/providers/memory.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,19 @@ function normalizeFlags(flags) {
8282
return 'r';
8383
}
8484

85+
/**
86+
* Converts a time argument (Date, number, or string) to milliseconds.
87+
* Numbers are treated as seconds (matching Node.js utimes convention).
88+
* @param {Date|number|string} time The time value
89+
* @returns {number} Milliseconds since epoch
90+
*/
91+
function toMs(time) {
92+
if (typeof time === 'number') return time * 1000;
93+
if (typeof time === 'string') return DateNow(); // Fallback for string timestamps
94+
if (typeof time === 'object' && time !== null) return +time;
95+
return time;
96+
}
97+
8598
// Private symbols
8699
const kRoot = Symbol('kRoot');
87100
const kReadonly = Symbol('kReadonly');
@@ -108,7 +121,11 @@ class MemoryEntry {
108121
this.children = null; // For directories
109122
this.populate = null; // For directories - lazy population callback
110123
this.populated = true; // For directories - has populate been called?
124+
this.nlink = 1;
125+
this.uid = 0;
126+
this.gid = 0;
111127
const now = DateNow();
128+
this.atime = now;
112129
this.mtime = now;
113130
this.ctime = now;
114131
this.birthtime = now;
@@ -409,6 +426,10 @@ class MemoryProvider extends VirtualProvider {
409426
#createStats(entry, size, bigint) {
410427
const options = {
411428
mode: entry.mode,
429+
nlink: entry.nlink,
430+
uid: entry.uid,
431+
gid: entry.gid,
432+
atimeMs: entry.atime,
412433
mtimeMs: entry.mtime,
413434
ctimeMs: entry.ctime,
414435
birthtimeMs: entry.birthtime,
@@ -715,6 +736,7 @@ class MemoryProvider extends VirtualProvider {
715736
const parent = this.#ensureParent(normalized, false, 'unlink');
716737
const name = pathPosix.basename(normalized);
717738
parent.children.delete(name);
739+
entry.nlink--;
718740
}
719741

720742
async unlink(path) {
@@ -786,6 +808,7 @@ class MemoryProvider extends VirtualProvider {
786808
const name = pathPosix.basename(normalizedNew);
787809
// Hard link: same entry object referenced by both names
788810
parent.children.set(name, entry);
811+
entry.nlink++;
789812
}
790813

791814
async link(existingPath, newPath) {
@@ -846,6 +869,36 @@ class MemoryProvider extends VirtualProvider {
846869
return this.realpathSync(path, options);
847870
}
848871

872+
// === METADATA OPERATIONS ===
873+
874+
chmodSync(path, mode) {
875+
const entry = this.#getEntry(path, 'chmod', true);
876+
// Preserve file type bits, update permission bits
877+
entry.mode = (entry.mode & ~0o7777) | (mode & 0o7777);
878+
entry.ctime = DateNow();
879+
}
880+
881+
chownSync(path, uid, gid) {
882+
const entry = this.#getEntry(path, 'chown', true);
883+
if (uid >= 0) entry.uid = uid;
884+
if (gid >= 0) entry.gid = gid;
885+
entry.ctime = DateNow();
886+
}
887+
888+
utimesSync(path, atime, mtime) {
889+
const entry = this.#getEntry(path, 'utime', true);
890+
entry.atime = toMs(atime);
891+
entry.mtime = toMs(mtime);
892+
entry.ctime = DateNow();
893+
}
894+
895+
lutimesSync(path, atime, mtime) {
896+
const entry = this.#getEntry(path, 'utime', false);
897+
entry.atime = toMs(atime);
898+
entry.mtime = toMs(mtime);
899+
entry.ctime = DateNow();
900+
}
901+
849902
// === WATCH OPERATIONS ===
850903

851904
/**

0 commit comments

Comments
 (0)