'use strict' const fs = require('graceful-fs') const path = require('path') const NODE_VERSION_MAJOR_WITH_BIGINT = 10 const NODE_VERSION_MINOR_WITH_BIGINT = 5 const NODE_VERSION_PATCH_WITH_BIGINT = 0 const nodeVersion = process.versions.node.split('.') const nodeVersionMajor = Number.parseInt(nodeVersion[0], 10) const nodeVersionMinor = Number.parseInt(nodeVersion[1], 10) const nodeVersionPatch = Number.parseInt(nodeVersion[2], 10) function nodeSupportsBigInt () { if (nodeVersionMajor > NODE_VERSION_MAJOR_WITH_BIGINT) { return true } else if (nodeVersionMajor === NODE_VERSION_MAJOR_WITH_BIGINT) { if (nodeVersionMinor > NODE_VERSION_MINOR_WITH_BIGINT) { return true } else if (nodeVersionMinor === NODE_VERSION_MINOR_WITH_BIGINT) { if (nodeVersionPatch >= NODE_VERSION_PATCH_WITH_BIGINT) { return true } } } return false } function getStats (src, dest, cb) { if (nodeSupportsBigInt()) { fs.stat(src, { bigint: true }, (err, srcStat) => { if (err) return cb(err) fs.stat(dest, { bigint: true }, (err, destStat) => { if (err) { if (err.code === 'ENOENT') return cb(null, { srcStat, destStat: null }) return cb(err) } return cb(null, { srcStat, destStat }) }) }) } else { fs.stat(src, (err, srcStat) => { if (err) return cb(err) fs.stat(dest, (err, destStat) => { if (err) { if (err.code === 'ENOENT') return cb(null, { srcStat, destStat: null }) return cb(err) } return cb(null, { srcStat, destStat }) }) }) } } function getStatsSync (src, dest) { let srcStat, destStat if (nodeSupportsBigInt()) { srcStat = fs.statSync(src, { bigint: true }) } else { srcStat = fs.statSync(src) } try { if (nodeSupportsBigInt()) { destStat = fs.statSync(dest, { bigint: true }) } else { destStat = fs.statSync(dest) } } catch (err) { if (err.code === 'ENOENT') return { srcStat, destStat: null } throw err } return { srcStat, destStat } } function checkPaths (src, dest, funcName, cb) { getStats(src, dest, (err, stats) => { if (err) return cb(err) const { srcStat, destStat } = stats if (destStat && destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev) { return cb(new Error('Source and destination must not be the same.')) } if (srcStat.isDirectory() && isSrcSubdir(src, dest)) { return cb(new Error(errMsg(src, dest, funcName))) } return cb(null, { srcStat, destStat }) }) } function checkPathsSync (src, dest, funcName) { const { srcStat, destStat } = getStatsSync(src, dest) if (destStat && destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev) { throw new Error('Source and destination must not be the same.') } if (srcStat.isDirectory() && isSrcSubdir(src, dest)) { throw new Error(errMsg(src, dest, funcName)) } return { srcStat, destStat } } // recursively check if dest parent is a subdirectory of src. // It works for all file types including symlinks since it // checks the src and dest inodes. It starts from the deepest // parent and stops once it reaches the src parent or the root path. function checkParentPaths (src, srcStat, dest, funcName, cb) { const srcParent = path.resolve(path.dirname(src)) const destParent = path.resolve(path.dirname(dest)) if (destParent === srcParent || destParent === path.parse(destParent).root) return cb() if (nodeSupportsBigInt()) { fs.stat(destParent, { bigint: true }, (err, destStat) => { if (err) { if (err.code === 'ENOENT') return cb() return cb(err) } if (destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev) { return cb(new Error(errMsg(src, dest, funcName))) } return checkParentPaths(src, srcStat, destParent, funcName, cb) }) } else { fs.stat(destParent, (err, destStat) => { if (err) { if (err.code === 'ENOENT') return cb() return cb(err) } if (destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev) { return cb(new Error(errMsg(src, dest, funcName))) } return checkParentPaths(src, srcStat, destParent, funcName, cb) }) } } function checkParentPathsSync (src, srcStat, dest, funcName) { const srcParent = path.resolve(path.dirname(src)) const destParent = path.resolve(path.dirname(dest)) if (destParent === srcParent || destParent === path.parse(destParent).root) return let destStat try { if (nodeSupportsBigInt()) { destStat = fs.statSync(destParent, { bigint: true }) } else { destStat = fs.statSync(destParent) } } catch (err) { if (err.code === 'ENOENT') return throw err } if (destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev) { throw new Error(errMsg(src, dest, funcName)) } return checkParentPathsSync(src, srcStat, destParent, funcName) } // return true if dest is a subdir of src, otherwise false. // It only checks the path strings. function isSrcSubdir (src, dest) { const srcArr = path.resolve(src).split(path.sep).filter(i => i) const destArr = path.resolve(dest).split(path.sep).filter(i => i) return srcArr.reduce((acc, cur, i) => acc && destArr[i] === cur, true) } function errMsg (src, dest, funcName) { return `Cannot ${funcName} '${src}' to a subdirectory of itself, '${dest}'.` } module.exports = { checkPaths, checkPathsSync, checkParentPaths, checkParentPathsSync, isSrcSubdir }