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