stat.js
  1  'use strict'
  2  
  3  const fs = require('../fs')
  4  const path = require('path')
  5  const util = require('util')
  6  const atLeastNode = require('at-least-node')
  7  
  8  const nodeSupportsBigInt = atLeastNode('10.5.0')
  9  const stat = (file) => nodeSupportsBigInt ? fs.stat(file, { bigint: true }) : fs.stat(file)
 10  const statSync = (file) => nodeSupportsBigInt ? fs.statSync(file, { bigint: true }) : fs.statSync(file)
 11  
 12  function getStats (src, dest) {
 13    return Promise.all([
 14      stat(src),
 15      stat(dest).catch(err => {
 16        if (err.code === 'ENOENT') return null
 17        throw err
 18      })
 19    ]).then(([srcStat, destStat]) => ({ srcStat, destStat }))
 20  }
 21  
 22  function getStatsSync (src, dest) {
 23    let destStat
 24    const srcStat = statSync(src)
 25    try {
 26      destStat = statSync(dest)
 27    } catch (err) {
 28      if (err.code === 'ENOENT') return { srcStat, destStat: null }
 29      throw err
 30    }
 31    return { srcStat, destStat }
 32  }
 33  
 34  function checkPaths (src, dest, funcName, cb) {
 35    util.callbackify(getStats)(src, dest, (err, stats) => {
 36      if (err) return cb(err)
 37      const { srcStat, destStat } = stats
 38      if (destStat && areIdentical(srcStat, destStat)) {
 39        return cb(new Error('Source and destination must not be the same.'))
 40      }
 41      if (srcStat.isDirectory() && isSrcSubdir(src, dest)) {
 42        return cb(new Error(errMsg(src, dest, funcName)))
 43      }
 44      return cb(null, { srcStat, destStat })
 45    })
 46  }
 47  
 48  function checkPathsSync (src, dest, funcName) {
 49    const { srcStat, destStat } = getStatsSync(src, dest)
 50    if (destStat && areIdentical(srcStat, destStat)) {
 51      throw new Error('Source and destination must not be the same.')
 52    }
 53    if (srcStat.isDirectory() && isSrcSubdir(src, dest)) {
 54      throw new Error(errMsg(src, dest, funcName))
 55    }
 56    return { srcStat, destStat }
 57  }
 58  
 59  // recursively check if dest parent is a subdirectory of src.
 60  // It works for all file types including symlinks since it
 61  // checks the src and dest inodes. It starts from the deepest
 62  // parent and stops once it reaches the src parent or the root path.
 63  function checkParentPaths (src, srcStat, dest, funcName, cb) {
 64    const srcParent = path.resolve(path.dirname(src))
 65    const destParent = path.resolve(path.dirname(dest))
 66    if (destParent === srcParent || destParent === path.parse(destParent).root) return cb()
 67    const callback = (err, destStat) => {
 68      if (err) {
 69        if (err.code === 'ENOENT') return cb()
 70        return cb(err)
 71      }
 72      if (areIdentical(srcStat, destStat)) {
 73        return cb(new Error(errMsg(src, dest, funcName)))
 74      }
 75      return checkParentPaths(src, srcStat, destParent, funcName, cb)
 76    }
 77    if (nodeSupportsBigInt) fs.stat(destParent, { bigint: true }, callback)
 78    else fs.stat(destParent, callback)
 79  }
 80  
 81  function checkParentPathsSync (src, srcStat, dest, funcName) {
 82    const srcParent = path.resolve(path.dirname(src))
 83    const destParent = path.resolve(path.dirname(dest))
 84    if (destParent === srcParent || destParent === path.parse(destParent).root) return
 85    let destStat
 86    try {
 87      destStat = statSync(destParent)
 88    } catch (err) {
 89      if (err.code === 'ENOENT') return
 90      throw err
 91    }
 92    if (areIdentical(srcStat, destStat)) {
 93      throw new Error(errMsg(src, dest, funcName))
 94    }
 95    return checkParentPathsSync(src, srcStat, destParent, funcName)
 96  }
 97  
 98  function areIdentical (srcStat, destStat) {
 99    if (destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev) {
100      if (nodeSupportsBigInt || destStat.ino < Number.MAX_SAFE_INTEGER) {
101        // definitive answer
102        return true
103      }
104      // Use additional heuristics if we can't use 'bigint'.
105      // Different 'ino' could be represented the same if they are >= Number.MAX_SAFE_INTEGER
106      // See issue 657
107      if (destStat.size === srcStat.size &&
108          destStat.mode === srcStat.mode &&
109          destStat.nlink === srcStat.nlink &&
110          destStat.atimeMs === srcStat.atimeMs &&
111          destStat.mtimeMs === srcStat.mtimeMs &&
112          destStat.ctimeMs === srcStat.ctimeMs &&
113          destStat.birthtimeMs === srcStat.birthtimeMs) {
114        // heuristic answer
115        return true
116      }
117    }
118    return false
119  }
120  
121  // return true if dest is a subdir of src, otherwise false.
122  // It only checks the path strings.
123  function isSrcSubdir (src, dest) {
124    const srcArr = path.resolve(src).split(path.sep).filter(i => i)
125    const destArr = path.resolve(dest).split(path.sep).filter(i => i)
126    return srcArr.reduce((acc, cur, i) => acc && destArr[i] === cur, true)
127  }
128  
129  function errMsg (src, dest, funcName) {
130    return `Cannot ${funcName} '${src}' to a subdirectory of itself, '${dest}'.`
131  }
132  
133  module.exports = {
134    checkPaths,
135    checkPathsSync,
136    checkParentPaths,
137    checkParentPathsSync,
138    isSrcSubdir
139  }