UNPKG

@cowasm/memfs

Version:

In-memory file-system with Node's fs API.

1,378 lines (1,375 loc) 78.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FSWatcher = exports.StatWatcher = exports.Volume = exports.toUnixTimestamp = exports.bufferToEncoding = exports.dataToBuffer = exports.dataToStr = exports.pathToSteps = exports.filenameToSteps = exports.pathToFilename = exports.flagsToNumber = exports.FLAGS = void 0; const pathModule = require("path"); const node_1 = require("./node"); const Stats_1 = require("./Stats"); const Dirent_1 = require("./Dirent"); const buffer_1 = require("./internal/buffer"); const setImmediate_1 = require("./setImmediate"); const process_1 = require("./process"); const setTimeoutUnref_1 = require("./setTimeoutUnref"); const stream_1 = require("stream"); const constants_1 = require("./constants"); const events_1 = require("events"); const encoding_1 = require("./encoding"); const errors = require("./internal/errors"); const util = require("util"); const promises_1 = require("./promises"); const resolveCrossPlatform = pathModule.resolve; const { O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_EXCL, O_TRUNC, O_APPEND, O_SYNC, O_DIRECTORY, F_OK, COPYFILE_EXCL, COPYFILE_FICLONE_FORCE, } = constants_1.constants; const { sep, relative, join, dirname } = pathModule.posix ? pathModule.posix : pathModule; const isWin = process_1.default.platform === "win32"; const kMinPoolSpace = 128; // const kMaxLength = require('buffer').kMaxLength; // ---------------------------------------- Error messages // TODO: Use `internal/errors.js` in the future. const ERRSTR = { PATH_STR: "path must be a string or Buffer", // FD: 'file descriptor must be a unsigned 32-bit integer', FD: "fd must be a file descriptor", MODE_INT: "mode must be an int", CB: "callback must be a function", UID: "uid must be an unsigned int", GID: "gid must be an unsigned int", LEN: "len must be an integer", ATIME: "atime must be an integer", MTIME: "mtime must be an integer", PREFIX: "filename prefix is required", BUFFER: "buffer must be an instance of Buffer or StaticBuffer", OFFSET: "offset must be an integer", LENGTH: "length must be an integer", POSITION: "position must be an integer", }; const ERRSTR_OPTS = (tipeof) => `Expected options to be either an object or a string, but got ${tipeof} instead`; // const ERRSTR_FLAG = flag => `Unknown file open flag: ${flag}`; const ENOENT = "ENOENT"; const EBADF = "EBADF"; const EINVAL = "EINVAL"; const EPERM = "EPERM"; const EPROTO = "EPROTO"; const EEXIST = "EEXIST"; const ENOTDIR = "ENOTDIR"; const EMFILE = "EMFILE"; const EACCES = "EACCES"; const EISDIR = "EISDIR"; const ENOTEMPTY = "ENOTEMPTY"; const ENOSYS = "ENOSYS"; const ERR_FS_EISDIR = "ERR_FS_EISDIR"; function formatError(errorCode, func = "", path = "", path2 = "") { let pathFormatted = ""; if (path) pathFormatted = ` '${path}'`; if (path2) pathFormatted += ` -> '${path2}'`; switch (errorCode) { case ENOENT: return `ENOENT: no such file or directory, ${func}${pathFormatted}`; case EBADF: return `EBADF: bad file descriptor, ${func}${pathFormatted}`; case EINVAL: return `EINVAL: invalid argument, ${func}${pathFormatted}`; case EPERM: return `EPERM: operation not permitted, ${func}${pathFormatted}`; case EPROTO: return `EPROTO: protocol error, ${func}${pathFormatted}`; case EEXIST: return `EEXIST: file already exists, ${func}${pathFormatted}`; case ENOTDIR: return `ENOTDIR: not a directory, ${func}${pathFormatted}`; case EISDIR: return `EISDIR: illegal operation on a directory, ${func}${pathFormatted}`; case EACCES: return `EACCES: permission denied, ${func}${pathFormatted}`; case ENOTEMPTY: return `ENOTEMPTY: directory not empty, ${func}${pathFormatted}`; case EMFILE: return `EMFILE: too many open files, ${func}${pathFormatted}`; case ENOSYS: return `ENOSYS: function not implemented, ${func}${pathFormatted}`; case ERR_FS_EISDIR: return `[ERR_FS_EISDIR]: Path is a directory: ${func} returned EISDIR (is a directory) ${path}`; default: return `${errorCode}: error occurred, ${func}${pathFormatted}`; } } function createError(errorCode, func = "", path = "", path2 = "", Constructor = Error) { const error = new Constructor(formatError(errorCode, func, path, path2)); error.code = errorCode; return error; } // ---------------------------------------- Flags // List of file `flags` as defined by Node. var FLAGS; (function (FLAGS) { // Open file for reading. An exception occurs if the file does not exist. FLAGS[FLAGS["r"] = O_RDONLY] = "r"; // Open file for reading and writing. An exception occurs if the file does not exist. FLAGS[FLAGS["r+"] = O_RDWR] = "r+"; // Open file for reading in synchronous mode. Instructs the operating system to bypass the local file system cache. FLAGS[FLAGS["rs"] = O_RDONLY | O_SYNC] = "rs"; FLAGS[FLAGS["sr"] = FLAGS.rs] = "sr"; // Open file for reading and writing, telling the OS to open it synchronously. See notes for 'rs' about using this with caution. FLAGS[FLAGS["rs+"] = O_RDWR | O_SYNC] = "rs+"; FLAGS[FLAGS["sr+"] = FLAGS["rs+"]] = "sr+"; // Open file for writing. The file is created (if it does not exist) or truncated (if it exists). FLAGS[FLAGS["w"] = O_WRONLY | O_CREAT | O_TRUNC] = "w"; // Like 'w' but fails if path exists. FLAGS[FLAGS["wx"] = O_WRONLY | O_CREAT | O_TRUNC | O_EXCL] = "wx"; FLAGS[FLAGS["xw"] = FLAGS.wx] = "xw"; // Open file for reading and writing. The file is created (if it does not exist) or truncated (if it exists). FLAGS[FLAGS["w+"] = O_RDWR | O_CREAT | O_TRUNC] = "w+"; // Like 'w+' but fails if path exists. FLAGS[FLAGS["wx+"] = O_RDWR | O_CREAT | O_TRUNC | O_EXCL] = "wx+"; FLAGS[FLAGS["xw+"] = FLAGS["wx+"]] = "xw+"; // Open file for appending. The file is created if it does not exist. FLAGS[FLAGS["a"] = O_WRONLY | O_APPEND | O_CREAT] = "a"; // Like 'a' but fails if path exists. FLAGS[FLAGS["ax"] = O_WRONLY | O_APPEND | O_CREAT | O_EXCL] = "ax"; FLAGS[FLAGS["xa"] = FLAGS.ax] = "xa"; // Open file for reading and appending. The file is created if it does not exist. FLAGS[FLAGS["a+"] = O_RDWR | O_APPEND | O_CREAT] = "a+"; // Like 'a+' but fails if path exists. FLAGS[FLAGS["ax+"] = O_RDWR | O_APPEND | O_CREAT | O_EXCL] = "ax+"; FLAGS[FLAGS["xa+"] = FLAGS["ax+"]] = "xa+"; })(FLAGS = exports.FLAGS || (exports.FLAGS = {})); function flagsToNumber(flags) { if (typeof flags === "number") return flags; if (typeof flags === "string") { const flagsNum = FLAGS[flags]; if (typeof flagsNum !== "undefined") return flagsNum; } // throw new TypeError(formatError(ERRSTR_FLAG(flags))); throw new errors.TypeError("ERR_INVALID_OPT_VALUE", "flags", flags); } exports.flagsToNumber = flagsToNumber; // ---------------------------------------- Options function getOptions(defaults, options) { let opts; if (!options) return defaults; else { const tipeof = typeof options; switch (tipeof) { case "string": opts = Object.assign({}, defaults, { encoding: options }); break; case "object": opts = Object.assign({}, defaults, options); break; default: throw TypeError(ERRSTR_OPTS(tipeof)); } } if (opts.encoding !== "buffer") (0, encoding_1.assertEncoding)(opts.encoding); return opts; } function optsGenerator(defaults) { return (options) => getOptions(defaults, options); } function validateCallback(callback) { if (typeof callback !== "function") throw TypeError(ERRSTR.CB); return callback; } function optsAndCbGenerator(getOpts) { return (options, callback) => typeof options === "function" ? [getOpts(), options] : [getOpts(options), validateCallback(callback)]; } const optsDefaults = { encoding: "utf8", }; const getDefaultOpts = optsGenerator(optsDefaults); const getDefaultOptsAndCb = optsAndCbGenerator(getDefaultOpts); const readFileOptsDefaults = { flag: "r", }; const getReadFileOptions = optsGenerator(readFileOptsDefaults); const writeFileDefaults = { encoding: "utf8", mode: 438 /* MODE.DEFAULT */, flag: FLAGS[FLAGS.w], }; const getWriteFileOptions = optsGenerator(writeFileDefaults); const appendFileDefaults = { encoding: "utf8", mode: 438 /* MODE.DEFAULT */, flag: FLAGS[FLAGS.a], }; const getAppendFileOpts = optsGenerator(appendFileDefaults); const getAppendFileOptsAndCb = optsAndCbGenerator(getAppendFileOpts); const realpathDefaults = optsDefaults; const getRealpathOptions = optsGenerator(realpathDefaults); const getRealpathOptsAndCb = optsAndCbGenerator(getRealpathOptions); const mkdirDefaults = { mode: 511 /* MODE.DIR */, recursive: false, }; const getMkdirOptions = (options) => { if (typeof options === "number") return Object.assign({}, mkdirDefaults, { mode: options }); return Object.assign({}, mkdirDefaults, options); }; const rmdirDefaults = { recursive: false, }; const getRmdirOptions = (options) => { return Object.assign({}, rmdirDefaults, options); }; const getRmOpts = optsGenerator(optsDefaults); const getRmOptsAndCb = optsAndCbGenerator(getRmOpts); const readdirDefaults = { encoding: "utf8", withFileTypes: false, }; const getReaddirOptions = optsGenerator(readdirDefaults); const getReaddirOptsAndCb = optsAndCbGenerator(getReaddirOptions); const statDefaults = { bigint: false, }; const getStatOptions = (options = {}) => Object.assign({}, statDefaults, options); const getStatOptsAndCb = (options, callback) => typeof options === "function" ? [getStatOptions(), options] : [getStatOptions(options), validateCallback(callback)]; // ---------------------------------------- Utility functions function getPathFromURLPosix(url) { if (url.hostname !== "") { throw new errors.TypeError("ERR_INVALID_FILE_URL_HOST", process_1.default.platform); } const pathname = url.pathname; for (let n = 0; n < pathname.length; n++) { if (pathname[n] === "%") { const third = pathname.codePointAt(n + 2) | 0x20; if (pathname[n + 1] === "2" && third === 102) { throw new errors.TypeError("ERR_INVALID_FILE_URL_PATH", "must not include encoded / characters"); } } } return decodeURIComponent(pathname); } function pathToFilename(path) { if (typeof path !== "string" && !buffer_1.Buffer.isBuffer(path)) { try { if (!(path instanceof require("url").URL)) throw new TypeError(ERRSTR.PATH_STR); } catch (err) { throw new TypeError(ERRSTR.PATH_STR); } path = getPathFromURLPosix(path); } const pathString = String(path); nullCheck(pathString); // return slash(pathString); return pathString; } exports.pathToFilename = pathToFilename; let resolve = (filename, base = process_1.default.cwd()) => resolveCrossPlatform(base, filename); if (isWin) { const _resolve = resolve; const { unixify } = require("fs-monkey/lib/correctPath"); resolve = (filename, base) => unixify(_resolve(filename, base)); } function filenameToSteps(filename, base) { const fullPath = resolve(filename, base); const fullPathSansSlash = fullPath.substr(1); if (!fullPathSansSlash) return []; return fullPathSansSlash.split(sep); } exports.filenameToSteps = filenameToSteps; function pathToSteps(path) { return filenameToSteps(pathToFilename(path)); } exports.pathToSteps = pathToSteps; function dataToStr(data, encoding = encoding_1.ENCODING_UTF8) { if (buffer_1.Buffer.isBuffer(data)) return data.toString(encoding); else if (data instanceof Uint8Array) return (0, buffer_1.bufferFrom)(data).toString(encoding); else return String(data); } exports.dataToStr = dataToStr; function dataToBuffer(data, encoding = encoding_1.ENCODING_UTF8) { if (buffer_1.Buffer.isBuffer(data)) { return data; } else if (data instanceof Uint8Array) { return (0, buffer_1.bufferFrom)(data); } else { // This "encoding as ..." is a little bit of a cheat. return (0, buffer_1.bufferFrom)(String(data), encoding); } } exports.dataToBuffer = dataToBuffer; function bufferToEncoding(buffer, encoding) { if (!encoding || encoding === "buffer") return buffer; else return buffer.toString(encoding); } exports.bufferToEncoding = bufferToEncoding; function nullCheck(path, callback) { if (("" + path).indexOf("\u0000") !== -1) { const er = new Error("Path must be a string without null bytes"); er.code = ENOENT; if (typeof callback !== "function") throw er; process_1.default.nextTick(callback, er); return false; } return true; } function _modeToNumber(mode, def) { if (typeof mode === "number") return mode; if (typeof mode === "string") return parseInt(mode, 8); if (def) return modeToNumber(def); return undefined; } function modeToNumber(mode, def) { const result = _modeToNumber(mode, def); if (typeof result !== "number" || isNaN(result)) throw new TypeError(ERRSTR.MODE_INT); return result; } function isFd(path) { return path >>> 0 === path; } function validateFd(fd) { if (!isFd(fd)) throw TypeError(ERRSTR.FD); } // converts Date or number to a fractional UNIX timestamp function toUnixTimestamp(time) { // tslint:disable-next-line triple-equals if (typeof time === "string" && +time == time) { return +time; } if (time instanceof Date) { return time.getTime() / 1000; } if (isFinite(time)) { if (time < 0) { return Date.now() / 1000; } return time; } throw new Error("Cannot parse time: " + time); } exports.toUnixTimestamp = toUnixTimestamp; function validateUid(uid) { if (typeof uid !== "number") throw TypeError(ERRSTR.UID); } function validateGid(gid) { if (typeof gid !== "number") throw TypeError(ERRSTR.GID); } function flattenJSON(nestedJSON) { const flatJSON = {}; function flatten(pathPrefix, node) { for (const path in node) { const contentOrNode = node[path]; const joinedPath = join(pathPrefix, path); if (typeof contentOrNode === "string") { flatJSON[joinedPath] = contentOrNode; } else if (typeof contentOrNode === "object" && contentOrNode !== null && Object.keys(contentOrNode).length > 0) { // empty directories need an explicit entry and therefore get handled in `else`, non-empty ones are implicitly considered flatten(joinedPath, contentOrNode); } else { // without this branch null, empty-object or non-object entries would not be handled in the same way // by both fromJSON() and fromNestedJSON() flatJSON[joinedPath] = null; } } } flatten("", nestedJSON); return flatJSON; } /** * `Volume` represents a file system. */ class Volume { constructor(props = {}) { // I-node number counter. this.ino = 0; // A mapping for i-node numbers to i-nodes (`Node`); this.inodes = {}; // List of released i-node numbers, for reuse. this.releasedInos = []; // A mapping for file descriptors to `File`s. this.fds = {}; // A list of reusable (opened and closed) file descriptors, that should be // used first before creating a new file descriptor. this.releasedFds = []; // Max number of open files. this.maxFiles = 10000; // Current number of open files. this.openFiles = 0; this.promisesApi = (0, promises_1.default)(this); this.statWatchers = {}; this.props = Object.assign({ Node: node_1.Node, Link: node_1.Link, File: node_1.File }, props); const root = this.createLink(); root.setNode(this.createNode(true)); const self = this; // tslint:disable-line no-this-assignment this.StatWatcher = class extends StatWatcher { constructor() { super(self); } }; const _ReadStream = FsReadStream; this.ReadStream = class extends _ReadStream { constructor(...args) { super(self, ...args); } }; const _WriteStream = FsWriteStream; this.WriteStream = class extends _WriteStream { constructor(...args) { super(self, ...args); } }; this.FSWatcher = class extends FSWatcher { constructor() { super(self); } }; // root.setChild('.', root); // root.getNode().nlink++; // root.setChild('..', root); // root.getNode().nlink++; this.root = root; } static fromJSON(json, cwd) { const vol = new Volume(); vol.fromJSON(json, cwd); return vol; } static fromNestedJSON(json, cwd) { const vol = new Volume(); vol.fromNestedJSON(json, cwd); return vol; } get promises() { if (this.promisesApi === null) throw new Error("Promise is not supported in this environment."); return this.promisesApi; } createLink(parent, name, isDirectory = false, perm) { if (!parent) { return new this.props.Link(this, null, ""); } if (!name) { throw new Error("createLink: name cannot be empty"); } return parent.createChild(name, this.createNode(isDirectory, perm)); } deleteLink(link) { const parent = link.parent; if (parent) { parent.deleteChild(link); return true; } return false; } newInoNumber() { const releasedFd = this.releasedInos.pop(); if (releasedFd) return releasedFd; else { this.ino = (this.ino + 1) % 0xffffffff; return this.ino; } } newFdNumber() { const releasedFd = this.releasedFds.pop(); return typeof releasedFd === "number" ? releasedFd : Volume.fd--; } createNode(isDirectory = false, perm) { const node = new this.props.Node(this.newInoNumber(), perm); if (isDirectory) node.setIsDirectory(); this.inodes[node.ino] = node; return node; } getNode(ino) { return this.inodes[ino]; } deleteNode(node) { node.del(); delete this.inodes[node.ino]; this.releasedInos.push(node.ino); } // Generates 6 character long random string, used by `mkdtemp`. genRndStr() { const str = (Math.random() + 1).toString(36).substr(2, 6); if (str.length === 6) return str; else return this.genRndStr(); } // Returns a `Link` (hard link) referenced by path "split" into steps. getLink(steps) { return this.root.walk(steps); } // Just link `getLink`, but throws a correct user error, if link to found. getLinkOrThrow(filename, funcName) { const steps = filenameToSteps(filename); const link = this.getLink(steps); if (!link) throw createError(ENOENT, funcName, filename); return link; } // Just like `getLink`, but also dereference/resolves symbolic links. getResolvedLink(filenameOrSteps) { let steps = typeof filenameOrSteps === "string" ? filenameToSteps(filenameOrSteps) : filenameOrSteps; let link = this.root; let i = 0; while (i < steps.length) { const step = steps[i]; link = link.getChild(step); if (!link) return null; const node = link.getNode(); if (node.isSymlink()) { steps = node.symlink.concat(steps.slice(i + 1)); link = this.root; i = 0; continue; } i++; } return link; } // Just like `getLinkOrThrow`, but also dereference/resolves symbolic links. getResolvedLinkOrThrow(filename, funcName) { const link = this.getResolvedLink(filename); if (!link) throw createError(ENOENT, funcName, filename); return link; } resolveSymlinks(link) { // let node: Node = link.getNode(); // while(link && node.isSymlink()) { // link = this.getLink(node.symlink); // if(!link) return null; // node = link.getNode(); // } // return link; return this.getResolvedLink(link.steps.slice(1)); } // Just like `getLinkOrThrow`, but also verifies that the link is a directory. getLinkAsDirOrThrow(filename, funcName) { const link = this.getLinkOrThrow(filename, funcName); if (!link.getNode().isDirectory()) throw createError(ENOTDIR, funcName, filename); return link; } // Get the immediate parent directory of the link. getLinkParent(steps) { return this.root.walk(steps, steps.length - 1); } getLinkParentAsDirOrThrow(filenameOrSteps, funcName) { const steps = filenameOrSteps instanceof Array ? filenameOrSteps : filenameToSteps(filenameOrSteps); const link = this.getLinkParent(steps); if (!link) throw createError(ENOENT, funcName, sep + steps.join(sep)); if (!link.getNode().isDirectory()) throw createError(ENOTDIR, funcName, sep + steps.join(sep)); return link; } getFileByFd(fd) { return this.fds[String(fd)]; } getFileByFdOrThrow(fd, funcName) { if (!isFd(fd)) throw TypeError(ERRSTR.FD); const file = this.getFileByFd(fd); if (!file) throw createError(EBADF, funcName); return file; } /** * @todo This is not used anymore. Remove. */ /* private getNodeByIdOrCreate(id: TFileId, flags: number, perm: number): Node { if (typeof id === 'number') { const file = this.getFileByFd(id); if (!file) throw Error('File nto found'); return file.node; } else { const steps = pathToSteps(id as PathLike); let link = this.getLink(steps); if (link) return link.getNode(); // Try creating a node if not found. if (flags & O_CREAT) { const dirLink = this.getLinkParent(steps); if (dirLink) { const name = steps[steps.length - 1]; link = this.createLink(dirLink, name, false, perm); return link.getNode(); } } throw createError(ENOENT, 'getNodeByIdOrCreate', pathToFilename(id)); } } */ wrapAsync(method, args, callback) { validateCallback(callback); (0, setImmediate_1.default)(() => { let result; try { result = method.apply(this, args); } catch (err) { callback(err); return; } callback(null, result); }); } _toJSON(link = this.root, json = {}, path) { let isEmpty = true; let children = link.children; if (link.getNode().isFile()) { children = { [link.getName()]: link.parent.getChild(link.getName()) }; link = link.parent; } for (const name in children) { isEmpty = false; const child = link.getChild(name); if (!child) { throw new Error("_toJSON: unexpected undefined"); } const node = child.getNode(); if (node.isFile()) { let filename = child.getPath(); if (path) filename = relative(path, filename); json[filename] = node.getString(); } else if (node.isDirectory()) { this._toJSON(child, json, path); } } let dirPath = link.getPath(); if (path) dirPath = relative(path, dirPath); if (dirPath && isEmpty) { json[dirPath] = null; } return json; } toJSON(paths, json = {}, isRelative = false) { const links = []; if (paths) { if (!(paths instanceof Array)) paths = [paths]; for (const path of paths) { const filename = pathToFilename(path); const link = this.getResolvedLink(filename); if (!link) continue; links.push(link); } } else { links.push(this.root); } if (!links.length) return json; for (const link of links) this._toJSON(link, json, isRelative ? link.getPath() : ""); return json; } fromJSON(json, cwd = process_1.default.cwd()) { for (let filename in json) { const data = json[filename]; filename = resolve(filename, cwd); if (typeof data === "string") { const dir = dirname(filename); this.mkdirpBase(dir, 511 /* MODE.DIR */); this.writeFileSync(filename, data); } else { this.mkdirpBase(filename, 511 /* MODE.DIR */); } } } fromNestedJSON(json, cwd) { this.fromJSON(flattenJSON(json), cwd); } reset() { this.ino = 0; this.inodes = {}; this.releasedInos = []; this.fds = {}; this.releasedFds = []; this.openFiles = 0; this.root = this.createLink(); this.root.setNode(this.createNode(true)); } // Legacy interface mountSync(mountpoint, json) { this.fromJSON(json, mountpoint); } openLink(link, flagsNum, resolveSymlinks = true) { if (this.openFiles >= this.maxFiles) { // Too many open files. throw createError(EMFILE, "open", link.getPath()); } // Resolve symlinks. let realLink = link; if (resolveSymlinks) realLink = this.resolveSymlinks(link); if (!realLink) throw createError(ENOENT, "open", link.getPath()); const node = realLink.getNode(); // Check whether node is a directory if (node.isDirectory()) { if ((flagsNum & (O_RDONLY | O_RDWR | O_WRONLY)) !== O_RDONLY) throw createError(EISDIR, "open", link.getPath()); } else { if (flagsNum & O_DIRECTORY) throw createError(ENOTDIR, "open", link.getPath()); } // Check node permissions if (!(flagsNum & O_WRONLY)) { if (!node.canRead()) { throw createError(EACCES, "open", link.getPath()); } } if (flagsNum & O_RDWR) { } const file = new this.props.File(link, node, flagsNum, this.newFdNumber()); this.fds[file.fd] = file; this.openFiles++; if (flagsNum & O_TRUNC) file.truncate(); return file; } openFile(filename, flagsNum, modeNum, resolveSymlinks = true) { const steps = filenameToSteps(filename); let link = resolveSymlinks ? this.getResolvedLink(steps) : this.getLink(steps); if (link && flagsNum & O_EXCL) throw createError(EEXIST, "open", filename); // Try creating a new file, if it does not exist. if (!link && flagsNum & O_CREAT) { // const dirLink: Link = this.getLinkParent(steps); const dirLink = this.getResolvedLink(steps.slice(0, steps.length - 1)); // if(!dirLink) throw createError(ENOENT, 'open', filename); if (!dirLink) throw createError(ENOENT, "open", sep + steps.join(sep)); if (flagsNum & O_CREAT && typeof modeNum === "number") { link = this.createLink(dirLink, steps[steps.length - 1], false, modeNum); } } if (link) return this.openLink(link, flagsNum, resolveSymlinks); throw createError(ENOENT, "open", filename); } openBase(filename, flagsNum, modeNum, resolveSymlinks = true) { const file = this.openFile(filename, flagsNum, modeNum, resolveSymlinks); if (!file) throw createError(ENOENT, "open", filename); return file.fd; } openSync(path, flags, mode = 438 /* MODE.DEFAULT */) { // Validate (1) mode; (2) path; (3) flags - in that order. const modeNum = modeToNumber(mode); const fileName = pathToFilename(path); const flagsNum = flagsToNumber(flags); return this.openBase(fileName, flagsNum, modeNum); } open(path, flags, a, b) { let mode = a; let callback = b; if (typeof a === "function") { mode = 438 /* MODE.DEFAULT */; callback = a; } mode = mode || 438 /* MODE.DEFAULT */; const modeNum = modeToNumber(mode); const fileName = pathToFilename(path); const flagsNum = flagsToNumber(flags); this.wrapAsync(this.openBase, [fileName, flagsNum, modeNum], callback); } closeFile(file) { if (!this.fds[file.fd]) return; this.openFiles--; delete this.fds[file.fd]; this.releasedFds.push(file.fd); } closeSync(fd) { validateFd(fd); const file = this.getFileByFdOrThrow(fd, "close"); this.closeFile(file); } close(fd, callback) { validateFd(fd); this.wrapAsync(this.closeSync, [fd], callback); } openFileOrGetById(id, flagsNum, modeNum) { if (typeof id === "number") { const file = this.fds[id]; if (!file) throw createError(ENOENT); return file; } else { return this.openFile(pathToFilename(id), flagsNum, modeNum); } } readBase(fd, buffer, offset, length, position) { const file = this.getFileByFdOrThrow(fd); return file.read(buffer, Number(offset), Number(length), position); } readSync(fd, buffer, offset, length, position) { validateFd(fd); return this.readBase(fd, buffer, offset, length, position); } read(fd, buffer, offset, length, position, callback) { validateCallback(callback); // This `if` branch is from Node.js if (length === 0) { return process_1.default.nextTick(() => { if (callback) callback(null, 0, buffer); }); } (0, setImmediate_1.default)(() => { try { const bytes = this.readBase(fd, buffer, offset, length, position); callback(null, bytes, buffer); } catch (err) { callback(err); } }); } readFileBase(id, flagsNum, encoding) { let result; const isUserFd = typeof id === "number"; const userOwnsFd = isUserFd && isFd(id); let fd; if (userOwnsFd) fd = id; else { const filename = pathToFilename(id); const steps = filenameToSteps(filename); const link = this.getResolvedLink(steps); if (link) { const node = link.getNode(); if (node.isDirectory()) throw createError(EISDIR, "open", link.getPath()); } fd = this.openSync(id, flagsNum); } try { result = bufferToEncoding(this.getFileByFdOrThrow(fd).getBuffer(), encoding); } finally { if (!userOwnsFd) { this.closeSync(fd); } } return result; } readFileSync(file, options) { const opts = getReadFileOptions(options); const flagsNum = flagsToNumber(opts.flag); return this.readFileBase(file, flagsNum, opts.encoding); } readFile(id, a, b) { const [opts, callback] = optsAndCbGenerator(getReadFileOptions)(a, b); const flagsNum = flagsToNumber(opts.flag); this.wrapAsync(this.readFileBase, [id, flagsNum, opts.encoding], callback); } writeBase(fd, buf, offset, length, position) { const file = this.getFileByFdOrThrow(fd, "write"); return file.write(buf, offset, length, position); } writeSync(fd, a, b, c, d) { validateFd(fd); let encoding; let offset; let length; let position; const isBuffer = typeof a !== "string"; if (isBuffer) { offset = (b || 0) | 0; length = c; position = d; } else { position = b; encoding = c; } const buf = dataToBuffer(a, encoding); if (isBuffer) { if (typeof length === "undefined") { length = buf.length; } } else { offset = 0; length = buf.length; } return this.writeBase(fd, buf, offset, length, position); } write(fd, a, b, c, d, e) { validateFd(fd); let offset; let length; let position; let encoding; let callback; const tipa = typeof a; const tipb = typeof b; const tipc = typeof c; const tipd = typeof d; if (tipa !== "string") { if (tipb === "function") { callback = b; } else if (tipc === "function") { offset = b | 0; callback = c; } else if (tipd === "function") { offset = b | 0; length = c; callback = d; } else { offset = b | 0; length = c; position = d; callback = e; } } else { if (tipb === "function") { callback = b; } else if (tipc === "function") { position = b; callback = c; } else if (tipd === "function") { position = b; encoding = c; callback = d; } } const buf = dataToBuffer(a, encoding); if (tipa !== "string") { if (typeof length === "undefined") length = buf.length; } else { offset = 0; length = buf.length; } const cb = validateCallback(callback); (0, setImmediate_1.default)(() => { try { const bytes = this.writeBase(fd, buf, offset, length, position); if (tipa !== "string") { cb(null, bytes, buf); } else { cb(null, bytes, a); } } catch (err) { cb(err); } }); } writeFileBase(id, buf, flagsNum, modeNum) { // console.log('writeFileBase', id, buf, flagsNum, modeNum); // const node = this.getNodeByIdOrCreate(id, flagsNum, modeNum); // node.setBuffer(buf); const isUserFd = typeof id === "number"; let fd; if (isUserFd) fd = id; else { fd = this.openBase(pathToFilename(id), flagsNum, modeNum); // fd = this.openSync(id as PathLike, flagsNum, modeNum); } let offset = 0; let length = buf.length; let position = flagsNum & O_APPEND ? undefined : 0; try { while (length > 0) { const written = this.writeSync(fd, buf, offset, length, position); offset += written; length -= written; if (position !== undefined) position += written; } } finally { if (!isUserFd) this.closeSync(fd); } } writeFileSync(id, data, options) { const opts = getWriteFileOptions(options); const flagsNum = flagsToNumber(opts.flag); const modeNum = modeToNumber(opts.mode); const buf = dataToBuffer(data, opts.encoding); this.writeFileBase(id, buf, flagsNum, modeNum); } writeFile(id, data, a, b) { let options = a; let callback = b; if (typeof a === "function") { options = writeFileDefaults; callback = a; } const cb = validateCallback(callback); const opts = getWriteFileOptions(options); const flagsNum = flagsToNumber(opts.flag); const modeNum = modeToNumber(opts.mode); const buf = dataToBuffer(data, opts.encoding); this.wrapAsync(this.writeFileBase, [id, buf, flagsNum, modeNum], cb); } linkBase(filename1, filename2) { const steps1 = filenameToSteps(filename1); const link1 = this.getLink(steps1); if (!link1) throw createError(ENOENT, "link", filename1, filename2); const steps2 = filenameToSteps(filename2); // Check new link directory exists. const dir2 = this.getLinkParent(steps2); if (!dir2) throw createError(ENOENT, "link", filename1, filename2); const name = steps2[steps2.length - 1]; // Check if new file already exists. if (dir2.getChild(name)) throw createError(EEXIST, "link", filename1, filename2); const node = link1.getNode(); node.nlink++; dir2.createChild(name, node); } copyFileBase(src, dest, flags) { const buf = this.readFileSync(src); if (flags & COPYFILE_EXCL) { if (this.existsSync(dest)) { throw createError(EEXIST, "copyFile", src, dest); } } if (flags & COPYFILE_FICLONE_FORCE) { throw createError(ENOSYS, "copyFile", src, dest); } this.writeFileBase(dest, buf, FLAGS.w, 438 /* MODE.DEFAULT */); } copyFileSync(src, dest, flags) { const srcFilename = pathToFilename(src); const destFilename = pathToFilename(dest); return this.copyFileBase(srcFilename, destFilename, (flags || 0) | 0); } copyFile(src, dest, a, b) { const srcFilename = pathToFilename(src); const destFilename = pathToFilename(dest); let flags; let callback; if (typeof a === "function") { flags = 0; callback = a; } else { flags = a; callback = b; } validateCallback(callback); this.wrapAsync(this.copyFileBase, [srcFilename, destFilename, flags], callback); } linkSync(existingPath, newPath) { const existingPathFilename = pathToFilename(existingPath); const newPathFilename = pathToFilename(newPath); this.linkBase(existingPathFilename, newPathFilename); } link(existingPath, newPath, callback) { const existingPathFilename = pathToFilename(existingPath); const newPathFilename = pathToFilename(newPath); this.wrapAsync(this.linkBase, [existingPathFilename, newPathFilename], callback); } unlinkBase(filename) { const steps = filenameToSteps(filename); const link = this.getLink(steps); if (!link) throw createError(ENOENT, "unlink", filename); // TODO: Check if it is file, dir, other... if (link.length) throw Error("Dir not empty..."); this.deleteLink(link); const node = link.getNode(); node.nlink--; // When all hard links to i-node are deleted, remove the i-node, too. if (node.nlink <= 0) { this.deleteNode(node); } } unlinkSync(path) { const filename = pathToFilename(path); this.unlinkBase(filename); } unlink(path, callback) { const filename = pathToFilename(path); this.wrapAsync(this.unlinkBase, [filename], callback); } symlinkBase(targetFilename, pathFilename) { const pathSteps = filenameToSteps(pathFilename); // Check if directory exists, where we about to create a symlink. const dirLink = this.getLinkParent(pathSteps); if (!dirLink) throw createError(ENOENT, "symlink", targetFilename, pathFilename); const name = pathSteps[pathSteps.length - 1]; // Check if new file already exists. if (dirLink.getChild(name)) throw createError(EEXIST, "symlink", targetFilename, pathFilename); // Create symlink. const symlink = dirLink.createChild(name); symlink.getNode().makeSymlink(filenameToSteps(targetFilename)); return symlink; } // `type` argument works only on Windows. symlinkSync(target, path, type) { const targetFilename = pathToFilename(target); const pathFilename = pathToFilename(path); this.symlinkBase(targetFilename, pathFilename); } symlink(target, path, a, b) { const callback = validateCallback(typeof a === "function" ? a : b); const targetFilename = pathToFilename(target); const pathFilename = pathToFilename(path); this.wrapAsync(this.symlinkBase, [targetFilename, pathFilename], callback); } realpathBase(filename, encoding) { const steps = filenameToSteps(filename); const realLink = this.getResolvedLink(steps); if (!realLink) throw createError(ENOENT, "realpath", filename); const path = realLink.getPath(); return (0, encoding_1.strToEncoding)(path ? path : "/", encoding); } realpathSync(path, options) { return this.realpathBase(pathToFilename(path), getRealpathOptions(options).encoding); } realpath(path, a, b) { const [opts, callback] = getRealpathOptsAndCb(a, b); const pathFilename = pathToFilename(path); this.wrapAsync(this.realpathBase, [pathFilename, opts.encoding], callback); } lstatBase(filename, bigint = false, throwIfNoEntry = false) { const link = this.getLink(filenameToSteps(filename)); if (link) { return Stats_1.default.build(link.getNode(), bigint); } else if (!throwIfNoEntry) { return undefined; } else { throw createError(ENOENT, "lstat", filename); } } lstatSync(path, options) { const { throwIfNoEntry = true, bigint = false } = getStatOptions(options); return this.lstatBase(pathToFilename(path), bigint, throwIfNoEntry); } lstat(path, a, b) { const [{ throwIfNoEntry = true, bigint = false }, callback] = getStatOptsAndCb(a, b); this.wrapAsync(this.lstatBase, [pathToFilename(path), bigint, throwIfNoEntry], callback); } statBase(filename, bigint = false, throwIfNoEntry = true) { const link = this.getResolvedLink(filenameToSteps(filename)); if (link) { return Stats_1.default.build(link.getNode(), bigint); } else if (!throwIfNoEntry) { return undefined; } else { throw createError(ENOENT, "stat", filename); } } statSync(path, options) { const { bigint = true, throwIfNoEntry = true } = getStatOptions(options); return this.statBase(pathToFilename(path), bigint, throwIfNoEntry); } stat(path, a, b) { const [{ bigint = false, throwIfNoEntry = true }, callback] = getStatOptsAndCb(a, b); this.wrapAsync(this.statBase, [pathToFilename(path), bigint, throwIfNoEntry], callback); } fstatBase(fd, bigint = false) { const file = this.getFileByFd(fd); if (!file) throw createError(EBADF, "fstat"); return Stats_1.default.build(file.node, bigint); } fstatSync(fd, options) { return this.fstatBase(fd, getStatOptions(options).bigint); } fstat(fd, a, b) { const [opts, callback] = getStatOptsAndCb(a, b); this.wrapAsync(this.fstatBase, [fd, opts.bigint], callback); } renameBase(oldPathFilename, newPathFilename) { const link = this.getLink(filenameToSteps(oldPathFilename)); if (!link) throw createError(ENOENT, "rename", oldPathFilename, newPathFilename); // TODO: Check if it is directory, if non-empty, we cannot move it, right? const newPathSteps = filenameToSteps(newPathFilename); // Check directory exists for the new location. const newPathDirLink = this.getLinkParent(newPathSteps); if (!newPathDirLink) throw createError(ENOENT, "rename", oldPathFilename, newPathFilename); // TODO: Also treat cases with directories and symbolic links. // TODO: See: http://man7.org/linux/man-pages/man2/rename.2.html // Remove hard link from old folder. const oldLinkParent = link.parent; if (oldLinkParent) { oldLinkParent.deleteChild(link); } // Rename should overwrite the new path, if that exists. const name = newPathSteps[newPathSteps.length - 1]; link.name = name; link.steps = [...newPathDirLink.steps, name]; newPathDirLink.setChild(link.getName(), link); } renameSync(oldPath, newPath) { const oldPathFilename = pathToFilename(oldPath); const newPathFilename = pathToFilename(newPath); this.renameBase(oldPathFilename, newPathFilename); } rename(oldPath, newPath, callback) { const oldPathFilename = pathToFilename(oldPath); const newPathFilename = pathToFilename(newPath); this.wrapAsync(this.renameBase, [oldPathFilename, newPathFilename], callback); } existsBase(filename) { return !!this.statBase(filename); } existsSync(path) { try { return this.existsBase(pathToFilename(path)); } catch (err) { return false; } } exists(path, callback) { const filename = pathToFilename(path); if (typeof callback !== "function") throw Error(ERRSTR.CB); (0, setImmediate_1.default)(() => { try { callback(this.existsBase(filename)); } catch (err) { callback(false); } }); } accessBase(filename, mode) { const link = this.getLinkOrThrow(filename, "access"); // TODO: Verify permissions } accessSync(path, mode = F_OK) { const filename = pathToFilename(path); mode = mode | 0; this.accessBase(filename, mode); } access(path, a, b) { let mode = F_OK; let callback; if (typeof a !== "function") { mode = a | 0; // cast to number callback = validateCallback(b); } else { callback = a; } const filename = pathToFilename(path); this.wrapAsync(this.accessBase, [filename, mode], callback); } appendFileSync(id, data, options = appendFileDefaults) { const opts = getAppendFileOpts(options); // force append behavior when using a supplied file descriptor if (!opts.flag || isFd(id)) opts.flag = "a"; this.writeFileSync(id, data, opts); } appendFile(id, data, a, b) { const [opts, callback] = getAppendFileOptsAndCb(a, b); // force append behavior when using a supplied file descriptor if (!opts.flag || isFd(id)) opts.flag = "a"; this.writeFile(id, data, opts, callback); } readdirBase(filename, options) { const steps = filenameToSteps(filename); const link = this.getResolvedLink(steps); if (!link) throw createError(ENOENT, "readdir", filename); const node = link.getNode(); if (!node.isDirectory()) throw createError(ENOTDIR, "scandir", filename); if (options.withFileTypes) { const list = []; for (const name in link.children) { const child = link.getChild(name); if (!child) { continue; } list.push(Dirent_1.default.build(child, options.encoding)); } if (!isWin && options.encoding !== "buffer") list.sort((a, b) => { if (a.name < b.name) return -1; if (a.name > b.name) return 1; return 0; }); return list; } const list = []; for (const name in link.children) { list.push((0, encoding_1.strToEncoding)(name, options.encoding)); } if (!isWin && options.encoding !== "buffer") list.sort(); return list; } readdirSync(path, options) { const opts = getReaddirOptions(options); const filename = pathToFilename(path);