diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 005acbc..24e50c2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,6 +26,8 @@ jobs: node: - 16 - 18 + - 20 + - 22 steps: - name: Clone repository diff --git a/lib/binding.js b/lib/binding.js index a4a3e6c..3870308 100644 --- a/lib/binding.js +++ b/lib/binding.js @@ -44,7 +44,7 @@ function maybeCallback(callback, ctx, thisArg, func) { let err = null; let val; - if (kUsePromises && callback === kUsePromises) { + if (usePromises(callback)) { // support nodejs v10+ fs.promises try { val = func.call(thisArg); @@ -86,6 +86,10 @@ function maybeCallback(callback, ctx, thisArg, func) { } } +function usePromises(callback) { + return kUsePromises && callback === kUsePromises; +} + /** * set syscall property on context object, only for nodejs v10+. * @param {object} ctx Context object (optional), only for nodejs v10+. @@ -306,6 +310,17 @@ Binding.prototype.stat = function (filepath, bigint, callback, ctx) { }); }; +/** + * Stat an item. + * @param {string} filepath Path. + * @param {boolean} bigint Use BigInt. + * @param {object} ctx Context object (optional), only for nodejs v10+. + * @return {Float64Array|BigUint64Array|undefined} Stats or undefined if sync. + */ +Binding.prototype.statSync = function (filepath, bigint, ctx) { + return this.stat(filepath, bigint, undefined, ctx); +}; + /** * Stat an item. * @param {number} fd File descriptor. @@ -341,6 +356,16 @@ Binding.prototype.close = function (fd, callback, ctx) { }); }; +/** + * Close a file descriptor. + * @param {number} fd File descriptor. + * @param {object} ctx Context object (optional), only for nodejs v10+. + * @return {*} The return. + */ +Binding.prototype.closeSync = function (fd, ctx) { + return this.close(fd, undefined, ctx); +}; + /** * Open and possibly create a file. * @param {string} pathname File path. @@ -355,7 +380,7 @@ Binding.prototype.open = function (pathname, flags, mode, callback, ctx) { return maybeCallback(normalizeCallback(callback), ctx, this, function () { pathname = deBuffer(pathname); - const descriptor = new FileDescriptor(flags); + const descriptor = new FileDescriptor(flags, usePromises(callback)); let item = this._system.getItem(pathname); while (item instanceof SymbolicLink) { item = this._system.getItem( @@ -410,6 +435,18 @@ Binding.prototype.open = function (pathname, flags, mode, callback, ctx) { }); }; +/** + * Open and possibly create a file. + * @param {string} pathname File path. + * @param {number} flags Flags. + * @param {number} mode Mode. + * @param {object} ctx Context object (optional), only for nodejs v10+. + * @return {string} File descriptor. + */ +Binding.prototype.openSync = function (pathname, flags, mode, ctx) { + return this.open(pathname, flags, mode, undefined, ctx); +}; + /** * Open a file handler. A new api in nodejs v10+ for fs.promises * @param {string} pathname File path. @@ -531,6 +568,18 @@ Binding.prototype.copyFile = function (src, dest, flags, callback, ctx) { }); }; +/** + * Write to a file descriptor given a buffer. + * @param {string} src Source file. + * @param {string} dest Destination file. + * @param {number} flags Modifiers for copy operation. + * @param {object} ctx Context object (optional), only for nodejs v10+. + * @return {*} The return if no callback is provided. + */ +Binding.prototype.copyFileSync = function (src, dest, flags, ctx) { + return this.copyFile(src, dest, flags, undefined, ctx); +}; + /** * Write to a file descriptor given a buffer. * @param {string} fd File descriptor. @@ -632,7 +681,12 @@ Binding.prototype.writeBuffer = function ( ); file.setContent(content); descriptor.setPosition(newLength); - return written; + // If we're in fs.promises / FileHandle we need to return a promise + // Both fs.promises.open().then(fd => fs.write()) + // and fs.openSync().writeSync() use this function + // without a callback, so we have to check if the descriptor was opened + // with kUsePromises + return descriptor.isPromise() ? Promise.resolve(written) : written; }); }; @@ -722,6 +776,17 @@ Binding.prototype.rename = function (oldPath, newPath, callback, ctx) { }); }; +/** + * Rename a file. + * @param {string} oldPath Old pathname. + * @param {string} newPath New pathname. + * @param {object} ctx Context object (optional), only for nodejs v10+. + * @return {undefined} + */ +Binding.prototype.renameSync = function (oldPath, newPath, ctx) { + return this.rename(oldPath, newPath, undefined, ctx); +}; + /** * Read a directory. * @param {string} dirpath Path to directory. @@ -779,6 +844,43 @@ Binding.prototype.readdir = function ( }); }; +/** + * Read file as utf8 string. + * @param {string} name file to write. + * @param {number} flags Flags. + * @return {string} the file content. + */ +Binding.prototype.readFileUtf8 = function (name, flags) { + const fd = this.open(name, flags); + const descriptor = this.getDescriptorById(fd); + + if (!descriptor.isRead()) { + throw new FSError('EBADF'); + } + const file = descriptor.getItem(); + if (file instanceof Directory) { + throw new FSError('EISDIR'); + } + if (!(file instanceof File)) { + // deleted or not a regular file + throw new FSError('EBADF'); + } + const content = file.getContent(); + return content.toString('utf8'); +}; + +/** + * Write a utf8 string. + * @param {string} filepath file to write. + * @param {string} data data to write to filepath. + * @param {number} flags Flags. + * @param {number} mode Mode. + */ +Binding.prototype.writeFileUtf8 = function (filepath, data, flags, mode) { + const destFd = this.open(filepath, flags, mode); + this.writeBuffer(destFd, data, 0, data.length); +}; + /** * Create a directory. * @param {string} pathname Path to new directory. @@ -1059,6 +1161,16 @@ Binding.prototype.unlink = function (pathname, callback, ctx) { }); }; +/** + * Delete a named item. + * @param {string} pathname Path to item. + * @param {object} ctx Context object (optional), only for nodejs v10+. + * @return {*} The return if no callback is provided. + */ +Binding.prototype.unlinkSync = function (pathname, ctx) { + return this.unlink(pathname, undefined, ctx); +}; + /** * Update timestamps. * @param {string} pathname Path to item. @@ -1241,6 +1353,18 @@ Binding.prototype.symlink = function (srcPath, destPath, type, callback, ctx) { }); }; +/** + * Create a symbolic link. + * @param {string} srcPath Path from link to the source file. + * @param {string} destPath Path for the generated link. + * @param {string} type Ignored (used for Windows only). + * @param {object} ctx Context object (optional), only for nodejs v10+. + * @return {*} The return if no callback is provided. + */ +Binding.prototype.symlinkSync = function (srcPath, destPath, type, ctx) { + return this.symlink(srcPath, destPath, type, undefined, ctx); +}; + /** * Read the contents of a symbolic link. * @param {string} pathname Path to symbolic link. @@ -1338,6 +1462,51 @@ Binding.prototype.access = function (filepath, mode, callback, ctx) { }); }; +/** + * Tests user permissions. + * @param {string} filepath Path. + * @param {number} mode Mode. + * @param {object} ctx Context object (optional), only for nodejs v10+. + * @return {*} The return if no callback is provided. + */ +Binding.prototype.accessSync = function (filepath, mode, ctx) { + return this.access(filepath, mode, undefined, ctx); +}; + +/** + * Tests whether or not the given path exists. + * @param {string} filepath Path. + * @param {function(Error)} callback Callback (optional). + * @param {object} ctx Context object (optional), only for nodejs v10+. + * @return {*} The return if no callback is provided. + */ +Binding.prototype.exists = function (filepath, callback, ctx) { + markSyscall(ctx, 'exists'); + + return maybeCallback(normalizeCallback(callback), ctx, this, function () { + filepath = deBuffer(filepath); + const item = this._system.getItem(filepath); + + if (item) { + if (item instanceof SymbolicLink) { + return this.exists(item.getPath(), callback, ctx); + } + return true; + } + return false; + }); +}; + +/** + * Tests whether or not the given path exists. + * @param {string} filepath Path. + * @param {object} ctx Context object (optional), only for nodejs v10+. + * @return {*} The return if no callback is provided. + */ +Binding.prototype.existsSync = function (filepath, ctx) { + return this.exists(filepath, undefined, ctx); +}; + /** * Not yet implemented. * @type {function()} diff --git a/lib/descriptor.js b/lib/descriptor.js index 107e25c..1ad2095 100644 --- a/lib/descriptor.js +++ b/lib/descriptor.js @@ -5,9 +5,10 @@ const constants = require('constants'); /** * Create a new file descriptor. * @param {number} flags Flags. + * @param {boolean} isPromise descriptor was opened via fs.promises * @class */ -function FileDescriptor(flags) { +function FileDescriptor(flags, isPromise = false) { /** * Flags. * @type {number} @@ -25,6 +26,8 @@ function FileDescriptor(flags) { * @type {number} */ this._position = 0; + + this._isPromise = isPromise; } /** @@ -110,6 +113,14 @@ FileDescriptor.prototype.isExclusive = function () { return (this._flags & constants.O_EXCL) === constants.O_EXCL; }; +/** + * Check if the file descriptor was opened as a promise + * @return {boolean} Opened from fs.promise + */ +FileDescriptor.prototype.isPromise = function () { + return this._isPromise; +}; + /** * Export the constructor. * @type {function()} diff --git a/lib/filesystem.js b/lib/filesystem.js index dd0b431..aebba26 100644 --- a/lib/filesystem.js +++ b/lib/filesystem.js @@ -264,15 +264,23 @@ FileSystem.file = function (config) { } if (config.hasOwnProperty('atime')) { file.setATime(config.atime); + } else if (config.hasOwnProperty('atimeMs')) { + file.setATime(new Date(config.atimeMs)); } if (config.hasOwnProperty('ctime')) { file.setCTime(config.ctime); + } else if (config.hasOwnProperty('ctimeMs')) { + file.setCTime(new Date(config.ctimeMs)); } if (config.hasOwnProperty('mtime')) { file.setMTime(config.mtime); + } else if (config.hasOwnProperty('mtimeMs')) { + file.setMTime(new Date(config.mtimeMs)); } if (config.hasOwnProperty('birthtime')) { file.setBirthtime(config.birthtime); + } else if (config.hasOwnProperty('birthtimeMs')) { + file.setBirthtime(new Date(config.birthtimeMs)); } return file; }; @@ -305,15 +313,23 @@ FileSystem.symlink = function (config) { } if (config.hasOwnProperty('atime')) { link.setATime(config.atime); + } else if (config.hasOwnProperty('atimeMs')) { + link.setATime(new Date(config.atimeMs)); } if (config.hasOwnProperty('ctime')) { link.setCTime(config.ctime); + } else if (config.hasOwnProperty('ctimeMs')) { + link.setCTime(new Date(config.ctimeMs)); } if (config.hasOwnProperty('mtime')) { link.setMTime(config.mtime); + } else if (config.hasOwnProperty('mtimeMs')) { + link.setMTime(new Date(config.mtimeMs)); } if (config.hasOwnProperty('birthtime')) { link.setBirthtime(config.birthtime); + } else if (config.hasOwnProperty('birthtimeMs')) { + link.setBirthtime(new Date(config.birthtimeMs)); } return link; }; @@ -344,15 +360,23 @@ FileSystem.directory = function (config) { } if (config.hasOwnProperty('atime')) { dir.setATime(config.atime); + } else if (config.hasOwnProperty('atimeMs')) { + dir.setATime(new Date(config.atimeMs)); } if (config.hasOwnProperty('ctime')) { dir.setCTime(config.ctime); + } else if (config.hasOwnProperty('ctimeMs')) { + dir.setCTime(new Date(config.ctimeMs)); } if (config.hasOwnProperty('mtime')) { dir.setMTime(config.mtime); + } else if (config.hasOwnProperty('mtimeMs')) { + dir.setMTime(new Date(config.mtimeMs)); } if (config.hasOwnProperty('birthtime')) { dir.setBirthtime(config.birthtime); + } else if (config.hasOwnProperty('birthtimeMs')) { + dir.setBirthtime(new Date(config.birthtimeMs)); } return dir; }; diff --git a/test/lib/bypass.spec.js b/test/lib/bypass.spec.js index 23bbc49..078cf81 100644 --- a/test/lib/bypass.spec.js +++ b/test/lib/bypass.spec.js @@ -13,7 +13,7 @@ describe('mock.bypass()', () => { it('runs a synchronous function using the real filesystem', () => { mock({'/path/to/file': 'content'}); - assert.equal(fs.readFileSync('/path/to/file', 'utf8'), 'content'); + assert.equal(fs.readFileSync('/path/to/file', 'utf-8'), 'content'); assert.isNotOk(fs.existsSync(__filename)); assert.isOk(mock.bypass(() => fs.existsSync(__filename))); diff --git a/test/lib/index.spec.js b/test/lib/index.spec.js index 65d2314..c8639ab 100644 --- a/test/lib/index.spec.js +++ b/test/lib/index.spec.js @@ -191,7 +191,6 @@ describe('The API', function () { 'mtime', 'gid', 'uid', - 'mtime', 'mode', ]; const filterStats = (stats) => { @@ -199,12 +198,15 @@ describe('The API', function () { for (const key of statsCompareKeys) { const k = (stats.hasOwnProperty(key) && key) || - (stats.hasOwnProperty(`_${key}`) && `_${key}`); + (stats.hasOwnProperty(`_${key}`) && `_${key}`) || + (stats.hasOwnProperty(`${key}Ms`) && `${key}Ms`); if (k) { res[key] = k === 'mode' && stats.isDirectory() ? fixWin32Permissions(stats[k]) + : k.endsWith('Ms') + ? new Date(stats[k]) : stats[k]; } }