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 { lstat, readlink } = require('fs/promises') 9const { resolve, basename, dirname } = require('path') 10 11const realpathCached = (path, rpcache, stcache, depth) => { 12 // just a safety against extremely deep eloops 13 /* istanbul ignore next */ 14 if (depth > 2000) { 15 throw eloop(path) 16 } 17 18 path = resolve(path) 19 if (rpcache.has(path)) { 20 return Promise.resolve(rpcache.get(path)) 21 } 22 23 const dir = dirname(path) 24 const base = basename(path) 25 26 if (base && rpcache.has(dir)) { 27 return realpathChild(dir, base, rpcache, stcache, depth) 28 } 29 30 // if it's the root, then we know it's real 31 if (!base) { 32 rpcache.set(dir, dir) 33 return Promise.resolve(dir) 34 } 35 36 // the parent, what is that? 37 // find out, and then come back. 38 return realpathCached(dir, rpcache, stcache, depth + 1).then(() => 39 realpathCached(path, rpcache, stcache, depth + 1)) 40} 41 42const lstatCached = (path, stcache) => { 43 if (stcache.has(path)) { 44 return Promise.resolve(stcache.get(path)) 45 } 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 74 const realish = resolve(realdir, base) 75 return lstatCached(realish, stcache).then(st => { 76 if (!st.isSymbolicLink()) { 77 rpcache.set(resolve(dir, base), realish) 78 return realish 79 } 80 81 return readlink(realish).then(target => { 82 const resolved = resolve(realdir, target) 83 if (realish === resolved) { 84 throw eloop(realish) 85 } 86 87 return realpathCached(resolved, rpcache, stcache, depth + 1) 88 }).then(real => { 89 rpcache.set(resolve(dir, base), real) 90 return real 91 }) 92 }) 93} 94 95module.exports = realpathCached 96