From c88b3fc3683fc9ee481548e7018dd183975b2f4b Mon Sep 17 00:00:00 2001 From: Chunpeng Huo Date: Sat, 2 Feb 2019 21:23:04 +1100 Subject: [PATCH 1/7] chore: add nodejs v11 to test matrix, remove v5 and v7 --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 45b59bde..75f10e52 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,11 +3,10 @@ sudo: false language: node_js node_js: - "4" - - "5" - "6" - - "7" - "8" - "10" + - "11" before_install: - if [[ `npm -v` == 2.* ]]; then npm install --global npm@3; fi From 564b9c7ab37cefaab5bf8f14cc0efd5a486c01cb Mon Sep 17 00:00:00 2001 From: Chunpeng Huo Date: Fri, 1 Feb 2019 14:12:10 +1100 Subject: [PATCH 2/7] feat: support nodejs v10+ --- lib/binding.js | 365 +++++++++++++++++++++++++++++------------ lib/error.js | 307 +++++++++++++++++++++------------- lib/index.js | 41 +++++ test/lib/index.spec.js | 29 +++- 4 files changed, 525 insertions(+), 217 deletions(-) diff --git a/lib/binding.js b/lib/binding.js index a6d2e4f7..5951c5a2 100644 --- a/lib/binding.js +++ b/lib/binding.js @@ -37,11 +37,12 @@ var MAX_LINKS = 50; * Call the provided function and either return the result or call the callback * with it (depending on if a callback is provided). * @param {function()} callback Optional callback. + * @param {Object} ctx Context object (optional), only for nodejs v10+. * @param {Object} thisArg This argument for the following function. * @param {function()} func Function to call. * @return {*} Return (if callback is not provided). */ -function maybeCallback(callback, thisArg, func) { +function maybeCallback(callback, ctx, thisArg, func) { if (callback && typeof callback === 'function') { var err = null; var val; @@ -57,11 +58,29 @@ function maybeCallback(callback, thisArg, func) { callback(err, val); } }); + } else if (ctx && typeof ctx === 'object') { + try { + return func.call(thisArg); + } catch (e) { + ctx.code = e.code; + ctx.errno = e.errno || -1; + } } else { return func.call(thisArg); } } +/** + * set syscall property on context object, only for nodejs v10+. + * @param {Object} ctx Context object (optional), only for nodejs v10+. + * @param {String} syscall Name of syscall. + */ +function markSyscall(ctx, syscall) { + if (ctx && typeof ctx === 'object') { + ctx.syscall = syscall; + } +} + /** * Handle FSReqWrap oncomplete. * @param {Function} callback The callback. @@ -174,6 +193,19 @@ Stats.prototype.isSocket = function() { return this._checkModeProperty(constants.S_IFSOCK); }; +// I don't know exactly what is going on. +// If _openFiles is a property of binding instance, there is a strange +// bug in nodejs v10+ that something cleaned up this._openFiles from +// nowhere. It happens after second mockfs(), after first mockfs()+restore(). + +// So I moved _openFiles to a private var. The other two vars (_system, +// _counter) do not hurt. +// This fixed https://github.com/tschaub/mock-fs/issues/254 +// But I did not dig deep enough to understand what exactly happened. +var _system; +var _openFiles = {}; +var _counter = 0; + /** * Create a new binding with the given file system. * @param {FileSystem} system Mock file system. @@ -184,7 +216,7 @@ function Binding(system) { * Mock file system. * @type {FileSystem} */ - this._system = system; + _system = system; /** * Stats constructor. @@ -196,13 +228,13 @@ function Binding(system) { * Lookup of open files. * @type {Object.} */ - this._openFiles = {}; + _openFiles = {}; /** * Counter for file descriptors. * @type {number} */ - this._counter = 0; + _counter = 0; } /** @@ -210,7 +242,7 @@ function Binding(system) { * @return {FileSystem} The underlying file system. */ Binding.prototype.getSystem = function() { - return this._system; + return _system; }; /** @@ -218,7 +250,7 @@ Binding.prototype.getSystem = function() { * @param {FileSystem} system The new file system. */ Binding.prototype.setSystem = function(system) { - this._system = system; + _system = system; }; /** @@ -227,10 +259,10 @@ Binding.prototype.setSystem = function(system) { * @return {FileDescriptor} File descriptor. */ Binding.prototype._getDescriptorById = function(fd) { - if (!this._openFiles.hasOwnProperty(fd)) { + if (!_openFiles.hasOwnProperty(fd)) { throw new FSError('EBADF'); } - return this._openFiles[fd]; + return _openFiles[fd]; }; /** @@ -239,8 +271,8 @@ Binding.prototype._getDescriptorById = function(fd) { * @return {number} Identifier for file descriptor. */ Binding.prototype._trackDescriptor = function(descriptor) { - var fd = ++this._counter; - this._openFiles[fd] = descriptor; + var fd = ++_counter; + _openFiles[fd] = descriptor; return fd; }; @@ -249,34 +281,37 @@ Binding.prototype._trackDescriptor = function(descriptor) { * @param {number} fd Identifier for file descriptor. */ Binding.prototype._untrackDescriptorById = function(fd) { - if (!this._openFiles.hasOwnProperty(fd)) { + if (!_openFiles.hasOwnProperty(fd)) { throw new FSError('EBADF'); } - delete this._openFiles[fd]; + delete _openFiles[fd]; }; /** * Resolve the canonicalized absolute pathname. * @param {string|Buffer} filepath The file path. * @param {string} encoding The encoding for the return. + * @param {Object} ctx Context object (optional), only for nodejs v10+. * @return {string|Buffer} The real path. */ -Binding.prototype.realpath = function(filepath, encoding, callback) { - return maybeCallback(normalizeCallback(callback), this, function() { +Binding.prototype.realpath = function(filepath, encoding, callback, ctx) { + markSyscall(ctx, 'realpath'); + + return maybeCallback(normalizeCallback(callback), ctx, this, function() { var realPath; if (Buffer.isBuffer(filepath)) { filepath = filepath.toString(); } var resolved = path.resolve(filepath); var parts = getPathParts(resolved); - var item = this._system.getRoot(); + var item = _system.getRoot(); var itemPath = '/'; var name, i, ii; for (i = 0, ii = parts.length; i < ii; ++i) { name = parts[i]; while (item instanceof SymbolicLink) { itemPath = path.resolve(path.dirname(itemPath), item.getPath()); - item = this._system.getItem(itemPath); + item = _system.getItem(itemPath); } if (!item) { throw new FSError('ENOENT', filepath); @@ -291,7 +326,7 @@ Binding.prototype.realpath = function(filepath, encoding, callback) { if (item) { while (item instanceof SymbolicLink) { itemPath = path.resolve(path.dirname(itemPath), item.getPath()); - item = this._system.getItem(itemPath); + item = _system.getItem(itemPath); } realPath = itemPath; } else { @@ -335,17 +370,22 @@ function fillStatsArray(stats, statValues) { * @param {string} filepath Path. * @param {function(Error, Stats)|Float64Array|BigUint64Array} callback Callback (optional). In Node 7.7.0+ this will be a Float64Array * that should be filled with stat values. + * @param {Object} ctx Context object (optional), only for nodejs v10+. * @return {Stats|undefined} Stats or undefined (if sync). */ -Binding.prototype.stat = function(filepath, options, callback) { +Binding.prototype.stat = function(filepath, options, callback, ctx) { + // this seems wound not happen in nodejs v10+ if (arguments.length < 3) { callback = options; options = {}; } - return maybeCallback(wrapStatsCallback(callback), this, function() { - var item = this._system.getItem(filepath); + + markSyscall(ctx, 'stat'); + + return maybeCallback(wrapStatsCallback(callback), ctx, this, function() { + var item = _system.getItem(filepath); if (item instanceof SymbolicLink) { - item = this._system.getItem( + item = _system.getItem( path.resolve(path.dirname(filepath), item.getPath()) ); } @@ -374,14 +414,18 @@ Binding.prototype.stat = function(filepath, options, callback) { * @param {number} fd File descriptor. * @param {function(Error, Stats)|Float64Array|BigUint64Array} callback Callback (optional). In Node 7.7.0+ this will be a Float64Array * that should be filled with stat values. + * @param {Object} ctx Context object (optional), only for nodejs v10+. * @return {Stats|undefined} Stats or undefined (if sync). */ -Binding.prototype.fstat = function(fd, options, callback) { +Binding.prototype.fstat = function(fd, options, callback, ctx) { if (arguments.length < 3) { callback = options; options = {}; } - return maybeCallback(wrapStatsCallback(callback), this, function() { + + markSyscall(ctx, 'fstat'); + + return maybeCallback(wrapStatsCallback(callback), ctx, this, function() { var descriptor = this._getDescriptorById(fd); var item = descriptor.getItem(); var stats = item.getStats(); @@ -405,9 +449,12 @@ Binding.prototype.fstat = function(fd, options, callback) { * Close a file descriptor. * @param {number} fd File descriptor. * @param {function(Error)} callback Callback (optional). + * @param {Object} ctx Context object (optional), only for nodejs v10+. */ -Binding.prototype.close = function(fd, callback) { - maybeCallback(normalizeCallback(callback), this, function() { +Binding.prototype.close = function(fd, callback, ctx) { + markSyscall(ctx, 'close'); + + maybeCallback(normalizeCallback(callback), ctx, this, function() { this._untrackDescriptorById(fd); }); }; @@ -418,14 +465,17 @@ Binding.prototype.close = function(fd, callback) { * @param {number} flags Flags. * @param {number} mode Mode. * @param {function(Error, string)} callback Callback (optional). + * @param {Object} ctx Context object (optional), only for nodejs v10+. * @return {string} File descriptor (if sync). */ -Binding.prototype.open = function(pathname, flags, mode, callback) { - return maybeCallback(normalizeCallback(callback), this, function() { +Binding.prototype.open = function(pathname, flags, mode, callback, ctx) { + markSyscall(ctx, 'open'); + + return maybeCallback(normalizeCallback(callback), ctx, this, function() { var descriptor = new FileDescriptor(flags); - var item = this._system.getItem(pathname); + var item = _system.getItem(pathname); while (item instanceof SymbolicLink) { - item = this._system.getItem( + item = _system.getItem( path.resolve(path.dirname(pathname), item.getPath()) ); } @@ -433,7 +483,7 @@ Binding.prototype.open = function(pathname, flags, mode, callback) { throw new FSError('EEXIST', pathname); } if (descriptor.isCreate() && !item) { - var parent = this._system.getItem(path.dirname(pathname)); + var parent = _system.getItem(path.dirname(pathname)); if (!parent) { throw new FSError('ENOENT', pathname); } @@ -487,6 +537,7 @@ Binding.prototype.open = function(pathname, flags, mode, callback) { * data will be read from the current file position. * @param {function(Error, number, Buffer)} callback Callback (optional) called * with any error, number of bytes read, and the buffer. + * @param {Object} ctx Context object (optional), only for nodejs v10+. * @return {number} Number of bytes read (if sync). */ Binding.prototype.read = function( @@ -495,9 +546,12 @@ Binding.prototype.read = function( offset, length, position, - callback + callback, + ctx ) { - return maybeCallback(normalizeCallback(callback), this, function() { + markSyscall(ctx, 'read'); + + return maybeCallback(normalizeCallback(callback), ctx, this, function() { var descriptor = this._getDescriptorById(fd); if (!descriptor.isRead()) { throw new FSError('EBADF'); @@ -526,9 +580,12 @@ Binding.prototype.read = function( * @param {number} flags Modifiers for copy operation. * @param {function(Error)} callback Callback (optional) called * with any error. + * @param {Object} ctx Context object (optional), only for nodejs v10+. */ -Binding.prototype.copyFile = function(src, dest, flags, callback) { - return maybeCallback(normalizeCallback(callback), this, function() { +Binding.prototype.copyFile = function(src, dest, flags, callback, ctx) { + markSyscall(ctx, 'copyfile'); + + return maybeCallback(normalizeCallback(callback), ctx, this, function() { var srcFd = this.open(src, constants.O_RDONLY); try { @@ -570,10 +627,19 @@ Binding.prototype.copyFile = function(src, dest, flags, callback) { * data will be written to the current file position. * @param {function(Error, number, Buffer)} callback Callback (optional) called * with any error, number of bytes written, and the buffer. + * @param {Object} ctx Context object (optional), only for nodejs v10+. * @return {number} Number of bytes written (if sync). */ -Binding.prototype.writeBuffers = function(fd, buffers, position, callback) { - return maybeCallback(normalizeCallback(callback), this, function() { +Binding.prototype.writeBuffers = function( + fd, + buffers, + position, + callback, + ctx +) { + markSyscall(ctx, 'write'); + + return maybeCallback(normalizeCallback(callback), ctx, this, function() { var descriptor = this._getDescriptorById(fd); if (!descriptor.isWrite()) { throw new FSError('EBADF'); @@ -611,6 +677,7 @@ Binding.prototype.writeBuffers = function(fd, buffers, position, callback) { * data will be written to the current file position. * @param {function(Error, number, Buffer)} callback Callback (optional) called * with any error, number of bytes written, and the buffer. + * @param {Object} ctx Context object (optional), only for nodejs v10+. * @return {number} Number of bytes written (if sync). */ Binding.prototype.writeBuffer = function( @@ -619,9 +686,12 @@ Binding.prototype.writeBuffer = function( offset, length, position, - callback + callback, + ctx ) { - return maybeCallback(normalizeCallback(callback), this, function() { + markSyscall(ctx, 'write'); + + return maybeCallback(normalizeCallback(callback), ctx, this, function() { var descriptor = this._getDescriptorById(fd); if (!descriptor.isWrite()) { throw new FSError('EBADF'); @@ -659,6 +729,7 @@ Binding.prototype.writeBuffer = function( * data will be written to the current file position. * @param {function(Error, number, Buffer)} callback Callback (optional) called * with any error, number of bytes written, and the buffer. + * @param {Object} ctx Context object (optional), only for nodejs v10+. * @return {number} Number of bytes written (if sync). */ Binding.prototype.write = Binding.prototype.writeBuffer; @@ -679,8 +750,11 @@ Binding.prototype.writeString = function( string, position, encoding, - callback + callback, + ctx ) { + markSyscall(ctx, 'write'); + var buffer = bufferFrom(string, encoding); var wrapper; if (callback) { @@ -691,7 +765,7 @@ Binding.prototype.writeString = function( callback(err, written, returned && string); }; } - return this.writeBuffer(fd, buffer, 0, string.length, position, wrapper); + return this.writeBuffer(fd, buffer, 0, string.length, position, wrapper, ctx); }; /** @@ -699,18 +773,21 @@ Binding.prototype.writeString = function( * @param {string} oldPath Old pathname. * @param {string} newPath New pathname. * @param {function(Error)} callback Callback (optional). + * @param {Object} ctx Context object (optional), only for nodejs v10+. * @return {undefined} */ -Binding.prototype.rename = function(oldPath, newPath, callback) { - return maybeCallback(normalizeCallback(callback), this, function() { - var oldItem = this._system.getItem(oldPath); +Binding.prototype.rename = function(oldPath, newPath, callback, ctx) { + markSyscall(ctx, 'rename'); + + return maybeCallback(normalizeCallback(callback), ctx, this, function() { + var oldItem = _system.getItem(oldPath); if (!oldItem) { throw new FSError('ENOENT', oldPath); } - var oldParent = this._system.getItem(path.dirname(oldPath)); + var oldParent = _system.getItem(path.dirname(oldPath)); var oldName = path.basename(oldPath); - var newItem = this._system.getItem(newPath); - var newParent = this._system.getItem(path.dirname(newPath)); + var newItem = _system.getItem(newPath); + var newParent = _system.getItem(path.dirname(newPath)); var newName = path.basename(newPath); if (newItem) { // make sure they are the same type @@ -747,14 +824,17 @@ Binding.prototype.rename = function(oldPath, newPath, callback) { * @param {boolean} withFileTypes whether or not to return fs.Dirent objects * @param {function(Error, (Array.|Array.)} callback Callback * (optional) called with any error or array of items in the directory. + * @param {Object} ctx Context object (optional), only for nodejs v10+. * @return {Array.|Array.} Array of items in directory (if sync). */ Binding.prototype.readdir = function( dirpath, encoding, withFileTypes, - callback + callback, + ctx ) { + // again, the shorter arguments would not happen in nodejs v10+ if (arguments.length === 2) { callback = encoding; encoding = 'utf-8'; @@ -764,12 +844,15 @@ Binding.prototype.readdir = function( if (withFileTypes === true) { notImplemented(); } - return maybeCallback(normalizeCallback(callback), this, function() { + + markSyscall(ctx, 'scandir'); + + return maybeCallback(normalizeCallback(callback), ctx, this, function() { var dpath = dirpath; - var dir = this._system.getItem(dirpath); + var dir = _system.getItem(dirpath); while (dir instanceof SymbolicLink) { dpath = path.resolve(path.dirname(dpath), dir.getPath()); - dir = this._system.getItem(dpath); + dir = _system.getItem(dpath); } if (!dir) { throw new FSError('ENOENT', dirpath); @@ -791,15 +874,30 @@ Binding.prototype.readdir = function( * Create a directory. * @param {string} pathname Path to new directory. * @param {number} mode Permissions. + * @param {boolean} recursive Recursively create deep directory. (added in nodejs v10+) * @param {function(Error)} callback Optional callback. + * @param {Object} ctx Context object (optional), only for nodejs v10+. */ -Binding.prototype.mkdir = function(pathname, mode, callback) { - maybeCallback(normalizeCallback(callback), this, function() { - var item = this._system.getItem(pathname); +Binding.prototype.mkdir = function(pathname, mode, recursive, callback, ctx) { + if (typeof recursive !== 'boolean') { + // when running nodejs < 10 + ctx = callback; + callback = recursive; + recursive = false; + } + + if (recursive) { + notImplemented(); + } + + markSyscall(ctx, 'mkdir'); + + maybeCallback(normalizeCallback(callback), ctx, this, function() { + var item = _system.getItem(pathname); if (item) { throw new FSError('EEXIST', pathname); } - var parent = this._system.getItem(path.dirname(pathname)); + var parent = _system.getItem(path.dirname(pathname)); if (!parent) { throw new FSError('ENOENT', pathname); } @@ -816,10 +914,13 @@ Binding.prototype.mkdir = function(pathname, mode, callback) { * Remove a directory. * @param {string} pathname Path to directory. * @param {function(Error)} callback Optional callback. + * @param {Object} ctx Context object (optional), only for nodejs v10+. */ -Binding.prototype.rmdir = function(pathname, callback) { - maybeCallback(normalizeCallback(callback), this, function() { - var item = this._system.getItem(pathname); +Binding.prototype.rmdir = function(pathname, callback, ctx) { + markSyscall(ctx, 'rmdir'); + + maybeCallback(normalizeCallback(callback), ctx, this, function() { + var item = _system.getItem(pathname); if (!item) { throw new FSError('ENOENT', pathname); } @@ -830,7 +931,7 @@ Binding.prototype.rmdir = function(pathname, callback) { throw new FSError('ENOTEMPTY', pathname); } this.access(path.dirname(pathname), parseInt('0002', 8)); - var parent = this._system.getItem(path.dirname(pathname)); + var parent = _system.getItem(path.dirname(pathname)); parent.removeItem(path.basename(pathname)); }); }; @@ -846,16 +947,20 @@ var MAX_ATTEMPTS = 62 * 62 * 62; * @param {string} template Path template (trailing Xs will be replaced). * @param {string} encoding The encoding ('utf-8' or 'buffer'). * @param {function(Error, string)} callback Optional callback. + * @param {Object} ctx Context object (optional), only for nodejs v10+. */ -Binding.prototype.mkdtemp = function(prefix, encoding, callback) { +Binding.prototype.mkdtemp = function(prefix, encoding, callback, ctx) { if (encoding && typeof encoding !== 'string') { callback = encoding; encoding = 'utf-8'; } - return maybeCallback(normalizeCallback(callback), this, function() { + + markSyscall(ctx, 'mkdtemp'); + + return maybeCallback(normalizeCallback(callback), ctx, this, function() { prefix = prefix.replace(/X{0,6}$/, 'XXXXXX'); var parentPath = path.dirname(prefix); - var parent = this._system.getItem(parentPath); + var parent = _system.getItem(parentPath); if (!parent) { throw new FSError('ENOENT', prefix); } @@ -901,9 +1006,12 @@ Binding.prototype.mkdtemp = function(prefix, encoding, callback) { * @param {number} fd File descriptor. * @param {number} len Number of bytes. * @param {function(Error)} callback Optional callback. + * @param {Object} ctx Context object (optional), only for nodejs v10+. */ -Binding.prototype.ftruncate = function(fd, len, callback) { - maybeCallback(normalizeCallback(callback), this, function() { +Binding.prototype.ftruncate = function(fd, len, callback, ctx) { + markSyscall(ctx, 'ftruncate'); + + maybeCallback(normalizeCallback(callback), ctx, this, function() { var descriptor = this._getDescriptorById(fd); if (!descriptor.isWrite()) { throw new FSError('EINVAL'); @@ -924,6 +1032,7 @@ Binding.prototype.ftruncate = function(fd, len, callback) { * @param {number} fd File descriptor. * @param {number} len Number of bytes. * @param {function(Error)} callback Optional callback. + * @param {Object} ctx Context object (optional), only for nodejs v10+. */ Binding.prototype.truncate = Binding.prototype.ftruncate; @@ -933,10 +1042,13 @@ Binding.prototype.truncate = Binding.prototype.ftruncate; * @param {number} uid User id. * @param {number} gid Group id. * @param {function(Error)} callback Optional callback. + * @param {Object} ctx Context object (optional), only for nodejs v10+. */ -Binding.prototype.chown = function(pathname, uid, gid, callback) { - maybeCallback(normalizeCallback(callback), this, function() { - var item = this._system.getItem(pathname); +Binding.prototype.chown = function(pathname, uid, gid, callback, ctx) { + markSyscall(ctx, 'chown'); + + maybeCallback(normalizeCallback(callback), ctx, this, function() { + var item = _system.getItem(pathname); if (!item) { throw new FSError('ENOENT', pathname); } @@ -951,9 +1063,12 @@ Binding.prototype.chown = function(pathname, uid, gid, callback) { * @param {number} uid User id. * @param {number} gid Group id. * @param {function(Error)} callback Optional callback. + * @param {Object} ctx Context object (optional), only for nodejs v10+. */ -Binding.prototype.fchown = function(fd, uid, gid, callback) { - maybeCallback(normalizeCallback(callback), this, function() { +Binding.prototype.fchown = function(fd, uid, gid, callback, ctx) { + markSyscall(ctx, 'fchown'); + + maybeCallback(normalizeCallback(callback), ctx, this, function() { var descriptor = this._getDescriptorById(fd); var item = descriptor.getItem(); item.setUid(uid); @@ -966,10 +1081,13 @@ Binding.prototype.fchown = function(fd, uid, gid, callback) { * @param {string} pathname Path. * @param {number} mode Mode. * @param {function(Error)} callback Optional callback. + * @param {Object} ctx Context object (optional), only for nodejs v10+. */ -Binding.prototype.chmod = function(pathname, mode, callback) { - maybeCallback(normalizeCallback(callback), this, function() { - var item = this._system.getItem(pathname); +Binding.prototype.chmod = function(pathname, mode, callback, ctx) { + markSyscall(ctx, 'chmod'); + + maybeCallback(normalizeCallback(callback), ctx, this, function() { + var item = _system.getItem(pathname); if (!item) { throw new FSError('ENOENT', pathname); } @@ -982,9 +1100,12 @@ Binding.prototype.chmod = function(pathname, mode, callback) { * @param {number} fd File descriptor. * @param {number} mode Mode. * @param {function(Error)} callback Optional callback. + * @param {Object} ctx Context object (optional), only for nodejs v10+. */ -Binding.prototype.fchmod = function(fd, mode, callback) { - maybeCallback(normalizeCallback(callback), this, function() { +Binding.prototype.fchmod = function(fd, mode, callback, ctx) { + markSyscall(ctx, 'fchmod'); + + maybeCallback(normalizeCallback(callback), ctx, this, function() { var descriptor = this._getDescriptorById(fd); var item = descriptor.getItem(); item.setMode(mode); @@ -995,17 +1116,20 @@ Binding.prototype.fchmod = function(fd, mode, callback) { * Delete a named item. * @param {string} pathname Path to item. * @param {function(Error)} callback Optional callback. + * @param {Object} ctx Context object (optional), only for nodejs v10+. */ -Binding.prototype.unlink = function(pathname, callback) { - maybeCallback(normalizeCallback(callback), this, function() { - var item = this._system.getItem(pathname); +Binding.prototype.unlink = function(pathname, callback, ctx) { + markSyscall(ctx, 'unlink'); + + maybeCallback(normalizeCallback(callback), ctx, this, function() { + var item = _system.getItem(pathname); if (!item) { throw new FSError('ENOENT', pathname); } if (item instanceof Directory) { throw new FSError('EPERM', pathname); } - var parent = this._system.getItem(path.dirname(pathname)); + var parent = _system.getItem(path.dirname(pathname)); parent.removeItem(path.basename(pathname)); }); }; @@ -1016,10 +1140,13 @@ Binding.prototype.unlink = function(pathname, callback) { * @param {number} atime Access time (in seconds). * @param {number} mtime Modification time (in seconds). * @param {function(Error)} callback Optional callback. + * @param {Object} ctx Context object (optional), only for nodejs v10+. */ -Binding.prototype.utimes = function(pathname, atime, mtime, callback) { - maybeCallback(normalizeCallback(callback), this, function() { - var item = this._system.getItem(pathname); +Binding.prototype.utimes = function(pathname, atime, mtime, callback, ctx) { + markSyscall(ctx, 'utimes'); + + maybeCallback(normalizeCallback(callback), ctx, this, function() { + var item = _system.getItem(pathname); if (!item) { throw new FSError('ENOENT', pathname); } @@ -1034,9 +1161,12 @@ Binding.prototype.utimes = function(pathname, atime, mtime, callback) { * @param {number} atime Access time (in seconds). * @param {number} mtime Modification time (in seconds). * @param {function(Error)} callback Optional callback. + * @param {Object} ctx Context object (optional), only for nodejs v10+. */ -Binding.prototype.futimes = function(fd, atime, mtime, callback) { - maybeCallback(normalizeCallback(callback), this, function() { +Binding.prototype.futimes = function(fd, atime, mtime, callback, ctx) { + markSyscall(ctx, 'futimes'); + + maybeCallback(normalizeCallback(callback), ctx, this, function() { var descriptor = this._getDescriptorById(fd); var item = descriptor.getItem(); item.setATime(new Date(atime * 1000)); @@ -1048,9 +1178,12 @@ Binding.prototype.futimes = function(fd, atime, mtime, callback) { * Synchronize in-core state with storage device. * @param {number} fd File descriptor. * @param {function(Error)} callback Optional callback. + * @param {Object} ctx Context object (optional), only for nodejs v10+. */ -Binding.prototype.fsync = function(fd, callback) { - maybeCallback(normalizeCallback(callback), this, function() { +Binding.prototype.fsync = function(fd, callback, ctx) { + markSyscall(ctx, 'fsync'); + + maybeCallback(normalizeCallback(callback), ctx, this, function() { this._getDescriptorById(fd); }); }; @@ -1059,9 +1192,12 @@ Binding.prototype.fsync = function(fd, callback) { * Synchronize in-core metadata state with storage device. * @param {number} fd File descriptor. * @param {function(Error)} callback Optional callback. + * @param {Object} ctx Context object (optional), only for nodejs v10+. */ -Binding.prototype.fdatasync = function(fd, callback) { - maybeCallback(normalizeCallback(callback), this, function() { +Binding.prototype.fdatasync = function(fd, callback, ctx) { + markSyscall(ctx, 'fdatasync'); + + maybeCallback(normalizeCallback(callback), ctx, this, function() { this._getDescriptorById(fd); }); }; @@ -1071,20 +1207,23 @@ Binding.prototype.fdatasync = function(fd, callback) { * @param {string} srcPath The existing file. * @param {string} destPath The new link to create. * @param {function(Error)} callback Optional callback. + * @param {Object} ctx Context object (optional), only for nodejs v10+. */ -Binding.prototype.link = function(srcPath, destPath, callback) { - maybeCallback(normalizeCallback(callback), this, function() { - var item = this._system.getItem(srcPath); +Binding.prototype.link = function(srcPath, destPath, callback, ctx) { + markSyscall(ctx, 'link'); + + maybeCallback(normalizeCallback(callback), ctx, this, function() { + var item = _system.getItem(srcPath); if (!item) { throw new FSError('ENOENT', srcPath); } if (item instanceof Directory) { throw new FSError('EPERM', srcPath); } - if (this._system.getItem(destPath)) { + if (_system.getItem(destPath)) { throw new FSError('EEXIST', destPath); } - var parent = this._system.getItem(path.dirname(destPath)); + var parent = _system.getItem(path.dirname(destPath)); if (!parent) { throw new FSError('ENOENT', destPath); } @@ -1101,13 +1240,16 @@ Binding.prototype.link = function(srcPath, destPath, callback) { * @param {string} destPath Path for the generated link. * @param {string} type Ignored (used for Windows only). * @param {function(Error)} callback Optional callback. + * @param {Object} ctx Context object (optional), only for nodejs v10+. */ -Binding.prototype.symlink = function(srcPath, destPath, type, callback) { - maybeCallback(normalizeCallback(callback), this, function() { - if (this._system.getItem(destPath)) { +Binding.prototype.symlink = function(srcPath, destPath, type, callback, ctx) { + markSyscall(ctx, 'symlink'); + + maybeCallback(normalizeCallback(callback), ctx, this, function() { + if (_system.getItem(destPath)) { throw new FSError('EEXIST', destPath); } - var parent = this._system.getItem(path.dirname(destPath)); + var parent = _system.getItem(path.dirname(destPath)); if (!parent) { throw new FSError('ENOENT', destPath); } @@ -1125,15 +1267,20 @@ Binding.prototype.symlink = function(srcPath, destPath, type, callback) { * @param {string} pathname Path to symbolic link. * @param {string} encoding The encoding ('utf-8' or 'buffer'). * @param {function(Error, (string|Buffer))} callback Optional callback. + * @param {Object} ctx Context object (optional), only for nodejs v10+. * @return {string|Buffer} Symbolic link contents (path to source). */ -Binding.prototype.readlink = function(pathname, encoding, callback) { +Binding.prototype.readlink = function(pathname, encoding, callback, ctx) { if (encoding && typeof encoding !== 'string') { + // this would not happend in nodejs v10+ callback = encoding; encoding = 'utf-8'; } - return maybeCallback(normalizeCallback(callback), this, function() { - var link = this._system.getItem(pathname); + + markSyscall(ctx, 'readlink'); + + return maybeCallback(normalizeCallback(callback), ctx, this, function() { + var link = _system.getItem(pathname); if (!link) { throw new FSError('ENOENT', pathname); } @@ -1153,15 +1300,20 @@ Binding.prototype.readlink = function(pathname, encoding, callback) { * @param {string} filepath Path. * @param {function(Error, Stats)|Float64Array|BigUint64Array} callback Callback (optional). In Node 7.7.0+ this will be a Float64Array * that should be filled with stat values. + * @param {Object} ctx Context object (optional), only for nodejs v10+. * @return {Stats|undefined} Stats or undefined (if sync). */ -Binding.prototype.lstat = function(filepath, options, callback) { +Binding.prototype.lstat = function(filepath, options, callback, ctx) { if (arguments.length < 3) { + // this would not happend in nodejs v10+ callback = options; options = {}; } - return maybeCallback(wrapStatsCallback(callback), this, function() { - var item = this._system.getItem(filepath); + + markSyscall(ctx, 'lstat'); + + return maybeCallback(wrapStatsCallback(callback), ctx, this, function() { + var item = _system.getItem(filepath); if (!item) { throw new FSError('ENOENT', filepath); } @@ -1187,17 +1339,20 @@ Binding.prototype.lstat = function(filepath, options, callback) { * @param {string} filepath Path. * @param {number} mode Mode. * @param {function(Error)} callback Callback (optional). + * @param {Object} ctx Context object (optional), only for nodejs v10+. */ -Binding.prototype.access = function(filepath, mode, callback) { - maybeCallback(normalizeCallback(callback), this, function() { - var item = this._system.getItem(filepath); +Binding.prototype.access = function(filepath, mode, callback, ctx) { + markSyscall(ctx, 'access'); + + maybeCallback(normalizeCallback(callback), ctx, this, function() { + var item = _system.getItem(filepath); var links = 0; while (item instanceof SymbolicLink) { if (links > MAX_LINKS) { throw new FSError('ELOOP', filepath); } filepath = path.resolve(path.dirname(filepath), item.getPath()); - item = this._system.getItem(filepath); + item = _system.getItem(filepath); ++links; } if (!item) { diff --git a/lib/error.js b/lib/error.js index 20caab57..0e4b0fae 100644 --- a/lib/error.js +++ b/lib/error.js @@ -6,7 +6,7 @@ */ var codes = { UNKNOWN: { - errno: -1, + errno: -4094, message: 'unknown error' }, OK: { @@ -14,232 +14,317 @@ var codes = { message: 'success' }, EOF: { - errno: 1, + errno: -4095, message: 'end of file' }, - EADDRINFO: { - errno: 2, - message: 'getaddrinfo error' + + E2BIG: { + errno: -7, + message: 'argument list too long' }, EACCES: { - errno: 3, + errno: -13, message: 'permission denied' }, - EAGAIN: { - errno: 4, - message: 'resource temporarily unavailable' - }, EADDRINUSE: { - errno: 5, + errno: -48, message: 'address already in use' }, EADDRNOTAVAIL: { - errno: 6, + errno: -49, message: 'address not available' }, EAFNOSUPPORT: { - errno: 7, + errno: -47, message: 'address family not supported' }, + EAGAIN: { + errno: -35, + message: 'resource temporarily unavailable' + }, + EAI_ADDRFAMILY: { + errno: -3000, + message: 'address family not supported' + }, + EAI_AGAIN: { + errno: -3001, + message: 'temporary failure' + }, + EAI_BADFLAGS: { + errno: -3002, + message: 'bad ai_flags value' + }, + EAI_BADHINTS: { + errno: -3013, + message: 'invalid value for hints' + }, + EAI_CANCELED: { + errno: -3003, + message: 'request canceled' + }, + EAI_FAIL: { + errno: -3004, + message: 'permanent failure' + }, + EAI_FAMILY: { + errno: -3005, + message: 'ai_family not supported' + }, + EAI_MEMORY: { + errno: -3006, + message: 'out of memory' + }, + EAI_NODATA: { + errno: -3007, + message: 'no address' + }, + EAI_NONAME: { + errno: -3008, + message: 'unknown node or service' + }, + EAI_OVERFLOW: { + errno: -3009, + message: 'argument buffer overflow' + }, + EAI_PROTOCOL: { + errno: -3014, + message: 'resolved protocol is unknown' + }, + EAI_SERVICE: { + errno: -3010, + message: 'service not available for socket type' + }, + EAI_SOCKTYPE: { + errno: -3011, + message: 'socket type not supported' + }, EALREADY: { - errno: 8, + errno: -37, message: 'connection already in progress' }, EBADF: { - errno: 9, + errno: -9, message: 'bad file descriptor' }, EBUSY: { - errno: 10, + errno: -16, message: 'resource busy or locked' }, + ECANCELED: { + errno: -89, + message: 'operation canceled' + }, + ECHARSET: { + errno: -4080, + message: 'invalid Unicode character' + }, ECONNABORTED: { - errno: 11, + errno: -53, message: 'software caused connection abort' }, ECONNREFUSED: { - errno: 12, + errno: -61, message: 'connection refused' }, ECONNRESET: { - errno: 13, + errno: -54, message: 'connection reset by peer' }, EDESTADDRREQ: { - errno: 14, + errno: -39, message: 'destination address required' }, + EEXIST: { + errno: -17, + message: 'file already exists' + }, EFAULT: { - errno: 15, + errno: -14, message: 'bad address in system call argument' }, + EFBIG: { + errno: -27, + message: 'file too large' + }, EHOSTUNREACH: { - errno: 16, + errno: -65, message: 'host is unreachable' }, EINTR: { - errno: 17, + errno: -4, message: 'interrupted system call' }, EINVAL: { - errno: 18, + errno: -22, message: 'invalid argument' }, + EIO: { + errno: -5, + message: 'i/o error' + }, EISCONN: { - errno: 19, + errno: -56, message: 'socket is already connected' }, + EISDIR: { + errno: -21, + message: 'illegal operation on a directory' + }, + ELOOP: { + errno: -62, + message: 'too many symbolic links encountered' + }, EMFILE: { - errno: 20, + errno: -24, message: 'too many open files' }, EMSGSIZE: { - errno: 21, + errno: -40, message: 'message too long' }, + ENAMETOOLONG: { + errno: -63, + message: 'name too long' + }, ENETDOWN: { - errno: 22, + errno: -50, message: 'network is down' }, ENETUNREACH: { - errno: 23, + errno: -51, message: 'network is unreachable' }, ENFILE: { - errno: 24, + errno: -23, message: 'file table overflow' }, ENOBUFS: { - errno: 25, + errno: -55, message: 'no buffer space available' }, - ENOMEM: { - errno: 26, - message: 'not enough memory' + ENODEV: { + errno: -19, + message: 'no such device' }, - ENOTDIR: { - errno: 27, - message: 'not a directory' + ENOENT: { + errno: -2, + message: 'no such file or directory' }, - EISDIR: { - errno: 28, - message: 'illegal operation on a directory' + ENOMEM: { + errno: -12, + message: 'not enough memory' }, ENONET: { - errno: 29, + errno: -4056, message: 'machine is not on the network' }, + ENOPROTOOPT: { + errno: -42, + message: 'protocol not available' + }, + ENOSPC: { + errno: -28, + message: 'no space left on device' + }, + ENOSYS: { + errno: -78, + message: 'function not implemented' + }, ENOTCONN: { - errno: 31, + errno: -57, message: 'socket is not connected' }, + ENOTDIR: { + errno: -20, + message: 'not a directory' + }, + ENOTEMPTY: { + errno: -66, + message: 'directory not empty' + }, ENOTSOCK: { - errno: 32, + errno: -38, message: 'socket operation on non-socket' }, ENOTSUP: { - errno: 33, + errno: -45, message: 'operation not supported on socket' }, - ENOENT: { - errno: 34, - message: 'no such file or directory' - }, - ENOSYS: { - errno: 35, - message: 'function not implemented' + EPERM: { + errno: -1, + message: 'operation not permitted' }, EPIPE: { - errno: 36, + errno: -32, message: 'broken pipe' }, EPROTO: { - errno: 37, + errno: -100, message: 'protocol error' }, EPROTONOSUPPORT: { - errno: 38, + errno: -43, message: 'protocol not supported' }, EPROTOTYPE: { - errno: 39, + errno: -41, message: 'protocol wrong type for socket' }, - ETIMEDOUT: { - errno: 40, - message: 'connection timed out' - }, - ECHARSET: { - errno: 41, - message: 'invalid Unicode character' + ERANGE: { + errno: -34, + message: 'result too large' }, - EAIFAMNOSUPPORT: { - errno: 42, - message: 'address family for hostname not supported' - }, - EAISERVICE: { - errno: 44, - message: 'servname not supported for ai_socktype' - }, - EAISOCKTYPE: { - errno: 45, - message: 'ai_socktype not supported' + EROFS: { + errno: -30, + message: 'read-only file system' }, ESHUTDOWN: { - errno: 46, + errno: -58, message: 'cannot send after transport endpoint shutdown' }, - EEXIST: { - errno: 47, - message: 'file already exists' + ESPIPE: { + errno: -29, + message: 'invalid seek' }, ESRCH: { - errno: 48, + errno: -3, message: 'no such process' }, - ENAMETOOLONG: { - errno: 49, - message: 'name too long' - }, - EPERM: { - errno: 50, - message: 'operation not permitted' + ETIMEDOUT: { + errno: -60, + message: 'connection timed out' }, - ELOOP: { - errno: 51, - message: 'too many symbolic links encountered' + ETXTBSY: { + errno: -26, + message: 'text file is busy' }, EXDEV: { - errno: 52, + errno: -18, message: 'cross-device link not permitted' }, - ENOTEMPTY: { - errno: 53, - message: 'directory not empty' + ENXIO: { + errno: -6, + message: 'no such device or address' }, - ENOSPC: { - errno: 54, - message: 'no space left on device' - }, - EIO: { - errno: 55, - message: 'i/o error' + EMLINK: { + errno: -31, + message: 'too many links' }, - EROFS: { - errno: 56, - message: 'read-only file system' + EHOSTDOWN: { + errno: -64, + message: 'host is down' }, - ENODEV: { - errno: 57, - message: 'no such device' + EREMOTEIO: { + errno: -4030, + message: 'remote I/O error' }, - ESPIPE: { - errno: 58, - message: 'invalid seek' + ENOTTY: { + errno: -25, + message: 'inappropriate ioctl for device' }, - ECANCELED: { - errno: 59, - message: 'peration canceled' + EFTYPE: { + errno: -79, + message: 'inappropriate file type or format' } }; diff --git a/lib/index.js b/lib/index.js index 38258e03..1a02e671 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,12 +5,14 @@ var FSError = require('./error'); var FileSystem = require('./filesystem'); var realBinding = process.binding('fs'); var path = require('path'); +var fs = require('fs'); var realBindingProps = Object.assign({}, realBinding); var realProcessProps = { cwd: process.cwd, chdir: process.chdir }; +var realCreateWriteStream = fs.createWriteStream; function overrideBinding(binding) { for (var key in binding) { @@ -27,6 +29,38 @@ function overrideProcess(cwd, chdir) { process.chdir = chdir; } +// Have to disable write stream _writev on nodejs v10+. +// +// nodejs v8 lib/fs.js +// note binding.writeBuffers will use mock-fs patched writeBuffers. +// +// const binding = process.binding('fs'); +// function writev(fd, chunks, position, callback) { +// // ... +// binding.writeBuffers(fd, chunks, position, req); +// } +// +// nodejs v10+ lib/internal/fs/streams.js +// note it uses original writeBuffers, bypassed mock-fs patched writeBuffers. +// +// const {writeBuffers} = internalBinding('fs'); +// function writev(fd, chunks, position, callback) { +// // ... +// writeBuffers(fd, chunks, position, req); +// } +// +// Luckily _writev is an optional method on Writeable stream implementation. +// When _writev is missing, it will fall back to make multiple _write calls. + +function overrideCreateWriteStream() { + fs.createWriteStream = function(path, options) { + var output = realCreateWriteStream(path, options); + // disable _writev, this will over shadow WriteStream.prototype._writev + output._writev = undefined; + return output; + }; +} + function restoreBinding() { var key; for (key in realBindingProps) { @@ -46,6 +80,10 @@ function restoreProcess() { } } +function restoreCreateWriteStream() { + fs.createWriteStream = realCreateWriteStream; +} + /** * Swap out the fs bindings for a mock file system. * @param {Object} config Mock file system configuration. @@ -73,6 +111,8 @@ var exports = (module.exports = function mock(config, options) { currentPath = path.resolve(currentPath, directory); } ); + + overrideCreateWriteStream(); }); /** @@ -93,6 +133,7 @@ exports.getMockRoot = function() { exports.restore = function() { restoreBinding(); restoreProcess(); + restoreCreateWriteStream(); }; /** diff --git a/test/lib/index.spec.js b/test/lib/index.spec.js index dfb5eb36..b0bfadd7 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -3123,7 +3123,7 @@ describe('Mocking the file system', function() { }); afterEach(mock.restore); - it('provides a write stream for a file', function(done) { + it('provides a write stream for a file in buffered mode', function(done) { var output = fs.createWriteStream('test.txt'); output.on('close', function() { fs.readFile('test.txt', function(err, data) { @@ -3136,12 +3136,39 @@ describe('Mocking the file system', function() { }); output.on('error', done); + // if output._writev is available, buffered multiple writes will hit _writev. + // otherwise, hit multiple _write. output.write(bufferFrom('lots ')); output.write(bufferFrom('of ')); output.write(bufferFrom('source ')); output.end(bufferFrom('content')); }); + it('provides a write stream for a file', function(done) { + var output = fs.createWriteStream('test.txt'); + output.on('close', function() { + fs.readFile('test.txt', function(err, data) { + if (err) { + return done(err); + } + assert.equal(String(data), 'lots of source content'); + done(); + }); + }); + output.on('error', done); + + output.write(bufferFrom('lots ')); + setTimeout(function() { + output.write(bufferFrom('of ')); + setTimeout(function() { + output.write(bufferFrom('source ')); + setTimeout(function() { + output.end(bufferFrom('content')); + }, 50); + }, 50); + }, 50); + }); + if (Writable && Writable.prototype.cork) { it('works when write stream is corked', function(done) { var output = fs.createWriteStream('test.txt'); From 7e7e1ba816ce9190e631d3984bc6007a6643fc40 Mon Sep 17 00:00:00 2001 From: Chunpeng Huo Date: Sat, 2 Feb 2019 22:44:01 +1100 Subject: [PATCH 3/7] chore: test on linux/windows/osx --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 75f10e52..c21f4154 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,10 @@ sudo: false +os: + - linux + - windows + - osx + language: node_js node_js: - "4" From 122c85922cc02f89a9a5a130b1c40650996d1174 Mon Sep 17 00:00:00 2001 From: Chunpeng Huo Date: Sun, 3 Feb 2019 10:31:19 +1100 Subject: [PATCH 4/7] fix: fix uid/gid on windows --- lib/item.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/item.js b/lib/item.js index 3b7e10e1..49c033e9 100644 --- a/lib/item.js +++ b/lib/item.js @@ -19,11 +19,13 @@ var permissions = { }; function getUid() { - return process.getuid && process.getuid(); + // force NaN on windows. + return process.getuid ? process.getuid() : NaN; } function getGid() { - return process.getgid && process.getgid(); + // force NaN on windows. + return process.getgid ? process.getgid() : NaN; } /** @@ -96,7 +98,8 @@ Item.prototype.canRead = function() { var can = false; if (uid === 0) { can = true; - } else if (uid === this._uid) { + } else if (uid === this._uid || uid !== uid) { + // (uid !== uid) means uid is NaN, only for windows can = (permissions.USER_READ & this._mode) === permissions.USER_READ; } else if (gid === this._gid) { can = (permissions.GROUP_READ & this._mode) === permissions.GROUP_READ; @@ -116,7 +119,8 @@ Item.prototype.canWrite = function() { var can = false; if (uid === 0) { can = true; - } else if (uid === this._uid) { + } else if (uid === this._uid || uid !== uid) { + // (uid !== uid) means uid is NaN, only for windows can = (permissions.USER_WRITE & this._mode) === permissions.USER_WRITE; } else if (gid === this._gid) { can = (permissions.GROUP_WRITE & this._mode) === permissions.GROUP_WRITE; @@ -136,7 +140,8 @@ Item.prototype.canExecute = function() { var can = false; if (uid === 0) { can = true; - } else if (uid === this._uid) { + } else if (uid === this._uid || uid !== uid) { + // (uid !== uid) means uid is NaN, only for windows can = (permissions.USER_EXEC & this._mode) === permissions.USER_EXEC; } else if (gid === this._gid) { can = (permissions.GROUP_EXEC & this._mode) === permissions.GROUP_EXEC; From 86bdd71df7a60403cc00810feb89db3d50065de5 Mon Sep 17 00:00:00 2001 From: Chunpeng Huo Date: Sun, 3 Feb 2019 10:31:53 +1100 Subject: [PATCH 5/7] feat: support nodejs v10+ fs.promises --- lib/binding.js | 45 +++++++++++++++++++++++++++++++++++++++--- test/lib/index.spec.js | 29 +++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/lib/binding.js b/lib/binding.js index 5951c5a2..8829b55e 100644 --- a/lib/binding.js +++ b/lib/binding.js @@ -14,6 +14,7 @@ var bufferAlloc = require('./buffer').alloc; /** Workaround for optimizations in node 8+ */ var fsBinding = process.binding('fs'); +var kUsePromises = fsBinding.kUsePromises; var statValues; if (fsBinding.statValues) { statValues = fsBinding.statValues; // node 10 @@ -43,9 +44,26 @@ var MAX_LINKS = 50; * @return {*} Return (if callback is not provided). */ function maybeCallback(callback, ctx, thisArg, func) { - if (callback && typeof callback === 'function') { - var err = null; - var val; + var err = null; + var val; + + if (kUsePromises && callback === kUsePromises) { + // support nodejs v10+ fs.promises + try { + val = func.call(thisArg); + } catch (e) { + err = e; + } + return new Promise(function(resolve, reject) { + process.nextTick(function() { + if (val === undefined) { + reject(err); + } else { + resolve(val); + } + }); + }); + } else if (callback && typeof callback === 'function') { try { val = func.call(thisArg); } catch (e) { @@ -527,6 +545,27 @@ Binding.prototype.open = function(pathname, flags, mode, callback, ctx) { }); }; +/** + * Open a file handler. A new api in nodejs v10+ for fs.promises + * @param {string} pathname File path. + * @param {number} flags Flags. + * @param {number} mode Mode. + * @param {function} callback Callback (optional), expecting kUsePromises in nodejs v10+. + */ +Binding.prototype.openFileHandle = function(pathname, flags, mode, callback) { + var self = this; + return this.open(pathname, flags, mode, kUsePromises).then(function(fd) { + // nodejs v10+ fs.promises FileHandler constructor only ask these three properties. + return { + getAsyncId: notImplemented, + fd: fd, + close: function() { + return self.close(fd, kUsePromises); + } + }; + }); +}; + /** * Read from a file descriptor. * @param {string} fd File descriptor. diff --git a/test/lib/index.spec.js b/test/lib/index.spec.js index b0bfadd7..de4adf44 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -1575,6 +1575,21 @@ describe('Mocking the file system', function() { }); }); + if (fs.promises) { + it('allows a file to be read asynchronously in promise', function(done) { + fs.promises.readFile('path/to/file.txt').then( + function(data) { + assert.isTrue(Buffer.isBuffer(data)); + assert.equal(String(data), 'file content'); + done(); + }, + function(err) { + done(err); + } + ); + }); + } + it('fails for directory', function(done) { fs.readFile('path/to', function(err, data) { assert.instanceOf(err, Error); @@ -1849,6 +1864,20 @@ describe('Mocking the file system', function() { }); }); + if (fs.promises) { + it('writes a string to a file in promise', function(done) { + fs.promises.writeFile('dir/foo', 'bar').then( + function() { + assert.equal(String(fs.readFileSync('dir/foo')), 'bar'); + done(); + }, + function(err) { + done(err); + } + ); + }); + } + it('updates mtime of parent directory', function(done) { var oldTime = fs.statSync('dir').mtime; fs.writeFile('dir/foo', 'bar', function(err) { From b374e76de17364f2a03047a81d0e3267e172452e Mon Sep 17 00:00:00 2001 From: Chunpeng Huo Date: Sun, 3 Feb 2019 11:18:29 +1100 Subject: [PATCH 6/7] chore: remove nodejs v4 from travis matrix mock-fs works in nodejs v4, but travis windows has trouble to install npm@3 for nodejs v4. --- .travis.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index c21f4154..b96b07e5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,3 @@ -sudo: false - os: - linux - windows @@ -7,11 +5,7 @@ os: language: node_js node_js: - - "4" - "6" - "8" - "10" - "11" - -before_install: - - if [[ `npm -v` == 2.* ]]; then npm install --global npm@3; fi From dea8e5542ef4f2729f633683b8a1cab2266ee771 Mon Sep 17 00:00:00 2001 From: Chunpeng Huo Date: Mon, 4 Feb 2019 08:23:08 +1100 Subject: [PATCH 7/7] fix: fix errno on windows, windows has different errmap than posix --- lib/binding.js | 5 +- lib/error.js | 342 +++-------------------------------------- test/lib/index.spec.js | 2 + 3 files changed, 25 insertions(+), 324 deletions(-) diff --git a/lib/binding.js b/lib/binding.js index 8829b55e..91bbce3c 100644 --- a/lib/binding.js +++ b/lib/binding.js @@ -80,8 +80,9 @@ function maybeCallback(callback, ctx, thisArg, func) { try { return func.call(thisArg); } catch (e) { - ctx.code = e.code; - ctx.errno = e.errno || -1; + // default to errno for UNKNOWN + ctx.code = e.code || 'UNKNOWN'; + ctx.errno = e.errno || FSError.codes.UNKNOWN.errno; } } else { return func.call(thisArg); diff --git a/lib/error.js b/lib/error.js index 0e4b0fae..f29bd9ee 100644 --- a/lib/error.js +++ b/lib/error.js @@ -1,332 +1,29 @@ 'use strict'; +var uvBinding = process.binding('uv'); /** * Error codes from libuv. * @enum {number} */ -var codes = { - UNKNOWN: { - errno: -4094, - message: 'unknown error' - }, - OK: { - errno: 0, - message: 'success' - }, - EOF: { - errno: -4095, - message: 'end of file' - }, +var codes = {}; - E2BIG: { - errno: -7, - message: 'argument list too long' - }, - EACCES: { - errno: -13, - message: 'permission denied' - }, - EADDRINUSE: { - errno: -48, - message: 'address already in use' - }, - EADDRNOTAVAIL: { - errno: -49, - message: 'address not available' - }, - EAFNOSUPPORT: { - errno: -47, - message: 'address family not supported' - }, - EAGAIN: { - errno: -35, - message: 'resource temporarily unavailable' - }, - EAI_ADDRFAMILY: { - errno: -3000, - message: 'address family not supported' - }, - EAI_AGAIN: { - errno: -3001, - message: 'temporary failure' - }, - EAI_BADFLAGS: { - errno: -3002, - message: 'bad ai_flags value' - }, - EAI_BADHINTS: { - errno: -3013, - message: 'invalid value for hints' - }, - EAI_CANCELED: { - errno: -3003, - message: 'request canceled' - }, - EAI_FAIL: { - errno: -3004, - message: 'permanent failure' - }, - EAI_FAMILY: { - errno: -3005, - message: 'ai_family not supported' - }, - EAI_MEMORY: { - errno: -3006, - message: 'out of memory' - }, - EAI_NODATA: { - errno: -3007, - message: 'no address' - }, - EAI_NONAME: { - errno: -3008, - message: 'unknown node or service' - }, - EAI_OVERFLOW: { - errno: -3009, - message: 'argument buffer overflow' - }, - EAI_PROTOCOL: { - errno: -3014, - message: 'resolved protocol is unknown' - }, - EAI_SERVICE: { - errno: -3010, - message: 'service not available for socket type' - }, - EAI_SOCKTYPE: { - errno: -3011, - message: 'socket type not supported' - }, - EALREADY: { - errno: -37, - message: 'connection already in progress' - }, - EBADF: { - errno: -9, - message: 'bad file descriptor' - }, - EBUSY: { - errno: -16, - message: 'resource busy or locked' - }, - ECANCELED: { - errno: -89, - message: 'operation canceled' - }, - ECHARSET: { - errno: -4080, - message: 'invalid Unicode character' - }, - ECONNABORTED: { - errno: -53, - message: 'software caused connection abort' - }, - ECONNREFUSED: { - errno: -61, - message: 'connection refused' - }, - ECONNRESET: { - errno: -54, - message: 'connection reset by peer' - }, - EDESTADDRREQ: { - errno: -39, - message: 'destination address required' - }, - EEXIST: { - errno: -17, - message: 'file already exists' - }, - EFAULT: { - errno: -14, - message: 'bad address in system call argument' - }, - EFBIG: { - errno: -27, - message: 'file too large' - }, - EHOSTUNREACH: { - errno: -65, - message: 'host is unreachable' - }, - EINTR: { - errno: -4, - message: 'interrupted system call' - }, - EINVAL: { - errno: -22, - message: 'invalid argument' - }, - EIO: { - errno: -5, - message: 'i/o error' - }, - EISCONN: { - errno: -56, - message: 'socket is already connected' - }, - EISDIR: { - errno: -21, - message: 'illegal operation on a directory' - }, - ELOOP: { - errno: -62, - message: 'too many symbolic links encountered' - }, - EMFILE: { - errno: -24, - message: 'too many open files' - }, - EMSGSIZE: { - errno: -40, - message: 'message too long' - }, - ENAMETOOLONG: { - errno: -63, - message: 'name too long' - }, - ENETDOWN: { - errno: -50, - message: 'network is down' - }, - ENETUNREACH: { - errno: -51, - message: 'network is unreachable' - }, - ENFILE: { - errno: -23, - message: 'file table overflow' - }, - ENOBUFS: { - errno: -55, - message: 'no buffer space available' - }, - ENODEV: { - errno: -19, - message: 'no such device' - }, - ENOENT: { - errno: -2, - message: 'no such file or directory' - }, - ENOMEM: { - errno: -12, - message: 'not enough memory' - }, - ENONET: { - errno: -4056, - message: 'machine is not on the network' - }, - ENOPROTOOPT: { - errno: -42, - message: 'protocol not available' - }, - ENOSPC: { - errno: -28, - message: 'no space left on device' - }, - ENOSYS: { - errno: -78, - message: 'function not implemented' - }, - ENOTCONN: { - errno: -57, - message: 'socket is not connected' - }, - ENOTDIR: { - errno: -20, - message: 'not a directory' - }, - ENOTEMPTY: { - errno: -66, - message: 'directory not empty' - }, - ENOTSOCK: { - errno: -38, - message: 'socket operation on non-socket' - }, - ENOTSUP: { - errno: -45, - message: 'operation not supported on socket' - }, - EPERM: { - errno: -1, - message: 'operation not permitted' - }, - EPIPE: { - errno: -32, - message: 'broken pipe' - }, - EPROTO: { - errno: -100, - message: 'protocol error' - }, - EPROTONOSUPPORT: { - errno: -43, - message: 'protocol not supported' - }, - EPROTOTYPE: { - errno: -41, - message: 'protocol wrong type for socket' - }, - ERANGE: { - errno: -34, - message: 'result too large' - }, - EROFS: { - errno: -30, - message: 'read-only file system' - }, - ESHUTDOWN: { - errno: -58, - message: 'cannot send after transport endpoint shutdown' - }, - ESPIPE: { - errno: -29, - message: 'invalid seek' - }, - ESRCH: { - errno: -3, - message: 'no such process' - }, - ETIMEDOUT: { - errno: -60, - message: 'connection timed out' - }, - ETXTBSY: { - errno: -26, - message: 'text file is busy' - }, - EXDEV: { - errno: -18, - message: 'cross-device link not permitted' - }, - ENXIO: { - errno: -6, - message: 'no such device or address' - }, - EMLINK: { - errno: -31, - message: 'too many links' - }, - EHOSTDOWN: { - errno: -64, - message: 'host is down' - }, - EREMOTEIO: { - errno: -4030, - message: 'remote I/O error' - }, - ENOTTY: { - errno: -25, - message: 'inappropriate ioctl for device' - }, - EFTYPE: { - errno: -79, - message: 'inappropriate file type or format' - } -}; +if (uvBinding.errmap) { + // nodejs v8+ + uvBinding.errmap.forEach(function(value, errno) { + var code = value[0]; + var message = value[1]; + codes[code] = {errno: errno, message: message}; + }); +} else { + // nodejs v4 and v6 + Object.keys(uvBinding).forEach(function(key) { + if (key.startsWith('UV_')) { + var code = key.slice(3); + var errno = uvBinding[key]; + codes[code] = {errno: errno, message: key}; + } + }); +} /** * Create an error. @@ -353,6 +50,7 @@ function FSError(code, path) { Error.captureStackTrace(this, FSError); } FSError.prototype = new Error(); +FSError.codes = codes; /** * Error constructor. diff --git a/test/lib/index.spec.js b/test/lib/index.spec.js index de4adf44..40225648 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -1600,6 +1600,8 @@ describe('Mocking the file system', function() { it('fails for bad path', function(done) { fs.readFile('path/to/bogus', function(err, data) { assert.instanceOf(err, Error); + // windows has different errno for ENOENT + assert.equal(err.code, 'ENOENT'); done(); }); });