Repository: filerjs/filer (js)

Issue:
BUG: fs.readdir fails when passing options
The following code:
```javascript
fs.readdir('/somedir', {/* options */}, callback);
```
Throws the error:
`
Error: Uncaught TypeError: callback is not a function (http://localhost:1234/tests.e31bb0bc.js:17740)
    at commonjsGlobal.onerror (mocha.947ba722.js:32655:12)
`

According to the [node documentation](https://nodejs.org/api/fs.html#fs_fs_readdir_path_options_callback), fs.readdir and fs.promises.readdir should optionally accept an options object.

Pull request:
Fix issue #775
Adds regression test and closes #775 

Patch:
diff --git a/README.md b/README.md
index 4bd3ff3..67a76a6 100644
--- a/README.md
+++ b/README.md
@@ -849,7 +849,7 @@ fs.mkdtemp("/myDir/tmp", function (error, path) {
 });
 ```
 
-#### fs.readdir(path, callback)<a name="readdir"></a>
+#### fs.readdir(path, [options], callback)<a name="readdir"></a>
 
 Reads the contents of a directory. Asynchronous [readdir(3)](http://pubs.opengroup.org/onlinepubs/009695399/functions/readdir.html).
 Callback gets `(error, files)`, where `files` is an array containing the names of each directory entry (i.e., file, directory, link) in the directory, excluding `.` and `..`.
@@ -870,6 +870,12 @@ fs.readdir('/docs', function(err, files) {
 });
 ```
 
+Optionally accepts an options parameter, which can be either an encoding (e.g. "utf8") or an object with optional properties `encoding` and `withFileTypes`.
+
+The `encoding` property is a `string` which will determine the character encoding to use for the names of each directory entry. The `withFileTypes` property is a `boolean` which defaults to `false`. If `true`, this method will return an array of [fs.Dirent](https://nodejs.org/api/fs.html#fs_class_fs_dirent) objects.
+
+The `name` property on the [fs.Dirent](https://nodejs.org/api/fs.html#fs_class_fs_dirent) objects will be encoded using the specified character encoding.
+
 #### fs.close(fd, callback)<a name="close"></a>
 
 Closes a file descriptor. Asynchronous [close(2)](http://pubs.opengroup.org/onlinepubs/009695399/functions/close.html).
diff --git a/src/dirent.js b/src/dirent.js
new file mode 100644
index 0000000..76b3d49
--- /dev/null
+++ b/src/dirent.js
@@ -0,0 +1,12 @@
+'use strict';
+
+const Stats = require('./stats.js');
+
+function Dirent(path, fileNode, devName) {
+  this.constructor = Dirent;
+  Stats.call(this, path, fileNode, devName);
+}
+
+Dirent.prototype = Stats.prototype;
+
+module.exports = Dirent;
diff --git a/src/filesystem/implementation.js b/src/filesystem/implementation.js
index f58542f..fda0d8e 100644
--- a/src/filesystem/implementation.js
+++ b/src/filesystem/implementation.js
@@ -4,6 +4,7 @@ var dirname = Path.dirname;
 var basename = Path.basename;
 var isAbsolutePath = Path.isAbsolute;
 var shared = require('../shared.js');
+var async = require('../../lib/async.js');
 
 var Constants = require('../constants.js');
 var NODE_TYPE_FILE = Constants.NODE_TYPE_FILE;
@@ -35,6 +36,7 @@ var openFiles = require('../open-files.js');
 var OpenFileDescription = require('../open-file-description.js');
 var SuperNode = require('../super-node.js');
 var Node = require('../node.js');
+var Dirent = require('../dirent.js');
 var Stats = require('../stats.js');
 
 /**
@@ -1120,26 +1122,72 @@ function unlink_node(context, path, callback) {
   find_node(context, parentPath, read_directory_data);
 }
 
-function read_directory(context, path, callback) {
+function read_directory(context, path, options, callback) {
   path = normalize(path);
 
+  if (typeof options === 'function') {
+    callback = options;
+    options = {};
+  }
+  options = validate_directory_options(options);
+
   var directoryNode;
   var directoryData;
 
   function handle_directory_data(error, result) {
-    if(error) {
+    if (error) {
       callback(error);
     } else {
       directoryData = result;
       var files = Object.keys(directoryData);
-      callback(null, files);
+
+      if (options.encoding) {
+        var fileBuffers = files.map(function (file) {
+          return Buffer.from(file);
+        });
+
+        if (options.encoding === 'buffer') {
+          files = fileBuffers;
+        }
+        else {
+          files = fileBuffers.map(function (fileBuffer) {
+            return fileBuffer.toString(options.encoding);
+          });
+        }
+      }
+
+      if (options.withFileTypes) {
+        var dirEnts = [];
+
+        // eslint-disable-next-line no-inner-declarations
+        function to_dir_entry(file, callback) {
+          const filename = Buffer.from(file, options.encoding).toString();
+          const filepath = Path.join(path, filename);
+          get_dir_entry(context, filepath, function(error, dirEnt) {
+            if (error) {
+              callback(error);
+            }
+            dirEnt.name = file;
+            dirEnts.push(dirEnt);
+            callback();
+          });
+        }
+
+        async.eachSeries(files, to_dir_entry, function (error) {
+          callback(error, dirEnts);
+        });
+      }
+      
+      else {
+        callback(null, files);
+      }
     }
   }
 
   function read_directory_data(error, result) {
-    if(error) {
+    if (error) {
       callback(error);
-    } else if(result.type !== NODE_TYPE_DIRECTORY) {
+    } else if (result.type !== NODE_TYPE_DIRECTORY) {
       callback(new Errors.ENOTDIR(null, path));
     } else {
       directoryNode = result;
@@ -1150,6 +1198,30 @@ function read_directory(context, path, callback) {
   find_node(context, path, read_directory_data);
 }
 
+function get_dir_entry(context, path, callback) {
+  function check_result(error, result) {
+    if(error) {
+      callback(error);
+    } else {
+      var stats = new Dirent(path, result, context.name);
+      callback(null, stats);
+    }
+  }
+
+  lstat_file(context, path, check_result);
+}
+
+function validate_directory_options(options, enc) {
+  if (!options) {
+    options = { encoding: enc };
+  } else if (typeof options === 'function') {
+    options = { encoding: enc };
+  } else if (typeof options === 'string') {
+    options = { encoding: options };
+  }
+  return options;
+}
+
 function make_symbolic_link(context, srcpath, dstpath, callback) {
   dstpath = normalize(dstpath);
   var name = basename(dstpath);
@@ -2163,8 +2235,8 @@ function lseek(context, fd, offset, whence, callback) {
   }
 }
 
-function readdir(context, path, callback) {
-  read_directory(context, path, callback);
+function readdir(context, path, options, callback) {
+  read_directory(context, path, options, callback);
 }
 
 function toUnixTimestamp(time) {


Test patch:
diff --git a/tests/bugs/issue775.js b/tests/bugs/issue775.js
new file mode 100644
index 0000000..6ca5ead
--- /dev/null
+++ b/tests/bugs/issue775.js
@@ -0,0 +1,115 @@
+'use strict';
+var util = require('../lib/test-utils.js');
+var expect = require('chai').expect;
+
+describe('fs.readdir fails when passing options, issue775', function () {
+  beforeEach(util.setup);
+  afterEach(util.cleanup);
+
+  function setup(fs, dir, cb) {
+    fs.mkdir(dir, undefined, (err) => {
+      if (err) {
+        cb(err);
+      }
+      else {
+        fs.writeFile(dir + '/file', '', (err) => {
+          if (err) {
+            cb(err);
+          }
+          else {
+            fs.mkdir(dir + '/folder', (err) => {
+              if (err) {
+                cb(err);
+              }
+              else {
+                fs.symlink(dir + '/file', dir + '/symlink', (err) => {
+                  if (err) {
+                    cb(err);
+                  }
+                  else {
+                    cb();
+                  }
+                });
+              }
+            });
+          }
+        });
+      }
+    });
+  }
+
+  it('should create a directory, add a file, folder and symbolic link then call fs.readdir with buffer encoding', function (done) {
+    var fs = util.fs();
+    setup(fs, '/test_dir', (err) => {
+      if (err) {
+        done(err);
+      }
+      fs.readdir('/test_dir', 'buffer', (err, data) => {
+        if (err) {
+          done(err);
+        }
+        else {
+          expect(data).to.have.length(3);
+
+          expect(data[0].toString()).to.equal('file');
+          expect(data[1].toString()).to.equal('folder');
+          expect(data[2].toString()).to.equal('symlink');
+
+          done();
+        }
+      });
+    });
+  });
+
+  it('should create a directory, add a file, folder and symbolic link then call fs.readdir with withFileTypes and encoding options', function (done) {
+    var fs = util.fs();
+    setup(fs, '/test_dir', (err) => {
+      if (err) {
+        done(err);
+      }
+      fs.readdir('/test_dir', { encoding: 'base64', withFileTypes: true }, (err, data) => {
+        if (err) {
+          done(err);
+        }
+        else {
+          expect(data).to.have.length(3);
+
+          expect(Buffer.from(data[0].name, 'base64').toString()).to.equal('file');
+          expect(Buffer.from(data[1].name, 'base64').toString()).to.equal('folder');
+          expect(Buffer.from(data[2].name, 'base64').toString()).to.equal('symlink');
+
+          expect(data[0].isFile()).to.be.true;
+          expect(data[1].isDirectory()).to.be.true;
+          expect(data[2].isSymbolicLink()).to.be.true;
+
+          done();
+        }
+      });
+    });
+  });
+
+  it('should create a directory then call fs.readdir without options', function (done) {
+    var fs = util.fs();
+    setup(fs, '/test_dir', (err) => {
+      if (err) {
+        done(err);
+      }
+      else {
+        fs.readdir('/test_dir', (err, data) => {
+          if (err) {
+            done(err);
+          }
+          else {
+            expect(data).to.have.length(3);
+
+            expect(data[0]).to.equal('file');
+            expect(data[1]).to.equal('folder');
+            expect(data[2]).to.equal('symlink');
+
+            done();
+          }
+        });
+      }
+    });
+  });
+});
diff --git a/tests/index.js b/tests/index.js
index b1215c8..f1dcfb0 100644
--- a/tests/index.js
+++ b/tests/index.js
@@ -92,6 +92,7 @@ require('./bugs/issue270.js');
 require('./bugs/rename-dir-trailing-slash.js');
 require('./bugs/issue357.js');
 require('./bugs/issue773.js');
+require('./bugs/issue775.js');
 require('./bugs/issue776.js');
 
 // Sample code from README