/** * 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. * * @flow * @format */ 'use strict'; /* eslint-disable no-bitwise */ // $FlowFixMe: not defined by Flow const constants = require('constants'); const stream = require('stream'); const {EventEmitter} = require('events'); type NodeBase = {| gid: number, id: number, mode: number, uid: number, watchers: Array<NodeWatcher>, |}; type DirectoryNode = {| ...NodeBase, type: 'directory', entries: Map<string, EntityNode>, |}; type FileNode = {| ...NodeBase, type: 'file', content: Buffer, |}; type SymbolicLinkNode = {| ...NodeBase, type: 'symbolicLink', target: string, |}; type EntityNode = DirectoryNode | FileNode | SymbolicLinkNode; type NodeWatcher = { recursive: boolean, listener: (eventType: 'change' | 'rename', filePath: string) => void, }; type Encoding = | 'ascii' | 'base64' | 'binary' | 'buffer' | 'hex' | 'latin1' | 'ucs2' | 'utf16le' | 'utf8'; type Resolution = {| +basename: string, +dirNode: DirectoryNode, +dirPath: Array<[string, EntityNode]>, +drive: string, +node: ?EntityNode, +realpath: string, |}; type Descriptor = {| +nodePath: Array<[string, EntityNode]>, +node: FileNode, +readable: boolean, +writable: boolean, position: number, |}; type FilePath = string | Buffer; const FLAGS_SPECS: { [string]: { exclusive?: true, mustExist?: true, readable?: true, truncate?: true, writable?: true, }, } = { 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', ]; type Options = { /** * On a win32 FS, there will be drives at the root, like "C:\". On a Posix FS, * there is only one root "/". */ platform?: 'win32' | 'posix', /** * To be able to use relative paths, this function must provide the current * working directory. A possible implementation is to forward `process.cwd`, * but one must ensure to create that directory in the memory FS (no * directory is ever created automatically). */ cwd?: () => string, }; /** * 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 { _roots: Map<string, DirectoryNode>; _fds: Map<number, Descriptor>; _nextId: number; _platform: 'win32' | 'posix'; _pathSep: string; _cwd: ?() => string; constants = constants; close: (fd: number, callback: (error: ?Error) => mixed) => void; copyFile: (( src: FilePath, dest: FilePath, callback: (error: Error) => mixed, ) => void) & (( src: FilePath, dest: FilePath, flags?: number, callback: (error: ?Error) => mixed, ) => void); open: ( filePath: FilePath, flag: string | number, mode?: number, callback: (error: ?Error, fd: ?number) => mixed, ) => void; read: ( fd: number, buffer: Buffer, offset: number, length: number, position: ?number, callback: (?Error, ?number) => mixed, ) => void; readFile: ( filePath: FilePath, options?: | { encoding?: Encoding, flag?: string, } | Encoding | ((?Error, ?Buffer | string) => mixed), callback?: (?Error, ?Buffer | string) => mixed, ) => void; realpath: (filePath: FilePath, callback: (?Error, ?string) => mixed) => void; write: ( fd: number, bufferOrString: Buffer | string, offsetOrPosition?: number | ((?Error, number) => mixed), lengthOrEncoding?: number | string | ((?Error, number) => mixed), position?: number | ((?Error, number) => mixed), callback?: (?Error, number) => mixed, ) => void; writeFile: ( filePath: FilePath, data: Buffer | string, options?: | { encoding?: ?Encoding, mode?: ?number, flag?: ?string, } | Encoding | ((?Error) => mixed), callback?: (?Error) => mixed, ) => void; constructor(options?: ?Options) { 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: $FlowFixMe)[`${funcName}Sync`]; (this: $FlowFixMe)[funcName] = function(...args) { 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(); } accessSync = (filePath: FilePath, mode?: number): void => { 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'); } } }; closeSync = (fd: number): void => { const desc = this._getDesc(fd); if (desc.writable) { this._emitFileChange(desc.nodePath.slice(), {eventType: 'change'}); } this._fds.delete(fd); }; copyFileSync = (src: FilePath, dest: FilePath, flags?: number = 0) => { const options = flags & constants.COPYFILE_EXCL ? {flag: 'wx'} : {}; this.writeFileSync(dest, this.readFileSync(src), options); }; fsyncSync = (fd: number): void => { this._getDesc(fd); }; fdatasyncSync = (fd: number): void => { this._getDesc(fd); }; openSync = ( filePath: FilePath, flags: string | number, mode?: number, ): number => { if (typeof flags === 'number') { throw new Error(`numeric flags not supported: ${flags}`); } return this._open(pathStr(filePath), flags, mode); }; readSync = ( fd: number, buffer: Buffer, offset: number, length: number, position: ?number, ): number => { 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; }; readdirSync = ( filePath: FilePath, options?: | { encoding?: Encoding, } | Encoding, ): Array<string | Buffer> => { let encoding; if (typeof options === 'string') { encoding = options; } else if (options != null) { ({encoding} = options); } filePath = pathStr(filePath); const {node} = this._resolve(filePath); 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); }); }; readFileSync = ( filePath: FilePath, options?: | { encoding?: Encoding, flag?: string, } | Encoding, ): Buffer | string => { let encoding, flag; if (typeof options === 'string') { encoding = options; } else if (options != null) { ({encoding, flag} = options); } 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); }; readlinkSync = ( filePath: FilePath, options: ?Encoding | {encoding: ?Encoding}, ): string | Buffer => { let encoding; if (typeof options === 'string') { encoding = options; } else if (options != null) { ({encoding} = options); } filePath = pathStr(filePath); const {node} = this._resolve(filePath, {keepFinalSymlink: true}); 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); }; realpathSync = (filePath: FilePath): string => { return this._resolve(pathStr(filePath)).realpath; }; writeSync = ( fd: number, bufferOrString: Buffer | string, offsetOrPosition?: number, lengthOrEncoding?: number | string, position?: number, ): number => { let encoding, offset, length, buffer; if (typeof bufferOrString === 'string') { position = offsetOrPosition; encoding = lengthOrEncoding; buffer = (Buffer: $FlowFixMe).from( bufferOrString, (encoding: $FlowFixMe) || '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); }; writeFileSync = ( filePathOrFd: FilePath | number, data: Buffer | string, options?: | { encoding?: ?Encoding, mode?: ?number, flag?: ?string, } | Encoding, ): void => { let encoding, mode, flag; if (typeof options === 'string') { encoding = options; } else if (options != null) { ({encoding, mode, flag} = options); } if (encoding == null) { encoding = 'utf8'; } if (typeof data === 'string') { data = (Buffer: $FlowFixMe).from(data, encoding); } const fd: number = 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); } } }; mkdirSync = (dirPath: string | Buffer, mode?: number): void => { if (mode == null) { mode = 0o777; } dirPath = pathStr(dirPath); const {dirNode, node, basename} = this._resolve(dirPath); if (node != null) { throw makeError('EEXIST', dirPath, 'directory or file already exists'); } dirNode.entries.set(basename, this._makeDir(mode)); }; symlinkSync = ( target: string | Buffer, filePath: FilePath, type?: string, ) => { if (type == null) { type = 'file'; } if (type !== 'file') { throw new Error('symlink type not supported'); } filePath = pathStr(filePath); const {dirNode, node, basename} = this._resolve(filePath); 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: [], }); }; existsSync = (filePath: FilePath): boolean => { try { const {node} = this._resolve(pathStr(filePath)); return node != null; } catch (error) { if (error.code === 'ENOENT') { return false; } throw error; } }; statSync = (filePath: FilePath) => { filePath = pathStr(filePath); const {node} = this._resolve(filePath); if (node == null) { throw makeError('ENOENT', filePath, 'no such file or directory'); } return new Stats(node); }; lstatSync = (filePath: FilePath) => { filePath = pathStr(filePath); const {node} = this._resolve(filePath, { keepFinalSymlink: true, }); if (node == null) { throw makeError('ENOENT', filePath, 'no such file or directory'); } return new Stats(node); }; fstatSync = (fd: number) => { const desc = this._getDesc(fd); return new Stats(desc.node); }; createReadStream = ( filePath: FilePath, options?: | { autoClose?: ?boolean, encoding?: ?Encoding, end?: ?number, fd?: ?number, flags?: ?string, highWaterMark?: ?number, mode?: ?number, start?: ?number, } | Encoding, ) => { let autoClose, encoding, fd, flags, mode, start, end, highWaterMark; if (typeof options === 'string') { encoding = options; } else if (options != null) { ({autoClose, encoding, fd, flags, mode, start} = options); ({end, highWaterMark} = options); } let st = null; if (fd == null) { fd = this._open(pathStr(filePath), flags || 'r', mode); process.nextTick(() => (st: any).emit('open', fd)); } const ffd = fd; const {readSync} = this; 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; }; unlinkSync = (filePath: FilePath) => { filePath = pathStr(filePath); const {basename, dirNode, dirPath, node} = this._resolve(filePath, { keepFinalSymlink: true, }); 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', }); }; createWriteStream = ( filePath: FilePath, options?: | { autoClose?: boolean, encoding?: Encoding, fd?: ?number, flags?: string, mode?: number, start?: number, } | Encoding, ) => { let autoClose, fd, flags, mode, start; if (typeof options !== 'string' && options != null) { ({autoClose, fd, flags, mode, start} = options); } let st = null; if (fd == null) { fd = this._open(pathStr(filePath), flags || 'w', mode); process.nextTick(() => (st: any).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; }; watch = ( filePath: FilePath, options?: | { encoding?: Encoding, recursive?: boolean, persistent?: boolean, } | Encoding, listener?: ( eventType: 'rename' | 'change', filePath: ?string | Buffer, ) => mixed, ) => { filePath = pathStr(filePath); const {node} = this._resolve(filePath); 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, recursive, persistent} = options); } 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; }; _makeDir(mode: number): DirectoryNode { return { entries: new Map(), gid: getgid(), id: this._getId(), mode, uid: getuid(), type: 'directory', watchers: [], }; } _getId() { return ++this._nextId; } _open(filePath: string, flags: string, mode: ?number): number { if (mode == null) { mode = 0o666; } const spec = FLAGS_SPECS[flags]; if (spec == null) { throw new Error(`flags not supported: \`${flags}\``); } const {writable = false, readable = false} = spec; const {exclusive, mustExist, truncate} = spec; let {dirNode, node, basename, dirPath} = this._resolve(filePath); 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: string, ): {| +drive: ?string, +entNames: Array<string>, |} { 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: string, options?: {keepFinalSymlink: boolean}, ): Resolution { let keepFinalSymlink = false; if (options != null) { ({keepFinalSymlink} = options); } if (filePath === '') { throw makeError('ENOENT', filePath, 'no such file or directory'); } let {drive, entNames} = this._parsePath(filePath); if (drive == null) { const {_cwd} = this; 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; 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; 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; if (entName === '' || entName === '.') { return; } if (entName === '..') { const {nodePath} = context; 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 {entNames, drive} = this._parsePath(childNode.target); 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: string, filePath: string): DirectoryNode { const root = this._roots.get(drive.toUpperCase()); if (root == null) { throw makeError('ENOENT', filePath, `no such drive: \`${drive}\``); } return root; } _write( fd: number, buffer: Buffer, offset: number, length: number, position: ?number, ): number { 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; 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: string, desc: Descriptor): number { 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: number): Descriptor { const desc = this._fds.get(fd); if (desc == null) { throw makeError('EBADF', null, 'file descriptor is not open'); } return desc; } _emitFileChange( nodePath: Array<[string, EntityNode]>, options: {eventType: 'rename' | 'change'}, ): void { 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 { _type: string; dev: number; mode: number; nlink: number; uid: number; gid: number; rdev: number; blksize: number; ino: number; size: number; blocks: number; atimeMs: number; mtimeMs: number; ctimeMs: number; birthtimeMs: number; atime: Date; mtime: Date; ctime: Date; birthtime: Date; /** * Don't keep a reference to the node as it may get mutated over time. */ constructor(node: EntityNode) { 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(): boolean { return this._type === 'file'; } isDirectory(): boolean { return this._type === 'directory'; } isBlockDevice(): boolean { return false; } isCharacterDevice(): boolean { return false; } isSymbolicLink(): boolean { return this._type === 'symbolicLink'; } isFIFO(): boolean { return false; } isSocket(): boolean { return false; } } type ReadSync = ( fd: number, buffer: Buffer, offset: number, length: number, position: ?number, ) => number; class ReadFileSteam extends stream.Readable { _buffer: Buffer; _fd: number; _positions: ?{current: number, last: number}; _readSync: ReadSync; bytesRead: number; path: string | Buffer; constructor(options: { filePath: FilePath, encoding: ?Encoding, end: ?number, fd: number, highWaterMark: ?number, readSync: ReadSync, start: ?number, }) { const {highWaterMark, fd} = options; // 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, end} = options; 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; 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, _buffer} = this; if (_positions == null) { return _buffer.length; } const leftToRead = Math.max(0, _positions.last - _positions.current); return Math.min(_buffer.length, leftToRead); } } type WriteSync = ( fd: number, buffer: Buffer, offset: number, length: number, position?: number, ) => number; class WriteFileStream extends stream.Writable { bytesWritten: number; path: string | Buffer; _fd: number; _writeSync: WriteSync; constructor(opts: { fd: number, filePath: FilePath, writeSync: WriteSync, start?: number, }) { 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 { _encoding: Encoding; _node: EntityNode; _nodeWatcher: NodeWatcher; _persistIntervalId: IntervalID; constructor( node: EntityNode, options: {encoding: Encoding, recursive: boolean, persistent: boolean}, ) { super(); 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); } _listener = (eventType, filePath: string) => { const encFilePath = this._encoding === 'buffer' ? Buffer.from(filePath, 'utf8') : filePath; try { this.emit('change', eventType, encFilePath); } catch (error) { this.close(); this.emit('error', error); } }; } 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: FilePath): string { if (typeof filePath === 'string') { return filePath; } return filePath.toString('utf8'); } function makeError(code: string, filePath: ?string, message: string) { const err: $FlowFixMe = new Error( filePath != null ? `${code}: \`${filePath}\`: ${message}` : `${code}: ${message}`, ); err.code = code; err.errno = constants[code]; err.path = filePath; return err; } function nullthrows<T>(x: ?T): T { if (x == null) { throw new Error('item was null or undefined'); } return x; } function getgid(): number { return process.getgid != null ? process.getgid() : -1; } function getuid(): number { return process.getuid != null ? process.getuid() : -1; } module.exports = MemoryFs;