Skip to content

Commit 6570316

Browse files
pipobscureclaude
andcommitted
vfs: report host paths as readdir parentPath
A recursive `readdirSync()` joined each entry's subdirectory onto the listed path with `path.posix.join()`, so on Windows `parentPath` mixed separators (`\.\nul\vfs\0/dir`). The callback and promise `readdir()` did not rewrite `parentPath` at all, returning the provider-relative `/`, and left recursive names such as `dir/file.txt` unsplit. Rewrite the Dirents of all three in one place, joining with the host's separator for a mounted VFS, as fs does, and keeping POSIX paths for an unmounted one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 9cc0ed9 commit 6570316

2 files changed

Lines changed: 48 additions & 24 deletions

File tree

‎lib/internal/vfs/file_system.js‎

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const {
44
MathRandom,
55
ObjectFreeze,
66
StringPrototypeIncludes,
7+
StringPrototypeLastIndexOf,
8+
StringPrototypeSlice,
79
StringPrototypeStartsWith,
810
Symbol,
911
SymbolDispose,
@@ -379,28 +381,36 @@ class VirtualFileSystem {
379381
readdirSync(dirPath, options) {
380382
const providerPath = this.#toProviderPath(dirPath);
381383
const result = this[kProvider].readdirSync(providerPath, options);
384+
return this.#toMountedDirents(dirPath, result, options);
385+
}
382386

383-
// Rewrite Dirent parentPath from provider-relative to VFS path.
384-
if (options?.withFileTypes === true) {
385-
const recursive = options?.recursive === true;
386-
for (let i = 0; i < result.length; i++) {
387-
const dirent = result[i];
388-
if (recursive) {
389-
// In recursive mode, name may contain slashes (e.g. 'a/b.txt').
390-
const slashIdx = dirent.name.lastIndexOf('/');
391-
if (slashIdx !== -1) {
392-
const subdir = dirent.name.slice(0, slashIdx);
393-
dirent.parentPath = joinPath(dirPath, subdir);
394-
dirent.name = dirent.name.slice(slashIdx + 1);
395-
} else {
396-
dirent.parentPath = dirPath;
397-
}
398-
} else {
399-
dirent.parentPath = dirPath;
387+
/**
388+
* Rewrites the Dirents of a listing of `dirPath` from provider-relative to
389+
* VFS paths, so that each `parentPath` is the directory the entry is in.
390+
* @param {string} dirPath The listed directory, as given by the caller
391+
* @param {string[]|Dirent[]} result The provider's listing
392+
* @param {object} [options] The readdir options
393+
* @returns {string[]|Dirent[]}
394+
*/
395+
#toMountedDirents(dirPath, result, options) {
396+
if (options?.withFileTypes !== true) return result;
397+
const recursive = options?.recursive === true;
398+
// A mounted VFS is addressed by host paths, so, like fs, join with the
399+
// host's separator; an unmounted one uses POSIX paths throughout.
400+
const join = this[kMounted] ? path.join : joinPath;
401+
for (let i = 0; i < result.length; i++) {
402+
const dirent = result[i];
403+
dirent.parentPath = dirPath;
404+
if (recursive) {
405+
// In recursive mode, name may contain slashes (e.g. 'a/b.txt').
406+
const slashIdx = StringPrototypeLastIndexOf(dirent.name, '/');
407+
if (slashIdx !== -1) {
408+
const subdir = StringPrototypeSlice(dirent.name, 0, slashIdx);
409+
dirent.parentPath = join(dirPath, subdir);
410+
dirent.name = StringPrototypeSlice(dirent.name, slashIdx + 1);
400411
}
401412
}
402413
}
403-
404414
return result;
405415
}
406416

@@ -813,7 +823,8 @@ class VirtualFileSystem {
813823
}
814824

815825
this[kProvider].readdir(this.#toProviderPath(dirPath), options)
816-
.then((entries) => callback(null, entries), (err) => callback(err));
826+
.then((entries) => callback(null, this.#toMountedDirents(dirPath, entries, options)),
827+
(err) => callback(err));
817828
}
818829

819830
/**
@@ -1173,6 +1184,8 @@ class VirtualFileSystem {
11731184
const toProviderPath = (p) => this.#toProviderPath(p);
11741185
const toProviderPrefix = (p) => this.#toProviderPrefix(p);
11751186
const toMountedPath = (p) => this.#toMountedPath(p);
1187+
const toMountedDirents = (p, result, options) =>
1188+
this.#toMountedDirents(p, result, options);
11761189

11771190
return ObjectFreeze({
11781191
async readFile(filePath, options) {
@@ -1202,7 +1215,8 @@ class VirtualFileSystem {
12021215

12031216
async readdir(dirPath, options) {
12041217
const providerPath = toProviderPath(dirPath);
1205-
return provider.readdir(providerPath, options);
1218+
const result = await provider.readdir(providerPath, options);
1219+
return toMountedDirents(dirPath, result, options);
12061220
},
12071221

12081222
async mkdir(dirPath, options) {

‎test/parallel/test-vfs-reserved-root.js‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,20 @@ const linkB = path.join(root, 'b');
7575
assert.deepStrictEqual(fs.readdirSync(root, { recursive: true }).sort(),
7676
expected.sort());
7777

78-
const dirents = fs.readdirSync(root, { recursive: true, withFileTypes: true });
79-
const file = dirents.find((d) => d.name === 'file.txt' &&
80-
d.parentPath === path.join(mountA, 'dir'));
81-
assert.ok(file?.isFile());
78+
// Every API names each entry's directory as a host path.
79+
const options = { recursive: true, withFileTypes: true };
80+
const check = common.mustCall((dirents) => {
81+
const file = dirents.find((d) => d.name === 'file.txt' &&
82+
d.parentPath === path.join(mountA, 'dir'));
83+
assert.ok(file?.isFile());
84+
const layer = dirents.find((d) => d.name === idA);
85+
assert.strictEqual(layer?.parentPath, root);
86+
}, 3);
87+
check(fs.readdirSync(root, options));
88+
fs.readdir(root, options, common.mustSucceed(check));
89+
fs.promises.readdir(root, options).then(common.mustCall((dirents) => {
90+
check(dirents);
91+
}));
8292
}
8393

8494
// The root is a directory, and a name is a symbolic link to its layer.

0 commit comments

Comments
 (0)