|
| 1 | +// Flags: --expose-internals |
| 2 | +'use strict'; |
| 3 | + |
| 4 | +const common = require('../common'); |
| 5 | +const assert = require('assert'); |
| 6 | + |
| 7 | +// Test internal VFS utility functions for coverage. |
| 8 | + |
| 9 | +// === Router utility functions === |
| 10 | +const { |
| 11 | + splitPath, |
| 12 | + getParentPath, |
| 13 | + getBaseName, |
| 14 | + isUnderMountPoint, |
| 15 | + getRelativePath, |
| 16 | + isAbsolutePath, |
| 17 | +} = require('internal/vfs/router'); |
| 18 | + |
| 19 | +// splitPath |
| 20 | +assert.deepStrictEqual(splitPath('/'), []); |
| 21 | +assert.deepStrictEqual(splitPath('/foo'), ['foo']); |
| 22 | +assert.deepStrictEqual(splitPath('/foo/bar'), ['foo', 'bar']); |
| 23 | +assert.deepStrictEqual(splitPath('/a/b/c/d'), ['a', 'b', 'c', 'd']); |
| 24 | + |
| 25 | +// getParentPath |
| 26 | +assert.strictEqual(getParentPath('/'), null); |
| 27 | +assert.strictEqual(getParentPath('/foo'), '/'); |
| 28 | +assert.strictEqual(getParentPath('/foo/bar'), '/foo'); |
| 29 | +assert.strictEqual(getParentPath('/a/b/c'), '/a/b'); |
| 30 | + |
| 31 | +// getBaseName |
| 32 | +assert.strictEqual(getBaseName('/foo'), 'foo'); |
| 33 | +assert.strictEqual(getBaseName('/foo/bar'), 'bar'); |
| 34 | +assert.strictEqual(getBaseName('/a/b/c.txt'), 'c.txt'); |
| 35 | + |
| 36 | +// isAbsolutePath |
| 37 | +assert.strictEqual(isAbsolutePath('/foo'), true); |
| 38 | +assert.strictEqual(isAbsolutePath('foo'), false); |
| 39 | +assert.strictEqual(isAbsolutePath('./foo'), false); |
| 40 | + |
| 41 | +// isUnderMountPoint (already tested indirectly but exercised here directly) |
| 42 | +assert.strictEqual(isUnderMountPoint('/mount', '/mount'), true); |
| 43 | +assert.strictEqual(isUnderMountPoint('/mount/file', '/mount'), true); |
| 44 | +assert.strictEqual(isUnderMountPoint('/mountx', '/mount'), false); |
| 45 | +assert.strictEqual(isUnderMountPoint('/other', '/mount'), false); |
| 46 | +// Root mount point |
| 47 | +assert.strictEqual(isUnderMountPoint('/anything', '/'), true); |
| 48 | + |
| 49 | +// getRelativePath |
| 50 | +assert.strictEqual(getRelativePath('/mount', '/mount'), '/'); |
| 51 | +assert.strictEqual(getRelativePath('/mount/file.js', '/mount'), '/file.js'); |
| 52 | +assert.strictEqual(getRelativePath('/mount/a/b', '/mount'), '/a/b'); |
| 53 | +// Root mount point |
| 54 | +assert.strictEqual(getRelativePath('/foo/bar', '/'), '/foo/bar'); |
| 55 | + |
| 56 | +// === Provider base class readonly checks === |
| 57 | +const { VirtualProvider } = require('internal/vfs/provider'); |
| 58 | + |
| 59 | +class ReadonlyProvider extends VirtualProvider { |
| 60 | + get readonly() { return true; } |
| 61 | +} |
| 62 | + |
| 63 | +const readonlyProvider = new ReadonlyProvider(); |
| 64 | + |
| 65 | +// All write operations should throw EROFS when readonly |
| 66 | +assert.throws(() => readonlyProvider.mkdirSync('/dir'), { code: 'EROFS' }); |
| 67 | +assert.throws(() => readonlyProvider.rmdirSync('/dir'), { code: 'EROFS' }); |
| 68 | +assert.throws(() => readonlyProvider.unlinkSync('/file'), { code: 'EROFS' }); |
| 69 | +assert.throws(() => readonlyProvider.renameSync('/a', '/b'), { code: 'EROFS' }); |
| 70 | +assert.throws(() => readonlyProvider.writeFileSync('/f', 'data'), { code: 'EROFS' }); |
| 71 | +assert.throws(() => readonlyProvider.appendFileSync('/f', 'data'), { code: 'EROFS' }); |
| 72 | +assert.throws(() => readonlyProvider.copyFileSync('/a', '/b'), { code: 'EROFS' }); |
| 73 | +assert.throws(() => readonlyProvider.symlinkSync('/target', '/link'), { code: 'EROFS' }); |
| 74 | + |
| 75 | +// Async versions |
| 76 | +assert.rejects(readonlyProvider.mkdir('/dir'), { code: 'EROFS' }).then(common.mustCall()); |
| 77 | +assert.rejects(readonlyProvider.rmdir('/dir'), { code: 'EROFS' }).then(common.mustCall()); |
| 78 | +assert.rejects(readonlyProvider.unlink('/file'), { code: 'EROFS' }).then(common.mustCall()); |
| 79 | +assert.rejects(readonlyProvider.rename('/a', '/b'), { code: 'EROFS' }).then(common.mustCall()); |
| 80 | +assert.rejects(readonlyProvider.writeFile('/f', 'data'), { code: 'EROFS' }).then(common.mustCall()); |
| 81 | +assert.rejects(readonlyProvider.appendFile('/f', 'data'), { code: 'EROFS' }).then(common.mustCall()); |
| 82 | +assert.rejects(readonlyProvider.copyFile('/a', '/b'), { code: 'EROFS' }).then(common.mustCall()); |
| 83 | +assert.rejects(readonlyProvider.symlink('/target', '/link'), { code: 'EROFS' }).then(common.mustCall()); |
| 84 | + |
| 85 | +// === Provider base class ERR_METHOD_NOT_IMPLEMENTED for non-readonly === |
| 86 | +const baseProvider = new VirtualProvider(); |
| 87 | + |
| 88 | +// These should throw ERR_METHOD_NOT_IMPLEMENTED (not readonly, just unimplemented) |
| 89 | +assert.throws(() => baseProvider.mkdirSync('/dir'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); |
| 90 | +assert.throws(() => baseProvider.rmdirSync('/dir'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); |
| 91 | +assert.throws(() => baseProvider.unlinkSync('/file'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); |
| 92 | +assert.throws(() => baseProvider.renameSync('/a', '/b'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); |
| 93 | +assert.throws(() => baseProvider.symlinkSync('/t', '/l'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); |
| 94 | +assert.throws(() => baseProvider.readdirSync('/dir'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); |
| 95 | +assert.throws(() => baseProvider.readlinkSync('/link'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); |
| 96 | +assert.throws(() => baseProvider.watch('/path'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); |
| 97 | +assert.throws(() => baseProvider.watchAsync('/path'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); |
| 98 | +assert.throws(() => baseProvider.watchFile('/path'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); |
| 99 | +assert.throws(() => baseProvider.unwatchFile('/path'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); |
| 100 | + |
| 101 | +// Async unimplemented methods |
| 102 | +assert.rejects(baseProvider.mkdir('/dir'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall()); |
| 103 | +assert.rejects(baseProvider.rmdir('/dir'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall()); |
| 104 | +assert.rejects(baseProvider.unlink('/file'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall()); |
| 105 | +assert.rejects(baseProvider.rename('/a', '/b'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall()); |
| 106 | +assert.rejects(baseProvider.readlink('/link'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall()); |
| 107 | +assert.rejects(baseProvider.symlink('/t', '/l'), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall()); |
| 108 | + |
| 109 | +// === Provider default implementations via VFS (covers realpath, access, exists, etc.) === |
| 110 | +const vfs = require('node:vfs'); |
| 111 | + |
| 112 | +{ |
| 113 | + const myVfs = vfs.create(); |
| 114 | + myVfs.writeFileSync('/file.txt', 'hello'); |
| 115 | + myVfs.mkdirSync('/dir'); |
| 116 | + myVfs.mount('/internals-test'); |
| 117 | + |
| 118 | + // Realpath (default provider impl returns path as-is after stat check) |
| 119 | + assert.strictEqual(myVfs.realpathSync('/internals-test/file.txt'), '/internals-test/file.txt'); |
| 120 | + assert.strictEqual(myVfs.realpathSync('/internals-test/dir'), '/internals-test/dir'); |
| 121 | + |
| 122 | + // Realpath for non-existent path should throw |
| 123 | + assert.throws(() => myVfs.realpathSync('/internals-test/nonexistent'), { code: 'ENOENT' }); |
| 124 | + |
| 125 | + // access (default provider impl just checks stat) |
| 126 | + myVfs.accessSync('/internals-test/file.txt'); |
| 127 | + assert.throws(() => myVfs.accessSync('/internals-test/nonexistent'), { code: 'ENOENT' }); |
| 128 | + |
| 129 | + // existsSync |
| 130 | + assert.strictEqual(myVfs.existsSync('/internals-test/file.txt'), true); |
| 131 | + assert.strictEqual(myVfs.existsSync('/internals-test/nonexistent'), false); |
| 132 | + assert.strictEqual(myVfs.existsSync('/internals-test/dir'), true); |
| 133 | + |
| 134 | + // internalModuleStat (0 = file, 1 = dir, -2 = not found) |
| 135 | + assert.strictEqual(myVfs.internalModuleStat('/internals-test/file.txt'), 0); |
| 136 | + assert.strictEqual(myVfs.internalModuleStat('/internals-test/dir'), 1); |
| 137 | + assert.strictEqual(myVfs.internalModuleStat('/internals-test/nope'), -2); |
| 138 | + |
| 139 | + // Callback-based realpath |
| 140 | + myVfs.realpath('/internals-test/file.txt', common.mustSucceed((resolved) => { |
| 141 | + assert.ok(resolved); |
| 142 | + })); |
| 143 | + |
| 144 | + // Callback-based access |
| 145 | + myVfs.access('/internals-test/file.txt', common.mustSucceed()); |
| 146 | + |
| 147 | + myVfs.unmount(); |
| 148 | +} |
| 149 | + |
| 150 | +// === VirtualFileHandle base class ERR_METHOD_NOT_IMPLEMENTED === |
| 151 | +const { VirtualFileHandle } = require('internal/vfs/file_handle'); |
| 152 | + |
| 153 | +{ |
| 154 | + const handle = new VirtualFileHandle('/test', 'r'); |
| 155 | + |
| 156 | + // Property accessors |
| 157 | + assert.strictEqual(handle.path, '/test'); |
| 158 | + assert.strictEqual(handle.flags, 'r'); |
| 159 | + assert.strictEqual(handle.mode, 0o644); |
| 160 | + assert.strictEqual(handle.position, 0); |
| 161 | + assert.strictEqual(handle.closed, false); |
| 162 | + |
| 163 | + // Position setter |
| 164 | + handle.position = 42; |
| 165 | + assert.strictEqual(handle.position, 42); |
| 166 | + |
| 167 | + // Sync methods should throw ERR_METHOD_NOT_IMPLEMENTED |
| 168 | + assert.throws(() => handle.readSync(Buffer.alloc(10), 0, 10, 0), |
| 169 | + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); |
| 170 | + assert.throws(() => handle.writeSync(Buffer.alloc(10), 0, 10, 0), |
| 171 | + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); |
| 172 | + assert.throws(() => handle.readFileSync(), |
| 173 | + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); |
| 174 | + assert.throws(() => handle.writeFileSync('data'), |
| 175 | + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); |
| 176 | + assert.throws(() => handle.statSync(), |
| 177 | + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); |
| 178 | + assert.throws(() => handle.truncateSync(), |
| 179 | + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); |
| 180 | + |
| 181 | + // Async methods should reject with ERR_METHOD_NOT_IMPLEMENTED |
| 182 | + assert.rejects(handle.read(Buffer.alloc(10), 0, 10, 0), |
| 183 | + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall()); |
| 184 | + assert.rejects(handle.write(Buffer.alloc(10), 0, 10, 0), |
| 185 | + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall()); |
| 186 | + assert.rejects(handle.readFile(), |
| 187 | + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall()); |
| 188 | + assert.rejects(handle.writeFile('data'), |
| 189 | + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall()); |
| 190 | + assert.rejects(handle.stat(), |
| 191 | + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall()); |
| 192 | + assert.rejects(handle.truncate(), |
| 193 | + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall()); |
| 194 | + |
| 195 | + // Close |
| 196 | + handle.closeSync(); |
| 197 | + assert.strictEqual(handle.closed, true); |
| 198 | + |
| 199 | + // Methods on closed handle should throw EBADF |
| 200 | + assert.throws(() => handle.readSync(Buffer.alloc(10), 0, 10, 0), { code: 'EBADF' }); |
| 201 | + assert.throws(() => handle.writeSync(Buffer.alloc(10), 0, 10, 0), { code: 'EBADF' }); |
| 202 | + assert.throws(() => handle.readFileSync(), { code: 'EBADF' }); |
| 203 | + assert.throws(() => handle.writeFileSync('data'), { code: 'EBADF' }); |
| 204 | + assert.throws(() => handle.statSync(), { code: 'EBADF' }); |
| 205 | + assert.throws(() => handle.truncateSync(), { code: 'EBADF' }); |
| 206 | +} |
| 207 | + |
| 208 | +// === MemoryFileHandle via VFS fd operations === |
| 209 | +{ |
| 210 | + const myVfs = vfs.create(); |
| 211 | + myVfs.writeFileSync('/fdtest.txt', 'abcdefghij'); |
| 212 | + myVfs.mount('/fd-test'); |
| 213 | + |
| 214 | + // Open and read with auto-advancing position |
| 215 | + const fd = myVfs.openSync('/fd-test/fdtest.txt'); |
| 216 | + const buf = Buffer.alloc(5); |
| 217 | + let bytesRead = myVfs.readSync(fd, buf, 0, 5, null); |
| 218 | + assert.strictEqual(bytesRead, 5); |
| 219 | + assert.strictEqual(buf.toString(), 'abcde'); |
| 220 | + |
| 221 | + // Read more - position should have advanced |
| 222 | + const buf2 = Buffer.alloc(5); |
| 223 | + bytesRead = myVfs.readSync(fd, buf2, 0, 5, null); |
| 224 | + assert.strictEqual(bytesRead, 5); |
| 225 | + assert.strictEqual(buf2.toString(), 'fghij'); |
| 226 | + |
| 227 | + // Read past end - should return 0 |
| 228 | + const buf3 = Buffer.alloc(5); |
| 229 | + bytesRead = myVfs.readSync(fd, buf3, 0, 5, null); |
| 230 | + assert.strictEqual(bytesRead, 0); |
| 231 | + |
| 232 | + // Read with explicit position (doesn't advance internal position) |
| 233 | + const buf4 = Buffer.alloc(3); |
| 234 | + bytesRead = myVfs.readSync(fd, buf4, 0, 3, 2); |
| 235 | + assert.strictEqual(bytesRead, 3); |
| 236 | + assert.strictEqual(buf4.toString(), 'cde'); |
| 237 | + |
| 238 | + // fstatSync |
| 239 | + const stats = myVfs.fstatSync(fd); |
| 240 | + assert.strictEqual(stats.size, 10); |
| 241 | + assert.strictEqual(stats.isFile(), true); |
| 242 | + |
| 243 | + // Close |
| 244 | + myVfs.closeSync(fd); |
| 245 | + |
| 246 | + // EBADF after close |
| 247 | + assert.throws(() => myVfs.readSync(fd, Buffer.alloc(5), 0, 5, 0), { code: 'EBADF' }); |
| 248 | + assert.throws(() => myVfs.closeSync(fd), { code: 'EBADF' }); |
| 249 | + assert.throws(() => myVfs.fstatSync(fd), { code: 'EBADF' }); |
| 250 | + |
| 251 | + myVfs.unmount(); |
| 252 | +} |
| 253 | + |
| 254 | +// === VirtualReadStream === |
| 255 | +{ |
| 256 | + const myVfs = vfs.create(); |
| 257 | + myVfs.writeFileSync('/stream.txt', 'hello world'); |
| 258 | + myVfs.mount('/stream-test'); |
| 259 | + |
| 260 | + // Basic stream read |
| 261 | + const chunks = []; |
| 262 | + const stream = myVfs.createReadStream('/stream-test/stream.txt'); |
| 263 | + |
| 264 | + stream.on('open', common.mustCall((fd) => { |
| 265 | + assert.strictEqual(typeof fd, 'number'); |
| 266 | + })); |
| 267 | + |
| 268 | + stream.on('ready', common.mustCall()); |
| 269 | + |
| 270 | + stream.on('data', (chunk) => { |
| 271 | + chunks.push(chunk); |
| 272 | + }); |
| 273 | + |
| 274 | + stream.on('end', common.mustCall(() => { |
| 275 | + const result = Buffer.concat(chunks).toString(); |
| 276 | + assert.strictEqual(result, 'hello world'); |
| 277 | + })); |
| 278 | + |
| 279 | + stream.on('close', common.mustCall(() => { |
| 280 | + myVfs.unmount(); |
| 281 | + })); |
| 282 | +} |
| 283 | + |
| 284 | +// === VirtualReadStream with start/end options === |
| 285 | +{ |
| 286 | + const myVfs = vfs.create(); |
| 287 | + myVfs.writeFileSync('/range.txt', '0123456789'); |
| 288 | + myVfs.mount('/stream-range'); |
| 289 | + |
| 290 | + const chunks = []; |
| 291 | + // Read bytes 2-5 (inclusive), should get "2345" |
| 292 | + const stream = myVfs.createReadStream('/stream-range/range.txt', { |
| 293 | + start: 2, |
| 294 | + end: 5, |
| 295 | + }); |
| 296 | + |
| 297 | + stream.on('data', (chunk) => { |
| 298 | + chunks.push(chunk); |
| 299 | + }); |
| 300 | + |
| 301 | + stream.on('end', common.mustCall(() => { |
| 302 | + const result = Buffer.concat(chunks).toString(); |
| 303 | + assert.strictEqual(result, '2345'); |
| 304 | + myVfs.unmount(); |
| 305 | + })); |
| 306 | +} |
| 307 | + |
| 308 | +// === VirtualReadStream path property === |
| 309 | +{ |
| 310 | + const myVfs = vfs.create(); |
| 311 | + myVfs.writeFileSync('/prop.txt', 'x'); |
| 312 | + myVfs.mount('/stream-prop'); |
| 313 | + |
| 314 | + const stream = myVfs.createReadStream('/stream-prop/prop.txt'); |
| 315 | + assert.strictEqual(stream.path, '/stream-prop/prop.txt'); |
| 316 | + stream.destroy(); |
| 317 | + myVfs.unmount(); |
| 318 | +} |
0 commit comments