/**
* 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;