diff --git a/lib/copy-sync/__tests__/copy-sync-case-insensitive-paths.test.js b/lib/copy-sync/__tests__/copy-sync-case-insensitive-paths.test.js new file mode 100644 index 00000000..5bc70e4a --- /dev/null +++ b/lib/copy-sync/__tests__/copy-sync-case-insensitive-paths.test.js @@ -0,0 +1,115 @@ +'use strict' + +const assert = require('assert') +const os = require('os') +const path = require('path') +const fs = require(process.cwd()) + +/* global beforeEach, afterEach, describe, it */ + +describe('+ copySync() - case insensitive paths', () => { + let TEST_DIR = '' + let src = '' + let dest = '' + + beforeEach(done => { + TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-sync-case-insensitive-paths') + fs.emptyDir(TEST_DIR, done) + }) + + afterEach(done => fs.remove(TEST_DIR, done)) + + describe('> when the source is a directory', () => { + it('should behave correctly based on the OS', () => { + src = path.join(TEST_DIR, 'srcdir') + fs.outputFileSync(path.join(src, 'subdir', 'file.txt'), 'some data') + dest = path.join(TEST_DIR, 'srcDir') + + try { + fs.copySync(src, dest) + } catch (err) { + if (os === 'darwin' || os === 'win32') { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + assert(fs.existsSync(src)) + assert(!fs.existsSync(dest)) + } + } + if (os === 'linux') { + assert(fs.existsSync(dest)) + assert.strictEqual(fs.readFileSync(path.join(dest, 'subdir', 'file.txt'), 'utf8'), 'some data') + } + }) + }) + + describe('> when the source is a file', () => { + it('should behave correctly based on the OS', () => { + src = path.join(TEST_DIR, 'srcfile') + fs.outputFileSync(src, 'some data') + dest = path.join(TEST_DIR, 'srcFile') + + try { + fs.copySync(src, dest) + } catch (err) { + if (os === 'darwin' || os === 'win32') { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + assert(fs.existsSync(src)) + assert(!fs.existsSync(dest)) + } + } + if (os === 'linux') { + assert(fs.existsSync(dest)) + assert.strictEqual(fs.readFileSync(dest, 'utf8'), 'some data') + } + }) + }) + + describe('> when the source is a symlink', () => { + it('should behave correctly based on the OS, symlink dir', () => { + src = path.join(TEST_DIR, 'srcdir') + fs.outputFileSync(path.join(src, 'subdir', 'file.txt'), 'some data') + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'dir') + dest = path.join(TEST_DIR, 'srcDir') + + try { + fs.copySync(src, dest) + } catch (err) { + if (os === 'darwin' || os === 'win32') { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + assert(fs.existsSync(src)) + assert(!fs.existsSync(dest)) + } + } + if (os === 'linux') { + assert(fs.existsSync(dest)) + assert.strictEqual(fs.readFileSync(path.join(dest, 'subdir', 'file.txt'), 'utf8'), 'some data') + const link = fs.readlinkSync(srcLink) + assert.strictEqual(link, dest) + } + }) + + it('should behave correctly based on the OS, symlink file', () => { + src = path.join(TEST_DIR, 'srcfile') + fs.outputFileSync(src, 'some data') + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'file') + dest = path.join(TEST_DIR, 'srcFile') + + try { + fs.copySync(src, dest) + } catch (err) { + if (os === 'darwin' || os === 'win32') { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + assert(fs.existsSync(src)) + assert(!fs.existsSync(dest)) + } + } + if (os === 'linux') { + assert(fs.existsSync(dest)) + assert.strictEqual(fs.readFileSync(dest, 'utf8'), 'some data') + const link = fs.readlinkSync(srcLink) + assert.strictEqual(link, dest) + } + }) + }) +}) diff --git a/lib/copy-sync/__tests__/copy-sync-prevent-copying-identical.test.js b/lib/copy-sync/__tests__/copy-sync-prevent-copying-identical.test.js index 1e0f5980..6dabcc47 100644 --- a/lib/copy-sync/__tests__/copy-sync-prevent-copying-identical.test.js +++ b/lib/copy-sync/__tests__/copy-sync-prevent-copying-identical.test.js @@ -37,7 +37,7 @@ describe('+ copySync() - prevent copying identical files and dirs', () => { describe('> when the source is a directory', () => { describe(`>> when src is regular and dest is a symlink that points to src`, () => { - it('should not copy and return', () => { + it('should error', () => { src = path.join(TEST_DIR, 'src') fs.mkdirsSync(src) const subdir = path.join(TEST_DIR, 'src', 'subdir') @@ -49,7 +49,11 @@ describe('+ copySync() - prevent copying identical files and dirs', () => { const oldlen = klawSync(src).length - fs.copySync(src, destLink) + try { + fs.copySync(src, destLink) + } catch (err) { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + } const newlen = klawSync(src).length assert.strictEqual(newlen, oldlen) @@ -119,7 +123,7 @@ describe('+ copySync() - prevent copying identical files and dirs', () => { describe('> when the source is a file', () => { describe(`>> when src is regular and dest is a symlink that points to src`, () => { - it('should not copy and return', () => { + it('should error', () => { src = path.join(TEST_DIR, 'src', 'somefile.txt') fs.ensureFileSync(src) fs.writeFileSync(src, 'some data') @@ -127,7 +131,11 @@ describe('+ copySync() - prevent copying identical files and dirs', () => { const destLink = path.join(TEST_DIR, 'dest-symlink') fs.symlinkSync(src, destLink, 'file') - fs.copySync(src, destLink) + try { + fs.copySync(src, destLink) + } catch (err) { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + } const link = fs.readlinkSync(destLink) assert.strictEqual(link, src) diff --git a/lib/copy-sync/__tests__/copy-sync-prevent-copying-into-itself.test.js b/lib/copy-sync/__tests__/copy-sync-prevent-copying-into-itself.test.js index 0c5773d4..e5178044 100644 --- a/lib/copy-sync/__tests__/copy-sync-prevent-copying-into-itself.test.js +++ b/lib/copy-sync/__tests__/copy-sync-prevent-copying-into-itself.test.js @@ -146,14 +146,18 @@ describe('+ copySync() - prevent copying into itself', () => { }) describe('>> when dest is a symlink', () => { - it('should not copy and return when dest points exactly to src', () => { + it('should error when dest points exactly to src', () => { const destLink = path.join(TEST_DIR, 'dest-symlink') fs.symlinkSync(src, destLink, 'dir') const srclenBefore = klawSync(src).length assert(srclenBefore > 2) - fs.copySync(src, destLink) + try { + fs.copySync(src, destLink) + } catch (err) { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + } const srclenAfter = klawSync(src).length assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change') diff --git a/lib/copy-sync/copy-sync.js b/lib/copy-sync/copy-sync.js index acfa4bc6..0f9f8b3f 100644 --- a/lib/copy-sync/copy-sync.js +++ b/lib/copy-sync/copy-sync.js @@ -23,42 +23,44 @@ function copySync (src, dest, opts) { see https://github.com/jprichardson/node-fs-extra/issues/269`) } - // don't allow src and dest to be the same - if (path.resolve(src) === path.resolve(dest)) throw new Error('Source and destination must not be the same.') + const resolvedDest = checkPaths(src, dest) if (opts.filter && !opts.filter(src, dest)) return const destParent = path.dirname(dest) if (!fs.existsSync(destParent)) mkdirpSync(destParent) - return startCopy(src, dest, opts) + return startCopy(resolvedDest, src, dest, opts) } -function startCopy (src, dest, opts) { +function startCopy (resolvedDest, src, dest, opts) { + // resovledDest is only truthy in the first call of startCopy. + // when copying directory items, startCopy is called recursively and + // resolvedDest is null, so we need to check paths in that case. + if (resolvedDest) return resumeCopy(resolvedDest, src, dest, opts) + const resolvedDestNested = checkPaths(src, dest) + return resumeCopy(resolvedDestNested, src, dest, opts) +} + +function resumeCopy (resolvedDest, src, dest, opts) { if (opts.filter && !opts.filter(src, dest)) return - return getStats(src, dest, opts) + return getStats(resolvedDest, src, dest, opts) } -function getStats (src, dest, opts) { +function getStats (resolvedDest, src, dest, opts) { const statSync = opts.dereference ? fs.statSync : fs.lstatSync const st = statSync(src) - if (st.isDirectory()) return onDir(st, src, dest, opts) + if (st.isDirectory()) return onDir(st, resolvedDest, src, dest, opts) else if (st.isFile() || st.isCharacterDevice() || - st.isBlockDevice()) return onFile(st, src, dest, opts) - else if (st.isSymbolicLink()) return onLink(src, dest, opts) + st.isBlockDevice()) return onFile(st, resolvedDest, src, dest, opts) + else if (st.isSymbolicLink()) return onLink(resolvedDest, src, dest, opts) } -function onFile (srcStat, src, dest, opts) { - const resolvedPath = checkDest(dest) - if (resolvedPath === notExist) { - return copyFile(srcStat, src, dest, opts) - } else if (resolvedPath === existsReg) { - return mayCopyFile(srcStat, src, dest, opts) - } else { - if (src === resolvedPath) return - return mayCopyFile(srcStat, src, dest, opts) - } +function onFile (srcStat, resolvedDest, src, dest, opts) { + if (resolvedDest === notExist) return copyFile(srcStat, src, dest, opts) + else if (resolvedDest === existsReg) return mayCopyFile(srcStat, src, dest, opts) + else return mayCopyFile(srcStat, src, dest, opts) } function mayCopyFile (srcStat, src, dest, opts) { @@ -102,20 +104,18 @@ function copyFileFallback (srcStat, src, dest, opts) { fs.closeSync(fdw) } -function onDir (srcStat, src, dest, opts) { - const resolvedPath = checkDest(dest) - if (resolvedPath === notExist) { +function onDir (srcStat, resolvedDest, src, dest, opts) { + if (resolvedDest === notExist) { if (isSrcSubdir(src, dest)) { throw new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`) } return mkDirAndCopy(srcStat, src, dest, opts) - } else if (resolvedPath === existsReg) { + } else if (resolvedDest === existsReg) { if (isSrcSubdir(src, dest)) { throw new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`) } return mayCopyDir(src, dest, opts) } else { - if (src === resolvedPath) return return copyDir(src, dest, opts) } } @@ -135,41 +135,51 @@ function mkDirAndCopy (srcStat, src, dest, opts) { function copyDir (src, dest, opts) { fs.readdirSync(src).forEach(item => { - startCopy(path.join(src, item), path.join(dest, item), opts) + startCopy(null, path.join(src, item), path.join(dest, item), opts) }) } -function onLink (src, dest, opts) { - let resolvedSrcPath = fs.readlinkSync(src) +function onLink (resolvedDest, src, dest, opts) { + let resolvedSrc = fs.readlinkSync(src) if (opts.dereference) { - resolvedSrcPath = path.resolve(process.cwd(), resolvedSrcPath) + resolvedSrc = path.resolve(process.cwd(), resolvedSrc) } - let resolvedDestPath = checkDest(dest) - if (resolvedDestPath === notExist || resolvedDestPath === existsReg) { + if (resolvedDest === notExist || resolvedDest === existsReg) { // if dest already exists, fs throws error anyway, // so no need to guard against it here. - return fs.symlinkSync(resolvedSrcPath, dest) + return fs.symlinkSync(resolvedSrc, dest) } else { if (opts.dereference) { - resolvedDestPath = path.resolve(process.cwd(), resolvedDestPath) + resolvedDest = path.resolve(process.cwd(), resolvedDest) } - if (resolvedDestPath === resolvedSrcPath) return + if (pathsAreIdentical(resolvedSrc, resolvedDest)) return // prevent copy if src is a subdir of dest since unlinking // dest in this case would result in removing src contents // and therefore a broken symlink would be created. - if (fs.statSync(dest).isDirectory() && isSrcSubdir(resolvedDestPath, resolvedSrcPath)) { - throw new Error(`Cannot overwrite '${resolvedDestPath}' with '${resolvedSrcPath}'.`) + if (fs.statSync(dest).isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) { + throw new Error(`Cannot overwrite '${resolvedDest}' with '${resolvedSrc}'.`) } - return copyLink(resolvedSrcPath, dest) + return copyLink(resolvedSrc, dest) } } -function copyLink (resolvedSrcPath, dest) { +function copyLink (resolvedSrc, dest) { fs.unlinkSync(dest) - return fs.symlinkSync(resolvedSrcPath, dest) + return fs.symlinkSync(resolvedSrc, dest) +} + +// return true if dest is a subdir of src, otherwise false. +// extract dest base dir and check if that is the same as src basename. +function isSrcSubdir (src, dest) { + const srcArray = path.resolve(src).split(path.sep) + const destArray = path.resolve(dest).split(path.sep) + + return srcArray.reduce((acc, current, i) => { + return acc && destArray[i] === current + }, true) } // check if dest exists and is a symlink. @@ -188,15 +198,27 @@ function checkDest (dest) { return resolvedPath // dest exists and is a symlink } -// return true if dest is a subdir of src, otherwise false. -// extract dest base dir and check if that is the same as src basename. -function isSrcSubdir (src, dest) { - const srcArray = path.resolve(src).split(path.sep) - const destArray = path.resolve(dest).split(path.sep) +function pathsAreIdentical (src, dest) { + const os = process.platform + const resolvedSrc = path.resolve(src) + const resolvedDest = path.resolve(dest) + // case-insensitive paths + if (os === 'darwin' || os === 'win32') { + return resolvedSrc.toLowerCase() === resolvedDest.toLowerCase() + } + return resolvedSrc === resolvedDest +} - return srcArray.reduce((acc, current, i) => { - return acc && destArray[i] === current - }, true) +function checkPaths (src, dest) { + const resolvedDest = checkDest(dest) + if (resolvedDest === notExist || resolvedDest === existsReg) { + if (pathsAreIdentical(src, dest)) throw new Error('Source and destination must not be the same.') + return resolvedDest + } else { + // check resolved dest path if dest is a symlink + if (pathsAreIdentical(src, resolvedDest)) throw new Error('Source and destination must not be the same.') + return resolvedDest + } } module.exports = copySync diff --git a/lib/copy/__tests__/copy-case-insensitive-paths.test.js b/lib/copy/__tests__/copy-case-insensitive-paths.test.js new file mode 100644 index 00000000..46d93cf7 --- /dev/null +++ b/lib/copy/__tests__/copy-case-insensitive-paths.test.js @@ -0,0 +1,115 @@ +'use strict' + +const assert = require('assert') +const os = require('os') +const path = require('path') +const fs = require(process.cwd()) + +/* global beforeEach, afterEach, describe, it */ + +describe('+ copy() - case insensitive paths', () => { + let TEST_DIR = '' + let src = '' + let dest = '' + + beforeEach(done => { + TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-case-insensitive-paths') + fs.emptyDir(TEST_DIR, done) + }) + + afterEach(done => fs.remove(TEST_DIR, done)) + + describe('> when the source is a directory', () => { + it('should behave correctly based on the OS', done => { + src = path.join(TEST_DIR, 'srcdir') + fs.outputFileSync(path.join(src, 'subdir', 'file.txt'), 'some data') + dest = path.join(TEST_DIR, 'srcDir') + + fs.copy(src, dest, err => { + if (os === 'linux') { + assert.ifError(err) + assert(fs.existsSync(dest)) + assert.strictEqual(fs.readFileSync(path.join(dest, 'subdir', 'file.txt'), 'utf8'), 'some data') + } + if (os === 'darwin' || os === 'win32') { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + assert(fs.existsSync(src)) + assert(!fs.existsSync(dest)) + } + done() + }) + }) + }) + + describe('> when the source is a file', () => { + it('should behave correctly based on the OS', done => { + src = path.join(TEST_DIR, 'srcfile') + fs.outputFileSync(src, 'some data') + dest = path.join(TEST_DIR, 'srcFile') + + fs.copy(src, dest, err => { + if (os === 'linux') { + assert.ifError(err) + assert(fs.existsSync(dest)) + assert.strictEqual(fs.readFileSync(dest, 'utf8'), 'some data') + } + if (os === 'darwin' || os === 'win32') { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + assert(fs.existsSync(src)) + assert(!fs.existsSync(dest)) + } + done() + }) + }) + }) + + describe('> when the source is a symlink', () => { + it('should behave correctly based on the OS, symlink dir', done => { + src = path.join(TEST_DIR, 'srcdir') + fs.outputFileSync(path.join(src, 'subdir', 'file.txt'), 'some data') + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'dir') + dest = path.join(TEST_DIR, 'srcDir') + + fs.copy(src, dest, err => { + if (os === 'linux') { + assert.ifError(err) + assert(fs.existsSync(dest)) + assert.strictEqual(fs.readFileSync(path.join(dest, 'subdir', 'file.txt'), 'utf8'), 'some data') + const link = fs.readlinkSync(srcLink) + assert.strictEqual(link, dest) + } + if (os === 'darwin' || os === 'win32') { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + assert(fs.existsSync(src)) + assert(!fs.existsSync(dest)) + } + done() + }) + }) + + it('should behave correctly based on the OS, symlink file', done => { + src = path.join(TEST_DIR, 'srcfile') + fs.outputFileSync(src, 'some data') + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'file') + dest = path.join(TEST_DIR, 'srcFile') + + fs.copy(src, dest, err => { + if (os === 'linux') { + assert.ifError(err) + assert(fs.existsSync(dest)) + assert.strictEqual(fs.readFileSync(dest, 'utf8'), 'some data') + const link = fs.readlinkSync(srcLink) + assert.strictEqual(link, dest) + } + if (os === 'darwin' || os === 'win32') { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + assert(fs.existsSync(src)) + assert(!fs.existsSync(dest)) + } + done() + }) + }) + }) +}) diff --git a/lib/copy/copy.js b/lib/copy/copy.js index dd16c8fc..d0c90135 100644 --- a/lib/copy/copy.js +++ b/lib/copy/copy.js @@ -49,8 +49,10 @@ function checkParentDir (resolvedDest, src, dest, opts, cb) { } function startCopy (resolvedDest, src, dest, opts, cb) { + // resovledDest is only truthy in the first call of startCopy. + // when copying directory items, startCopy is called recursively and + // resolvedDest is null, so we need to check paths in that case. if (resolvedDest) return resumeCopy(resolvedDest, src, dest, opts, cb) - // need to check paths when copying directory items return checkPaths(src, dest, (err, resolvedDest) => { if (err) return cb(err) return resumeCopy(resolvedDest, src, dest, opts, cb) @@ -253,7 +255,7 @@ function pathsAreIdentical (src, dest) { const os = process.platform const resolvedSrc = path.resolve(src) const resolvedDest = path.resolve(dest) - // case-insensitive systems + // case-insensitive paths if (os === 'darwin' || os === 'win32') { return resolvedSrc.toLowerCase() === resolvedDest.toLowerCase() }