'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
}