Skip to content

Commit

Permalink
add check case-insensitive paths to copySync, add tests for case-inse…
Browse files Browse the repository at this point in the history
…nsitive paths
  • Loading branch information
manidlou committed Apr 17, 2018
1 parent 188b0ef commit 662f139
Show file tree
Hide file tree
Showing 6 changed files with 320 additions and 54 deletions.
115 changes: 115 additions & 0 deletions 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)
}
})
})
})
Expand Up @@ -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')
Expand All @@ -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)
Expand Down Expand Up @@ -119,15 +123,19 @@ 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')

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)
Expand Down
Expand Up @@ -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')
Expand Down
114 changes: 68 additions & 46 deletions lib/copy-sync/copy-sync.js
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
}
Expand All @@ -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.
Expand All @@ -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

0 comments on commit 662f139

Please sign in to comment.