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