Skip to content

Commit b047092

Browse files
committed
vfs: fix validation, symlink, timestamp, and bigint bugs
- Throw ERR_INVALID_PACKAGE_CONFIG for malformed package.json in VFS instead of silently falling through to index.js resolution - Move writeFile/appendFile options validation (getOptions, parseFileMode, validateBoolean) before VFS fast path so invalid options are rejected - Validate flags with stringToFlags() before VFS check in open/openSync so invalid flag values like {} throw ERR_INVALID_ARG_VALUE - Fix rmdirSync to not follow symlinks (use getEntry with false) so symlinks to directories correctly throw ENOTDIR - Update parent directory mtime/ctime when children are added or removed in openSync, mkdirSync, rmdirSync, unlinkSync, linkSync, symlinkSync, and renameSync - Pass bigint option through to VFS statfs handlers and return BigInt values when options.bigint is true
1 parent be92138 commit b047092

File tree

10 files changed

+457
-48
lines changed

10 files changed

+457
-48
lines changed

lib/fs.js

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -611,10 +611,12 @@ function open(path, flags, mode, callback) {
611611
mode = parseFileMode(mode, 'mode', 0o666);
612612
}
613613

614+
const flagsNumber = stringToFlags(flags);
615+
614616
const h = vfsState.handlers;
615617
if (h !== null) {
616618
try {
617-
const result = h.openSync(path, flags, mode);
619+
const result = h.openSync(path, flagsNumber, mode);
618620
if (result !== undefined) {
619621
process.nextTick(callback, null, result);
620622
return;
@@ -626,7 +628,6 @@ function open(path, flags, mode, callback) {
626628
}
627629

628630
path = getValidatedPath(path);
629-
const flagsNumber = stringToFlags(flags);
630631
callback = makeCallback(callback);
631632

632633
const req = new FSReqCallback();
@@ -643,15 +644,17 @@ function open(path, flags, mode, callback) {
643644
* @returns {number}
644645
*/
645646
function openSync(path, flags, mode) {
647+
flags = stringToFlags(flags);
648+
mode = parseFileMode(mode, 'mode', 0o666);
646649
const h = vfsState.handlers;
647650
if (h !== null) {
648651
const result = h.openSync(path, flags, mode);
649652
if (result !== undefined) return result;
650653
}
651654
return binding.open(
652655
getValidatedPath(path),
653-
stringToFlags(flags),
654-
parseFileMode(mode, 'mode', 0o666),
656+
flags,
657+
mode,
655658
);
656659
}
657660

@@ -2070,7 +2073,7 @@ function statfs(path, options = { bigint: false }, callback) {
20702073

20712074
const h = vfsState.handlers;
20722075
if (h !== null) {
2073-
const result = h.statfsSync(path);
2076+
const result = h.statfsSync(path, options);
20742077
if (result !== undefined) {
20752078
process.nextTick(callback, null, result);
20762079
return;
@@ -2174,7 +2177,7 @@ function statSync(path, options = { bigint: false, throwIfNoEntry: true }) {
21742177
function statfsSync(path, options = { bigint: false }) {
21752178
const h = vfsState.handlers;
21762179
if (h !== null) {
2177-
const result = h.statfsSync(path);
2180+
const result = h.statfsSync(path, options);
21782181
if (result !== undefined) return result;
21792182
}
21802183

@@ -2996,12 +2999,21 @@ function writeFile(path, data, options, callback) {
29962999
callback ||= options;
29973000
validateFunction(callback, 'cb');
29983001

3002+
options = getOptions(typeof options === 'function' ? null : options, {
3003+
encoding: 'utf8',
3004+
mode: 0o666,
3005+
flag: 'w',
3006+
flush: false,
3007+
});
3008+
const flush = options.flush ?? false;
3009+
validateBoolean(flush, 'options.flush');
3010+
parseFileMode(options.mode, 'mode', 0o666);
3011+
29993012
const h = vfsState.handlers;
30003013
if (h !== null) {
3001-
const opts = typeof options === 'function' ? undefined : options;
3002-
if (checkAborted(opts?.signal, callback)) return;
3014+
if (checkAborted(options.signal, callback)) return;
30033015
try {
3004-
const result = h.writeFileSync(path, data, opts);
3016+
const result = h.writeFileSync(path, data, options);
30053017
if (result !== undefined) {
30063018
process.nextTick(callback, null);
30073019
return;
@@ -3012,16 +3024,7 @@ function writeFile(path, data, options, callback) {
30123024
}
30133025
}
30143026

3015-
options = getOptions(options, {
3016-
encoding: 'utf8',
3017-
mode: 0o666,
3018-
flag: 'w',
3019-
flush: false,
3020-
});
30213027
const flag = options.flag || 'w';
3022-
const flush = options.flush ?? false;
3023-
3024-
validateBoolean(flush, 'options.flush');
30253028

30263029
if (!isArrayBufferView(data)) {
30273030
validateStringAfterArrayBufferView(data, 'data');
@@ -3062,21 +3065,21 @@ function writeFile(path, data, options, callback) {
30623065
* @returns {void}
30633066
*/
30643067
function writeFileSync(path, data, options) {
3065-
const h = vfsState.handlers;
3066-
if (h !== null) {
3067-
const result = h.writeFileSync(path, data, options);
3068-
if (result !== undefined) return;
3069-
}
30703068
options = getOptions(options, {
30713069
encoding: 'utf8',
30723070
mode: 0o666,
30733071
flag: 'w',
30743072
flush: false,
30753073
});
3076-
30773074
const flush = options.flush ?? false;
3078-
30793075
validateBoolean(flush, 'options.flush');
3076+
parseFileMode(options.mode, 'mode', 0o666);
3077+
3078+
const h = vfsState.handlers;
3079+
if (h !== null) {
3080+
const result = h.writeFileSync(path, data, options);
3081+
if (result !== undefined) return;
3082+
}
30803083

30813084
const flag = options.flag || 'w';
30823085

@@ -3136,12 +3139,16 @@ function appendFile(path, data, options, callback) {
31363139
callback ||= options;
31373140
validateFunction(callback, 'cb');
31383141

3142+
options = getOptions(typeof options === 'function' ? null : options, {
3143+
encoding: 'utf8', mode: 0o666, flag: 'a',
3144+
});
3145+
parseFileMode(options.mode, 'mode', 0o666);
3146+
31393147
const h = vfsState.handlers;
31403148
if (h !== null) {
3141-
const opts = typeof options === 'function' ? undefined : options;
3142-
if (checkAborted(opts?.signal, callback)) return;
3149+
if (checkAborted(options.signal, callback)) return;
31433150
try {
3144-
const result = h.appendFileSync(path, data, opts);
3151+
const result = h.appendFileSync(path, data, options);
31453152
if (result !== undefined) {
31463153
process.nextTick(callback, null);
31473154
return;
@@ -3152,8 +3159,6 @@ function appendFile(path, data, options, callback) {
31523159
}
31533160
}
31543161

3155-
options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'a' });
3156-
31573162
// Don't make changes directly on options object
31583163
options = copyObject(options);
31593164

@@ -3176,12 +3181,14 @@ function appendFile(path, data, options, callback) {
31763181
* @returns {void}
31773182
*/
31783183
function appendFileSync(path, data, options) {
3184+
options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'a' });
3185+
parseFileMode(options.mode, 'mode', 0o666);
3186+
31793187
const h = vfsState.handlers;
31803188
if (h !== null) {
31813189
const result = h.appendFileSync(path, data, options);
31823190
if (result !== undefined) return;
31833191
}
3184-
options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'a' });
31853192

31863193
// Don't make changes directly on options object
31873194
options = copyObject(options);

lib/internal/fs/promises.js

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,7 +1103,7 @@ async function stat(path, options = { bigint: false, throwIfNoEntry: true }) {
11031103
async function statfs(path, options = { bigint: false }) {
11041104
const h = vfsState.handlers;
11051105
if (h !== null) {
1106-
const vfsResult = await h.statfs(path);
1106+
const vfsResult = await h.statfs(path, options);
11071107
if (vfsResult !== undefined) return vfsResult;
11081108
}
11091109

@@ -1344,22 +1344,24 @@ async function mkdtempDisposable(prefix, options) {
13441344
}
13451345

13461346
async function writeFile(path, data, options) {
1347-
const h = vfsState.handlers;
1348-
if (h !== null) {
1349-
checkAborted(options?.signal);
1350-
const vfsResult = await h.writeFile(path, data, options);
1351-
if (vfsResult !== undefined) return;
1352-
}
13531347
options = getOptions(options, {
13541348
encoding: 'utf8',
13551349
mode: 0o666,
13561350
flag: 'w',
13571351
flush: false,
13581352
});
1359-
const flag = options.flag || 'w';
13601353
const flush = options.flush ?? false;
1361-
13621354
validateBoolean(flush, 'options.flush');
1355+
parseFileMode(options.mode, 'mode', 0o666);
1356+
1357+
const h = vfsState.handlers;
1358+
if (h !== null) {
1359+
checkAborted(options.signal);
1360+
const vfsResult = await h.writeFile(path, data, options);
1361+
if (vfsResult !== undefined) return;
1362+
}
1363+
1364+
const flag = options.flag || 'w';
13631365

13641366
if (!isArrayBufferView(data) && !isCustomIterable(data)) {
13651367
validateStringAfterArrayBufferView(data, 'data');
@@ -1387,13 +1389,15 @@ function isCustomIterable(obj) {
13871389
}
13881390

13891391
async function appendFile(path, data, options) {
1392+
options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'a' });
1393+
parseFileMode(options.mode, 'mode', 0o666);
1394+
13901395
const h = vfsState.handlers;
13911396
if (h !== null) {
1392-
checkAborted(options?.signal);
1397+
checkAborted(options.signal);
13931398
const vfsResult = await h.appendFile(path, data, options);
13941399
if (vfsResult !== undefined) return;
13951400
}
1396-
options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'a' });
13971401
options = copyObject(options);
13981402
options.flag ||= 'a';
13991403
return writeFile(path, data, options);

lib/internal/vfs/providers/memory.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,9 @@ class MemoryProvider extends VirtualProvider {
521521
entry = new MemoryEntry(TYPE_FILE, { mode });
522522
entry.content = Buffer.alloc(0);
523523
parent.children.set(name, entry);
524+
const now = DateNow();
525+
parent.mtime = now;
526+
parent.ctime = now;
524527
}
525528

526529
if (entry.isDirectory()) {
@@ -689,6 +692,9 @@ class MemoryProvider extends VirtualProvider {
689692
const entry = new MemoryEntry(TYPE_DIR, { mode: options?.mode });
690693
entry.children = new SafeMap();
691694
parent.children.set(name, entry);
695+
const now = DateNow();
696+
parent.mtime = now;
697+
parent.ctime = now;
692698
return undefined;
693699
}
694700

@@ -702,7 +708,7 @@ class MemoryProvider extends VirtualProvider {
702708
}
703709

704710
const normalized = this.#normalizePath(path);
705-
const entry = this.#getEntry(normalized, 'rmdir', true);
711+
const entry = this.#getEntry(normalized, 'rmdir', false);
706712

707713
if (!entry.isDirectory()) {
708714
throw createENOTDIR('rmdir', path);
@@ -715,6 +721,9 @@ class MemoryProvider extends VirtualProvider {
715721
const parent = this.#ensureParent(normalized, false, 'rmdir');
716722
const name = pathPosix.basename(normalized);
717723
parent.children.delete(name);
724+
const now = DateNow();
725+
parent.mtime = now;
726+
parent.ctime = now;
718727
}
719728

720729
async rmdir(path) {
@@ -737,6 +746,9 @@ class MemoryProvider extends VirtualProvider {
737746
const name = pathPosix.basename(normalized);
738747
parent.children.delete(name);
739748
entry.nlink--;
749+
const now = DateNow();
750+
parent.mtime = now;
751+
parent.ctime = now;
740752
}
741753

742754
async unlink(path) {
@@ -778,6 +790,14 @@ class MemoryProvider extends VirtualProvider {
778790

779791
// Add to new location
780792
newParent.children.set(newName, entry);
793+
794+
const now = DateNow();
795+
oldParent.mtime = now;
796+
oldParent.ctime = now;
797+
if (newParent !== oldParent) {
798+
newParent.mtime = now;
799+
newParent.ctime = now;
800+
}
781801
}
782802

783803
async rename(oldPath, newPath) {
@@ -809,6 +829,9 @@ class MemoryProvider extends VirtualProvider {
809829
// Hard link: same entry object referenced by both names
810830
parent.children.set(name, entry);
811831
entry.nlink++;
832+
const now = DateNow();
833+
parent.mtime = now;
834+
parent.ctime = now;
812835
}
813836

814837
async link(existingPath, newPath) {
@@ -848,6 +871,9 @@ class MemoryProvider extends VirtualProvider {
848871
const entry = new MemoryEntry(TYPE_SYMLINK);
849872
entry.target = target;
850873
parent.children.set(name, entry);
874+
const now = DateNow();
875+
parent.mtime = now;
876+
parent.ctime = now;
851877
}
852878

853879
async symlink(target, path, type) {

lib/internal/vfs/setup.js

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const { validateObject } = require('internal/validators');
2020
const {
2121
codes: {
2222
ERR_INVALID_ARG_TYPE,
23+
ERR_INVALID_PACKAGE_CONFIG,
2324
ERR_MODULE_NOT_FOUND,
2425
},
2526
} = require('internal/errors');
@@ -358,8 +359,12 @@ function findVFSPackageJSON(startPath) {
358359
const content = vfs.readFileSync(pjsonPath, 'utf8');
359360
const parsed = JSONParse(content);
360361
return { vfs, pjsonPath, parsed };
361-
} catch {
362-
// Invalid JSON, continue walking
362+
} catch (err) {
363+
if (err?.name === 'SyntaxError') {
364+
throw new ERR_INVALID_PACKAGE_CONFIG(
365+
pjsonPath, null, err.message);
366+
}
367+
// Other errors (ENOENT etc), continue walking
363368
}
364369
}
365370
}
@@ -481,9 +486,15 @@ function createVfsHandlers() {
481486
}
482487
return undefined;
483488
},
484-
statfsSync(path) {
489+
statfsSync(path, options) {
485490
const pathStr = toPathStr(path);
486491
if (pathStr !== null && findVFSForPath(pathStr) !== null) {
492+
if (options?.bigint) {
493+
return {
494+
type: 0n, bsize: 4096n, blocks: 0n,
495+
bfree: 0n, bavail: 0n, files: 0n, ffree: 0n,
496+
};
497+
}
487498
return { type: 0, bsize: 4096, blocks: 0, bfree: 0, bavail: 0, files: 0, ffree: 0 };
488499
}
489500
return undefined;
@@ -918,9 +929,16 @@ function createVfsHandlers() {
918929
}
919930
return undefined;
920931
},
921-
async statfs(path) {
932+
async statfs(path, options) {
922933
const pathStr = toPathStr(path);
923934
if (pathStr !== null && findVFSForPath(pathStr) !== null) {
935+
if (options?.bigint) {
936+
return {
937+
__proto__: null,
938+
type: 0n, bsize: 4096n, blocks: 0n,
939+
bfree: 0n, bavail: 0n, files: 0n, ffree: 0n,
940+
};
941+
}
924942
return {
925943
__proto__: null,
926944
type: 0, bsize: 4096, blocks: 0,
@@ -1140,7 +1158,11 @@ function installModuleLoaderOverrides() {
11401158
const content = vfs.readFileSync(normalized, 'utf8');
11411159
const parsed = JSONParse(content);
11421160
return serializePackageJSON(parsed, normalized);
1143-
} catch {
1161+
} catch (err) {
1162+
if (err?.name === 'SyntaxError') {
1163+
throw new ERR_INVALID_PACKAGE_CONFIG(
1164+
normalized, isESM ? base : undefined, err.message);
1165+
}
11441166
return undefined;
11451167
}
11461168
}

0 commit comments

Comments
 (0)