Skip to content

Commit 164ba61

Browse files
committed
vfs: fix path normalization on Windows
Use POSIX path normalization for VFS paths starting with '/' to preserve forward slashes on Windows. The platform's path.normalize() converts forward slashes to backslashes on Windows, breaking VFS path matching. Add normalizeVFSPath() helper that uses path.posix.normalize() for Unix-style paths and path.normalize() for Windows drive letter paths. Fixes test-vfs-chdir, test-vfs-real-provider, test-vfs-mount-events, and test-vfs-watch on Windows.
1 parent 38bf3ad commit 164ba61

File tree

2 files changed

+70
-20
lines changed

2 files changed

+70
-20
lines changed

lib/internal/vfs/file_system.js

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,37 @@ const {
1313
} = require('internal/errors');
1414
const { validateBoolean } = require('internal/validators');
1515
const { MemoryProvider } = require('internal/vfs/providers/memory');
16-
const { normalize, resolve, join, isAbsolute } = require('path');
16+
const path = require('path');
17+
const pathPosix = path.posix;
18+
const { isAbsolute, resolve: resolvePath } = path;
19+
20+
/**
21+
* Normalizes a VFS path. Uses POSIX normalization for Unix-style paths (starting with /)
22+
* and platform normalization for Windows drive letter paths.
23+
* @param {string} inputPath The path to normalize
24+
* @returns {string} The normalized path
25+
*/
26+
function normalizeVFSPath(inputPath) {
27+
// If path starts with / (Unix-style), use posix normalization to preserve forward slashes
28+
if (inputPath.startsWith('/')) {
29+
return pathPosix.normalize(inputPath);
30+
}
31+
// Otherwise use platform normalization (for Windows drive letters like C:\)
32+
return path.normalize(inputPath);
33+
}
34+
35+
/**
36+
* Joins VFS paths. Uses POSIX join for Unix-style base paths.
37+
* @param {string} base The base path
38+
* @param {string} part The path part to join
39+
* @returns {string} The joined path
40+
*/
41+
function joinVFSPath(base, part) {
42+
if (base.startsWith('/')) {
43+
return pathPosix.join(base, part);
44+
}
45+
return path.join(base, part);
46+
}
1747
const {
1848
isUnderMountPoint,
1949
getRelativePath,
@@ -200,17 +230,17 @@ class VirtualFileSystem {
200230
resolvePath(inputPath) {
201231
// If path is absolute, return as-is
202232
if (isAbsolute(inputPath)) {
203-
return normalize(inputPath);
233+
return normalizeVFSPath(inputPath);
204234
}
205235

206236
// If virtual cwd is enabled and set, resolve relative to it
207237
if (this[kVirtualCwdEnabled] && this[kVirtualCwd] !== null) {
208238
const resolved = `${this[kVirtualCwd]}/${inputPath}`;
209-
return normalize(resolved);
239+
return normalizeVFSPath(resolved);
210240
}
211241

212242
// Fall back to resolving the path (will use real cwd)
213-
return resolve(inputPath);
243+
return resolvePath(inputPath);
214244
}
215245

216246
// ==================== Mount ====================
@@ -224,7 +254,7 @@ class VirtualFileSystem {
224254
if (this[kMounted]) {
225255
throw new ERR_INVALID_STATE('VFS is already mounted');
226256
}
227-
this[kMountPoint] = normalize(prefix);
257+
this[kMountPoint] = normalizeVFSPath(prefix);
228258
this[kMounted] = true;
229259
if (this[kModuleHooks]) {
230260
loadModuleHooks();
@@ -292,7 +322,10 @@ class VirtualFileSystem {
292322
this[kOriginalCwd] = process.cwd;
293323

294324
process.chdir = function chdir(directory) {
295-
const normalized = resolve(directory);
325+
// Normalize path for VFS comparison (preserves forward slashes for Unix-style paths)
326+
const normalized = isAbsolute(directory) ?
327+
normalizeVFSPath(directory) :
328+
resolvePath(directory);
296329

297330
if (vfs.shouldHandle(normalized)) {
298331
vfs.chdir(normalized);
@@ -356,7 +389,7 @@ class VirtualFileSystem {
356389
*/
357390
_toMountedPath(providerPath) {
358391
if (this[kMounted] && this[kMountPoint]) {
359-
return join(this[kMountPoint], providerPath);
392+
return joinVFSPath(this[kMountPoint], providerPath);
360393
}
361394
return providerPath;
362395
}
@@ -373,7 +406,7 @@ class VirtualFileSystem {
373406
return false;
374407
}
375408

376-
const normalized = normalize(inputPath);
409+
const normalized = normalizeVFSPath(inputPath);
377410
if (!isUnderMountPoint(normalized, this[kMountPoint])) {
378411
return false;
379412
}

lib/internal/vfs/module_hooks.js

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,25 @@ const {
88
StringPrototypeStartsWith,
99
} = primordials;
1010

11-
const { dirname, extname, isAbsolute, normalize, resolve } = require('path');
11+
const path = require('path');
12+
const { dirname, extname, isAbsolute, resolve } = path;
13+
const pathPosix = path.posix;
1214
const { extensionFormatMap } = require('internal/modules/esm/formats');
15+
16+
/**
17+
* Normalizes a VFS path. Uses POSIX normalization for Unix-style paths (starting with /)
18+
* and platform normalization for Windows drive letter paths.
19+
* @param {string} inputPath The path to normalize
20+
* @returns {string} The normalized path
21+
*/
22+
function normalizeVFSPath(inputPath) {
23+
// If path starts with / (Unix-style), use posix normalization to preserve forward slashes
24+
if (inputPath.startsWith('/')) {
25+
return pathPosix.normalize(inputPath);
26+
}
27+
// Otherwise use platform normalization (for Windows drive letters like C:\)
28+
return path.normalize(inputPath);
29+
}
1330
const { isURL, pathToFileURL, fileURLToPath, toPathIfFileURL, URL } = require('internal/url');
1431
const { kEmptyObject } = require('internal/util');
1532
const { validateObject } = require('internal/validators');
@@ -79,7 +96,7 @@ function unregisterVFS(vfs) {
7996
* @returns {{ vfs: VirtualFileSystem, result: number }|null}
8097
*/
8198
function findVFSForStat(filename) {
82-
const normalized = normalize(filename);
99+
const normalized = normalizeVFSPath(filename);
83100
for (let i = 0; i < activeVFSList.length; i++) {
84101
const vfs = activeVFSList[i];
85102
if (vfs.shouldHandle(normalized)) {
@@ -101,7 +118,7 @@ function findVFSForStat(filename) {
101118
* @returns {{ vfs: VirtualFileSystem, content: Buffer|string }|null}
102119
*/
103120
function findVFSForRead(filename, options) {
104-
const normalized = normalize(filename);
121+
const normalized = normalizeVFSPath(filename);
105122
for (let i = 0; i < activeVFSList.length; i++) {
106123
const vfs = activeVFSList[i];
107124
if (vfs.shouldHandle(normalized)) {
@@ -140,7 +157,7 @@ function findVFSForRead(filename, options) {
140157
* @returns {{ vfs: VirtualFileSystem, exists: boolean }|null}
141158
*/
142159
function findVFSForExists(filename) {
143-
const normalized = normalize(filename);
160+
const normalized = normalizeVFSPath(filename);
144161
for (let i = 0; i < activeVFSList.length; i++) {
145162
const vfs = activeVFSList[i];
146163
if (vfs.shouldHandle(normalized)) {
@@ -161,7 +178,7 @@ function findVFSForExists(filename) {
161178
* @returns {{ vfs: VirtualFileSystem, realpath: string }|null}
162179
*/
163180
function findVFSForRealpath(filename) {
164-
const normalized = normalize(filename);
181+
const normalized = normalizeVFSPath(filename);
165182
for (let i = 0; i < activeVFSList.length; i++) {
166183
const vfs = activeVFSList[i];
167184
if (vfs.shouldHandle(normalized)) {
@@ -188,7 +205,7 @@ function findVFSForRealpath(filename) {
188205
* @returns {{ vfs: VirtualFileSystem, stats: Stats }|null}
189206
*/
190207
function findVFSForFsStat(filename) {
191-
const normalized = normalize(filename);
208+
const normalized = normalizeVFSPath(filename);
192209
for (let i = 0; i < activeVFSList.length; i++) {
193210
const vfs = activeVFSList[i];
194211
if (vfs.shouldHandle(normalized)) {
@@ -216,7 +233,7 @@ function findVFSForFsStat(filename) {
216233
* @returns {{ vfs: VirtualFileSystem, entries: string[]|Dirent[] }|null}
217234
*/
218235
function findVFSForReaddir(dirname, options) {
219-
const normalized = normalize(dirname);
236+
const normalized = normalizeVFSPath(dirname);
220237
for (let i = 0; i < activeVFSList.length; i++) {
221238
const vfs = activeVFSList[i];
222239
if (vfs.shouldHandle(normalized)) {
@@ -244,7 +261,7 @@ function findVFSForReaddir(dirname, options) {
244261
* @returns {Promise<{ vfs: VirtualFileSystem, entries: string[]|Dirent[] }|null>}
245262
*/
246263
async function findVFSForReaddirAsync(dirname, options) {
247-
const normalized = normalize(dirname);
264+
const normalized = normalizeVFSPath(dirname);
248265
for (let i = 0; i < activeVFSList.length; i++) {
249266
const vfs = activeVFSList[i];
250267
if (vfs.shouldHandle(normalized)) {
@@ -271,7 +288,7 @@ async function findVFSForReaddirAsync(dirname, options) {
271288
* @returns {Promise<{ vfs: VirtualFileSystem, stats: Stats }|null>}
272289
*/
273290
async function findVFSForLstatAsync(filename) {
274-
const normalized = normalize(filename);
291+
const normalized = normalizeVFSPath(filename);
275292
for (let i = 0; i < activeVFSList.length; i++) {
276293
const vfs = activeVFSList[i];
277294
if (vfs.shouldHandle(normalized)) {
@@ -299,7 +316,7 @@ async function findVFSForLstatAsync(filename) {
299316
* @returns {{ vfs: VirtualFileSystem }|null}
300317
*/
301318
function findVFSForWatch(filename) {
302-
const normalized = normalize(filename);
319+
const normalized = normalizeVFSPath(filename);
303320
for (let i = 0; i < activeVFSList.length; i++) {
304321
const vfs = activeVFSList[i];
305322
if (vfs.shouldHandle(normalized)) {
@@ -382,7 +399,7 @@ function vfsResolveHook(specifier, context, nextResolve) {
382399
}
383400

384401
// Check if any VFS handles this path
385-
const normalized = normalize(checkPath);
402+
const normalized = normalizeVFSPath(checkPath);
386403
for (let i = 0; i < activeVFSList.length; i++) {
387404
const vfs = activeVFSList[i];
388405
if (vfs.shouldHandle(normalized) && vfs.existsSync(normalized)) {
@@ -426,7 +443,7 @@ function vfsLoadHook(url, context, nextLoad) {
426443
}
427444

428445
const filePath = fileURLToPath(url);
429-
const normalized = normalize(filePath);
446+
const normalized = normalizeVFSPath(filePath);
430447

431448
// Check if any VFS handles this path
432449
for (let i = 0; i < activeVFSList.length; i++) {

0 commit comments

Comments
 (0)