1// look up the realpath, but cache stats to minimize overhead 2// If the parent folder is in the realpath cache, then we just 3// lstat the child, since there's no need to do a full realpath 4// This is not a separate module, and is much simpler than Node's 5// built-in fs.realpath, because we only care about symbolic links, 6// so we can handle many fewer edge cases. 7 8const fs = require('fs') 9/* istanbul ignore next */ 10const promisify = require('util').promisify || require('util-promisify') 11const readlink = promisify(fs.readlink) 12const lstat = promisify(fs.lstat) 13const { resolve, basename, dirname } = require('path') 14 15const realpathCached = (path, rpcache, stcache, depth) => { 16 // just a safety against extremely deep eloops 17 /* istanbul ignore next */ 18 if (depth > 2000) 19 throw eloop(path) 20 21 path = resolve(path) 22 if (rpcache.has(path)) 23 return Promise.resolve(rpcache.get(path)) 24 25 const dir = dirname(path) 26 const base = basename(path) 27 28 if (base && rpcache.has(dir)) 29 return realpathChild(dir, base, rpcache, stcache, depth) 30 31 // if it's the root, then we know it's real 32 if (!base) { 33 rpcache.set(dir, dir) 34 return Promise.resolve(dir) 35 } 36 37 // the parent, what is that? 38 // find out, and then come back. 39 return realpathCached(dir, rpcache, stcache, depth + 1).then(() => 40 realpathCached(path, rpcache, stcache, depth + 1)) 41} 42 43const lstatCached = (path, stcache) => { 44 if (stcache.has(path)) 45 return Promise.resolve(stcache.get(path)) 46 47 const p = lstat(path).then(st => { 48 stcache.set(path, st) 49 return st 50 }) 51 stcache.set(path, p) 52 return p 53} 54 55// This is a slight fib, as it doesn't actually occur during a stat syscall. 56// But file systems are giant piles of lies, so whatever. 57const eloop = path => 58 Object.assign(new Error( 59 `ELOOP: too many symbolic links encountered, stat '${path}'`), { 60 errno: -62, 61 syscall: 'stat', 62 code: 'ELOOP', 63 path: path, 64 }) 65 66const realpathChild = (dir, base, rpcache, stcache, depth) => { 67 const realdir = rpcache.get(dir) 68 // that unpossible 69 /* istanbul ignore next */ 70 if (typeof realdir === 'undefined') 71 throw new Error('in realpathChild without parent being in realpath cache') 72 73 const realish = resolve(realdir, base) 74 return lstatCached(realish, stcache).then(st => { 75 if (!st.isSymbolicLink()) { 76 rpcache.set(resolve(dir, base), realish) 77 return realish 78 } 79 80 let res 81 return readlink(realish).then(target => { 82 const resolved = res = resolve(realdir, target) 83 if (realish === resolved) 84 throw eloop(realish) 85 86 return realpathCached(resolved, rpcache, stcache, depth + 1) 87 }).then(real => { 88 rpcache.set(resolve(dir, base), real) 89 return real 90 }) 91 }) 92} 93 94module.exports = realpathCached 95