/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * * @format */ "use strict"; /* eslint-disable no-bitwise */ // $FlowFixMe: not defined by Flow function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } const constants = require("constants"); const stream = require("stream"); const _require = require("events"), EventEmitter = _require.EventEmitter; const FLAGS_SPECS = { r: { mustExist: true, readable: true }, "r+": { mustExist: true, readable: true, writable: true }, "rs+": { mustExist: true, readable: true, writable: true }, w: { truncate: true, writable: true }, wx: { exclusive: true, truncate: true, writable: true }, "w+": { readable: true, truncate: true, writable: true }, "wx+": { exclusive: true, readable: true, truncate: true, writable: true } }; const ASYNC_FUNC_NAMES = [ "access", "close", "copyFile", "fstat", "fsync", "fdatasync", "lstat", "open", "read", "readdir", "readFile", "readlink", "realpath", "stat", "unlink", "write", "writeFile" ]; /** * Simulates `fs` API in an isolated, memory-based filesystem. This is useful * for testing systems that rely on `fs` without affecting the real filesystem. * This is meant to be a drop-in replacement/mock for `fs`, so it mimics * closely the behavior of file path resolution and file accesses. */ class MemoryFs { constructor(_options) { var _this = this; _defineProperty(this, "constants", constants); _defineProperty(this, "accessSync", (filePath, mode) => { if (mode == null) { mode = constants.F_OK; } const stats = this.statSync(filePath); if (mode == constants.F_OK) { return; } const filePathStr = pathStr(filePath); if ((mode & constants.R_OK) !== 0) { if ( !( (stats.mode & constants.S_IROTH) !== 0 || ((stats.mode & constants.S_IRGRP) !== 0 && stats.gid === getgid()) || ((stats.mode & constants.S_IRUSR) !== 0 && stats.uid === getuid()) ) ) { throw makeError("EPERM", filePathStr, "file cannot be read"); } } if ((mode & constants.W_OK) !== 0) { if ( !( (stats.mode & constants.S_IWOTH) !== 0 || ((stats.mode & constants.S_IWGRP) !== 0 && stats.gid === getgid()) || ((stats.mode & constants.S_IWUSR) !== 0 && stats.uid === getuid()) ) ) { throw makeError("EPERM", filePathStr, "file cannot be written to"); } } if ((mode & constants.X_OK) !== 0) { if ( !( (stats.mode & constants.S_IXOTH) !== 0 || ((stats.mode & constants.S_IXGRP) !== 0 && stats.gid === getgid()) || ((stats.mode & constants.S_IXUSR) !== 0 && stats.uid === getuid()) ) ) { throw makeError("EPERM", filePathStr, "file cannot be executed"); } } }); _defineProperty(this, "closeSync", fd => { const desc = this._getDesc(fd); if (desc.writable) { this._emitFileChange(desc.nodePath.slice(), { eventType: "change" }); } this._fds.delete(fd); }); _defineProperty(this, "copyFileSync", function(src, dest) { let flags = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; const options = flags & constants.COPYFILE_EXCL ? { flag: "wx" } : {}; _this.writeFileSync(dest, _this.readFileSync(src), options); }); _defineProperty(this, "fsyncSync", fd => { this._getDesc(fd); }); _defineProperty(this, "fdatasyncSync", fd => { this._getDesc(fd); }); _defineProperty(this, "openSync", (filePath, flags, mode) => { if (typeof flags === "number") { throw new Error(`numeric flags not supported: ${flags}`); } return this._open(pathStr(filePath), flags, mode); }); _defineProperty( this, "readSync", (fd, buffer, offset, length, position) => { const desc = this._getDesc(fd); if (!desc.readable) { throw makeError( "EBADF", null, "file descriptor cannot be written to" ); } if (position != null) { desc.position = position; } const endPos = Math.min( desc.position + length, desc.node.content.length ); desc.node.content.copy(buffer, offset, desc.position, endPos); const bytesRead = endPos - desc.position; desc.position = endPos; return bytesRead; } ); _defineProperty(this, "readdirSync", (filePath, options) => { let encoding; if (typeof options === "string") { encoding = options; } else if (options != null) { encoding = options.encoding; } filePath = pathStr(filePath); const _this$_resolve = this._resolve(filePath), node = _this$_resolve.node; if (node == null) { throw makeError("ENOENT", filePath, "no such file or directory"); } if (node.type !== "directory") { throw makeError("ENOTDIR", filePath, "not a directory"); } return Array.from(node.entries.keys()).map(str => { if (encoding === "utf8") { return str; } const buffer = Buffer.from(str); if (encoding === "buffer") { return buffer; } return buffer.toString(encoding); }); }); _defineProperty(this, "readFileSync", (filePath, options) => { let encoding, flag; if (typeof options === "string") { encoding = options; } else if (options != null) { encoding = options.encoding; flag = options.flag; } const fd = this._open(pathStr(filePath), flag || "r"); const chunks = []; try { const buffer = Buffer.alloc(1024); let bytesRead; do { bytesRead = this.readSync(fd, buffer, 0, buffer.length, null); if (bytesRead === 0) { continue; } const chunk = Buffer.alloc(bytesRead); buffer.copy(chunk, 0, 0, bytesRead); chunks.push(chunk); } while (bytesRead > 0); } finally { this.closeSync(fd); } const result = Buffer.concat(chunks); if (encoding == null) { return result; } return result.toString(encoding); }); _defineProperty(this, "readlinkSync", (filePath, options) => { let encoding; if (typeof options === "string") { encoding = options; } else if (options != null) { encoding = options.encoding; } filePath = pathStr(filePath); const _this$_resolve2 = this._resolve(filePath, { keepFinalSymlink: true }), node = _this$_resolve2.node; if (node == null) { throw makeError("ENOENT", filePath, "no such file or directory"); } if (node.type !== "symbolicLink") { throw makeError("EINVAL", filePath, "entity is not a symlink"); } if (encoding == null || encoding === "utf8") { return node.target; } const buf = Buffer.from(node.target); if (encoding == "buffer") { return buf; } return buf.toString(encoding); }); _defineProperty(this, "realpathSync", filePath => { return this._resolve(pathStr(filePath)).realpath; }); _defineProperty( this, "writeSync", (fd, bufferOrString, offsetOrPosition, lengthOrEncoding, position) => { let encoding, offset, length, buffer; if (typeof bufferOrString === "string") { position = offsetOrPosition; encoding = lengthOrEncoding; buffer = Buffer.from(bufferOrString, encoding || "utf8"); } else { offset = offsetOrPosition; if ( lengthOrEncoding != null && typeof lengthOrEncoding !== "number" ) { throw new Error("invalid length"); } length = lengthOrEncoding; buffer = bufferOrString; } if (offset == null) { offset = 0; } if (length == null) { length = buffer.length; } return this._write(fd, buffer, offset, length, position); } ); _defineProperty(this, "writeFileSync", (filePathOrFd, data, options) => { let encoding, mode, flag; if (typeof options === "string") { encoding = options; } else if (options != null) { encoding = options.encoding; mode = options.mode; flag = options.flag; } if (encoding == null) { encoding = "utf8"; } if (typeof data === "string") { data = Buffer.from(data, encoding); } const fd = typeof filePathOrFd === "number" ? filePathOrFd : this._open(pathStr(filePathOrFd), flag || "w", mode); try { this._write(fd, data, 0, data.length); } finally { if (typeof filePathOrFd !== "number") { this.closeSync(fd); } } }); _defineProperty(this, "mkdirSync", (dirPath, mode) => { if (mode == null) { mode = 0o777; } dirPath = pathStr(dirPath); const _this$_resolve3 = this._resolve(dirPath), dirNode = _this$_resolve3.dirNode, node = _this$_resolve3.node, basename = _this$_resolve3.basename; if (node != null) { throw makeError("EEXIST", dirPath, "directory or file already exists"); } dirNode.entries.set(basename, this._makeDir(mode)); }); _defineProperty(this, "symlinkSync", (target, filePath, type) => { if (type == null) { type = "file"; } if (type !== "file") { throw new Error("symlink type not supported"); } filePath = pathStr(filePath); const _this$_resolve4 = this._resolve(filePath), dirNode = _this$_resolve4.dirNode, node = _this$_resolve4.node, basename = _this$_resolve4.basename; if (node != null) { throw makeError("EEXIST", filePath, "directory or file already exists"); } dirNode.entries.set(basename, { id: this._getId(), gid: getgid(), target: pathStr(target), mode: 0o666, uid: getuid(), type: "symbolicLink", watchers: [] }); }); _defineProperty(this, "existsSync", filePath => { try { const _this$_resolve5 = this._resolve(pathStr(filePath)), node = _this$_resolve5.node; return node != null; } catch (error) { if (error.code === "ENOENT") { return false; } throw error; } }); _defineProperty(this, "statSync", filePath => { filePath = pathStr(filePath); const _this$_resolve6 = this._resolve(filePath), node = _this$_resolve6.node; if (node == null) { throw makeError("ENOENT", filePath, "no such file or directory"); } return new Stats(node); }); _defineProperty(this, "lstatSync", filePath => { filePath = pathStr(filePath); const _this$_resolve7 = this._resolve(filePath, { keepFinalSymlink: true }), node = _this$_resolve7.node; if (node == null) { throw makeError("ENOENT", filePath, "no such file or directory"); } return new Stats(node); }); _defineProperty(this, "fstatSync", fd => { const desc = this._getDesc(fd); return new Stats(desc.node); }); _defineProperty(this, "createReadStream", (filePath, options) => { let autoClose, encoding, fd, flags, mode, start, end, highWaterMark; if (typeof options === "string") { encoding = options; } else if (options != null) { autoClose = options.autoClose; encoding = options.encoding; fd = options.fd; flags = options.flags; mode = options.mode; start = options.start; end = options.end; highWaterMark = options.highWaterMark; } let st = null; if (fd == null) { fd = this._open(pathStr(filePath), flags || "r", mode); process.nextTick(() => st.emit("open", fd)); } const ffd = fd; const readSync = this.readSync; const ropt = { filePath, encoding, fd, highWaterMark, start, end, readSync }; const rst = new ReadFileSteam(ropt); st = rst; if (autoClose !== false) { const doClose = () => { this.closeSync(ffd); rst.emit("close"); }; rst.on("end", doClose); rst.on("error", doClose); } return rst; }); _defineProperty(this, "unlinkSync", filePath => { filePath = pathStr(filePath); const _this$_resolve8 = this._resolve(filePath, { keepFinalSymlink: true }), basename = _this$_resolve8.basename, dirNode = _this$_resolve8.dirNode, dirPath = _this$_resolve8.dirPath, node = _this$_resolve8.node; if (node == null) { throw makeError("ENOENT", filePath, "no such file or directory"); } if (node.type !== "file" && node.type !== "symbolicLink") { throw makeError("EISDIR", filePath, "cannot unlink a directory"); } dirNode.entries.delete(basename); this._emitFileChange(dirPath.concat([[basename, node]]), { eventType: "rename" }); }); _defineProperty(this, "createWriteStream", (filePath, options) => { let autoClose, fd, flags, mode, start; if (typeof options !== "string" && options != null) { autoClose = options.autoClose; fd = options.fd; flags = options.flags; mode = options.mode; start = options.start; } let st = null; if (fd == null) { fd = this._open(pathStr(filePath), flags || "w", mode); process.nextTick(() => st.emit("open", fd)); } const ffd = fd; const ropt = { fd, writeSync: this._write.bind(this), filePath, start }; const rst = new WriteFileStream(ropt); st = rst; if (autoClose !== false) { const doClose = () => { this.closeSync(ffd); rst.emit("close"); }; rst.on("finish", doClose); rst.on("error", doClose); } return st; }); _defineProperty(this, "watch", (filePath, options, listener) => { filePath = pathStr(filePath); const _this$_resolve9 = this._resolve(filePath), node = _this$_resolve9.node; if (node == null) { throw makeError("ENOENT", filePath, "no such file or directory"); } let encoding, recursive, persistent; if (typeof options === "string") { encoding = options; } else if (options != null) { encoding = options.encoding; recursive = options.recursive; persistent = options.persistent; } const watcher = new FSWatcher(node, { encoding: encoding != null ? encoding : "utf8", recursive: recursive != null ? recursive : false, persistent: persistent != null ? persistent : false }); if (listener != null) { watcher.on("change", listener); } return watcher; }); this._platform = (_options && _options.platform) || "posix"; this._cwd = _options && _options.cwd; this._pathSep = this._platform === "win32" ? "\\" : "/"; this.reset(); ASYNC_FUNC_NAMES.forEach(funcName => { const func = this[`${funcName}Sync`]; this[funcName] = function() { for ( var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++ ) { args[_key] = arguments[_key]; } const callback = args.pop(); process.nextTick(() => { let retval; try { retval = func.apply(null, args); } catch (error) { callback(error); return; } callback(null, retval); }); }; }); } reset() { this._nextId = 1; this._roots = new Map(); if (this._platform === "posix") { this._roots.set("", this._makeDir(0o777)); } else if (this._platform === "win32") { this._roots.set("C:", this._makeDir(0o777)); } this._fds = new Map(); } _makeDir(mode) { return { entries: new Map(), gid: getgid(), id: this._getId(), mode, uid: getuid(), type: "directory", watchers: [] }; } _getId() { return ++this._nextId; } _open(filePath, flags, mode) { if (mode == null) { mode = 0o666; } const spec = FLAGS_SPECS[flags]; if (spec == null) { throw new Error(`flags not supported: \`${flags}\``); } const _spec$writable = spec.writable, writable = _spec$writable === void 0 ? false : _spec$writable, _spec$readable = spec.readable, readable = _spec$readable === void 0 ? false : _spec$readable; const exclusive = spec.exclusive, mustExist = spec.mustExist, truncate = spec.truncate; let _this$_resolve10 = this._resolve(filePath), dirNode = _this$_resolve10.dirNode, node = _this$_resolve10.node, basename = _this$_resolve10.basename, dirPath = _this$_resolve10.dirPath; let nodePath; if (node == null) { if (mustExist) { throw makeError("ENOENT", filePath, "no such file or directory"); } node = { content: Buffer.alloc(0), gid: getgid(), id: this._getId(), mode, uid: getuid(), type: "file", watchers: [] }; dirNode.entries.set(basename, node); nodePath = dirPath.concat([[basename, node]]); this._emitFileChange(nodePath.slice(), { eventType: "rename" }); } else { if (exclusive) { throw makeError("EEXIST", filePath, "directory or file already exists"); } if (node.type !== "file") { throw makeError("EISDIR", filePath, "cannot read/write to a directory"); } if (truncate) { node.content = Buffer.alloc(0); } nodePath = dirPath.concat([[basename, node]]); } return this._getFd(filePath, { nodePath, node, position: 0, readable, writable }); } _parsePath(filePath) { let drive; const sep = this._platform === "win32" ? /[\\/]/ : /\//; if (this._platform === "win32" && filePath.match(/^[a-zA-Z]:[\\/]/)) { drive = filePath.substring(0, 2); filePath = filePath.substring(3); } if (sep.test(filePath[0])) { if (this._platform === "posix") { drive = ""; filePath = filePath.substring(1); } else { throw makeError( "EINVAL", filePath, "path is invalid because it cannot start with a separator" ); } } return { entNames: filePath.split(sep), drive }; } /** * Implemented according with * http://man7.org/linux/man-pages/man7/path_resolution.7.html */ _resolve(filePath, options) { let keepFinalSymlink = false; if (options != null) { keepFinalSymlink = options.keepFinalSymlink; } if (filePath === "") { throw makeError("ENOENT", filePath, "no such file or directory"); } let _this$_parsePath = this._parsePath(filePath), drive = _this$_parsePath.drive, entNames = _this$_parsePath.entNames; if (drive == null) { const _cwd = this._cwd; if (_cwd == null) { throw new Error( `The path \`${filePath}\` cannot be resolved because no ` + "current working directory function has been specified. Set the " + "`cwd` option field to specify a current working directory." ); } const cwPath = this._parsePath(_cwd()); drive = cwPath.drive; if (drive == null) { throw new Error( "On a win32 FS, the options' `cwd()` must return a valid win32 " + "absolute path. This happened while trying to " + `resolve: \`${filePath}\`` ); } entNames = cwPath.entNames.concat(entNames); } checkPathLength(entNames, filePath); const root = this._getRoot(drive, filePath); const context = { drive, node: root, nodePath: [["", root]], entNames, symlinkCount: 0, keepFinalSymlink }; while (context.entNames.length > 0) { const entName = context.entNames.shift(); this._resolveEnt(context, filePath, entName); } const nodePath = context.nodePath; return { drive: context.drive, realpath: context.drive + nodePath.map(x => x[0]).join(this._pathSep), dirNode: (() => { const dirNode = nodePath.length >= 2 ? nodePath[nodePath.length - 2][1] : context.node; if (dirNode == null || dirNode.type !== "directory") { throw new Error("failed to resolve"); } return dirNode; })(), node: context.node, basename: nullthrows(nodePath[nodePath.length - 1][0]), dirPath: nodePath .slice(0, -1) .map(nodePair => [nodePair[0], nullthrows(nodePair[1])]) }; } _resolveEnt(context, filePath, entName) { const node = context.node; if (node == null) { throw makeError("ENOENT", filePath, "no such file or directory"); } if (node.type !== "directory") { throw makeError("ENOTDIR", filePath, "not a directory"); } const entries = node.entries; if (entName === "" || entName === ".") { return; } if (entName === "..") { const nodePath = context.nodePath; if (nodePath.length > 1) { nodePath.pop(); context.node = nodePath[nodePath.length - 1][1]; } return; } const childNode = entries.get(entName); if ( childNode == null || childNode.type !== "symbolicLink" || (context.keepFinalSymlink && context.entNames.length === 0) ) { context.node = childNode; context.nodePath.push([entName, childNode]); return; } if (context.symlinkCount >= 10) { throw makeError("ELOOP", filePath, "too many levels of symbolic links"); } const _this$_parsePath2 = this._parsePath(childNode.target), entNames = _this$_parsePath2.entNames, drive = _this$_parsePath2.drive; if (drive != null) { context.drive = drive; context.node = this._getRoot(drive, filePath); context.nodePath = [["", context.node]]; } context.entNames = entNames.concat(context.entNames); checkPathLength(context.entNames, filePath); ++context.symlinkCount; } _getRoot(drive, filePath) { const root = this._roots.get(drive.toUpperCase()); if (root == null) { throw makeError("ENOENT", filePath, `no such drive: \`${drive}\``); } return root; } _write(fd, buffer, offset, length, position) { const desc = this._getDesc(fd); if (!desc.writable) { throw makeError("EBADF", null, "file descriptor cannot be written to"); } if (position == null) { position = desc.position; } const node = desc.node; if (node.content.length < position + length) { const newBuffer = Buffer.alloc(position + length); node.content.copy(newBuffer, 0, 0, node.content.length); node.content = newBuffer; } buffer.copy(node.content, position, offset, offset + length); desc.position = position + length; return buffer.length; } _getFd(filePath, desc) { let fd = 3; while (this._fds.has(fd)) { ++fd; } if (fd >= 256) { throw makeError("EMFILE", filePath, "too many open files"); } this._fds.set(fd, desc); return fd; } _getDesc(fd) { const desc = this._fds.get(fd); if (desc == null) { throw makeError("EBADF", null, "file descriptor is not open"); } return desc; } _emitFileChange(nodePath, options) { const fileNode = nodePath.pop(); let filePath = fileNode[0]; let recursive = false; for (const watcher of fileNode[1].watchers) { watcher.listener(options.eventType, filePath); } while (nodePath.length > 0) { const dirNode = nodePath.pop(); for (const watcher of dirNode[1].watchers) { if (recursive && !watcher.recursive) { continue; } watcher.listener(options.eventType, filePath); } filePath = dirNode[0] + this._pathSep + filePath; recursive = true; } } } class Stats { /** * Don't keep a reference to the node as it may get mutated over time. */ constructor(node) { this._type = node.type; this.dev = 1; this.mode = node.mode; this.nlink = 1; this.uid = node.uid; this.gid = node.gid; this.rdev = 0; this.blksize = 1024; this.ino = node.id; this.size = node.type === "file" ? node.content.length : node.type === "symbolicLink" ? node.target.length : 0; this.blocks = Math.ceil(this.size / 512); this.atimeMs = 1; this.mtimeMs = 1; this.ctimeMs = 1; this.birthtimeMs = 1; this.atime = new Date(this.atimeMs); this.mtime = new Date(this.mtimeMs); this.ctime = new Date(this.ctimeMs); this.birthtime = new Date(this.birthtimeMs); } isFile() { return this._type === "file"; } isDirectory() { return this._type === "directory"; } isBlockDevice() { return false; } isCharacterDevice() { return false; } isSymbolicLink() { return this._type === "symbolicLink"; } isFIFO() { return false; } isSocket() { return false; } } class ReadFileSteam extends stream.Readable { constructor(options) { const highWaterMark = options.highWaterMark, fd = options.fd; // eslint-disable-next-line lint/flow-no-fixme // $FlowFixMe: Readable does accept null of undefined for that value. super({ highWaterMark }); this.bytesRead = 0; this.path = options.filePath; this._readSync = options.readSync; this._fd = fd; this._buffer = Buffer.alloc(1024); const start = options.start, end = options.end; if (start != null) { this._readSync(fd, Buffer.alloc(0), 0, 0, start); } if (end != null) { this._positions = { current: start || 0, last: end + 1 }; } } _read(size) { let bytesRead; const _buffer = this._buffer; do { const length = this._getLengthToRead(); const position = this._positions && this._positions.current; bytesRead = this._readSync(this._fd, _buffer, 0, length, position); if (this._positions != null) { this._positions.current += bytesRead; } this.bytesRead += bytesRead; } while (this.push(bytesRead > 0 ? _buffer.slice(0, bytesRead) : null)); } _getLengthToRead() { const _positions = this._positions, _buffer = this._buffer; if (_positions == null) { return _buffer.length; } const leftToRead = Math.max(0, _positions.last - _positions.current); return Math.min(_buffer.length, leftToRead); } } class WriteFileStream extends stream.Writable { constructor(opts) { super(); this.path = opts.filePath; this.bytesWritten = 0; this._fd = opts.fd; this._writeSync = opts.writeSync; if (opts.start != null) { this._writeSync(opts.fd, Buffer.alloc(0), 0, 0, opts.start); } } _write(buffer, encoding, callback) { try { const bytesWritten = this._writeSync(this._fd, buffer, 0, buffer.length); this.bytesWritten += bytesWritten; } catch (error) { callback(error); return; } callback(); } } class FSWatcher extends EventEmitter { constructor(node, options) { super(); _defineProperty(this, "_listener", (eventType, filePath) => { const encFilePath = this._encoding === "buffer" ? Buffer.from(filePath, "utf8") : filePath; try { this.emit("change", eventType, encFilePath); } catch (error) { this.close(); this.emit("error", error); } }); this._encoding = options.encoding; this._nodeWatcher = { recursive: options.recursive, listener: this._listener }; node.watchers.push(this._nodeWatcher); this._node = node; if (options.persistent) { this._persistIntervalId = setInterval(() => {}, 60000); } } close() { this._node.watchers.splice(this._node.watchers.indexOf(this._nodeWatcher)); clearInterval(this._persistIntervalId); } } function checkPathLength(entNames, filePath) { if (entNames.length > 32) { throw makeError( "ENAMETOOLONG", filePath, "file path too long (or one of the intermediate " + "symbolic link resolutions)" ); } } function pathStr(filePath) { if (typeof filePath === "string") { return filePath; } return filePath.toString("utf8"); } function makeError(code, filePath, message) { const err = new Error( filePath != null ? `${code}: \`${filePath}\`: ${message}` : `${code}: ${message}` ); err.code = code; err.errno = constants[code]; err.path = filePath; return err; } function nullthrows(x) { if (x == null) { throw new Error("item was null or undefined"); } return x; } function getgid() { return process.getgid != null ? process.getgid() : -1; } function getuid() { return process.getuid != null ? process.getuid() : -1; } module.exports = MemoryFs;