Skip to content

Commit f375bfe

Browse files
trivikraduh95
authored andcommitted
vfs: follow symlinked dirs in recursive mkdir
Recursive mkdir currently checks an existing intermediate entry directly, so a symlink to a directory is treated as a non-directory and throws ENOTDIR. Resolve symlinked intermediate components while walking the recursive mkdir path, creating new directories in the resolved target while preserving the caller-facing first-created return path. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5 PR-URL: #64287 Fixes: #64286 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 0f6f5cc commit f375bfe

2 files changed

Lines changed: 79 additions & 2 deletions

File tree

lib/internal/vfs/providers/memory.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -696,10 +696,13 @@ class MemoryProvider extends VirtualProvider {
696696
const segments = this.#splitPath(normalized);
697697
let current = this[kRoot];
698698
let currentPath = '/';
699+
let resolvedCurrentPath = '/';
699700
let firstCreated;
700701

701702
for (const segment of segments) {
702703
currentPath = pathPosix.join(currentPath, segment);
704+
const resolvedPath = pathPosix.join(resolvedCurrentPath, segment);
705+
this.#ensurePopulated(current, resolvedCurrentPath);
703706
let entry = current.children.get(segment);
704707
if (!entry) {
705708
entry = new MemoryEntry(TYPE_DIR, { mode: options?.mode });
@@ -708,7 +711,23 @@ class MemoryProvider extends VirtualProvider {
708711
if (firstCreated === undefined) {
709712
firstCreated = currentPath;
710713
}
711-
} else if (!entry.isDirectory()) {
714+
resolvedCurrentPath = resolvedPath;
715+
} else if (entry.isSymbolicLink()) {
716+
const targetPath = this.#resolveSymlinkTarget(resolvedPath, entry.target);
717+
const result = this.#lookupEntry(targetPath, true, 0);
718+
if (result.eloop) {
719+
throw createELOOP('mkdir', path);
720+
}
721+
if (!result.entry) {
722+
throw createENOENT('mkdir', path);
723+
}
724+
entry = result.entry;
725+
resolvedCurrentPath = result.resolvedPath;
726+
} else {
727+
resolvedCurrentPath = resolvedPath;
728+
}
729+
730+
if (!entry.isDirectory()) {
712731
throw createENOTDIR('mkdir', path);
713732
}
714733
current = entry;

test/parallel/test-vfs-mkdir.js

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// mkdirSync / rmdirSync behaviour: return value, recursive option, mode
55
// option, error cases.
66

7-
require('../common');
7+
const common = require('../common');
88
const assert = require('assert');
99
const vfs = require('node:vfs');
1010

@@ -17,6 +17,51 @@ const vfs = require('node:vfs');
1717
assert.strictEqual(result, '/a/b');
1818
}
1919

20+
// Recursive mkdir follows symlinked intermediate directories, but returns the
21+
// path of the first created directory as requested by the caller.
22+
{
23+
const myVfs = vfs.create();
24+
myVfs.mkdirSync('/target');
25+
myVfs.symlinkSync('/target', '/link');
26+
27+
const result = myVfs.mkdirSync('/link/subdir/deep', { recursive: true });
28+
29+
assert.strictEqual(result, '/link/subdir');
30+
assert.strictEqual(myVfs.existsSync('/target/subdir/deep'), true);
31+
assert.strictEqual(myVfs.existsSync('/link/subdir/deep'), true);
32+
}
33+
34+
// Recursive mkdir also resolves relative symlink targets from the symlink's
35+
// resolved parent directory.
36+
{
37+
const myVfs = vfs.create();
38+
myVfs.mkdirSync('/parent/target', { recursive: true });
39+
myVfs.symlinkSync('target', '/parent/link');
40+
41+
myVfs.mkdirSync('/parent/link/subdir', { recursive: true });
42+
43+
assert.strictEqual(myVfs.existsSync('/parent/target/subdir'), true);
44+
}
45+
46+
// Recursive mkdir through symlinks keeps native error behavior for bad
47+
// intermediate targets.
48+
{
49+
const myVfs = vfs.create();
50+
myVfs.symlinkSync('/missing', '/dangling');
51+
assert.throws(
52+
() => myVfs.mkdirSync('/dangling/subdir', { recursive: true }),
53+
{ code: 'ENOENT' });
54+
}
55+
56+
{
57+
const myVfs = vfs.create();
58+
myVfs.writeFileSync('/file', 'x');
59+
myVfs.symlinkSync('/file', '/link');
60+
assert.throws(
61+
() => myVfs.mkdirSync('/link/subdir', { recursive: true }),
62+
{ code: 'ENOTDIR' });
63+
}
64+
2065
// mkdirSync with explicit mode (non-recursive)
2166
{
2267
const myVfs = vfs.create();
@@ -47,3 +92,16 @@ const vfs = require('node:vfs');
4792
myVfs.writeFileSync('/d/x', '');
4893
assert.throws(() => myVfs.rmdirSync('/d'), { code: 'ENOTEMPTY' });
4994
}
95+
96+
// promises.mkdir uses the same recursive symlink handling.
97+
(async () => {
98+
const myVfs = vfs.create();
99+
myVfs.mkdirSync('/target');
100+
myVfs.symlinkSync('/target', '/link');
101+
102+
const result = await myVfs.promises.mkdir('/link/subdir/deep',
103+
{ recursive: true });
104+
105+
assert.strictEqual(result, '/link/subdir');
106+
assert.strictEqual(myVfs.existsSync('/target/subdir/deep'), true);
107+
})().then(common.mustCall());

0 commit comments

Comments
 (0)