Skip to content

Commit

Permalink
do not ever actually try to rmdir /
Browse files Browse the repository at this point in the history
When preserveRoot is set to false, we may remove all the children of
the root directory, but we'll never actually succeed in rmdir()-ing the
root dir itself, so don't try.
  • Loading branch information
isaacs committed Jan 10, 2023
1 parent 2442655 commit 4b228c9
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 4 deletions.
12 changes: 11 additions & 1 deletion lib/rimraf-posix.js
Expand Up @@ -14,7 +14,7 @@ const {
},
} = require('./fs.js')

const { resolve } = require('path')
const { resolve, parse } = require('path')

const {
readdirOrError,
Expand All @@ -38,6 +38,12 @@ const rimrafPosix = async (path, opt) => {
await Promise.all(entries.map(entry =>
rimrafPosix(resolve(path, entry), opt)))

// we don't ever ACTUALLY try to unlink /, because that can never work
// but when preserveRoot is false, we could be operating on it.
// No need to check if preserveRoot is not false.
if (opt.preserveRoot === false && path === parse(path).root)
return

return ignoreENOENT(rmdir(path))
}

Expand All @@ -52,6 +58,10 @@ const rimrafPosixSync = (path, opt) => {
}
for (const entry of entries)
rimrafPosixSync(resolve(path, entry), opt)

if (opt.preserveRoot === false && path === parse(path).root)
return

return ignoreENOENTSync(() => rmdirSync(path))
}

Expand Down
15 changes: 12 additions & 3 deletions lib/rimraf-windows.js
Expand Up @@ -9,7 +9,7 @@
// This works around the fact that unlink/rmdir is non-atomic and takes
// a non-deterministic amount of time to complete.

const { resolve, basename } = require('path')
const { resolve, basename, parse } = require('path')
const { defaultTmp, defaultTmpSync } = require('./default-tmp.js')

const {
Expand Down Expand Up @@ -75,7 +75,7 @@ const rimrafWindows = async (path, opt) => {
if (!opt.tmp)
return rimrafWindows(path, { ...opt, tmp: await defaultTmp(path) })

if (path === opt.tmp)
if (path === opt.tmp && parse(path).root !== path)
throw new Error('cannot delete temp directory used for deletion')

const entries = await readdirOrError(path)
Expand All @@ -92,6 +92,12 @@ const rimrafWindows = async (path, opt) => {
await Promise.all(entries.map(entry =>
rimrafWindows(resolve(path, entry), opt)))

// we don't ever ACTUALLY try to unlink /, because that can never work
// but when preserveRoot is false, we could be operating on it.
// No need to check if preserveRoot is not false.
if (opt.preserveRoot === false && path === parse(path).root)
return

return await ignoreENOENT(tmpUnlink(path, opt.tmp, rmdir))
}

Expand All @@ -105,7 +111,7 @@ const rimrafWindowsSync = (path, opt) => {
if (!opt.tmp)
return rimrafWindowsSync(path, { ...opt, tmp: defaultTmpSync(path) })

if (path === opt.tmp)
if (path === opt.tmp && parse(path).root !== path)
throw new Error('cannot delete temp directory used for deletion')

const entries = readdirOrErrorSync(path)
Expand All @@ -123,6 +129,9 @@ const rimrafWindowsSync = (path, opt) => {
for (const entry of entries)
rimrafWindowsSync(resolve(path, entry), opt)

if (opt.preserveRoot === false && path === parse(path).root)
return

return ignoreENOENTSync(() => tmpUnlinkSync(path, opt.tmp, rmdirSync))
}

Expand Down
29 changes: 29 additions & 0 deletions test/rimraf-posix.js
Expand Up @@ -170,3 +170,32 @@ t.test('ignore ENOENTs from unlink/rmdir', async t => {

t.end()
})

t.test('rimraffing root, do not actually rmdir root', async t => {
let ROOT = null
const { parse } = require('path')
const { rimrafPosix, rimrafPosixSync } = t.mock('../lib/rimraf-posix.js', {
path: {
...require('path'),
parse: (path) => {
const p = parse(path)
if (path === ROOT)
p.root = path
return p
},
},
})
t.test('async', async t => {
ROOT = t.testdir(fixture)
await rimrafPosix(ROOT, { preserveRoot: false })
t.equal(fs.statSync(ROOT).isDirectory(), true, 'root still present')
t.same(fs.readdirSync(ROOT), [], 'entries all gone')
})
t.test('sync', async t => {
ROOT = t.testdir(fixture)
rimrafPosixSync(ROOT, { preserveRoot: false })
t.equal(fs.statSync(ROOT).isDirectory(), true, 'root still present')
t.same(fs.readdirSync(ROOT), [], 'entries all gone')
})
t.end()
})
33 changes: 33 additions & 0 deletions test/rimraf-windows.js
Expand Up @@ -449,3 +449,36 @@ t.test('handle EPERMs, chmod raises something other than ENOENT', async t => {
})
t.end()
})

t.test('rimraffing root, do not actually rmdir root', async t => {
const fs = require('../lib/fs.js')
let ROOT = null
const { parse } = require('path')
const {
rimrafWindows,
rimrafWindowsSync,
} = t.mock('../lib/rimraf-windows.js', {
path: {
...require('path'),
parse: (path) => {
const p = parse(path)
if (path === ROOT)
p.root = path
return p
},
},
})
t.test('async', async t => {
ROOT = t.testdir(fixture)
await rimrafWindows(ROOT, { preserveRoot: false })
t.equal(fs.statSync(ROOT).isDirectory(), true, 'root still present')
t.same(fs.readdirSync(ROOT), [], 'entries all gone')
})
t.test('sync', async t => {
ROOT = t.testdir(fixture)
rimrafWindowsSync(ROOT, { preserveRoot: false })
t.equal(fs.statSync(ROOT).isDirectory(), true, 'root still present')
t.same(fs.readdirSync(ROOT), [], 'entries all gone')
})
t.end()
})

0 comments on commit 4b228c9

Please sign in to comment.