1// Flags: --expose-internals 2'use strict'; 3const common = require('../common'); 4const tmpdir = require('../common/tmpdir'); 5const assert = require('assert'); 6const fs = require('fs'); 7const path = require('path'); 8const { pathToFileURL } = require('url'); 9const { execSync } = require('child_process'); 10 11const { validateRmOptionsSync } = require('internal/fs/utils'); 12 13tmpdir.refresh(); 14 15let count = 0; 16const nextDirPath = (name = 'rm') => 17 path.join(tmpdir.path, `${name}-${count++}`); 18 19const isGitPresent = (() => { 20 try { execSync('git --version'); return true; } catch { return false; } 21})(); 22 23function gitInit(gitDirectory) { 24 fs.mkdirSync(gitDirectory); 25 execSync('git init', common.mustNotMutateObjectDeep({ cwd: gitDirectory })); 26} 27 28function makeNonEmptyDirectory(depth, files, folders, dirname, createSymLinks) { 29 fs.mkdirSync(dirname, common.mustNotMutateObjectDeep({ recursive: true })); 30 fs.writeFileSync(path.join(dirname, 'text.txt'), 'hello', 'utf8'); 31 32 const options = common.mustNotMutateObjectDeep({ flag: 'wx' }); 33 34 for (let f = files; f > 0; f--) { 35 fs.writeFileSync(path.join(dirname, `f-${depth}-${f}`), '', options); 36 } 37 38 if (createSymLinks) { 39 // Valid symlink 40 fs.symlinkSync( 41 `f-${depth}-1`, 42 path.join(dirname, `link-${depth}-good`), 43 'file' 44 ); 45 46 // Invalid symlink 47 fs.symlinkSync( 48 'does-not-exist', 49 path.join(dirname, `link-${depth}-bad`), 50 'file' 51 ); 52 53 // Symlinks that form a loop 54 [['a', 'b'], ['b', 'a']].forEach(([x, y]) => { 55 fs.symlinkSync( 56 `link-${depth}-loop-${x}`, 57 path.join(dirname, `link-${depth}-loop-${y}`), 58 'file' 59 ); 60 }); 61 } 62 63 // File with a name that looks like a glob 64 fs.writeFileSync(path.join(dirname, '[a-z0-9].txt'), '', options); 65 66 depth--; 67 if (depth <= 0) { 68 return; 69 } 70 71 for (let f = folders; f > 0; f--) { 72 fs.mkdirSync( 73 path.join(dirname, `folder-${depth}-${f}`), 74 { recursive: true } 75 ); 76 makeNonEmptyDirectory( 77 depth, 78 files, 79 folders, 80 path.join(dirname, `d-${depth}-${f}`), 81 createSymLinks 82 ); 83 } 84} 85 86function removeAsync(dir) { 87 // Removal should fail without the recursive option. 88 fs.rm(dir, common.mustCall((err) => { 89 assert.strictEqual(err.syscall, 'rm'); 90 91 // Removal should fail without the recursive option set to true. 92 fs.rm(dir, common.mustNotMutateObjectDeep({ recursive: false }), common.mustCall((err) => { 93 assert.strictEqual(err.syscall, 'rm'); 94 95 // Recursive removal should succeed. 96 fs.rm(dir, common.mustNotMutateObjectDeep({ recursive: true }), common.mustSucceed(() => { 97 98 // Attempted removal should fail now because the directory is gone. 99 fs.rm(dir, common.mustCall((err) => { 100 assert.strictEqual(err.syscall, 'lstat'); 101 })); 102 })); 103 })); 104 })); 105} 106 107// Test the asynchronous version 108{ 109 // Create a 4-level folder hierarchy including symlinks 110 let dir = nextDirPath(); 111 makeNonEmptyDirectory(4, 10, 2, dir, true); 112 removeAsync(dir); 113 114 // Create a 2-level folder hierarchy without symlinks 115 dir = nextDirPath(); 116 makeNonEmptyDirectory(2, 10, 2, dir, false); 117 removeAsync(dir); 118 119 // Same test using URL instead of a path 120 dir = nextDirPath(); 121 makeNonEmptyDirectory(2, 10, 2, dir, false); 122 removeAsync(pathToFileURL(dir)); 123 124 // Create a flat folder including symlinks 125 dir = nextDirPath(); 126 makeNonEmptyDirectory(1, 10, 2, dir, true); 127 removeAsync(dir); 128 129 // Should fail if target does not exist 130 fs.rm( 131 path.join(tmpdir.path, 'noexist.txt'), 132 common.mustNotMutateObjectDeep({ recursive: true }), 133 common.mustCall((err) => { 134 assert.strictEqual(err.code, 'ENOENT'); 135 }) 136 ); 137 138 // Should delete a file 139 const filePath = path.join(tmpdir.path, 'rm-async-file.txt'); 140 fs.writeFileSync(filePath, ''); 141 fs.rm(filePath, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall((err) => { 142 try { 143 assert.strictEqual(err, null); 144 assert.strictEqual(fs.existsSync(filePath), false); 145 } finally { 146 fs.rmSync(filePath, common.mustNotMutateObjectDeep({ force: true })); 147 } 148 })); 149 150 // Should delete a valid symlink 151 const linkTarget = path.join(tmpdir.path, 'link-target-async.txt'); 152 fs.writeFileSync(linkTarget, ''); 153 const validLink = path.join(tmpdir.path, 'valid-link-async'); 154 fs.symlinkSync(linkTarget, validLink); 155 fs.rm(validLink, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall((err) => { 156 try { 157 assert.strictEqual(err, null); 158 assert.strictEqual(fs.existsSync(validLink), false); 159 } finally { 160 fs.rmSync(linkTarget, common.mustNotMutateObjectDeep({ force: true })); 161 fs.rmSync(validLink, common.mustNotMutateObjectDeep({ force: true })); 162 } 163 })); 164 165 // Should delete an invalid symlink 166 const invalidLink = path.join(tmpdir.path, 'invalid-link-async'); 167 fs.symlinkSync('definitely-does-not-exist-async', invalidLink); 168 fs.rm(invalidLink, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall((err) => { 169 try { 170 assert.strictEqual(err, null); 171 assert.strictEqual(fs.existsSync(invalidLink), false); 172 } finally { 173 fs.rmSync(invalidLink, common.mustNotMutateObjectDeep({ force: true })); 174 } 175 })); 176 177 // Should delete a symlink that is part of a loop 178 const loopLinkA = path.join(tmpdir.path, 'loop-link-async-a'); 179 const loopLinkB = path.join(tmpdir.path, 'loop-link-async-b'); 180 fs.symlinkSync(loopLinkA, loopLinkB); 181 fs.symlinkSync(loopLinkB, loopLinkA); 182 fs.rm(loopLinkA, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall((err) => { 183 try { 184 assert.strictEqual(err, null); 185 assert.strictEqual(fs.existsSync(loopLinkA), false); 186 } finally { 187 fs.rmSync(loopLinkA, common.mustNotMutateObjectDeep({ force: true })); 188 fs.rmSync(loopLinkB, common.mustNotMutateObjectDeep({ force: true })); 189 } 190 })); 191} 192 193// Removing a .git directory should not throw an EPERM. 194// Refs: https://github.com/isaacs/rimraf/issues/21. 195if (isGitPresent) { 196 const gitDirectory = nextDirPath(); 197 gitInit(gitDirectory); 198 fs.rm(gitDirectory, common.mustNotMutateObjectDeep({ recursive: true }), common.mustSucceed(() => { 199 assert.strictEqual(fs.existsSync(gitDirectory), false); 200 })); 201} 202 203// Test the synchronous version. 204{ 205 const dir = nextDirPath(); 206 makeNonEmptyDirectory(4, 10, 2, dir, true); 207 208 // Removal should fail without the recursive option set to true. 209 assert.throws(() => { 210 fs.rmSync(dir); 211 }, { syscall: 'rm' }); 212 assert.throws(() => { 213 fs.rmSync(dir, common.mustNotMutateObjectDeep({ recursive: false })); 214 }, { syscall: 'rm' }); 215 216 // Should fail if target does not exist 217 assert.throws(() => { 218 fs.rmSync(path.join(tmpdir.path, 'noexist.txt'), common.mustNotMutateObjectDeep({ recursive: true })); 219 }, { 220 code: 'ENOENT', 221 name: 'Error', 222 message: /^ENOENT: no such file or directory, lstat/ 223 }); 224 225 // Should delete a file 226 const filePath = path.join(tmpdir.path, 'rm-file.txt'); 227 fs.writeFileSync(filePath, ''); 228 229 try { 230 fs.rmSync(filePath, common.mustNotMutateObjectDeep({ recursive: true })); 231 assert.strictEqual(fs.existsSync(filePath), false); 232 } finally { 233 fs.rmSync(filePath, common.mustNotMutateObjectDeep({ force: true })); 234 } 235 236 // Should delete a valid symlink 237 const linkTarget = path.join(tmpdir.path, 'link-target.txt'); 238 fs.writeFileSync(linkTarget, ''); 239 const validLink = path.join(tmpdir.path, 'valid-link'); 240 fs.symlinkSync(linkTarget, validLink); 241 try { 242 fs.rmSync(validLink); 243 assert.strictEqual(fs.existsSync(validLink), false); 244 } finally { 245 fs.rmSync(linkTarget, common.mustNotMutateObjectDeep({ force: true })); 246 fs.rmSync(validLink, common.mustNotMutateObjectDeep({ force: true })); 247 } 248 249 // Should delete an invalid symlink 250 const invalidLink = path.join(tmpdir.path, 'invalid-link'); 251 fs.symlinkSync('definitely-does-not-exist', invalidLink); 252 try { 253 fs.rmSync(invalidLink); 254 assert.strictEqual(fs.existsSync(invalidLink), false); 255 } finally { 256 fs.rmSync(invalidLink, common.mustNotMutateObjectDeep({ force: true })); 257 } 258 259 // Should delete a symlink that is part of a loop 260 const loopLinkA = path.join(tmpdir.path, 'loop-link-a'); 261 const loopLinkB = path.join(tmpdir.path, 'loop-link-b'); 262 fs.symlinkSync(loopLinkA, loopLinkB); 263 fs.symlinkSync(loopLinkB, loopLinkA); 264 try { 265 fs.rmSync(loopLinkA); 266 assert.strictEqual(fs.existsSync(loopLinkA), false); 267 } finally { 268 fs.rmSync(loopLinkA, common.mustNotMutateObjectDeep({ force: true })); 269 fs.rmSync(loopLinkB, common.mustNotMutateObjectDeep({ force: true })); 270 } 271 272 // Should accept URL 273 const fileURL = tmpdir.fileURL('rm-file.txt'); 274 fs.writeFileSync(fileURL, ''); 275 276 try { 277 fs.rmSync(fileURL, common.mustNotMutateObjectDeep({ recursive: true })); 278 assert.strictEqual(fs.existsSync(fileURL), false); 279 } finally { 280 fs.rmSync(fileURL, common.mustNotMutateObjectDeep({ force: true })); 281 } 282 283 // Recursive removal should succeed. 284 fs.rmSync(dir, { recursive: true }); 285 assert.strictEqual(fs.existsSync(dir), false); 286 287 // Attempted removal should fail now because the directory is gone. 288 assert.throws(() => fs.rmSync(dir), { syscall: 'lstat' }); 289} 290 291// Removing a .git directory should not throw an EPERM. 292// Refs: https://github.com/isaacs/rimraf/issues/21. 293if (isGitPresent) { 294 const gitDirectory = nextDirPath(); 295 gitInit(gitDirectory); 296 fs.rmSync(gitDirectory, common.mustNotMutateObjectDeep({ recursive: true })); 297 assert.strictEqual(fs.existsSync(gitDirectory), false); 298} 299 300// Test the Promises based version. 301(async () => { 302 const dir = nextDirPath(); 303 makeNonEmptyDirectory(4, 10, 2, dir, true); 304 305 // Removal should fail without the recursive option set to true. 306 await assert.rejects(fs.promises.rm(dir), { syscall: 'rm' }); 307 await assert.rejects(fs.promises.rm(dir, common.mustNotMutateObjectDeep({ recursive: false })), { 308 syscall: 'rm' 309 }); 310 311 // Recursive removal should succeed. 312 await fs.promises.rm(dir, common.mustNotMutateObjectDeep({ recursive: true })); 313 assert.strictEqual(fs.existsSync(dir), false); 314 315 // Attempted removal should fail now because the directory is gone. 316 await assert.rejects(fs.promises.rm(dir), { syscall: 'lstat' }); 317 318 // Should fail if target does not exist 319 await assert.rejects(fs.promises.rm( 320 path.join(tmpdir.path, 'noexist.txt'), 321 { recursive: true } 322 ), { 323 code: 'ENOENT', 324 name: 'Error', 325 message: /^ENOENT: no such file or directory, lstat/ 326 }); 327 328 // Should not fail if target does not exist and force option is true 329 await fs.promises.rm(path.join(tmpdir.path, 'noexist.txt'), common.mustNotMutateObjectDeep({ force: true })); 330 331 // Should delete file 332 const filePath = path.join(tmpdir.path, 'rm-promises-file.txt'); 333 fs.writeFileSync(filePath, ''); 334 335 try { 336 await fs.promises.rm(filePath, common.mustNotMutateObjectDeep({ recursive: true })); 337 assert.strictEqual(fs.existsSync(filePath), false); 338 } finally { 339 fs.rmSync(filePath, common.mustNotMutateObjectDeep({ force: true })); 340 } 341 342 // Should delete a valid symlink 343 const linkTarget = path.join(tmpdir.path, 'link-target-prom.txt'); 344 fs.writeFileSync(linkTarget, ''); 345 const validLink = path.join(tmpdir.path, 'valid-link-prom'); 346 fs.symlinkSync(linkTarget, validLink); 347 try { 348 await fs.promises.rm(validLink); 349 assert.strictEqual(fs.existsSync(validLink), false); 350 } finally { 351 fs.rmSync(linkTarget, common.mustNotMutateObjectDeep({ force: true })); 352 fs.rmSync(validLink, common.mustNotMutateObjectDeep({ force: true })); 353 } 354 355 // Should delete an invalid symlink 356 const invalidLink = path.join(tmpdir.path, 'invalid-link-prom'); 357 fs.symlinkSync('definitely-does-not-exist-prom', invalidLink); 358 try { 359 await fs.promises.rm(invalidLink); 360 assert.strictEqual(fs.existsSync(invalidLink), false); 361 } finally { 362 fs.rmSync(invalidLink, common.mustNotMutateObjectDeep({ force: true })); 363 } 364 365 // Should delete a symlink that is part of a loop 366 const loopLinkA = path.join(tmpdir.path, 'loop-link-prom-a'); 367 const loopLinkB = path.join(tmpdir.path, 'loop-link-prom-b'); 368 fs.symlinkSync(loopLinkA, loopLinkB); 369 fs.symlinkSync(loopLinkB, loopLinkA); 370 try { 371 await fs.promises.rm(loopLinkA); 372 assert.strictEqual(fs.existsSync(loopLinkA), false); 373 } finally { 374 fs.rmSync(loopLinkA, common.mustNotMutateObjectDeep({ force: true })); 375 fs.rmSync(loopLinkB, common.mustNotMutateObjectDeep({ force: true })); 376 } 377 378 // Should accept URL 379 const fileURL = tmpdir.fileURL('rm-promises-file.txt'); 380 fs.writeFileSync(fileURL, ''); 381 382 try { 383 await fs.promises.rm(fileURL, common.mustNotMutateObjectDeep({ recursive: true })); 384 assert.strictEqual(fs.existsSync(fileURL), false); 385 } finally { 386 fs.rmSync(fileURL, common.mustNotMutateObjectDeep({ force: true })); 387 } 388})().then(common.mustCall()); 389 390// Removing a .git directory should not throw an EPERM. 391// Refs: https://github.com/isaacs/rimraf/issues/21. 392if (isGitPresent) { 393 (async () => { 394 const gitDirectory = nextDirPath(); 395 gitInit(gitDirectory); 396 await fs.promises.rm(gitDirectory, common.mustNotMutateObjectDeep({ recursive: true })); 397 assert.strictEqual(fs.existsSync(gitDirectory), false); 398 })().then(common.mustCall()); 399} 400 401// Test input validation. 402{ 403 const dir = nextDirPath(); 404 makeNonEmptyDirectory(4, 10, 2, dir, true); 405 const filePath = (path.join(tmpdir.path, 'rm-args-file.txt')); 406 fs.writeFileSync(filePath, ''); 407 408 const defaults = { 409 retryDelay: 100, 410 maxRetries: 0, 411 recursive: false, 412 force: false 413 }; 414 const modified = { 415 retryDelay: 953, 416 maxRetries: 5, 417 recursive: true, 418 force: false 419 }; 420 421 assert.deepStrictEqual(validateRmOptionsSync(filePath), defaults); 422 assert.deepStrictEqual(validateRmOptionsSync(filePath, {}), defaults); 423 assert.deepStrictEqual(validateRmOptionsSync(filePath, modified), modified); 424 assert.deepStrictEqual(validateRmOptionsSync(filePath, { 425 maxRetries: 99 426 }), { 427 retryDelay: 100, 428 maxRetries: 99, 429 recursive: false, 430 force: false 431 }); 432 433 [null, 'foo', 5, NaN].forEach((bad) => { 434 assert.throws(() => { 435 validateRmOptionsSync(filePath, bad); 436 }, { 437 code: 'ERR_INVALID_ARG_TYPE', 438 name: 'TypeError', 439 message: /^The "options" argument must be of type object\./ 440 }); 441 }); 442 443 [undefined, null, 'foo', Infinity, function() {}].forEach((bad) => { 444 assert.throws(() => { 445 validateRmOptionsSync(filePath, { recursive: bad }); 446 }, { 447 code: 'ERR_INVALID_ARG_TYPE', 448 name: 'TypeError', 449 message: /^The "options\.recursive" property must be of type boolean\./ 450 }); 451 }); 452 453 [undefined, null, 'foo', Infinity, function() {}].forEach((bad) => { 454 assert.throws(() => { 455 validateRmOptionsSync(filePath, { force: bad }); 456 }, { 457 code: 'ERR_INVALID_ARG_TYPE', 458 name: 'TypeError', 459 message: /^The "options\.force" property must be of type boolean\./ 460 }); 461 }); 462 463 assert.throws(() => { 464 validateRmOptionsSync(filePath, { retryDelay: -1 }); 465 }, { 466 code: 'ERR_OUT_OF_RANGE', 467 name: 'RangeError', 468 message: /^The value of "options\.retryDelay" is out of range\./ 469 }); 470 471 assert.throws(() => { 472 validateRmOptionsSync(filePath, { maxRetries: -1 }); 473 }, { 474 code: 'ERR_OUT_OF_RANGE', 475 name: 'RangeError', 476 message: /^The value of "options\.maxRetries" is out of range\./ 477 }); 478} 479 480{ 481 // IBMi has a different access permission mechanism 482 // This test should not be run as `root` 483 if (!common.isIBMi && (common.isWindows || process.getuid() !== 0)) { 484 function makeDirectoryReadOnly(dir, mode) { 485 let accessErrorCode = 'EACCES'; 486 if (common.isWindows) { 487 accessErrorCode = 'EPERM'; 488 execSync(`icacls ${dir} /deny "everyone:(OI)(CI)(DE,DC)"`); 489 } else { 490 fs.chmodSync(dir, mode); 491 } 492 return accessErrorCode; 493 } 494 495 function makeDirectoryWritable(dir) { 496 if (fs.existsSync(dir)) { 497 if (common.isWindows) { 498 execSync(`icacls ${dir} /remove:d "everyone"`); 499 } else { 500 fs.chmodSync(dir, 0o777); 501 } 502 } 503 } 504 505 { 506 // Check that deleting a file that cannot be accessed using rmsync throws 507 // https://github.com/nodejs/node/issues/38683 508 const dirname = nextDirPath(); 509 const filePath = path.join(dirname, 'text.txt'); 510 try { 511 fs.mkdirSync(dirname, common.mustNotMutateObjectDeep({ recursive: true })); 512 fs.writeFileSync(filePath, 'hello'); 513 const code = makeDirectoryReadOnly(dirname, 0o444); 514 assert.throws(() => { 515 fs.rmSync(filePath, common.mustNotMutateObjectDeep({ force: true })); 516 }, { 517 code, 518 name: 'Error', 519 }); 520 } finally { 521 makeDirectoryWritable(dirname); 522 } 523 } 524 525 { 526 // Check endless recursion. 527 // https://github.com/nodejs/node/issues/34580 528 const dirname = nextDirPath(); 529 fs.mkdirSync(dirname, common.mustNotMutateObjectDeep({ recursive: true })); 530 const root = fs.mkdtempSync(path.join(dirname, 'fs-')); 531 const middle = path.join(root, 'middle'); 532 fs.mkdirSync(middle); 533 fs.mkdirSync(path.join(middle, 'leaf')); // Make `middle` non-empty 534 try { 535 const code = makeDirectoryReadOnly(middle, 0o555); 536 try { 537 assert.throws(() => { 538 fs.rmSync(root, common.mustNotMutateObjectDeep({ recursive: true })); 539 }, { 540 code, 541 name: 'Error', 542 }); 543 } catch (err) { 544 // Only fail the test if the folder was not deleted. 545 // as in some cases rmSync successfully deletes read-only folders. 546 if (fs.existsSync(root)) { 547 throw err; 548 } 549 } 550 } finally { 551 makeDirectoryWritable(middle); 552 } 553 } 554 } 555} 556