Skip to content

Commit

Permalink
feat: support bigint option in fs.stat, fs.lstat, and fs.fstat
Browse files Browse the repository at this point in the history
closes #321

BREAKING CHANGE: drop support of Nodejs before v12.0.0
  • Loading branch information
3cp committed May 9, 2021
1 parent ed48502 commit a9072c8
Show file tree
Hide file tree
Showing 35 changed files with 3,312 additions and 2,505 deletions.
8 changes: 8 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"env": {
"es6": true
},
"globals": {
"BigInt": "readonly"
}
}
3 changes: 0 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ jobs:
- macos-latest
- windows-latest
node:
- 6
- 8
- 10
- 12
- 14
- 16
Expand Down
275 changes: 38 additions & 237 deletions lib/binding.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,10 @@ const MODE_TO_KTYPE = {
const fsBinding = process.binding('fs');
const kUsePromises = fsBinding.kUsePromises;
let statValues;
let bigintStatValues;
if (fsBinding.statValues) {
statValues = fsBinding.statValues; // node 10+
} else if (fsBinding.getStatValues) {
statValues = fsBinding.getStatValues(); // node 8
} else {
statValues = [];
}

// nodejs v6,8,10 and v12 before v12.10.0 has length 28
// nodejs v12.10.0+ has length 36
const statContainsNs = statValues.length > 28;

/** Introduction of BigUint64Array in 10.5 */
let BigUint64Array;
if (global.BigUint64Array) {
BigUint64Array = global.BigUint64Array;
} else {
BigUint64Array = function() {};
bigintStatValues = fsBinding.bigintStatValues;
}

const MAX_LINKS = 50;
Expand Down Expand Up @@ -127,29 +113,6 @@ function normalizeCallback(callback) {
return callback;
}

/**
* Handle stat optimizations introduced in Node 8.
* See https://github.com/nodejs/node/pull/11665.
* @param {Function} callback The callback.
* @return {Function} The wrapped callback.
*/
function wrapStatsCallback(callback) {
if (callback && typeof callback.oncomplete === 'function') {
// Unpack callback from FSReqWrap
callback = callback.oncomplete.bind(callback);
}
if (typeof callback === 'function') {
return function(err, stats) {
if (stats) {
fillStatsArray(stats, statValues);
}
callback.apply(this, arguments);
};
} else {
return callback;
}
}

function getDirentType(mode) {
const ktype = MODE_TO_KTYPE[mode & constants.S_IFMT];

Expand All @@ -168,78 +131,6 @@ function deBuffer(p) {
return Buffer.isBuffer(p) ? p.toString() : p;
}

/**
* Create a new stats object.
* @param {Object} config Stats properties.
* @constructor
*/
function Stats(config) {
for (const key in config) {
this[key] = config[key];
}
// node 10 expects an array internally
// see https://github.com/nodejs/node/pull/19714
fillStatsArray(config, this);
}

/**
* Check if mode indicates property.
* @param {number} property Property to check.
* @return {boolean} Property matches mode.
*/
Stats.prototype._checkModeProperty = function(property) {
return (this.mode & constants.S_IFMT) === property;
};

/**
* @return {Boolean} Is a directory.
*/
Stats.prototype.isDirectory = function() {
return this._checkModeProperty(constants.S_IFDIR);
};

/**
* @return {Boolean} Is a regular file.
*/
Stats.prototype.isFile = function() {
return this._checkModeProperty(constants.S_IFREG);
};

/**
* @return {Boolean} Is a block device.
*/
Stats.prototype.isBlockDevice = function() {
return this._checkModeProperty(constants.S_IFBLK);
};

/**
* @return {Boolean} Is a character device.
*/
Stats.prototype.isCharacterDevice = function() {
return this._checkModeProperty(constants.S_IFCHR);
};

/**
* @return {Boolean} Is a symbolic link.
*/
Stats.prototype.isSymbolicLink = function() {
return this._checkModeProperty(constants.S_IFLNK);
};

/**
* @return {Boolean} Is a named pipe.
*/
Stats.prototype.isFIFO = function() {
return this._checkModeProperty(constants.S_IFIFO);
};

/**
* @return {Boolean} Is a socket.
*/
Stats.prototype.isSocket = function() {
return this._checkModeProperty(constants.S_IFSOCK);
};

/**
* Create a new binding with the given file system.
* @param {FileSystem} system Mock file system.
Expand All @@ -252,12 +143,6 @@ function Binding(system) {
*/
this._system = system;

/**
* Stats constructor.
* @type {function}
*/
this.Stats = Stats;

/**
* Lookup of open files.
* @type {Object.<number, FileDescriptor>}
Expand Down Expand Up @@ -390,67 +275,32 @@ Binding.prototype.realpath = function(filepath, encoding, callback, ctx) {
});
};

/**
* Fill a Float64Array with stat information
* This is based on the internal FillStatsArray function in Node.
* https://github.com/nodejs/node/blob/4e05952a8a75af6df625415db612d3a9a1322682/src/node_file.cc#L533
* @param {Object} stats An object with file stats
* @param {Float64Array} statValues A Float64Array where stat values should be inserted
* @returns {void}
*/
function fillStatsArray(stats, statValues) {
statValues[0] = stats.dev;
statValues[1] = stats.mode;
statValues[2] = stats.nlink;
statValues[3] = stats.uid;
statValues[4] = stats.gid;
statValues[5] = stats.rdev;
statValues[6] = stats.blksize;
statValues[7] = stats.ino;
statValues[8] = stats.size;
statValues[9] = stats.blocks;

if (statContainsNs) {
// nodejs v12.10.0+
// This is based on the internal FillStatsArray function in Node.
// https://github.com/nodejs/node/blob/3a2e75d9a5c31d20e429d505b82dd182e33f459a/src/node_file.h#L153-L187
statValues[10] = Math.floor(stats.atimeMs / 1000);
statValues[11] = (stats.atimeMs % 1000) * 1000000;
statValues[12] = Math.floor(stats.mtimeMs / 1000);
statValues[13] = (stats.mtimeMs % 1000) * 1000000;
statValues[14] = Math.floor(stats.ctimeMs / 1000);
statValues[15] = (stats.ctimeMs % 1000) * 1000000;
statValues[16] = Math.floor(stats.birthtimeMs / 1000);
statValues[17] = (stats.birthtimeMs % 1000) * 1000000;
} else {
// nodejs before v12.10.0
// This is based on the internal FillStatsArray function in Node.
// https://github.com/nodejs/node/blob/4e05952a8a75af6df625415db612d3a9a1322682/src/node_file.cc#L533
statValues[10] = stats.atimeMs;
statValues[11] = stats.mtimeMs;
statValues[12] = stats.ctimeMs;
statValues[13] = stats.birthtimeMs;
function fillStats(stats) {
const target = stats instanceof Float64Array ?
statValues :
bigintStatValues;
for (let i = 0; i < 36; i++) {
target[i] = stats[i];
}
}

/**
* Stat an item.
* @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 {function(Error, Float64Array|BigUint64Array)} callback Callback (optional).
* @param {Object} ctx Context object (optional), only for nodejs v10+.
* @return {Stats|undefined} Stats or undefined (if sync).
* @return {Float64Array|BigUint64Array|undefined} Stats or undefined (if sync).
*/
Binding.prototype.stat = function(filepath, options, callback, ctx) {
// this seems wound not happen in nodejs v10+
Binding.prototype.stat = function(filepath, bigint, callback, ctx) {
// this would not happend in nodejs v10.5.0+
if (arguments.length < 3) {
callback = options;
options = {};
callback = bigint;
bigint = false;
}

markSyscall(ctx, 'stat');

return maybeCallback(wrapStatsCallback(callback), ctx, this, function() {
return maybeCallback(normalizeCallback(callback), ctx, this, function() {
filepath = deBuffer(filepath);
let item = this._system.getItem(filepath);
if (item instanceof SymbolicLink) {
Expand All @@ -461,56 +311,34 @@ Binding.prototype.stat = function(filepath, options, callback, ctx) {
if (!item) {
throw new FSError('ENOENT', filepath);
}
const stats = item.getStats();

// In Node 7.7.0+, binding.stat accepts a Float64Array as the second argument,
// which should be filled with stat values.
// In prior versions of Node, binding.stat simply returns a Stats instance.
if (
callback instanceof Float64Array ||
callback instanceof BigUint64Array
) {
fillStatsArray(stats, callback);
} else {
fillStatsArray(stats, statValues);
return new Stats(stats);
}
const stats = item.getStats(bigint);
fillStats(stats);
return stats;
});
};

/**
* Stat an item.
* @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 {function(Error, Float64Array|BigUint64Array)} callback Callback (optional).
* @param {Object} ctx Context object (optional), only for nodejs v10+.
* @return {Stats|undefined} Stats or undefined (if sync).
* @return {Float64Array|BigUint64Array|undefined} Stats or undefined (if sync).
*/
Binding.prototype.fstat = function(fd, options, callback, ctx) {
Binding.prototype.fstat = function(fd, bigint, callback, ctx) {
// this would not happend in nodejs v10.5.0+
if (arguments.length < 3) {
callback = options;
options = {};
callback = bigint;
bigint = false;
}

markSyscall(ctx, 'fstat');

return maybeCallback(wrapStatsCallback(callback), ctx, this, function() {
return maybeCallback(normalizeCallback(callback), ctx, this, function() {
const descriptor = this.getDescriptorById(fd);
const item = descriptor.getItem();
const stats = item.getStats();

// In Node 7.7.0+, binding.stat accepts a Float64Array as the second argument,
// which should be filled with stat values.
// In prior versions of Node, binding.stat simply returns a Stats instance.
if (
callback instanceof Float64Array ||
callback instanceof BigUint64Array
) {
fillStatsArray(stats, callback);
} else {
fillStatsArray(stats, statValues);
return new Stats(stats);
}
const stats = item.getStats(bigint);
fillStats(stats);
return stats;
});
};

Expand Down Expand Up @@ -938,14 +766,6 @@ Binding.prototype.readdir = function(
callback,
ctx
) {
// again, the shorter arguments would not happen in nodejs v10+
if (arguments.length === 2) {
callback = encoding;
encoding = 'utf-8';
} else if (arguments.length === 3) {
callback = withFileTypes;
}

markSyscall(ctx, 'scandir');

return maybeCallback(normalizeCallback(callback), ctx, this, function() {
Expand Down Expand Up @@ -995,13 +815,6 @@ Binding.prototype.readdir = function(
* @param {Object} ctx Context object (optional), only for nodejs v10+.
*/
Binding.prototype.mkdir = function(pathname, mode, recursive, callback, ctx) {
if (typeof recursive !== 'boolean') {
// when running nodejs < 10
ctx = callback;
callback = recursive;
recursive = false;
}

markSyscall(ctx, 'mkdir');

return maybeCallback(normalizeCallback(callback), ctx, this, function() {
Expand Down Expand Up @@ -1434,40 +1247,28 @@ Binding.prototype.readlink = function(pathname, encoding, callback, ctx) {
/**
* Stat an item.
* @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 {function(Error, Float64Array|BigUint64Array)} callback Callback (optional).
* @param {Object} ctx Context object (optional), only for nodejs v10+.
* @return {Stats|undefined} Stats or undefined (if sync).
* @return {Float64Array|BigUint64Array|undefined} Stats or undefined (if sync).
*/
Binding.prototype.lstat = function(filepath, options, callback, ctx) {
Binding.prototype.lstat = function(filepath, bigint, callback, ctx) {
if (arguments.length < 3) {
// this would not happend in nodejs v10+
callback = options;
options = {};
// this would not happend in nodejs v10.5.0+
callback = bigint;
bigint = false;
}

markSyscall(ctx, 'lstat');

return maybeCallback(wrapStatsCallback(callback), ctx, this, function() {
return maybeCallback(normalizeCallback(callback), ctx, this, function() {
filepath = deBuffer(filepath);
const item = this._system.getItem(filepath);
if (!item) {
throw new FSError('ENOENT', filepath);
}
const stats = item.getStats();

// In Node 7.7.0+, binding.stat accepts a Float64Array as the second argument,
// which should be filled with stat values.
// In prior versions of Node, binding.stat simply returns a Stats instance.
if (
callback instanceof Float64Array ||
callback instanceof BigUint64Array
) {
fillStatsArray(stats, callback);
} else {
fillStatsArray(stats, statValues);
return new Stats(item.getStats());
}
const stats = item.getStats(bigint);
fillStats(stats);
return stats;
});
};

Expand Down
Loading

0 comments on commit a9072c8

Please # to comment.