// Flags: --experimental-vfs --expose-internals 'use strict'; // Error paths in the VFS mount layer: // - EXDEV when renaming/linking across different VFS instances or VFS<->real // - lastunmount handler cleanup (vfsState.handlers becomes null again) // - rename of root mount point is rejected as overlapping require('../common'); const assert = require('assert'); const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); const { vfsState } = require('internal/fs/utils'); const baseMountPoint = path.resolve('/tmp/vfs-mount-errors-' + process.pid); let mountCounter = 0; const nextMount = () => baseMountPoint + '-' + (mountCounter++); // EXDEV: rename across two different VFS instances { const mountA = nextMount(); const mountB = nextMount(); const a = vfs.create(); const b = vfs.create(); a.writeFileSync('/file.txt', 'a'); b.mkdirSync('/x', { recursive: true }); a.mount(mountA); b.mount(mountB); assert.throws( () => fs.renameSync(path.join(mountA, 'file.txt'), path.join(mountB, 'x/file.txt')), { code: 'EXDEV' }, ); a.unmount(); b.unmount(); } // EXDEV: copyFileSync across two different VFS instances { const mountA = nextMount(); const mountB = nextMount(); const a = vfs.create(); const b = vfs.create(); a.writeFileSync('/file.txt', 'a'); b.mkdirSync('/x', { recursive: true }); a.mount(mountA); b.mount(mountB); assert.throws( () => fs.copyFileSync(path.join(mountA, 'file.txt'), path.join(mountB, 'x/copy.txt')), { code: 'EXDEV' }, ); a.unmount(); b.unmount(); } // EXDEV: linkSync across two different VFS instances { const mountA = nextMount(); const mountB = nextMount(); const a = vfs.create(); const b = vfs.create(); a.writeFileSync('/file.txt', 'a'); b.mkdirSync('/x', { recursive: true }); a.mount(mountA); b.mount(mountB); assert.throws( () => fs.linkSync(path.join(mountA, 'file.txt'), path.join(mountB, 'x/lk.txt')), { code: 'EXDEV' }, ); a.unmount(); b.unmount(); } // EXDEV: rename from VFS to a real-fs path { const mountA = nextMount(); const a = vfs.create(); a.writeFileSync('/file.txt', 'a'); a.mount(mountA); const tmpReal = '/tmp/vfs-mount-real-' + process.pid + '.txt'; assert.throws( () => fs.renameSync(path.join(mountA, 'file.txt'), tmpReal), { code: 'EXDEV' }, ); a.unmount(); } // Handler cleanup: after last unmount, vfsState.handlers returns to null { assert.strictEqual(vfsState.handlers, null); const x = vfs.create(); x.mount(nextMount()); assert.notStrictEqual(vfsState.handlers, null); x.unmount(); assert.strictEqual(vfsState.handlers, null); // And it re-installs on a subsequent mount const y = vfs.create(); y.mount(nextMount()); assert.notStrictEqual(vfsState.handlers, null); y.unmount(); assert.strictEqual(vfsState.handlers, null); } // Two parallel non-overlapping mounts both register, last-out clears handlers { const a = vfs.create(); const b = vfs.create(); a.mount(nextMount()); b.mount(nextMount()); assert.notStrictEqual(vfsState.handlers, null); a.unmount(); assert.notStrictEqual(vfsState.handlers, null); b.unmount(); assert.strictEqual(vfsState.handlers, null); } // Overlap detection: nested-under and parent-of both rejected { const parent = nextMount(); const child = path.join(parent, 'child'); const a = vfs.create(); const b = vfs.create(); a.mount(parent); assert.throws(() => b.mount(child), { code: 'ERR_INVALID_STATE' }); a.unmount(); // Reverse direction: child first, then parent rejected const c = vfs.create(); const d = vfs.create(); c.mount(child); assert.throws(() => d.mount(parent), { code: 'ERR_INVALID_STATE' }); c.unmount(); } // Equal mount points: second one rejected { const m = nextMount(); const a = vfs.create(); const b = vfs.create(); a.mount(m); assert.throws(() => b.mount(m), { code: 'ERR_INVALID_STATE' }); a.unmount(); } // Double-mount of same instance rejected { const a = vfs.create(); a.mount(nextMount()); assert.throws(() => a.mount(nextMount()), { code: 'ERR_INVALID_STATE' }); a.unmount(); }