Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ jobs:
- 14
- 16
- 18
- 20
- 22

steps:
- name: Clone repository
Expand Down
175 changes: 172 additions & 3 deletions lib/binding.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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+.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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(
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
});
};

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()}
Expand Down
13 changes: 12 additions & 1 deletion lib/descriptor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -25,6 +26,8 @@ function FileDescriptor(flags) {
* @type {number}
*/
this._position = 0;

this._isPromise = isPromise;
}

/**
Expand Down Expand Up @@ -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()}
Expand Down
24 changes: 24 additions & 0 deletions lib/filesystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -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;
};
Expand Down
2 changes: 1 addition & 1 deletion test/lib/bypass.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)));

Expand Down
Loading
Loading