Skip to content

Commit

Permalink
Merge pull request #335 from Rugvip/readfilecontext
Browse files Browse the repository at this point in the history
Add support for Node.js v16.3.0+
  • Loading branch information
tschaub committed Sep 17, 2021
2 parents 535a948 + d7237ca commit ca4cfa2
Show file tree
Hide file tree
Showing 6 changed files with 366 additions and 5 deletions.
2 changes: 1 addition & 1 deletion lib/binding.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const File = require('./file');
const FileDescriptor = require('./descriptor');
const Directory = require('./directory');
const SymbolicLink = require('./symlink');
const FSError = require('./error');
const {FSError} = require('./error');
const constants = require('constants');
const getPathParts = require('./filesystem').getPathParts;

Expand Down
20 changes: 18 additions & 2 deletions lib/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ FSError.prototype = new Error();
FSError.codes = codes;

/**
* Error constructor.
* Create an abort error for when an asynchronous task was aborted.
* @constructor
*/
function AbortError() {
Error.call(this);
this.code = 'ABORT_ERR';
this.name = 'AbortError';
Error.captureStackTrace(this, AbortError);
}
AbortError.prototype = new Error();

/**
* FSError constructor.
*/
exports.FSError = FSError;
/**
* AbortError constructor.
*/
exports = module.exports = FSError;
exports.AbortError = AbortError;
2 changes: 1 addition & 1 deletion lib/filesystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const path = require('path');

const Directory = require('./directory');
const File = require('./file');
const FSError = require('./error');
const {FSError} = require('./error');
const SymbolicLink = require('./symlink');

const isWindows = process.platform === 'win32';
Expand Down
21 changes: 20 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
'use strict';

const Binding = require('./binding');
const FSError = require('./error');
const {FSError} = require('./error');
const FileSystem = require('./filesystem');
const realBinding = process.binding('fs');
const path = require('path');
const loader = require('./loader');
const bypass = require('./bypass');
const {
getReadFileContextPrototype,
patchReadFileContext
} = require('./readfilecontext');
const fs = require('fs');

const toNamespacedPath = FileSystem.toNamespacedPath;
Expand Down Expand Up @@ -50,6 +54,10 @@ for (const key in Binding.prototype) {
}
}

const readFileContextPrototype = getReadFileContextPrototype();

patchReadFileContext(readFileContextPrototype);

function overrideBinding(binding) {
realBinding._mockedBinding = binding;
}
Expand Down Expand Up @@ -94,6 +102,10 @@ function overrideCreateWriteStream() {
};
}

function overrideReadFileContext(binding) {
readFileContextPrototype._mockedBinding = binding;
}

function restoreBinding() {
delete realBinding._mockedBinding;
realBinding.Stats = realStats;
Expand All @@ -110,6 +122,10 @@ function restoreCreateWriteStream() {
fs.createWriteStream = realCreateWriteStream;
}

function restoreReadFileContext(binding) {
delete readFileContextPrototype._mockedBinding;
}

/**
* Swap out the fs bindings for a mock file system.
* @param {Object} config Mock file system configuration.
Expand All @@ -125,6 +141,8 @@ exports = module.exports = function mock(config, options) {

overrideBinding(binding);

overrideReadFileContext(binding);

let currentPath = process.cwd();
overrideProcess(
function cwd() {
Expand Down Expand Up @@ -167,6 +185,7 @@ exports.restore = function() {
restoreBinding();
restoreProcess();
restoreCreateWriteStream();
restoreReadFileContext();
};

/**
Expand Down
152 changes: 152 additions & 0 deletions lib/readfilecontext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
'use strict';

const {AbortError} = require('./error');
const {FSReqCallback} = process.binding('fs');

/**
* This is a workaround for getting access to the ReadFileContext
* prototype, which we need to be able to patch its methods.
* @returns {object}
*/
exports.getReadFileContextPrototype = function() {
const fs = require('fs');
const fsBinding = process.binding('fs');

const originalOpen = fsBinding.open;

let proto;
fsBinding.open = (_path, _flags, _mode, req) => {
proto = Object.getPrototypeOf(req.context);
};

fs.readFile('/ignored.txt', () => {});

fsBinding.open = originalOpen;

return proto;
};

/**
* This patches the ReadFileContext prototype to use mocked bindings
* when available. This entire implementation is more or less fully
* copied over from Node.js's /lib/internal/fs/read_file_context.js
*
* This patch is required to support Node.js v16+, where the ReadFileContext
* closes directly over the internal fs bindings, and is also eagerly loader.
*
* See https://github.com/tschaub/mock-fs/issues/332 for more information.
*
* @param {object} prototype The ReadFileContext prototype object to patch.
*/
exports.patchReadFileContext = function(prototype) {
const origRead = prototype.read;
const origClose = prototype.close;

const kReadFileUnknownBufferLength = 64 * 1024;
const kReadFileBufferLength = 512 * 1024;

function readFileAfterRead(err, bytesRead) {
const context = this.context;

if (err) {
return context.close(err);
}
context.pos += bytesRead;

if (context.pos === context.size || bytesRead === 0) {
context.close();
} else {
if (context.size === 0) {
// Unknown size, just read until we don't get bytes.
const buffer =
bytesRead === kReadFileUnknownBufferLength
? context.buffer
: context.buffer.slice(0, bytesRead);
context.buffers.push(buffer);
}
context.read();
}
}

function readFileAfterClose(err) {
const context = this.context;
const callback = context.callback;
let buffer = null;

if (context.err || err) {
// This is a simplification from Node.js, where we don't bother merging the errors
return callback(context.err || err);
}

try {
if (context.size === 0) {
buffer = Buffer.concat(context.buffers, context.pos);
} else if (context.pos < context.size) {
buffer = context.buffer.slice(0, context.pos);
} else {
buffer = context.buffer;
}

if (context.encoding) {
buffer = buffer.toString(context.encoding);
}
} catch (err) {
return callback(err);
}

callback(null, buffer);
}

prototype.read = function read() {
if (!prototype._mockedBinding) {
return origRead.apply(this, arguments);
}

let buffer;
let offset;
let length;

if (this.signal && this.signal.aborted) {
return this.close(new AbortError());
}
if (this.size === 0) {
buffer = Buffer.allocUnsafeSlow(kReadFileUnknownBufferLength);
offset = 0;
length = kReadFileUnknownBufferLength;
this.buffer = buffer;
} else {
buffer = this.buffer;
offset = this.pos;
length = Math.min(kReadFileBufferLength, this.size - this.pos);
}

const req = new FSReqCallback();
req.oncomplete = readFileAfterRead;
req.context = this;

// This call and the one in close() is what we want to change, the
// rest is pretty much the same as Node.js except we don't have access
// to some of the internal optimizations.
prototype._mockedBinding.read(this.fd, buffer, offset, length, -1, req);
};

prototype.close = function close(err) {
if (!prototype._mockedBinding) {
return origClose.apply(this, arguments);
}

if (this.isUserFd) {
process.nextTick(function tick(context) {
readFileAfterClose.apply({context}, [null]);
}, this);
return;
}

const req = new FSReqCallback();
req.oncomplete = readFileAfterClose;
req.context = this;
this.err = err;

prototype._mockedBinding.close(this.fd, req);
};
};
Loading

0 comments on commit ca4cfa2

Please # to comment.