140 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			140 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict'
 | |
| 
 | |
| const fs = require('../fs')
 | |
| const path = require('path')
 | |
| const util = require('util')
 | |
| const atLeastNode = require('at-least-node')
 | |
| 
 | |
| const nodeSupportsBigInt = atLeastNode('10.5.0')
 | |
| const stat = (file) => nodeSupportsBigInt ? fs.stat(file, { bigint: true }) : fs.stat(file)
 | |
| const statSync = (file) => nodeSupportsBigInt ? fs.statSync(file, { bigint: true }) : fs.statSync(file)
 | |
| 
 | |
| function getStats (src, dest) {
 | |
|   return Promise.all([
 | |
|     stat(src),
 | |
|     stat(dest).catch(err => {
 | |
|       if (err.code === 'ENOENT') return null
 | |
|       throw err
 | |
|     })
 | |
|   ]).then(([srcStat, destStat]) => ({ srcStat, destStat }))
 | |
| }
 | |
| 
 | |
| function getStatsSync (src, dest) {
 | |
|   let destStat
 | |
|   const srcStat = statSync(src)
 | |
|   try {
 | |
|     destStat = statSync(dest)
 | |
|   } catch (err) {
 | |
|     if (err.code === 'ENOENT') return { srcStat, destStat: null }
 | |
|     throw err
 | |
|   }
 | |
|   return { srcStat, destStat }
 | |
| }
 | |
| 
 | |
| function checkPaths (src, dest, funcName, cb) {
 | |
|   util.callbackify(getStats)(src, dest, (err, stats) => {
 | |
|     if (err) return cb(err)
 | |
|     const { srcStat, destStat } = stats
 | |
|     if (destStat && areIdentical(srcStat, destStat)) {
 | |
|       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 && areIdentical(srcStat, destStat)) {
 | |
|     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()
 | |
|   const callback = (err, destStat) => {
 | |
|     if (err) {
 | |
|       if (err.code === 'ENOENT') return cb()
 | |
|       return cb(err)
 | |
|     }
 | |
|     if (areIdentical(srcStat, destStat)) {
 | |
|       return cb(new Error(errMsg(src, dest, funcName)))
 | |
|     }
 | |
|     return checkParentPaths(src, srcStat, destParent, funcName, cb)
 | |
|   }
 | |
|   if (nodeSupportsBigInt) fs.stat(destParent, { bigint: true }, callback)
 | |
|   else fs.stat(destParent, callback)
 | |
| }
 | |
| 
 | |
| 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 {
 | |
|     destStat = statSync(destParent)
 | |
|   } catch (err) {
 | |
|     if (err.code === 'ENOENT') return
 | |
|     throw err
 | |
|   }
 | |
|   if (areIdentical(srcStat, destStat)) {
 | |
|     throw new Error(errMsg(src, dest, funcName))
 | |
|   }
 | |
|   return checkParentPathsSync(src, srcStat, destParent, funcName)
 | |
| }
 | |
| 
 | |
| function areIdentical (srcStat, destStat) {
 | |
|   if (destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev) {
 | |
|     if (nodeSupportsBigInt || destStat.ino < Number.MAX_SAFE_INTEGER) {
 | |
|       // definitive answer
 | |
|       return true
 | |
|     }
 | |
|     // Use additional heuristics if we can't use 'bigint'.
 | |
|     // Different 'ino' could be represented the same if they are >= Number.MAX_SAFE_INTEGER
 | |
|     // See issue 657
 | |
|     if (destStat.size === srcStat.size &&
 | |
|         destStat.mode === srcStat.mode &&
 | |
|         destStat.nlink === srcStat.nlink &&
 | |
|         destStat.atimeMs === srcStat.atimeMs &&
 | |
|         destStat.mtimeMs === srcStat.mtimeMs &&
 | |
|         destStat.ctimeMs === srcStat.ctimeMs &&
 | |
|         destStat.birthtimeMs === srcStat.birthtimeMs) {
 | |
|       // heuristic answer
 | |
|       return true
 | |
|     }
 | |
|   }
 | |
|   return false
 | |
| }
 | |
| 
 | |
| // 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
 | |
| }
 |