1'use strict' 2 3const path = require('path') 4const validate = require('aproba') 5const fs = require('graceful-fs') 6const isInside = require('path-is-inside') 7const vacuum = require('fs-vacuum') 8const chain = require('slide').chain 9const asyncMap = require('slide').asyncMap 10const readCmdShim = require('read-cmd-shim') 11const iferr = require('iferr') 12 13exports = module.exports = rm 14 15function rm (target, opts, cb) { 16 var targetPath = path.normalize(path.resolve(opts.prefix, target)) 17 if (opts.prefixes.indexOf(targetPath) !== -1) { 18 return cb(new Error('May not delete: ' + targetPath)) 19 } 20 var options = {} 21 if (opts.force) { options.purge = true } 22 if (opts.base) options.base = path.normalize(path.resolve(opts.prefix, opts.base)) 23 24 if (!opts.gently) { 25 options.purge = true 26 return vacuum(targetPath, options, cb) 27 } 28 29 var parent = options.base = options.base || path.normalize(opts.prefix) 30 31 // Do all the async work we'll need to do in order to tell if this is a 32 // safe operation 33 chain([ 34 [isEverInside, parent, opts.prefixes, opts.log], 35 [readLinkOrShim, targetPath], 36 [isEverInside, targetPath, opts.prefixes, opts.log], 37 [isEverInside, targetPath, [parent], opts.log] 38 ], function (er, results) { 39 if (er) { 40 if (er.code === 'ENOENT') return cb() 41 return cb(er) 42 } 43 var parentInfo = { 44 path: parent, 45 managed: results[0] 46 } 47 var targetInfo = { 48 path: targetPath, 49 symlink: results[1], 50 managed: results[2], 51 inParent: results[3] 52 } 53 54 isSafeToRm(parentInfo, targetInfo, opts.name, opts.log, iferr(cb, thenRemove)) 55 56 function thenRemove (toRemove, removeBase) { 57 if (!toRemove) return cb() 58 if (removeBase) options.base = removeBase 59 return vacuum(toRemove, options, cb) 60 } 61 }) 62} 63 64exports._isSafeToRm = isSafeToRm 65function isSafeToRm (parent, target, pkgName, log, cb) { 66 log.silly('gentlyRm', 'parent.path =', parent.path) 67 log.silly('gentlyRm', 'parent.managed =', 68 parent.managed && parent.managed.target + ' is in ' + parent.managed.path) 69 log.silly('gentlyRm', 'target.path = ', target.path) 70 log.silly('gentlyRm', 'target.symlink =', target.symlink) 71 log.silly('gentlyRm', 'target.managed =', 72 target.managed && target.managed.target + ' is in ' + target.managed.path) 73 log.silly('gentlyRm', 'target.inParent = ', target.inParent) 74 75 // The parent directory or something it symlinks to must eventually be in 76 // a folder that we maintain. 77 if (!parent.managed) { 78 log.info('gentlyRm', parent.path, 79 'is not contained in any directory ' + pkgName + ' is known to control or ' + 80 'any place they link to') 81 return cb(clobberFail(target.path, 'containing path ' + parent.path + 82 " isn't under " + pkgName + "'s control")) 83 } 84 85 // The target or something it symlinks to must eventually be in the parent 86 // or something the parent symlinks to 87 if (target.inParent) { 88 var actualTarget = target.inParent.target 89 var targetsParent = target.inParent.path 90 // if the target.path was what we found in some version of parent, remove 91 // using that parent as the base 92 if (target.path === actualTarget) { 93 return cb(null, target.path, targetsParent) 94 } else { 95 // If something the target.path links to was what was found, just 96 // remove target.path in the location it was found. 97 return cb(null, target.path, path.dirname(target.path)) 98 } 99 } 100 101 // If the target is in a managed directory and is in a symlink, but was 102 // not in our parent that usually means someone else installed a bin file 103 // with the same name as one of our bin files. 104 if (target.managed && target.symlink) { 105 log.warn('rm', 'not removing', target.path, 106 "as it wasn't installed by", parent.path) 107 return cb() 108 } 109 110 if (target.symlink) { 111 return cb(clobberFail(target.path, target.symlink + 112 ' symlink target is not controlled by ' + pkgName + ' ' + parent.path)) 113 } else { 114 return cb(clobberFail(target.path, 'is outside ' + parent.path + 115 ' and not a link')) 116 } 117} 118 119function clobberFail (target, msg) { 120 validate('SS', arguments) 121 var er = new Error('Refusing to delete ' + target + ': ' + msg) 122 er.code = 'EEXIST' 123 er.path = target 124 return er 125} 126 127function isENOENT (err) { 128 return err && err.code === 'ENOENT' 129} 130 131function notENOENT (err) { 132 return !isENOENT(err) 133} 134 135function skipENOENT (cb) { 136 return function (err, value) { 137 if (isENOENT(err)) { 138 return cb(null, false) 139 } else { 140 return cb(err, value) 141 } 142 } 143} 144 145function errorsToValues (fn) { 146 return function () { 147 var args = Array.prototype.slice.call(arguments) 148 var cb = args.pop() 149 args.push(function (err, value) { 150 if (err) { 151 return cb(null, err) 152 } else { 153 return cb(null, value) 154 } 155 }) 156 fn.apply(null, args) 157 } 158} 159 160function isNotError (value) { 161 return !(value instanceof Error) 162} 163 164exports._isEverInside = isEverInside 165// return the first of path, where target (or anything it symlinks to) 166// isInside the path (or anything it symlinks to) 167function isEverInside (target, paths, log, cb) { 168 validate('SAOF', arguments) 169 asyncMap(paths, errorsToValues(readAllLinks), iferr(cb, function (resolvedPaths) { 170 var errorFree = resolvedPaths.filter(isNotError) 171 if (errorFree.length === 0) { 172 var badErrors = resolvedPaths.filter(notENOENT) 173 if (badErrors.length === 0) { 174 return cb(null, false) 175 } else { 176 return cb(badErrors[0]) 177 } 178 } 179 readAllLinks(target, iferr(skipENOENT(cb), function (targets) { 180 cb(null, areAnyInsideAny(targets, errorFree, log)) 181 })) 182 })) 183} 184 185exports._areAnyInsideAny = areAnyInsideAny 186// Return the first path found that any target is inside 187function areAnyInsideAny (targets, paths, log) { 188 validate('AAO', arguments) 189 var toCheck = [] 190 paths.forEach(function (path) { 191 targets.forEach(function (target) { 192 toCheck.push([target, path]) 193 }) 194 }) 195 for (var ii = 0; ii < toCheck.length; ++ii) { 196 var target = toCheck[ii][0] 197 var path = toCheck[ii][1] 198 var inside = isInside(target, path) 199 if (!inside) log.silly('isEverInside', target, 'is not inside', path) 200 if (inside && path) return inside && path && {target: target, path: path} 201 } 202 return false 203} 204 205exports._readAllLinks = readAllLinks 206// resolves chains of symlinks of unlimited depth, returning a list of paths 207// it's seen in the process when it hits either a symlink cycle or a 208// non-symlink 209function readAllLinks (path, cb) { 210 validate('SF', arguments) 211 var seen = {} 212 _readAllLinks(path) 213 214 function _readAllLinks (path) { 215 if (seen[path]) return cb(null, Object.keys(seen)) 216 seen[path] = true 217 resolveSymlink(path, iferr(cb, _readAllLinks)) 218 } 219} 220 221exports._resolveSymlink = resolveSymlink 222var resolvedPaths = {} 223function resolveSymlink (symlink, cb) { 224 validate('SF', arguments) 225 var cached = resolvedPaths[symlink] 226 if (cached) return cb(null, cached) 227 228 readLinkOrShim(symlink, iferr(cb, function (symlinkTarget) { 229 if (symlinkTarget) { 230 resolvedPaths[symlink] = path.resolve(path.dirname(symlink), symlinkTarget) 231 } else { 232 resolvedPaths[symlink] = symlink 233 } 234 return cb(null, resolvedPaths[symlink]) 235 })) 236} 237 238exports._readLinkOrShim = readLinkOrShim 239function readLinkOrShim (path, cb) { 240 validate('SF', arguments) 241 fs.lstat(path, iferr(cb, function (stat) { 242 if (stat.isSymbolicLink()) { 243 fs.readlink(path, cb) 244 } else { 245 readCmdShim(path, function (er, source) { 246 if (!er) return cb(null, source) 247 // lstat wouldn't return an error on these, so we don't either. 248 if (er.code === 'ENOTASHIM' || er.code === 'EISDIR') { 249 return cb(null, null) 250 } else { 251 return cb(er) 252 } 253 }) 254 } 255 })) 256} 257