From cd6fbc6a4fbf94e036e4f69ef0a049a6648d996b Mon Sep 17 00:00:00 2001 From: isaacs Date: Mon, 6 Mar 2023 08:34:16 -0800 Subject: [PATCH] Only call directory removal method on actual dirs This prevents all symbolic link following, and also cuts down on the system calls quite considerably, while still preventing the "unlinked directory" issue on SunOS, with only the cost of a single lstat at the start of the process. Also added test of mid-stream AbortSignal triggering in sync rimrafs, because this is possible if called from a filter method. Fix: #259 --- src/bin.ts | 7 ++- src/fs.ts | 7 +++ src/ignore-enoent.ts | 1 - src/readdir-or-error.ts | 2 +- src/rimraf-move-remove.ts | 93 ++++++++++++++++++++++++++++-------- src/rimraf-posix.ts | 87 +++++++++++++++++++++++---------- src/rimraf-windows.ts | 98 +++++++++++++++++++++++++------------- test/index.js | 6 +-- test/rimraf-move-remove.js | 55 ++++++++++++++++++++- test/rimraf-posix.js | 52 ++++++++++++++++++++ test/rimraf-windows.js | 59 +++++++++++++++++++---- 11 files changed, 368 insertions(+), 99 deletions(-) diff --git a/src/bin.ts b/src/bin.ts index 73e64dc9..7aeffca9 100755 --- a/src/bin.ts +++ b/src/bin.ts @@ -91,10 +91,9 @@ const interactiveRimraf = async ( return false } while (!allRemaining) { - const a = (await prompt( - rl, - `rm? ${relative(cwd, path)}\n[(Yes)/No/All/Quit] > ` - )).trim() + const a = ( + await prompt(rl, `rm? ${relative(cwd, path)}\n[(Yes)/No/All/Quit] > `) + ).trim() if (/^n/i.test(a)) { return false } else if (/^a/i.test(a)) { diff --git a/src/fs.ts b/src/fs.ts index 3cafb66e..fe2a52ad 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -10,6 +10,7 @@ export { rmdirSync, rmSync, statSync, + lstatSync, unlinkSync, } from 'fs' @@ -66,6 +67,11 @@ const stat = (path: fs.PathLike): Promise => fs.stat(path, (er, data) => (er ? rej(er) : res(data))) ) +const lstat = (path: fs.PathLike): Promise => + new Promise((res, rej) => + fs.lstat(path, (er, data) => (er ? rej(er) : res(data))) + ) + const unlink = (path: fs.PathLike): Promise => new Promise((res, rej) => fs.unlink(path, (er, ...d: any[]) => (er ? rej(er) : res(...d))) @@ -79,5 +85,6 @@ export const promises = { rm, rmdir, stat, + lstat, unlink, } diff --git a/src/ignore-enoent.ts b/src/ignore-enoent.ts index 076f31c4..c0667912 100644 --- a/src/ignore-enoent.ts +++ b/src/ignore-enoent.ts @@ -1,4 +1,3 @@ - export const ignoreENOENT = async (p: Promise) => p.catch(er => { if (er.code !== 'ENOENT') { diff --git a/src/readdir-or-error.ts b/src/readdir-or-error.ts index 693365f8..79970df1 100644 --- a/src/readdir-or-error.ts +++ b/src/readdir-or-error.ts @@ -1,6 +1,6 @@ // returns an array of entries if readdir() works, // or the error that readdir() raised if not. -import { promises, readdirSync } from './fs.js' +import { promises, readdirSync } from './fs.js' const { readdir } = promises export const readdirOrError = (path: string) => readdir(path).catch(er => er as NodeJS.ErrnoException) diff --git a/src/rimraf-move-remove.ts b/src/rimraf-move-remove.ts index 73f08de7..c64cc224 100644 --- a/src/rimraf-move-remove.ts +++ b/src/rimraf-move-remove.ts @@ -18,13 +18,15 @@ import { ignoreENOENT, ignoreENOENTSync } from './ignore-enoent.js' import { chmodSync, + lstatSync, promises as fsPromises, renameSync, rmdirSync, unlinkSync, } from './fs.js' -const { rename, unlink, rmdir, chmod } = fsPromises +const { lstat, rename, unlink, rmdir, chmod } = fsPromises +import { Dirent, Stats } from 'fs' import { RimrafAsyncOptions, RimrafSyncOptions } from '.' import { readdirOrError, readdirOrErrorSync } from './readdir-or-error.js' @@ -72,25 +74,51 @@ const unlinkFixEPERMSync = (path: string) => { export const rimrafMoveRemove = async ( path: string, opt: RimrafAsyncOptions +) => { + if (opt?.signal?.aborted) { + throw opt.signal.reason + } + try { + return await rimrafMoveRemoveDir(path, opt, await lstat(path)) + } catch (er) { + if ((er as NodeJS.ErrnoException)?.code === 'ENOENT') return true + throw er + } +} + +const rimrafMoveRemoveDir = async ( + path: string, + opt: RimrafAsyncOptions, + ent: Dirent | Stats ): Promise => { if (opt?.signal?.aborted) { throw opt.signal.reason } if (!opt.tmp) { - return rimrafMoveRemove(path, { ...opt, tmp: await defaultTmp(path) }) + return rimrafMoveRemoveDir( + path, + { ...opt, tmp: await defaultTmp(path) }, + ent + ) } if (path === opt.tmp && parse(path).root !== path) { throw new Error('cannot delete temp directory used for deletion') } - const entries = await readdirOrError(path) + const entries = ent.isDirectory() ? await readdirOrError(path) : null if (!Array.isArray(entries)) { - if (entries.code === 'ENOENT') { - return true - } - if (entries.code !== 'ENOTDIR') { - throw entries + // this can only happen if lstat/readdir lied, or if the dir was + // swapped out with a file at just the right moment. + /* c8 ignore start */ + if (entries) { + if (entries.code === 'ENOENT') { + return true + } + if (entries.code !== 'ENOTDIR') { + throw entries + } } + /* c8 ignore stop */ if (opt.filter && !(await opt.filter(path))) { return false } @@ -100,7 +128,7 @@ export const rimrafMoveRemove = async ( const removedAll = ( await Promise.all( - entries.map(entry => rimrafMoveRemove(resolve(path, entry.name), opt)) + entries.map(ent => rimrafMoveRemoveDir(resolve(path, ent.name), opt, ent)) ) ).reduce((a, b) => a && b, true) if (!removedAll) { @@ -130,15 +158,32 @@ const tmpUnlink = async ( return await rm(tmpFile) } -export const rimrafMoveRemoveSync = ( +export const rimrafMoveRemoveSync = (path: string, opt: RimrafSyncOptions) => { + if (opt?.signal?.aborted) { + throw opt.signal.reason + } + try { + return rimrafMoveRemoveDirSync(path, opt, lstatSync(path)) + } catch (er) { + if ((er as NodeJS.ErrnoException)?.code === 'ENOENT') return true + throw er + } +} + +const rimrafMoveRemoveDirSync = ( path: string, - opt: RimrafSyncOptions + opt: RimrafSyncOptions, + ent: Dirent | Stats ): boolean => { if (opt?.signal?.aborted) { throw opt.signal.reason } if (!opt.tmp) { - return rimrafMoveRemoveSync(path, { ...opt, tmp: defaultTmpSync(path) }) + return rimrafMoveRemoveDirSync( + path, + { ...opt, tmp: defaultTmpSync(path) }, + ent + ) } const tmp: string = opt.tmp @@ -146,14 +191,20 @@ export const rimrafMoveRemoveSync = ( throw new Error('cannot delete temp directory used for deletion') } - const entries = readdirOrErrorSync(path) + const entries = ent.isDirectory() ? readdirOrErrorSync(path) : null if (!Array.isArray(entries)) { - if (entries.code === 'ENOENT') { - return true - } - if (entries.code !== 'ENOTDIR') { - throw entries + // this can only happen if lstat/readdir lied, or if the dir was + // swapped out with a file at just the right moment. + /* c8 ignore start */ + if (entries) { + if (entries.code === 'ENOENT') { + return true + } + if (entries.code !== 'ENOTDIR') { + throw entries + } } + /* c8 ignore stop */ if (opt.filter && !opt.filter(path)) { return false } @@ -162,9 +213,9 @@ export const rimrafMoveRemoveSync = ( } let removedAll = true - for (const entry of entries) { - removedAll = - rimrafMoveRemoveSync(resolve(path, entry.name), opt) && removedAll + for (const ent of entries) { + const p = resolve(path, ent.name) + removedAll = rimrafMoveRemoveDirSync(p, opt, ent) && removedAll } if (!removedAll) { return false diff --git a/src/rimraf-posix.ts b/src/rimraf-posix.ts index cb109037..c26b4884 100644 --- a/src/rimraf-posix.ts +++ b/src/rimraf-posix.ts @@ -1,35 +1,66 @@ // the simple recursive removal, where unlink and rmdir are atomic // Note that this approach does NOT work on Windows! -// We rmdir before unlink even though that is arguably less efficient -// (since the average folder contains >1 file, it means more system -// calls), because sunos will let root unlink a directory, and some +// We stat first and only unlink if the Dirent isn't a directory, +// because sunos will let root unlink a directory, and some // SUPER weird breakage happens as a result. -import { promises, rmdirSync, unlinkSync } from './fs.js' -const { rmdir, unlink } = promises +import { lstatSync, promises, rmdirSync, unlinkSync } from './fs.js' +const { lstat, rmdir, unlink } = promises import { parse, resolve } from 'path' import { readdirOrError, readdirOrErrorSync } from './readdir-or-error.js' +import { Dirent, Stats } from 'fs' import { RimrafAsyncOptions, RimrafSyncOptions } from '.' import { ignoreENOENT, ignoreENOENTSync } from './ignore-enoent.js' -export const rimrafPosix = async ( +export const rimrafPosix = async (path: string, opt: RimrafAsyncOptions) => { + if (opt?.signal?.aborted) { + throw opt.signal.reason + } + try { + return await rimrafPosixDir(path, opt, await lstat(path)) + } catch (er) { + if ((er as NodeJS.ErrnoException)?.code === 'ENOENT') return true + throw er + } +} + +export const rimrafPosixSync = (path: string, opt: RimrafSyncOptions) => { + if (opt?.signal?.aborted) { + throw opt.signal.reason + } + try { + return rimrafPosixDirSync(path, opt, lstatSync(path)) + } catch (er) { + if ((er as NodeJS.ErrnoException)?.code === 'ENOENT') return true + throw er + } +} + +const rimrafPosixDir = async ( path: string, - opt: RimrafAsyncOptions + opt: RimrafAsyncOptions, + ent: Dirent | Stats ): Promise => { if (opt?.signal?.aborted) { throw opt.signal.reason } - const entries = await readdirOrError(path) + const entries = ent.isDirectory() ? await readdirOrError(path) : null if (!Array.isArray(entries)) { - if (entries.code === 'ENOENT') { - return true - } - if (entries.code !== 'ENOTDIR') { - throw entries + // this can only happen if lstat/readdir lied, or if the dir was + // swapped out with a file at just the right moment. + /* c8 ignore start */ + if (entries) { + if (entries.code === 'ENOENT') { + return true + } + if (entries.code !== 'ENOTDIR') { + throw entries + } } + /* c8 ignore stop */ if (opt.filter && !(await opt.filter(path))) { return false } @@ -39,7 +70,7 @@ export const rimrafPosix = async ( const removedAll = ( await Promise.all( - entries.map(entry => rimrafPosix(resolve(path, entry.name), opt)) + entries.map(ent => rimrafPosixDir(resolve(path, ent.name), opt, ent)) ) ).reduce((a, b) => a && b, true) @@ -62,21 +93,28 @@ export const rimrafPosix = async ( return true } -export const rimrafPosixSync = ( +const rimrafPosixDirSync = ( path: string, - opt: RimrafSyncOptions + opt: RimrafSyncOptions, + ent: Dirent | Stats ): boolean => { if (opt?.signal?.aborted) { throw opt.signal.reason } - const entries = readdirOrErrorSync(path) + const entries = ent.isDirectory() ? readdirOrErrorSync(path) : null if (!Array.isArray(entries)) { - if (entries.code === 'ENOENT') { - return true - } - if (entries.code !== 'ENOTDIR') { - throw entries + // this can only happen if lstat/readdir lied, or if the dir was + // swapped out with a file at just the right moment. + /* c8 ignore start */ + if (entries) { + if (entries.code === 'ENOENT') { + return true + } + if (entries.code !== 'ENOTDIR') { + throw entries + } } + /* c8 ignore stop */ if (opt.filter && !opt.filter(path)) { return false } @@ -84,8 +122,9 @@ export const rimrafPosixSync = ( return true } let removedAll: boolean = true - for (const entry of entries) { - removedAll = rimrafPosixSync(resolve(path, entry.name), opt) && removedAll + for (const ent of entries) { + const p = resolve(path, ent.name) + removedAll = rimrafPosixDirSync(p, opt, ent) && removedAll } if (opt.preserveRoot === false && path === parse(path).root) { return false diff --git a/src/rimraf-windows.ts b/src/rimraf-windows.ts index 2b9b1296..7ee088e9 100644 --- a/src/rimraf-windows.ts +++ b/src/rimraf-windows.ts @@ -8,20 +8,21 @@ // // Note: "move then remove" is 2-10 times slower, and just as unreliable. +import { Dirent, Stats } from 'fs' import { parse, resolve } from 'path' import { RimrafAsyncOptions, RimrafSyncOptions } from '.' import { fixEPERM, fixEPERMSync } from './fix-eperm.js' -import { promises, rmdirSync, unlinkSync } from './fs.js' +import { lstatSync, promises, rmdirSync, unlinkSync } from './fs.js' import { ignoreENOENT, ignoreENOENTSync } from './ignore-enoent.js' import { readdirOrError, readdirOrErrorSync } from './readdir-or-error.js' import { retryBusy, retryBusySync } from './retry-busy.js' import { rimrafMoveRemove, rimrafMoveRemoveSync } from './rimraf-move-remove.js' -const { unlink, rmdir } = promises +const { unlink, rmdir, lstat } = promises const rimrafWindowsFile = retryBusy(fixEPERM(unlink)) const rimrafWindowsFileSync = retryBusySync(fixEPERMSync(unlinkSync)) -const rimrafWindowsDir = retryBusy(fixEPERM(rmdir)) -const rimrafWindowsDirSync = retryBusySync(fixEPERMSync(rmdirSync)) +const rimrafWindowsDirRetry = retryBusy(fixEPERM(rmdir)) +const rimrafWindowsDirRetrySync = retryBusySync(fixEPERMSync(rmdirSync)) const rimrafWindowsDirMoveRemoveFallback = async ( path: string, @@ -35,7 +36,7 @@ const rimrafWindowsDirMoveRemoveFallback = async ( // already filtered, remove from options so we don't call unnecessarily const { filter, ...options } = opt try { - return await rimrafWindowsDir(path, options) + return await rimrafWindowsDirRetry(path, options) } catch (er) { if ((er as NodeJS.ErrnoException)?.code === 'ENOTEMPTY') { return await rimrafMoveRemove(path, options) @@ -54,7 +55,7 @@ const rimrafWindowsDirMoveRemoveFallbackSync = ( // already filtered, remove from options so we don't call unnecessarily const { filter, ...options } = opt try { - return rimrafWindowsDirSync(path, options) + return rimrafWindowsDirRetrySync(path, options) } catch (er) { const fer = er as NodeJS.ErrnoException if (fer?.code === 'ENOTEMPTY') { @@ -67,28 +68,55 @@ const rimrafWindowsDirMoveRemoveFallbackSync = ( const START = Symbol('start') const CHILD = Symbol('child') const FINISH = Symbol('finish') -const states = new Set([START, CHILD, FINISH]) -export const rimrafWindows = async ( +export const rimrafWindows = async (path: string, opt: RimrafAsyncOptions) => { + if (opt?.signal?.aborted) { + throw opt.signal.reason + } + try { + return await rimrafWindowsDir(path, opt, await lstat(path), START) + } catch (er) { + if ((er as NodeJS.ErrnoException)?.code === 'ENOENT') return true + throw er + } +} + +export const rimrafWindowsSync = (path: string, opt: RimrafSyncOptions) => { + if (opt?.signal?.aborted) { + throw opt.signal.reason + } + try { + return rimrafWindowsDirSync(path, opt, lstatSync(path), START) + } catch (er) { + if ((er as NodeJS.ErrnoException)?.code === 'ENOENT') return true + throw er + } +} + +const rimrafWindowsDir = async ( path: string, opt: RimrafAsyncOptions, + ent: Dirent | Stats, state = START ): Promise => { if (opt?.signal?.aborted) { throw opt.signal.reason } - if (!states.has(state)) { - throw new TypeError('invalid third argument passed to rimraf') - } - const entries = await readdirOrError(path) + const entries = ent.isDirectory() ? await readdirOrError(path) : null if (!Array.isArray(entries)) { - if (entries.code === 'ENOENT') { - return true - } - if (entries.code !== 'ENOTDIR') { - throw entries + // this can only happen if lstat/readdir lied, or if the dir was + // swapped out with a file at just the right moment. + /* c8 ignore start */ + if (entries) { + if (entries.code === 'ENOENT') { + return true + } + if (entries.code !== 'ENOTDIR') { + throw entries + } } + /* c8 ignore stop */ if (opt.filter && !(await opt.filter(path))) { return false } @@ -100,12 +128,12 @@ export const rimrafWindows = async ( const s = state === START ? CHILD : state const removedAll = ( await Promise.all( - entries.map(entry => rimrafWindows(resolve(path, entry.name), opt, s)) + entries.map(ent => rimrafWindowsDir(resolve(path, ent.name), opt, ent, s)) ) ).reduce((a, b) => a && b, true) if (state === START) { - return rimrafWindows(path, opt, FINISH) + return rimrafWindowsDir(path, opt, ent, FINISH) } else if (state === FINISH) { if (opt.preserveRoot === false && path === parse(path).root) { return false @@ -121,23 +149,26 @@ export const rimrafWindows = async ( return true } -export const rimrafWindowsSync = ( +const rimrafWindowsDirSync = ( path: string, opt: RimrafSyncOptions, + ent: Dirent | Stats, state = START ): boolean => { - if (!states.has(state)) { - throw new TypeError('invalid third argument passed to rimraf') - } - - const entries = readdirOrErrorSync(path) + const entries = ent.isDirectory() ? readdirOrErrorSync(path) : null if (!Array.isArray(entries)) { - if (entries.code === 'ENOENT') { - return true - } - if (entries.code !== 'ENOTDIR') { - throw entries + // this can only happen if lstat/readdir lied, or if the dir was + // swapped out with a file at just the right moment. + /* c8 ignore start */ + if (entries) { + if (entries.code === 'ENOENT') { + return true + } + if (entries.code !== 'ENOTDIR') { + throw entries + } } + /* c8 ignore stop */ if (opt.filter && !opt.filter(path)) { return false } @@ -147,13 +178,14 @@ export const rimrafWindowsSync = ( } let removedAll = true - for (const entry of entries) { + for (const ent of entries) { const s = state === START ? CHILD : state - removedAll = rimrafWindowsSync(resolve(path, entry.name), opt, s) && removedAll + const p = resolve(path, ent.name) + removedAll = rimrafWindowsDirSync(p, opt, ent, s) && removedAll } if (state === START) { - return rimrafWindowsSync(path, opt, FINISH) + return rimrafWindowsDirSync(path, opt, ent, FINISH) } else if (state === FINISH) { if (opt.preserveRoot === false && path === parse(path).root) { return false diff --git a/test/index.js b/test/index.js index ca23438f..ee379bf4 100644 --- a/test/index.js +++ b/test/index.js @@ -1,4 +1,4 @@ -const {statSync} = require('fs') +const { statSync } = require('fs') const t = require('tap') t.same( @@ -225,14 +225,14 @@ t.test('deleting globs', t => { t.test('sync', t => { const cwd = t.testdir(fixture) - rimrafSync('**/f/**/m', { glob: { cwd }}) + rimrafSync('**/f/**/m', { glob: { cwd } }) t.throws(() => statSync(cwd + '/c/f/i/m')) statSync(cwd + '/c/f/i/l') t.end() }) t.test('async', async t => { const cwd = t.testdir(fixture) - await rimraf('**/f/**/m', { glob: { cwd }}) + await rimraf('**/f/**/m', { glob: { cwd } }) t.throws(() => statSync(cwd + '/c/f/i/m')) statSync(cwd + '/c/f/i/l') }) diff --git a/test/rimraf-move-remove.js b/test/rimraf-move-remove.js index 1c98e56e..78055b6d 100644 --- a/test/rimraf-move-remove.js +++ b/test/rimraf-move-remove.js @@ -275,11 +275,13 @@ t.test('refuse to delete the root dir', async t => { } ) + const d = t.testdir({}) + // not brave enough to pass the actual c:\\ here... - t.throws(() => rimrafMoveRemoveSync('some-path', { tmp: 'some-path' }), { + t.throws(() => rimrafMoveRemoveSync(d, { tmp: d }), { message: 'cannot delete temp directory used for deletion', }) - t.rejects(() => rimrafMoveRemove('some-path', { tmp: 'some-path' }), { + t.rejects(() => rimrafMoveRemove(d, { tmp: d }), { message: 'cannot delete temp directory used for deletion', }) }) @@ -520,6 +522,22 @@ t.test( t.throws(() => rimrafMoveRemoveSync(d, { signal })) t.end() }) + t.test('sync abort in filter', t => { + const d = t.testdir(fixture) + const ac = new AbortController() + const { signal } = ac + const opt = { + signal, + filter: p => { + if (basename(p) === 'g') { + ac.abort(new Error('done')) + } + return true + }, + } + t.throws(() => rimrafMoveRemoveSync(d, opt), { message: 'done' }) + t.end() + }) t.test('async', async t => { const ac = new AbortController() const { signal } = ac @@ -650,3 +668,36 @@ t.test('filter function', t => { } t.end() }) + +t.test('do not follow symlinks', t => { + const { + rimrafMoveRemove, + rimrafMoveRemoveSync, + } = require('../dist/cjs/src/rimraf-move-remove.js') + const fixture = { + x: { + y: t.fixture('symlink', '../z'), + z: '', + }, + z: { + a: '', + b: { c: '' }, + }, + } + t.test('sync', t => { + const d = t.testdir(fixture) + t.equal(rimrafMoveRemoveSync(d + '/x', {}), true) + statSync(d + '/z') + statSync(d + '/z/a') + statSync(d + '/z/b/c') + t.end() + }) + t.test('async', async t => { + const d = t.testdir(fixture) + t.equal(await rimrafMoveRemove(d + '/x', {}), true) + statSync(d + '/z') + statSync(d + '/z/a') + statSync(d + '/z/b/c') + }) + t.end() +}) diff --git a/test/rimraf-posix.js b/test/rimraf-posix.js index 7c7e0b28..897129bb 100644 --- a/test/rimraf-posix.js +++ b/test/rimraf-posix.js @@ -223,6 +223,22 @@ t.test( t.throws(() => rimrafPosixSync(d, { signal })) t.end() }) + t.test('sync abort in filter', t => { + const d = t.testdir(fixture) + const ac = new AbortController() + const { signal } = ac + const opt = { + signal, + filter: p => { + if (basename(p) === 'g') { + ac.abort(new Error('done')) + } + return true + }, + } + t.throws(() => rimrafPosixSync(d, opt), { message: 'done' }) + t.end() + }) t.test('async', async t => { const d = t.testdir(fixture) const ac = new AbortController() @@ -231,6 +247,13 @@ t.test( ac.abort(new Error('aborted rimraf')) await p }) + t.test('async preaborted', async t => { + const d = t.testdir(fixture) + const ac = new AbortController() + ac.abort(new Error('aborted rimraf')) + const { signal } = ac + await t.rejects(() => rimrafPosix(d, { signal })) + }) t.end() } ) @@ -346,3 +369,32 @@ t.test('filter function', t => { } t.end() }) + +t.test('do not follow symlinks', t => { + const fixture = { + x: { + y: t.fixture('symlink', '../z'), + z: '', + }, + z: { + a: '', + b: { c: '' }, + }, + } + t.test('sync', t => { + const d = t.testdir(fixture) + t.equal(rimrafPosixSync(d + '/x', {}), true) + statSync(d + '/z') + statSync(d + '/z/a') + statSync(d + '/z/b/c') + t.end() + }) + t.test('async', async t => { + const d = t.testdir(fixture) + t.equal(await rimrafPosix(d + '/x', {}), true) + statSync(d + '/z') + statSync(d + '/z/a') + statSync(d + '/z/b/c') + }) + t.end() +}) diff --git a/test/rimraf-windows.js b/test/rimraf-windows.js index 6e4722e6..34ed7d77 100644 --- a/test/rimraf-windows.js +++ b/test/rimraf-windows.js @@ -486,16 +486,6 @@ t.test('rimraffing root, do not actually rmdir root', async t => { t.end() }) -t.test('do not allow third arg', async t => { - const ROOT = t.testdir(fixture) - const { - rimrafWindows, - rimrafWindowsSync, - } = require('../dist/cjs/src/rimraf-windows.js') - t.rejects(rimrafWindows(ROOT, {}, true)) - t.throws(() => rimrafWindowsSync(ROOT, {}, true)) -}) - t.test( 'abort on signal', { skip: typeof AbortController === 'undefined' }, @@ -512,6 +502,22 @@ t.test( t.throws(() => rimrafWindowsSync(d, { signal })) t.end() }) + t.test('sync abort in filter', t => { + const d = t.testdir(fixture) + const ac = new AbortController() + const { signal } = ac + const opt = { + signal, + filter: p => { + if (basename(p) === 'g') { + ac.abort(new Error('done')) + } + return true + }, + } + t.throws(() => rimrafWindowsSync(d, opt), { message: 'done' }) + t.end() + }) t.test('async', async t => { const d = t.testdir(fixture) const ac = new AbortController() @@ -642,3 +648,36 @@ t.test('filter function', t => { } t.end() }) + +t.test('do not follow symlinks', t => { + const { + rimrafWindows, + rimrafWindowsSync, + } = require('../dist/cjs/src/rimraf-windows.js') + const fixture = { + x: { + y: t.fixture('symlink', '../z'), + z: '', + }, + z: { + a: '', + b: { c: '' }, + }, + } + t.test('sync', t => { + const d = t.testdir(fixture) + t.equal(rimrafWindowsSync(d + '/x', {}), true) + statSync(d + '/z') + statSync(d + '/z/a') + statSync(d + '/z/b/c') + t.end() + }) + t.test('async', async t => { + const d = t.testdir(fixture) + t.equal(await rimrafWindows(d + '/x', {}), true) + statSync(d + '/z') + statSync(d + '/z/a') + statSync(d + '/z/b/c') + }) + t.end() +})