Repository: jprichardson/node-fs-extra
Pull request #587 (2018-05-30 17:45:49)

Show support for mode
Node fs's `mkdir` (https://nodejs.org/api/fs.html#fs_fs_mkdir_path_mode_callback) supports mode specification. After reviewing the source
`fs-extra` does as well, but is not documented. Update the documentation
to include the `options` parameter and provide a few examples of using
`mode`.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/17237558/badge)](https://coveralls.io/builds/17237558)

Coverage remained the same at 86.826% when pulling **5a63e26f502c14c5a4bba6641d57e44c3989fc13 on robertjacobson:mkdirs-doc-mode** into **3eceb915bfd4f9e14ecbe3ff1e7db4dec626afc3 on jprichardson:master**.


Patch:
diff --git a/.github/issue_template.md b/.github/issue_template.md
index e3d10eb..80d6921 100644
--- a/.github/issue_template.md
+++ b/.github/issue_template.md
@@ -1,4 +1,5 @@
-<!-- Please fill out the following information if it applies to your issue: -->
+<!-- First ensure you installed the latest version of fs-extra -->
+<!-- If your bug still exists please fill out the following information if it applies to your issue: -->
 - **Operating System:**
 - **Node.js version:**
 - **`fs-extra` version:**
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e163f06..57d0f4e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+7.0.0 / 2018-07-16
+------------------
+
+- **BREAKING:** Refine `copy*()` handling of symlinks to properly detect symlinks that point to the same file. ([#582](https://github.com/jprichardson/node-fs-extra/pull/582))
+- Fix bug with copying write-protected directories ([#600](https://github.com/jprichardson/node-fs-extra/pull/600))
+- Universalify `fs.lchmod()` ([#596](https://github.com/jprichardson/node-fs-extra/pull/596))
+- Add `engines` field to `package.json` ([#580](https://github.com/jprichardson/node-fs-extra/pull/580))
+
 6.0.1 / 2018-05-09
 ------------------
 
diff --git a/README.md b/README.md
index 63c4a29..f09f79f 100644
--- a/README.md
+++ b/README.md
@@ -114,6 +114,7 @@ Methods
 - [ensureDir](docs/ensureDir.md)
 - [ensureLink](docs/ensureLink.md)
 - [ensureSymlink](docs/ensureSymlink.md)
+- [mkdirp](docs/ensureDir.md)
 - [mkdirs](docs/ensureDir.md)
 - [move](docs/move.md)
 - [outputFile](docs/outputFile.md)
@@ -131,6 +132,7 @@ Methods
 - [ensureDirSync](docs/ensureDir-sync.md)
 - [ensureLinkSync](docs/ensureLink-sync.md)
 - [ensureSymlinkSync](docs/ensureSymlink-sync.md)
+- [mkdirpSync](docs/ensureDir-sync.md)
 - [mkdirsSync](docs/ensureDir-sync.md)
 - [moveSync](docs/move-sync.md)
 - [outputFileSync](docs/outputFile-sync.md)
diff --git a/docs/ensureDir-sync.md b/docs/ensureDir-sync.md
index 8f083d2..e03d37b 100644
--- a/docs/ensureDir-sync.md
+++ b/docs/ensureDir-sync.md
@@ -1,17 +1,29 @@
-# ensureDirSync(dir)
+# ensureDirSync(dir[,options])
 
-Ensures that the directory exists. If the directory structure does not exist, it is created. Like `mkdir -p`.
+Ensures that the directory exists. If the directory structure does not exist, it is created. Like `mkdir -p`. If provided, options may specify the desired mode for the directory. 
 
 **Aliases:** `mkdirsSync()`, `mkdirpSync()`
 
 - `dir` `<String>`
-
+- `options` `<Integer>|<Object>`
 ## Example:
 
 ```js
 const fs = require('fs-extra')
 
 const dir = '/tmp/this/path/does/not/exist'
+
+const desiredMode = 0o2775
+const options = {
+  mode: 0o2775
+}
+
 fs.ensureDirSync(dir)
 // dir has now been created, including the directory it is to be placed in
+
+fs.ensureDirSync(dir, desiredMod)
+// dir has now been created, including the directory it is to be placed in with permission 0o2775
+
+fs.ensureDirSync(dir, options)
+// dir has now been created, including the directory it is to be placed in with permission 0o2775
 ```
diff --git a/docs/ensureDir.md b/docs/ensureDir.md
index 0d43a14..85e9392 100644
--- a/docs/ensureDir.md
+++ b/docs/ensureDir.md
@@ -1,10 +1,11 @@
-# ensureDir(dir, [callback])
+# ensureDir(dir[,options][,callback])
 
 Ensures that the directory exists. If the directory structure does not exist, it is created. Like `mkdir -p`.
 
 **Aliases:** `mkdirs()`, `mkdirp()`
 
 - `dir` `<String>`
+- `options` `<Integer>|<Object>`
 - `callback` `<Function>`
 
 ## Example:
@@ -13,6 +14,10 @@ Ensures that the directory exists. If the directory structure does not exist, it
 const fs = require('fs-extra')
 
 const dir = '/tmp/this/path/does/not/exist'
+const desiredMode = 0o2775
+const options = {
+  mode: 0o2775
+}
 
 // With a callback:
 fs.ensureDir(dir, err => {
@@ -20,6 +25,12 @@ fs.ensureDir(dir, err => {
   // dir has now been created, including the directory it is to be placed in
 })
 
+// With a callback and a mode integer
+fs.ensureDir(dir, desiredMode, err => {
+  console.log(err) // => null
+  // dir has now been created with mode 0o2775, including the directory it is to be placed in
+})
+
 // With Promises:
 fs.ensureDir(dir)
 .then(() => {
@@ -29,6 +40,15 @@ fs.ensureDir(dir)
   console.error(err)
 })
 
+// With Promises and a mode integer:
+fs.ensureDir(dir, desiredMode)
+.then(() => {
+  console.log('success!')
+})
+.catch(err => {
+  console.error(err)
+})
+
 // With async/await:
 async function example (directory) {
   try {
@@ -38,6 +58,16 @@ async function example (directory) {
     console.error(err)
   }
 }
-
 example(dir)
+
+// With async/await and an options object, containing mode:
+async function exampleMode (directory) {
+  try {
+    await fs.ensureDir(directory, options)
+    console.log('success!')
+  } catch (err) {
+    console.error(err)
+  }
+}
+exampleMode(dir)
 ```
diff --git a/lib/copy-sync/copy-sync.js b/lib/copy-sync/copy-sync.js
index 201cbf2..14ad993 100644
--- a/lib/copy-sync/copy-sync.js
+++ b/lib/copy-sync/copy-sync.js
@@ -6,7 +6,6 @@ const mkdirpSync = require('../mkdirs').mkdirsSync
 const utimesSync = require('../util/utimes.js').utimesMillisSync
 
 const notExist = Symbol('notExist')
-const existsReg = Symbol('existsReg')
 
 function copySync (src, dest, opts) {
   if (typeof opts === 'function') {
@@ -23,34 +22,33 @@ function copySync (src, dest, opts) {
     see https://github.com/jprichardson/node-fs-extra/issues/269`)
   }
 
-  const resolvedDest = checkPaths(src, dest)
+  const destStat = checkPaths(src, dest)
 
   if (opts.filter && !opts.filter(src, dest)) return
 
   const destParent = path.dirname(dest)
   if (!fs.existsSync(destParent)) mkdirpSync(destParent)
-  return startCopy(resolvedDest, src, dest, opts)
+  return startCopy(destStat, src, dest, opts)
 }
 
-function startCopy (resolvedDest, src, dest, opts) {
+function startCopy (destStat, src, dest, opts) {
   if (opts.filter && !opts.filter(src, dest)) return
-  return getStats(resolvedDest, src, dest, opts)
+  return getStats(destStat, src, dest, opts)
 }
 
-function getStats (resolvedDest, src, dest, opts) {
+function getStats (destStat, src, dest, opts) {
   const statSync = opts.dereference ? fs.statSync : fs.lstatSync
-  const st = statSync(src)
+  const srcStat = statSync(src)
 
-  if (st.isDirectory()) return onDir(st, resolvedDest, src, dest, opts)
-  else if (st.isFile() ||
-           st.isCharacterDevice() ||
-           st.isBlockDevice()) return onFile(st, resolvedDest, src, dest, opts)
-  else if (st.isSymbolicLink()) return onLink(resolvedDest, src, dest, opts)
+  if (srcStat.isDirectory()) return onDir(srcStat, destStat, src, dest, opts)
+  else if (srcStat.isFile() ||
+           srcStat.isCharacterDevice() ||
+           srcStat.isBlockDevice()) return onFile(srcStat, destStat, src, dest, opts)
+  else if (srcStat.isSymbolicLink()) return onLink(destStat, 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)
+function onFile (srcStat, destStat, src, dest, opts) {
+  if (destStat === notExist) return copyFile(srcStat, src, dest, opts)
   return mayCopyFile(srcStat, src, dest, opts)
 }
 
@@ -95,32 +93,18 @@ function copyFileFallback (srcStat, src, dest, opts) {
   fs.closeSync(fdw)
 }
 
-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 (resolvedDest === existsReg) {
-    if (isSrcSubdir(src, dest)) {
-      throw new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`)
-    }
-    return mayCopyDir(src, dest, opts)
-  }
-  return copyDir(src, dest, opts)
-}
-
-function mayCopyDir (src, dest, opts) {
-  if (!fs.statSync(dest).isDirectory()) {
+function onDir (srcStat, destStat, src, dest, opts) {
+  if (destStat === notExist) return mkDirAndCopy(srcStat, src, dest, opts)
+  if (destStat && !destStat.isDirectory()) {
     throw new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`)
   }
   return copyDir(src, dest, opts)
 }
 
 function mkDirAndCopy (srcStat, src, dest, opts) {
-  fs.mkdirSync(dest, srcStat.mode)
-  fs.chmodSync(dest, srcStat.mode)
-  return copyDir(src, dest, opts)
+  fs.mkdirSync(dest)
+  copyDir(src, dest, opts)
+  return fs.chmodSync(dest, srcStat.mode)
 }
 
 function copyDir (src, dest, opts) {
@@ -130,26 +114,36 @@ function copyDir (src, dest, opts) {
 function copyDirItem (item, src, dest, opts) {
   const srcItem = path.join(src, item)
   const destItem = path.join(dest, item)
-  const resolvedDest = checkPaths(srcItem, destItem)
-  return startCopy(resolvedDest, srcItem, destItem, opts)
+  const destStat = checkPaths(srcItem, destItem)
+  return startCopy(destStat, srcItem, destItem, opts)
 }
 
-function onLink (resolvedDest, src, dest, opts) {
+function onLink (destStat, src, dest, opts) {
   let resolvedSrc = fs.readlinkSync(src)
 
   if (opts.dereference) {
     resolvedSrc = path.resolve(process.cwd(), resolvedSrc)
   }
 
-  if (resolvedDest === notExist || resolvedDest === existsReg) {
-    // if dest already exists, fs throws error anyway,
-    // so no need to guard against it here.
+  if (destStat === notExist) {
     return fs.symlinkSync(resolvedSrc, dest)
   } else {
+    let resolvedDest
+    try {
+      resolvedDest = fs.readlinkSync(dest)
+    } catch (err) {
+      // dest exists and is a regular file or directory,
+      // Windows may throw UNKNOWN error. If dest already exists,
+      // fs throws error anyway, so no need to guard against it here.
+      if (err.code === 'EINVAL' || err.code === 'UNKNOWN') return fs.symlinkSync(resolvedSrc, dest)
+      throw err
+    }
     if (opts.dereference) {
       resolvedDest = path.resolve(process.cwd(), resolvedDest)
     }
-    if (pathsAreIdentical(resolvedSrc, resolvedDest)) return
+    if (isSrcSubdir(resolvedSrc, resolvedDest)) {
+      throw new Error(`Cannot copy '${resolvedSrc}' to a subdirectory of itself, '${resolvedDest}'.`)
+    }
 
     // prevent copy if src is a subdir of dest since unlinking
     // dest in this case would result in removing src contents
@@ -167,53 +161,33 @@ function copyLink (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)
+  return srcArray.reduce((acc, current, i) => acc && destArray[i] === current, true)
 }
 
-// check if dest exists and is a symlink.
-function checkDest (dest) {
-  let resolvedPath
+function checkStats (src, dest) {
+  const srcStat = fs.statSync(src)
+  let destStat
   try {
-    resolvedPath = fs.readlinkSync(dest)
+    destStat = fs.statSync(dest)
   } catch (err) {
-    if (err.code === 'ENOENT') return notExist
-
-    // dest exists and is a regular file or directory, Windows may throw UNKNOWN error.
-    if (err.code === 'EINVAL' || err.code === 'UNKNOWN') return existsReg
-
+    if (err.code === 'ENOENT') return {srcStat, destStat: notExist}
     throw err
   }
-  return resolvedPath // dest exists and is a symlink
-}
-
-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 {srcStat, destStat}
 }
 
 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
+  const {srcStat, destStat} = checkStats(src, dest)
+  if (destStat.ino && destStat.ino === srcStat.ino) {
+    throw new Error('Source and destination must not be the same.')
+  }
+  if (srcStat.isDirectory() && isSrcSubdir(src, dest)) {
+    throw new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`)
   }
+  return destStat
 }
 
 module.exports = copySync
diff --git a/lib/copy/copy.js b/lib/copy/copy.js
index 51d8fe8..3dfbc54 100644
--- a/lib/copy/copy.js
+++ b/lib/copy/copy.js
@@ -7,7 +7,6 @@ const pathExists = require('../path-exists').pathExists
 const utimes = require('../util/utimes').utimesMillis
 
 const notExist = Symbol('notExist')
-const existsReg = Symbol('existsReg')
 
 function copy (src, dest, opts, cb) {
   if (typeof opts === 'function' && !cb) {
@@ -29,56 +28,55 @@ function copy (src, dest, opts, cb) {
     see https://github.com/jprichardson/node-fs-extra/issues/269`)
   }
 
-  checkPaths(src, dest, (err, resolvedDest) => {
+  checkPaths(src, dest, (err, destStat) => {
     if (err) return cb(err)
-    if (opts.filter) return handleFilter(checkParentDir, resolvedDest, src, dest, opts, cb)
-    return checkParentDir(resolvedDest, src, dest, opts, cb)
+    if (opts.filter) return handleFilter(checkParentDir, destStat, src, dest, opts, cb)
+    return checkParentDir(destStat, src, dest, opts, cb)
   })
 }
 
-function checkParentDir (resolvedDest, src, dest, opts, cb) {
+function checkParentDir (destStat, src, dest, opts, cb) {
   const destParent = path.dirname(dest)
   pathExists(destParent, (err, dirExists) => {
     if (err) return cb(err)
-    if (dirExists) return startCopy(resolvedDest, src, dest, opts, cb)
+    if (dirExists) return startCopy(destStat, src, dest, opts, cb)
     mkdirp(destParent, err => {
       if (err) return cb(err)
-      return startCopy(resolvedDest, src, dest, opts, cb)
+      return startCopy(destStat, src, dest, opts, cb)
     })
   })
 }
 
-function startCopy (resolvedDest, src, dest, opts, cb) {
-  if (opts.filter) return handleFilter(getStats, resolvedDest, src, dest, opts, cb)
-  return getStats(resolvedDest, src, dest, opts, cb)
-}
-
-function handleFilter (onInclude, resolvedDest, src, dest, opts, cb) {
+function handleFilter (onInclude, destStat, src, dest, opts, cb) {
   Promise.resolve(opts.filter(src, dest)).then(include => {
     if (include) {
-      if (resolvedDest) return onInclude(resolvedDest, src, dest, opts, cb)
+      if (destStat) return onInclude(destStat, src, dest, opts, cb)
       return onInclude(src, dest, opts, cb)
     }
     return cb()
   }, error => cb(error))
 }
 
-function getStats (resolvedDest, src, dest, opts, cb) {
+function startCopy (destStat, src, dest, opts, cb) {
+  if (opts.filter) return handleFilter(getStats, destStat, src, dest, opts, cb)
+  return getStats(destStat, src, dest, opts, cb)
+}
+
+function getStats (destStat, src, dest, opts, cb) {
   const stat = opts.dereference ? fs.stat : fs.lstat
-  stat(src, (err, st) => {
+  stat(src, (err, srcStat) => {
     if (err) return cb(err)
 
-    if (st.isDirectory()) return onDir(st, resolvedDest, src, dest, opts, cb)
-    else if (st.isFile() ||
-             st.isCharacterDevice() ||
-             st.isBlockDevice()) return onFile(st, resolvedDest, src, dest, opts, cb)
-    else if (st.isSymbolicLink()) return onLink(resolvedDest, src, dest, opts, cb)
+    if (srcStat.isDirectory()) return onDir(srcStat, destStat, src, dest, opts, cb)
+    else if (srcStat.isFile() ||
+             srcStat.isCharacterDevice() ||
+             srcStat.isBlockDevice()) return onFile(srcStat, destStat, src, dest, opts, cb)
+    else if (srcStat.isSymbolicLink()) return onLink(destStat, src, dest, opts, cb)
   })
 }
 
-function onFile (srcStat, resolvedDest, src, dest, opts, cb) {
-  if (resolvedDest === notExist) return copyFile(srcStat, src, dest, opts, cb)
-  else if (resolvedDest === existsReg) return mayCopyFile(srcStat, src, dest, opts, cb)
+function onFile (srcStat, destStat, src, dest, opts, cb) {
+  if (destStat === notExist) return copyFile(srcStat, src, dest, opts, cb)
   return mayCopyFile(srcStat, src, dest, opts, cb)
 }
 
@@ -123,37 +121,20 @@ function setDestModeAndTimestamps (srcStat, dest, opts, cb) {
   })
 }
 
-function onDir (srcStat, resolvedDest, src, dest, opts, cb) {
-  if (resolvedDest === notExist) {
-    if (isSrcSubdir(src, dest)) {
-      return cb(new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`))
-    }
-    return mkDirAndCopy(srcStat, src, dest, opts, cb)
-  } else if (resolvedDest === existsReg) {
-    if (isSrcSubdir(src, dest)) {
-      return cb(new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`))
-    }
-    return mayCopyDir(src, dest, opts, cb)
+function onDir (srcStat, destStat, src, dest, opts, cb) {
+  if (destStat === notExist) return mkDirAndCopy(srcStat, src, dest, opts, cb)
+  if (destStat && !destStat.isDirectory()) {
+    return cb(new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`))
   }
   return copyDir(src, dest, opts, cb)
 }
 
-function mayCopyDir (src, dest, opts, cb) {
-  fs.stat(dest, (err, st) => {
-    if (err) return cb(err)
-    if (!st.isDirectory()) {
-      return cb(new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`))
-    }
-    return copyDir(src, dest, opts, cb)
-  })
-}
-
 function mkDirAndCopy (srcStat, src, dest, opts, cb) {
-  fs.mkdir(dest, srcStat.mode, err => {
+  fs.mkdir(dest, err => {
     if (err) return cb(err)
-    fs.chmod(dest, srcStat.mode, err => {
+    copyDir(src, dest, opts, err => {
       if (err) return cb(err)
-      return copyDir(src, dest, opts, cb)
+      return fs.chmod(dest, srcStat.mode, cb)
     })
   })
 }
@@ -174,16 +155,16 @@ function copyDirItems (items, src, dest, opts, cb) {
 function copyDirItem (items, item, src, dest, opts, cb) {
   const srcItem = path.join(src, item)
   const destItem = path.join(dest, item)
-  checkPaths(srcItem, destItem, (err, resolvedDest) => {
+  checkPaths(srcItem, destItem, (err, destStat) => {
     if (err) return cb(err)
-    startCopy(resolvedDest, srcItem, destItem, opts, err => {
+    startCopy(destStat, srcItem, destItem, opts, err => {
       if (err) return cb(err)
       return copyDirItems(items, src, dest, opts, cb)
     })
   })
 }
 
-function onLink (resolvedDest, src, dest, opts, cb) {
+function onLink (destStat, src, dest, opts, cb) {
   fs.readlink(src, (err, resolvedSrc) => {
     if (err) return cb(err)
 
@@ -191,22 +172,28 @@ function onLink (resolvedDest, src, dest, opts, cb) {
       resolvedSrc = path.resolve(process.cwd(), resolvedSrc)
     }
 
-    if (resolvedDest === notExist || resolvedDest === existsReg) {
-      // if dest already exists, fs throws error anyway,
-      // so no need to guard against it here.
+    if (destStat === notExist) {
       return fs.symlink(resolvedSrc, dest, cb)
     } else {
-      if (opts.dereference) {
-        resolvedDest = path.resolve(process.cwd(), resolvedDest)
-      }
-      if (pathsAreIdentical(resolvedSrc, resolvedDest)) return cb()
+      fs.readlink(dest, (err, resolvedDest) => {
+        if (err) {
+          // dest exists and is a regular file or directory,
+          // Windows may throw UNKNOWN error. If dest already exists,
+          // fs throws error anyway, so no need to guard against it here.
+          if (err.code === 'EINVAL' || err.code === 'UNKNOWN') return fs.symlink(resolvedSrc, dest, cb)
+          return cb(err)
+        }
+        if (opts.dereference) {
+          resolvedDest = path.resolve(process.cwd(), resolvedDest)
+        }
+        if (isSrcSubdir(resolvedSrc, resolvedDest)) {
+          return cb(new Error(`Cannot copy '${resolvedSrc}' to a subdirectory of itself, '${resolvedDest}'.`))
+        }
 
-      // 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.
-      fs.stat(dest, (err, st) => {
-        if (err) return cb(err)
-        if (st.isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) {
+        // do not 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 (destStat.isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) {
           return cb(new Error(`Cannot overwrite '${resolvedDest}' with '${resolvedSrc}'.`))
         }
         return copyLink(resolvedSrc, dest, cb)
@@ -223,53 +210,36 @@ function copyLink (resolvedSrc, dest, cb) {
 }
 
 // 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)
+  return srcArray.reduce((acc, current, i) => acc && destArray[i] === current, true)
 }
 
-// check if dest exists and is a symlink.
-function checkDest (dest, cb) {
-  fs.readlink(dest, (err, resolvedPath) => {
-    if (err) {
-      if (err.code === 'ENOENT') return cb(null, notExist)
-
-      // dest exists and is a regular file or directory, Windows may throw UNKNOWN error.
-      if (err.code === 'EINVAL' || err.code === 'UNKNOWN') return cb(null, existsReg)
-
-      return cb(err)
-    }
-    return cb(null, resolvedPath) // dest exists and is a symlink
+function checkStats (src, dest, cb) {
+  fs.stat(src, (err, srcStat) => {
+    if (err) return cb(err)
+    fs.stat(dest, (err, destStat) => {
+      if (err) {
+        if (err.code === 'ENOENT') return cb(null, {srcStat, destStat: notExist})
+        return cb(err)
+      }
+      return cb(null, {srcStat, destStat})
+    })
   })
 }
 
-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
-}
-
 function checkPaths (src, dest, cb) {
-  checkDest(dest, (err, resolvedDest) => {
+  checkStats(src, dest, (err, stats) => {
     if (err) return cb(err)
-    if (resolvedDest === notExist || resolvedDest === existsReg) {
-      if (pathsAreIdentical(src, dest)) return cb(new Error('Source and destination must not be the same.'))
-      return cb(null, resolvedDest)
-    } else {
-      // check resolved dest path if dest is a symlink
-      if (pathsAreIdentical(src, resolvedDest)) return cb(new Error('Source and destination must not be the same.'))
-      return cb(null, resolvedDest)
+    const {srcStat, destStat} = stats
+    if (destStat.ino && destStat.ino === srcStat.ino) {
+      return cb(new Error('Source and destination must not be the same.'))
+    }
+    if (srcStat.isDirectory() && isSrcSubdir(src, dest)) {
+      return cb(new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`))
     }
+    return cb(null, destStat)
   })
 }
 
diff --git a/lib/fs/index.js b/lib/fs/index.js
index d99f768..8ffdf7b 100644
--- a/lib/fs/index.js
+++ b/lib/fs/index.js
@@ -19,6 +19,7 @@ const api = [
   'ftruncate',
   'futimes',
   'lchown',
+  'lchmod',
   'link',
   'lstat',
   'mkdir',
diff --git a/package.json b/package.json
index 28f6978..3d4db47 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "fs-extra",
-  "version": "6.0.1",
+  "version": "7.0.0",
   "description": "fs-extra contains methods that aren't included in the vanilla Node.js fs package. Such as mkdir -p, cp -r, and rm -rf.",
   "engines": {
     "node": ">=6 <7 || >=8"


Test patch:
diff --git a/lib/copy-sync/__tests__/broken-symlink.test.js b/lib/copy-sync/__tests__/broken-symlink.test.js
index 3def54b..2744c64 100644
--- a/lib/copy-sync/__tests__/broken-symlink.test.js
+++ b/lib/copy-sync/__tests__/broken-symlink.test.js
@@ -23,9 +23,8 @@ describe('copy-sync / broken symlink', () => {
 
   afterEach(done => fse.remove(TEST_DIR, done))
 
-  it('should copy broken symlinks by default', () => {
-    assert.doesNotThrow(() => copySync(src, out))
-    assert.strictEqual(fs.readlinkSync(path.join(out, 'broken-symlink')), path.join(src, 'does-not-exist'))
+  it('should error if symlink is broken', () => {
+    assert.throws(() => copySync(src, out))
   })
 
   it('should throw an error when dereference=true', () => {
diff --git a/lib/copy-sync/__tests__/copy-sync-dir.test.js b/lib/copy-sync/__tests__/copy-sync-dir.test.js
index 9c32673..7ce6e98 100644
--- a/lib/copy-sync/__tests__/copy-sync-dir.test.js
+++ b/lib/copy-sync/__tests__/copy-sync-dir.test.js
@@ -6,9 +6,9 @@ const path = require('path')
 const assert = require('assert')
 const crypto = require('crypto')
 
-/* global beforeEach, describe, it */
+/* global beforeEach, afterEach, describe, it */
 
-describe('+ copySync()', () => {
+describe('+ copySync() / dir', () => {
   const SIZE = 16 * 64 * 1024 + 7
   let TEST_DIR
   let src, dest
@@ -20,6 +20,8 @@ describe('+ copySync()', () => {
     fs.emptyDir(TEST_DIR, done)
   })
 
+  afterEach(done => fs.remove(TEST_DIR, done))
+
   describe('> when src is a directory', () => {
     describe('> when dest exists and is a file', () => {
       it('should throw error', () => {
@@ -70,13 +72,15 @@ describe('+ copySync()', () => {
     })
 
     it('should preserve symbolic links', () => {
+      const srcTarget = path.join(TEST_DIR, 'destination')
       fs.mkdirSync(src)
-      fs.symlinkSync('destination', path.join(src, 'symlink'))
+      fs.mkdirSync(srcTarget)
+      fs.symlinkSync(srcTarget, path.join(src, 'symlink'))
 
       fs.copySync(src, dest)
 
       const link = fs.readlinkSync(path.join(dest, 'symlink'))
-      assert.strictEqual(link, 'destination')
+      assert.strictEqual(link, srcTarget)
     })
 
     describe('> when the destination dir does not exist', () => {
@@ -183,14 +187,9 @@ describe('+ copySync()', () => {
       const dest = path.join(TEST_DIR, 'dest')
 
       setTimeout(() => {
-        fs.mkdirSync(src)
-        fs.writeFileSync(path.join(src, 'somefile.html'), 'some data')
+        fs.outputFileSync(path.join(src, 'somefile.html'), 'some data')
         fs.mkdirSync(dest)
-        try {
-          fs.copySync(src, dest, filter)
-        } catch (err) {
-          assert.ifError(err)
-        }
+        fs.copySync(src, dest, filter)
         assert(!fs.existsSync(path.join(dest, 'somefile.html')))
         done()
       }, 1000)
diff --git a/lib/copy-sync/__tests__/copy-sync-file.test.js b/lib/copy-sync/__tests__/copy-sync-file.test.js
index fd58c1c..45ef334 100644
--- a/lib/copy-sync/__tests__/copy-sync-file.test.js
+++ b/lib/copy-sync/__tests__/copy-sync-file.test.js
@@ -10,11 +10,11 @@ const crypto = require('crypto')
 
 const SIZE = 16 * 64 * 1024 + 7
 
-describe('+ copySync()', () => {
+describe('+ copySync() / file', () => {
   let TEST_DIR
 
   beforeEach(done => {
-    TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-sync')
+    TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-sync-file')
     fs.emptyDir(TEST_DIR, done)
   })
 
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 056ca8a..d9dca5e 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
@@ -23,6 +23,8 @@ describe('+ copySync() - prevent copying identical files and dirs', () => {
   it('should return an error if src and dest are the same', () => {
     const fileSrc = path.join(TEST_DIR, 'TEST_fs-extra_copy_sync')
     const fileDest = path.join(TEST_DIR, 'TEST_fs-extra_copy_sync')
+    fs.ensureFileSync(fileSrc)
+
     try {
       fs.copySync(fileSrc, fileDest)
     } catch (err) {
@@ -30,6 +32,80 @@ describe('+ copySync() - prevent copying identical files and dirs', () => {
     }
   })
 
+  describe('dest with parent symlink', () => {
+    describe('first parent is symlink', () => {
+      it('should error when src is file', () => {
+        const src = path.join(TEST_DIR, 'a', 'file.txt')
+        const dest = path.join(TEST_DIR, 'b', 'file.txt')
+        const srcParent = path.join(TEST_DIR, 'a')
+        const destParent = path.join(TEST_DIR, 'b')
+        fs.ensureFileSync(src)
+        fs.ensureSymlinkSync(srcParent, destParent, 'dir')
+
+        try {
+          fs.copySync(src, dest)
+        } catch (err) {
+          assert.strictEqual(err.message, 'Source and destination must not be the same.')
+        } finally {
+          assert(fs.existsSync(src))
+        }
+      })
+
+      it('should error when src is directory', () => {
+        const src = path.join(TEST_DIR, 'a', 'foo')
+        const dest = path.join(TEST_DIR, 'b', 'foo')
+        const srcParent = path.join(TEST_DIR, 'a')
+        const destParent = path.join(TEST_DIR, 'b')
+        fs.ensureDirSync(src)
+        fs.ensureSymlinkSync(srcParent, destParent, 'dir')
+
+        try {
+          fs.copySync(src, dest)
+        } catch (err) {
+          assert.strictEqual(err.message, 'Source and destination must not be the same.')
+        } finally {
+          assert(fs.existsSync(src))
+        }
+      })
+    })
+
+    describe('nested dest', () => {
+      it('should error when src is file', () => {
+        const src = path.join(TEST_DIR, 'a', 'dir', 'file.txt')
+        const dest = path.join(TEST_DIR, 'b', 'dir', 'file.txt')
+        const srcParent = path.join(TEST_DIR, 'a')
+        const destParent = path.join(TEST_DIR, 'b')
+        fs.ensureFileSync(src)
+        fs.ensureSymlinkSync(srcParent, destParent, 'dir')
+
+        try {
+          fs.copySync(src, dest)
+        } catch (err) {
+          assert.strictEqual(err.message, 'Source and destination must not be the same.')
+        } finally {
+          assert(fs.existsSync(src))
+        }
+      })
+
+      it('should error when src is directory', () => {
+        const src = path.join(TEST_DIR, 'a', 'dir', 'foo')
+        const dest = path.join(TEST_DIR, 'b', 'dir', 'foo')
+        const srcParent = path.join(TEST_DIR, 'a')
+        const destParent = path.join(TEST_DIR, 'b')
+        fs.ensureDirSync(src)
+        fs.ensureSymlinkSync(srcParent, destParent, 'dir')
+
+        try {
+          fs.copySync(src, dest)
+        } catch (err) {
+          assert.strictEqual(err.message, 'Source and destination must not be the same.')
+        } finally {
+          assert(fs.existsSync(src))
+        }
+      })
+    })
+  })
+
   // src is directory:
   //  src is regular, dest is symlink
   //  src is symlink, dest is regular
@@ -90,7 +166,7 @@ describe('+ copySync() - prevent copying identical files and dirs', () => {
     })
 
     describe('>> when src and dest are symlinks that point to the exact same path', () => {
-      it('should not copy and return', () => {
+      it('should error src and dest are the same', () => {
         src = path.join(TEST_DIR, 'src')
         fs.mkdirsSync(src)
         const srcLink = path.join(TEST_DIR, 'src_symlink')
@@ -101,7 +177,11 @@ describe('+ copySync() - prevent copying identical files and dirs', () => {
         const srclenBefore = klawSync(srcLink).length
         const destlenBefore = klawSync(destLink).length
 
-        fs.copySync(srcLink, destLink)
+        try {
+          fs.copySync(srcLink, destLink)
+        } catch (err) {
+          assert.strictEqual(err.message, 'Source and destination must not be the same.')
+        }
 
         const srclenAfter = klawSync(srcLink).length
         assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change')
@@ -146,8 +226,7 @@ describe('+ copySync() - prevent copying identical files and dirs', () => {
     describe(`>> when src is a symlink that points to a regular dest`, () => {
       it('should throw error', () => {
         dest = path.join(TEST_DIR, 'dest', 'somefile.txt')
-        fs.ensureFileSync(dest)
-        fs.writeFileSync(dest, 'some data')
+        fs.outputFileSync(dest, 'some data')
 
         const srcLink = path.join(TEST_DIR, 'src-symlink')
         fs.symlinkSync(dest, srcLink, 'file')
@@ -164,10 +243,9 @@ describe('+ copySync() - prevent copying identical files and dirs', () => {
     })
 
     describe('>> when src and dest are symlinks that point to the exact same path', () => {
-      it('should not copy and return', () => {
+      it('should error src and dest are the same', () => {
         src = path.join(TEST_DIR, 'src', 'srcfile.txt')
-        fs.ensureFileSync(src)
-        fs.writeFileSync(src, 'src data')
+        fs.outputFileSync(src, 'src data')
 
         const srcLink = path.join(TEST_DIR, 'src_symlink')
         fs.symlinkSync(src, srcLink, 'file')
@@ -175,7 +253,11 @@ describe('+ copySync() - prevent copying identical files and dirs', () => {
         const destLink = path.join(TEST_DIR, 'dest_symlink')
         fs.symlinkSync(src, destLink, 'file')
 
-        fs.copySync(srcLink, destLink)
+        try {
+          fs.copySync(srcLink, destLink)
+        } catch (err) {
+          assert.strictEqual(err.message, 'Source and destination must not be the same.')
+        }
 
         const srcln = fs.readlinkSync(srcLink)
         assert.strictEqual(srcln, 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 e517804..487c997 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
@@ -39,7 +39,7 @@ describe('+ copySync() - prevent copying into itself', () => {
   afterEach(done => fs.remove(TEST_DIR, done))
 
   describe('> when source is a file', () => {
-    it('should copy the file successfully even when dest parent is a subdir of src', () => {
+    it('should copy the file successfully even if dest parent is a subdir of src', () => {
       const srcFile = path.join(TEST_DIR, 'src', 'srcfile.txt')
       const destFile = path.join(TEST_DIR, 'src', 'dest', 'destfile.txt')
       fs.writeFileSync(srcFile, dat0)
@@ -273,7 +273,7 @@ describe('+ copySync() - prevent copying into itself', () => {
     })
 
     describe('>> when dest is a symlink', () => {
-      it('should silently return when resolved dest path is exactly the same as resolved src path', () => {
+      it('should error when resolved dest path is exactly the same as resolved src path', () => {
         const srcLink = path.join(TEST_DIR, 'src-symlink')
         fs.symlinkSync(src, srcLink, 'dir')
         const destLink = path.join(TEST_DIR, 'dest-symlink')
@@ -284,16 +284,21 @@ describe('+ copySync() - prevent copying into itself', () => {
         assert(srclenBefore > 2)
         assert(destlenBefore > 2)
 
-        fs.copySync(srcLink, destLink)
-        const srclenAfter = klawSync(srcLink).length
-        assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change')
-        const destlenAfter = klawSync(destLink).length
-        assert.strictEqual(destlenAfter, destlenBefore, 'dest length should not change')
-
-        const srcln = fs.readlinkSync(srcLink)
-        assert.strictEqual(srcln, src)
-        const destln = fs.readlinkSync(destLink)
-        assert.strictEqual(destln, src)
+        try {
+          fs.copySync(srcLink, destLink)
+        } catch (err) {
+          assert.strictEqual(err.message, 'Source and destination must not be the same.')
+        } finally {
+          const srclenAfter = klawSync(srcLink).length
+          assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change')
+          const destlenAfter = klawSync(destLink).length
+          assert.strictEqual(destlenAfter, destlenBefore, 'dest length should not change')
+
+          const srcln = fs.readlinkSync(srcLink)
+          assert.strictEqual(srcln, src)
+          const destln = fs.readlinkSync(destLink)
+          assert.strictEqual(destln, src)
+        }
       })
 
       it('should error when resolved dest path is a subdir of resolved src path', () => {
@@ -309,10 +314,11 @@ describe('+ copySync() - prevent copying into itself', () => {
         try {
           fs.copySync(srcLink, destLink)
         } catch (err) {
-          assert(err)
+          assert.strictEqual(err.message, `Cannot copy '${src}' to a subdirectory of itself, '${resolvedDestPath}'.`)
+        } finally {
+          const destln = fs.readlinkSync(destLink)
+          assert.strictEqual(destln, resolvedDestPath)
         }
-        const destln = fs.readlinkSync(destLink)
-        assert.strictEqual(destln, src)
       })
 
       it('should error when resolved src path is a subdir of resolved dest path', () => {
@@ -322,18 +328,18 @@ describe('+ copySync() - prevent copying into itself', () => {
 
         const dest = path.join(TEST_DIR, 'dest')
 
-        fs.mkdirSync(dest)
-
-        fs.symlinkSync(srcInDest, srcLink, 'dir')
-        fs.symlinkSync(dest, destLink, 'dir')
+        fs.ensureDirSync(srcInDest)
+        fs.ensureSymlinkSync(srcInDest, srcLink, 'dir')
+        fs.ensureSymlinkSync(dest, destLink, 'dir')
 
         try {
           fs.copySync(srcLink, destLink)
         } catch (err) {
           assert.strictEqual(err.message, `Cannot overwrite '${dest}' with '${srcInDest}'.`)
+        } finally {
+          const destln = fs.readlinkSync(destLink)
+          assert.strictEqual(destln, dest)
         }
-        const destln = fs.readlinkSync(destLink)
-        assert.strictEqual(destln, dest)
       })
     })
   })
diff --git a/lib/copy-sync/__tests__/copy-sync-readonly-dir.test.js b/lib/copy-sync/__tests__/copy-sync-readonly-dir.test.js
new file mode 100644
index 0000000..b564804
--- /dev/null
+++ b/lib/copy-sync/__tests__/copy-sync-readonly-dir.test.js
@@ -0,0 +1,52 @@
+'use strict'
+
+// relevant: https://github.com/jprichardson/node-fs-extra/issues/599
+
+const fs = require(process.cwd())
+const os = require('os')
+const fse = require('../../')
+const path = require('path')
+const assert = require('assert')
+const klawSync = require('klaw-sync')
+
+/* global afterEach, beforeEach, describe, it */
+
+let TEST_DIR = ''
+
+const FILES = [
+  path.join('dir1', 'file1.txt'),
+  path.join('dir1', 'dir2', 'file2.txt'),
+  path.join('dir1', 'dir2', 'dir3', 'file3.txt')
+]
+
+describe('+ copySync() - copy a readonly directory with content', () => {
+  beforeEach(done => {
+    TEST_DIR = path.join(os.tmpdir(), 'test', 'fs-extra', 'copy-readonly-dir')
+    fse.emptyDir(TEST_DIR, done)
+  })
+
+  afterEach(done => {
+    klawSync(TEST_DIR).forEach(data => fs.chmodSync(data.path, 0o777))
+    fse.remove(TEST_DIR, done)
+  })
+
+  describe('> when src is readonly directory with content', () => {
+    it('should copy successfully', () => {
+      FILES.forEach(file => {
+        fs.outputFileSync(path.join(TEST_DIR, file), file)
+      })
+      const sourceDir = path.join(TEST_DIR, 'dir1')
+      const sourceHierarchy = klawSync(sourceDir)
+      sourceHierarchy.forEach(source => fs.chmodSync(source.path, source.stats.isDirectory() ? 0o555 : 0o444))
+
+      const targetDir = path.join(TEST_DIR, 'target')
+      fse.copySync(sourceDir, targetDir)
+
+      // Make sure copy was made and mode was preserved
+      assert(fs.existsSync(targetDir))
+      const targetHierarchy = klawSync(targetDir)
+      assert(targetHierarchy.length === sourceHierarchy.length)
+      targetHierarchy.forEach(target => assert(target.stats.mode === target.stats.isDirectory() ? 0o555 : 0o444))
+    })
+  })
+})
diff --git a/lib/copy/__tests__/copy-prevent-copying-identical.test.js b/lib/copy/__tests__/copy-prevent-copying-identical.test.js
index d0a67e9..b2aa211 100644
--- a/lib/copy/__tests__/copy-prevent-copying-identical.test.js
+++ b/lib/copy/__tests__/copy-prevent-copying-identical.test.js
@@ -23,6 +23,7 @@ describe('+ copy() - prevent copying identical files and dirs', () => {
   it('should return an error if src and dest are the same', done => {
     const fileSrc = path.join(TEST_DIR, 'TEST_fs-extra_copy')
     const fileDest = path.join(TEST_DIR, 'TEST_fs-extra_copy')
+    fs.ensureFileSync(fileSrc)
 
     fs.copy(fileSrc, fileDest, err => {
       assert.strictEqual(err.message, 'Source and destination must not be the same.')
@@ -30,6 +31,72 @@ describe('+ copy() - prevent copying identical files and dirs', () => {
     })
   })
 
+  describe('dest with parent symlink', () => {
+    describe('first parent is symlink', () => {
+      it('should error when src is file', done => {
+        const src = path.join(TEST_DIR, 'a', 'file.txt')
+        const dest = path.join(TEST_DIR, 'b', 'file.txt')
+        const srcParent = path.join(TEST_DIR, 'a')
+        const destParent = path.join(TEST_DIR, 'b')
+        fs.ensureFileSync(src)
+        fs.ensureSymlinkSync(srcParent, destParent, 'dir')
+
+        fs.copy(src, dest, err => {
+          assert.strictEqual(err.message, 'Source and destination must not be the same.')
+          assert(fs.existsSync(src))
+          done()
+        })
+      })
+
+      it('should error when src is directory', done => {
+        const src = path.join(TEST_DIR, 'a', 'foo')
+        const dest = path.join(TEST_DIR, 'b', 'foo')
+        const srcParent = path.join(TEST_DIR, 'a')
+        const destParent = path.join(TEST_DIR, 'b')
+        fs.ensureDirSync(src)
+        fs.ensureSymlinkSync(srcParent, destParent, 'dir')
+
+        fs.copy(src, dest, err => {
+          assert.strictEqual(err.message, 'Source and destination must not be the same.')
+          assert(fs.existsSync(src))
+          done()
+        })
+      })
+    })
+
+    describe('nested dest', () => {
+      it('should error when src is file', done => {
+        const src = path.join(TEST_DIR, 'a', 'dir', 'file.txt')
+        const dest = path.join(TEST_DIR, 'b', 'dir', 'file.txt')
+        const srcParent = path.join(TEST_DIR, 'a')
+        const destParent = path.join(TEST_DIR, 'b')
+        fs.ensureFileSync(src)
+        fs.ensureSymlinkSync(srcParent, destParent, 'dir')
+
+        fs.copy(src, dest, err => {
+          assert.strictEqual(err.message, 'Source and destination must not be the same.')
+          assert(fs.existsSync(src))
+          done()
+        })
+      })
+
+      it('should error when src is directory', done => {
+        const src = path.join(TEST_DIR, 'a', 'dir', 'foo')
+        const dest = path.join(TEST_DIR, 'b', 'dir', 'foo')
+        const srcParent = path.join(TEST_DIR, 'a')
+        const destParent = path.join(TEST_DIR, 'b')
+        fs.ensureDirSync(src)
+        fs.ensureSymlinkSync(srcParent, destParent, 'dir')
+
+        fs.copy(src, dest, err => {
+          assert.strictEqual(err.message, 'Source and destination must not be the same.')
+          assert(fs.existsSync(src))
+          done()
+        })
+      })
+    })
+  })
+
   // src is directory:
   //  src is regular, dest is symlink
   //  src is symlink, dest is regular
@@ -88,7 +155,7 @@ describe('+ copy() - prevent copying identical files and dirs', () => {
     })
 
     describe('>> when src and dest are symlinks that point to the exact same path', () => {
-      it('should not copy and return', done => {
+      it('should error src and dest are the same', done => {
         src = path.join(TEST_DIR, 'src')
         fs.mkdirsSync(src)
         const srcLink = path.join(TEST_DIR, 'src_symlink')
@@ -100,7 +167,7 @@ describe('+ copy() - prevent copying identical files and dirs', () => {
         const destlenBefore = klawSync(destLink).length
 
         fs.copy(srcLink, destLink, err => {
-          assert.ifError(err)
+          assert.strictEqual(err.message, 'Source and destination must not be the same.')
 
           const srclenAfter = klawSync(srcLink).length
           assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change')
@@ -125,19 +192,14 @@ describe('+ copy() - prevent copying identical files and dirs', () => {
   describe('> when src is a file', () => {
     describe(`>> when src is regular and dest is a symlink that points to src`, () => {
       it('should error', done => {
-        src = path.join(TEST_DIR, 'src', 'somefile.txt')
-        fs.ensureFileSync(src)
-        fs.writeFileSync(src, 'some data')
+        src = path.join(TEST_DIR, 'src.txt')
+        fs.outputFileSync(src, 'some data')
 
         const destLink = path.join(TEST_DIR, 'dest-symlink')
         fs.symlinkSync(src, destLink, 'file')
 
         fs.copy(src, destLink, err => {
           assert.strictEqual(err.message, 'Source and destination must not be the same.')
-
-          const link = fs.readlinkSync(destLink)
-          assert.strictEqual(link, src)
-          assert(fs.readFileSync(link, 'utf8'), 'some data')
           done()
         })
       })
@@ -146,14 +208,13 @@ describe('+ copy() - prevent copying identical files and dirs', () => {
     describe(`>> when src is a symlink that points to a regular dest`, () => {
       it('should throw error', done => {
         dest = path.join(TEST_DIR, 'dest', 'somefile.txt')
-        fs.ensureFileSync(dest)
-        fs.writeFileSync(dest, 'some data')
+        fs.outputFileSync(dest, 'some data')
 
         const srcLink = path.join(TEST_DIR, 'src-symlink')
         fs.symlinkSync(dest, srcLink, 'file')
 
         fs.copy(srcLink, dest, err => {
-          assert.ok(err)
+          assert.strictEqual(err.message, 'Source and destination must not be the same.')
 
           const link = fs.readlinkSync(srcLink)
           assert.strictEqual(link, dest)
@@ -164,10 +225,9 @@ describe('+ copy() - prevent copying identical files and dirs', () => {
     })
 
     describe('>> when src and dest are symlinks that point to the exact same path', () => {
-      it('should not copy and return', done => {
+      it('should error src and dest are the same', done => {
         src = path.join(TEST_DIR, 'src', 'srcfile.txt')
-        fs.ensureFileSync(src)
-        fs.writeFileSync(src, 'src data')
+        fs.outputFileSync(src, 'src data')
 
         const srcLink = path.join(TEST_DIR, 'src_symlink')
         fs.symlinkSync(src, srcLink, 'file')
@@ -176,7 +236,7 @@ describe('+ copy() - prevent copying identical files and dirs', () => {
         fs.symlinkSync(src, destLink, 'file')
 
         fs.copy(srcLink, destLink, err => {
-          assert.ifError(err)
+          assert.strictEqual(err.message, 'Source and destination must not be the same.')
 
           const srcln = fs.readlinkSync(srcLink)
           assert.strictEqual(srcln, src)
diff --git a/lib/copy/__tests__/copy-prevent-copying-into-itself.test.js b/lib/copy/__tests__/copy-prevent-copying-into-itself.test.js
index 60a7592..a3b944f 100644
--- a/lib/copy/__tests__/copy-prevent-copying-into-itself.test.js
+++ b/lib/copy/__tests__/copy-prevent-copying-into-itself.test.js
@@ -39,7 +39,7 @@ describe('+ copy() - prevent copying into itself', () => {
   afterEach(done => fs.remove(TEST_DIR, done))
 
   describe('> when source is a file', () => {
-    it('should copy the file successfully even when dest parent is a subdir of src', done => {
+    it('should copy the file successfully even if dest parent is a subdir of src', done => {
       const srcFile = path.join(TEST_DIR, 'src', 'srcfile.txt')
       const destFile = path.join(TEST_DIR, 'src', 'dest', 'destfile.txt')
       fs.writeFileSync(srcFile, dat0)
@@ -279,7 +279,7 @@ describe('+ copy() - prevent copying into itself', () => {
     })
 
     describe('>> when dest is a symlink', () => {
-      it('should silently return when resolved dest path is exactly the same as resolved src path', done => {
+      it('should error when resolved dest path is exactly the same as resolved src path', done => {
         const srcLink = path.join(TEST_DIR, 'src-symlink')
         fs.symlinkSync(src, srcLink, 'dir')
         const destLink = path.join(TEST_DIR, 'dest-symlink')
@@ -291,7 +291,7 @@ describe('+ copy() - prevent copying into itself', () => {
         assert(destlenBefore > 2)
 
         fs.copy(srcLink, destLink, err => {
-          assert.ifError(err)
+          assert.strictEqual(err.message, 'Source and destination must not be the same.')
 
           const srclenAfter = klawSync(srcLink).length
           assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change')
@@ -313,13 +313,12 @@ describe('+ copy() - prevent copying into itself', () => {
         const destLink = path.join(TEST_DIR, 'dest-symlink')
         const resolvedDestPath = path.join(TEST_DIR, 'src', 'some', 'nested', 'dest')
         fs.ensureFileSync(path.join(resolvedDestPath, 'subdir', 'somefile.txt'))
-
         fs.symlinkSync(resolvedDestPath, destLink, 'dir')
 
         fs.copy(srcLink, destLink, err => {
-          assert.ifError(err)
+          assert.strictEqual(err.message, `Cannot copy '${src}' to a subdirectory of itself, '${resolvedDestPath}'.`)
           const destln = fs.readlinkSync(destLink)
-          assert.strictEqual(destln, src)
+          assert.strictEqual(destln, resolvedDestPath)
           done()
         })
       })
@@ -328,13 +327,11 @@ describe('+ copy() - prevent copying into itself', () => {
         const srcInDest = path.join(TEST_DIR, 'dest', 'some', 'nested', 'src')
         const srcLink = path.join(TEST_DIR, 'src-symlink')
         const destLink = path.join(TEST_DIR, 'dest-symlink')
-
         const dest = path.join(TEST_DIR, 'dest')
 
-        fs.mkdirSync(dest)
-
-        fs.symlinkSync(srcInDest, srcLink, 'dir')
-        fs.symlinkSync(dest, destLink, 'dir')
+        fs.ensureDirSync(srcInDest)
+        fs.ensureSymlinkSync(srcInDest, srcLink, 'dir')
+        fs.ensureSymlinkSync(dest, destLink, 'dir')
 
         fs.copy(srcLink, destLink, err => {
           assert.strictEqual(err.message, `Cannot overwrite '${dest}' with '${srcInDest}'.`)
diff --git a/lib/copy/__tests__/copy-readonly-dir.test.js b/lib/copy/__tests__/copy-readonly-dir.test.js
new file mode 100644
index 0000000..99a3d9a
--- /dev/null
+++ b/lib/copy/__tests__/copy-readonly-dir.test.js
@@ -0,0 +1,56 @@
+'use strict'
+
+// relevant: https://github.com/jprichardson/node-fs-extra/issues/599
+
+const fs = require(process.cwd())
+const os = require('os')
+const fse = require('../../')
+const path = require('path')
+const assert = require('assert')
+const klawSync = require('klaw-sync')
+
+/* global afterEach, beforeEach, describe, it */
+
+let TEST_DIR = ''
+
+const FILES = [
+  path.join('dir1', 'file1.txt'),
+  path.join('dir1', 'dir2', 'file2.txt'),
+  path.join('dir1', 'dir2', 'dir3', 'file3.txt')
+]
+
+describe('+ copy() - copy a readonly directory with content', () => {
+  beforeEach(done => {
+    TEST_DIR = path.join(os.tmpdir(), 'test', 'fs-extra', 'copy-readonly-dir')
+    fse.emptyDir(TEST_DIR, done)
+  })
+
+  afterEach(done => {
+    klawSync(TEST_DIR).forEach(data => fs.chmodSync(data.path, 0o777))
+    fse.remove(TEST_DIR, done)
+  })
+
+  describe('> when src is readonly directory with content', () => {
+    it('should copy successfully', done => {
+      FILES.forEach(file => {
+        fs.outputFileSync(path.join(TEST_DIR, file), file)
+      })
+
+      const sourceDir = path.join(TEST_DIR, 'dir1')
+      const sourceHierarchy = klawSync(sourceDir)
+      sourceHierarchy.forEach(source => fs.chmodSync(source.path, source.stats.isDirectory() ? 0o555 : 0o444))
+
+      const targetDir = path.join(TEST_DIR, 'target')
+      fse.copy(sourceDir, targetDir, err => {
+        assert.ifError(err)
+
+        // Make sure copy was made and mode was preserved
+        assert(fs.existsSync(targetDir))
+        const targetHierarchy = klawSync(targetDir)
+        assert(targetHierarchy.length === sourceHierarchy.length)
+        targetHierarchy.forEach(target => assert(target.stats.mode === target.stats.isDirectory() ? 0o555 : 0o444))
+        done()
+      })
+    })
+  })
+})
diff --git a/lib/copy/__tests__/copy.test.js b/lib/copy/__tests__/copy.test.js
index db7a014..23d8086 100644
--- a/lib/copy/__tests__/copy.test.js
+++ b/lib/copy/__tests__/copy.test.js
@@ -24,6 +24,8 @@ describe('fs-extra', () => {
     it('should return an error if src and dest are the same', done => {
       const fileSrc = path.join(TEST_DIR, 'TEST_fs-extra_copy')
       const fileDest = path.join(TEST_DIR, 'TEST_fs-extra_copy')
+      fse.ensureFileSync(fileSrc)
+
       fse.copy(fileSrc, fileDest, err => {
         assert.strictEqual(err.message, 'Source and destination must not be the same.')
         done()
@@ -136,6 +138,22 @@ describe('fs-extra', () => {
         })
       })
 
+      it('should preserve symbolic links', done => {
+        const src = path.join(TEST_DIR, 'src')
+        const dest = path.join(TEST_DIR, 'dest')
+        const srcTarget = path.join(TEST_DIR, 'destination')
+        fse.mkdirSync(src)
+        fse.mkdirSync(srcTarget)
+        fse.symlinkSync(srcTarget, path.join(src, 'symlink'))
+
+        fse.copy(src, dest, err => {
+          assert.ifError(err)
+          const link = fs.readlinkSync(path.join(dest, 'symlink'))
+          assert.strictEqual(link, srcTarget)
+          done()
+        })
+      })
+
       it('should copy the directory asynchronously', done => {
         const FILES = 2
         const src = path.join(TEST_DIR, 'src')
diff --git a/lib/copy/__tests__/ncp/broken-symlink.test.js b/lib/copy/__tests__/ncp/broken-symlink.test.js
index e837491..461fa85 100644
--- a/lib/copy/__tests__/ncp/broken-symlink.test.js
+++ b/lib/copy/__tests__/ncp/broken-symlink.test.js
@@ -23,10 +23,9 @@ describe('ncp broken symlink', () => {
 
   afterEach(done => fse.remove(TEST_DIR, done))
 
-  it('should copy broken symlinks by default', done => {
+  it('should error if symlink is broken', done => {
     ncp(src, out, err => {
-      if (err) return done(err)
-      assert.strictEqual(fs.readlinkSync(path.join(out, 'broken-symlink')), path.join(src, 'does-not-exist'))
+      assert(err)
       done()
     })
   })
diff --git a/lib/json/__tests__/output-json-sync.test.js b/lib/json/__tests__/output-json-sync.test.js
index 44629dd..a4bc8c5 100644
--- a/lib/json/__tests__/output-json-sync.test.js
+++ b/lib/json/__tests__/output-json-sync.test.js
@@ -13,7 +13,7 @@ describe('json', () => {
   let TEST_DIR
 
   beforeEach(done => {
-    TEST_DIR = path.join(os.tmpdir(), 'fs-extra')
+    TEST_DIR = path.join(os.tmpdir(), 'fs-extra-output-json-sync')
     fse.emptyDir(TEST_DIR, done)
   })
 
diff --git a/lib/json/__tests__/output-json.test.js b/lib/json/__tests__/output-json.test.js
index 6668fa4..f69108a 100644
--- a/lib/json/__tests__/output-json.test.js
+++ b/lib/json/__tests__/output-json.test.js
@@ -13,7 +13,7 @@ describe('json', () => {
   let TEST_DIR
 
   beforeEach(done => {
-    TEST_DIR = path.join(os.tmpdir(), 'fs-extra')
+    TEST_DIR = path.join(os.tmpdir(), 'fs-extra-output-json')
     fse.emptyDir(TEST_DIR, done)
   })