diff options
| author | Philipp Tanlak <philipp.tanlak@gmail.com> | 2025-11-24 20:54:57 +0100 |
|---|---|---|
| committer | Philipp Tanlak <philipp.tanlak@gmail.com> | 2025-11-24 20:57:48 +0100 |
| commit | b1e2c8fd5cb5dfa46bc440a12eafaf56cd844b1c (patch) | |
| tree | 49d360fd6cbc6a2754efe93524ac47ff0fbe0f7d /node_modules/fs-extra/lib/util/stat.js | |
Docs
Diffstat (limited to 'node_modules/fs-extra/lib/util/stat.js')
| -rw-r--r-- | node_modules/fs-extra/lib/util/stat.js | 154 |
1 files changed, 154 insertions, 0 deletions
diff --git a/node_modules/fs-extra/lib/util/stat.js b/node_modules/fs-extra/lib/util/stat.js new file mode 100644 index 0000000..0ed5aec --- /dev/null +++ b/node_modules/fs-extra/lib/util/stat.js @@ -0,0 +1,154 @@ +'use strict' + +const fs = require('../fs') +const path = require('path') +const util = require('util') + +function getStats (src, dest, opts) { + const statFunc = opts.dereference + ? (file) => fs.stat(file, { bigint: true }) + : (file) => fs.lstat(file, { bigint: true }) + return Promise.all([ + statFunc(src), + statFunc(dest).catch(err => { + if (err.code === 'ENOENT') return null + throw err + }) + ]).then(([srcStat, destStat]) => ({ srcStat, destStat })) +} + +function getStatsSync (src, dest, opts) { + let destStat + const statFunc = opts.dereference + ? (file) => fs.statSync(file, { bigint: true }) + : (file) => fs.lstatSync(file, { bigint: true }) + const srcStat = statFunc(src) + try { + destStat = statFunc(dest) + } catch (err) { + if (err.code === 'ENOENT') return { srcStat, destStat: null } + throw err + } + return { srcStat, destStat } +} + +function checkPaths (src, dest, funcName, opts, cb) { + util.callbackify(getStats)(src, dest, opts, (err, stats) => { + if (err) return cb(err) + const { srcStat, destStat } = stats + + if (destStat) { + if (areIdentical(srcStat, destStat)) { + const srcBaseName = path.basename(src) + const destBaseName = path.basename(dest) + if (funcName === 'move' && + srcBaseName !== destBaseName && + srcBaseName.toLowerCase() === destBaseName.toLowerCase()) { + return cb(null, { srcStat, destStat, isChangingCase: true }) + } + return cb(new Error('Source and destination must not be the same.')) + } + if (srcStat.isDirectory() && !destStat.isDirectory()) { + return cb(new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`)) + } + if (!srcStat.isDirectory() && destStat.isDirectory()) { + return cb(new Error(`Cannot overwrite directory '${dest}' with non-directory '${src}'.`)) + } + } + + if (srcStat.isDirectory() && isSrcSubdir(src, dest)) { + return cb(new Error(errMsg(src, dest, funcName))) + } + return cb(null, { srcStat, destStat }) + }) +} + +function checkPathsSync (src, dest, funcName, opts) { + const { srcStat, destStat } = getStatsSync(src, dest, opts) + + if (destStat) { + if (areIdentical(srcStat, destStat)) { + const srcBaseName = path.basename(src) + const destBaseName = path.basename(dest) + if (funcName === 'move' && + srcBaseName !== destBaseName && + srcBaseName.toLowerCase() === destBaseName.toLowerCase()) { + return { srcStat, destStat, isChangingCase: true } + } + throw new Error('Source and destination must not be the same.') + } + if (srcStat.isDirectory() && !destStat.isDirectory()) { + throw new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`) + } + if (!srcStat.isDirectory() && destStat.isDirectory()) { + throw new Error(`Cannot overwrite directory '${dest}' with non-directory '${src}'.`) + } + } + + 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() + fs.stat(destParent, { bigint: true }, (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) + }) +} + +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 = fs.statSync(destParent, { bigint: true }) + } 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) { + return destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev +} + +// 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, + areIdentical +} |