diff --git a/lib/internal/vfs/providers/memory.js b/lib/internal/vfs/providers/memory.js index 995b8f236200d5..9dd44cc2561242 100644 --- a/lib/internal/vfs/providers/memory.js +++ b/lib/internal/vfs/providers/memory.js @@ -695,10 +695,13 @@ class MemoryProvider extends VirtualProvider { const segments = this.#splitPath(normalized); let current = this[kRoot]; let currentPath = '/'; + let resolvedCurrentPath = '/'; let firstCreated; for (const segment of segments) { currentPath = pathPosix.join(currentPath, segment); + const resolvedPath = pathPosix.join(resolvedCurrentPath, segment); + this.#ensurePopulated(current, resolvedCurrentPath); let entry = current.children.get(segment); if (!entry) { entry = new MemoryEntry(TYPE_DIR, { mode: options?.mode }); @@ -707,7 +710,23 @@ class MemoryProvider extends VirtualProvider { if (firstCreated === undefined) { firstCreated = currentPath; } - } else if (!entry.isDirectory()) { + resolvedCurrentPath = resolvedPath; + } else if (entry.isSymbolicLink()) { + const targetPath = this.#resolveSymlinkTarget(resolvedPath, entry.target); + const result = this.#lookupEntry(targetPath, true, 0); + if (result.eloop) { + throw createELOOP('mkdir', path); + } + if (!result.entry) { + throw createENOENT('mkdir', path); + } + entry = result.entry; + resolvedCurrentPath = result.resolvedPath; + } else { + resolvedCurrentPath = resolvedPath; + } + + if (!entry.isDirectory()) { throw createENOTDIR('mkdir', path); } current = entry; diff --git a/test/parallel/test-vfs-mkdir.js b/test/parallel/test-vfs-mkdir.js index 87a823b77d87ca..22487324d242c4 100644 --- a/test/parallel/test-vfs-mkdir.js +++ b/test/parallel/test-vfs-mkdir.js @@ -4,7 +4,7 @@ // mkdirSync / rmdirSync behaviour: return value, recursive option, mode // option, error cases. -require('../common'); +const common = require('../common'); const assert = require('assert'); const vfs = require('node:vfs'); @@ -17,6 +17,51 @@ const vfs = require('node:vfs'); assert.strictEqual(result, '/a/b'); } +// Recursive mkdir follows symlinked intermediate directories, but returns the +// path of the first created directory as requested by the caller. +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/target'); + myVfs.symlinkSync('/target', '/link'); + + const result = myVfs.mkdirSync('/link/subdir/deep', { recursive: true }); + + assert.strictEqual(result, '/link/subdir'); + assert.strictEqual(myVfs.existsSync('/target/subdir/deep'), true); + assert.strictEqual(myVfs.existsSync('/link/subdir/deep'), true); +} + +// Recursive mkdir also resolves relative symlink targets from the symlink's +// resolved parent directory. +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/parent/target', { recursive: true }); + myVfs.symlinkSync('target', '/parent/link'); + + myVfs.mkdirSync('/parent/link/subdir', { recursive: true }); + + assert.strictEqual(myVfs.existsSync('/parent/target/subdir'), true); +} + +// Recursive mkdir through symlinks keeps native error behavior for bad +// intermediate targets. +{ + const myVfs = vfs.create(); + myVfs.symlinkSync('/missing', '/dangling'); + assert.throws( + () => myVfs.mkdirSync('/dangling/subdir', { recursive: true }), + { code: 'ENOENT' }); +} + +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/file', 'x'); + myVfs.symlinkSync('/file', '/link'); + assert.throws( + () => myVfs.mkdirSync('/link/subdir', { recursive: true }), + { code: 'ENOTDIR' }); +} + // mkdirSync with explicit mode (non-recursive) { const myVfs = vfs.create(); @@ -47,3 +92,16 @@ const vfs = require('node:vfs'); myVfs.writeFileSync('/d/x', ''); assert.throws(() => myVfs.rmdirSync('/d'), { code: 'ENOTEMPTY' }); } + +// promises.mkdir uses the same recursive symlink handling. +(async () => { + const myVfs = vfs.create(); + myVfs.mkdirSync('/target'); + myVfs.symlinkSync('/target', '/link'); + + const result = await myVfs.promises.mkdir('/link/subdir/deep', + { recursive: true }); + + assert.strictEqual(result, '/link/subdir'); + assert.strictEqual(myVfs.existsSync('/target/subdir/deep'), true); +})().then(common.mustCall());