UNPKG

@ezdevlol/memfs

Version:

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

1,312 lines (1,310 loc) 85.6 kB
import * as pathModule from 'path'; import { Node, Link, File } from './node'; import Stats from './Stats'; import Dirent from './Dirent'; import { Buffer, bufferAllocUnsafe, bufferFrom } from './internal/buffer'; import queueMicrotask from './queueMicrotask'; import process from './process'; import setTimeoutUnref from './setTimeoutUnref'; import { ReadableStream as Readable, WritableStream as Writable } from 'web-streams-polyfill'; import { constants } from './constants'; import { EventEmitter } from 'events'; import { strToEncoding, ENCODING_UTF8 } from './encoding'; import { FileHandle } from './node/FileHandle'; import * as util from 'util'; import { FsPromises } from './node/FsPromises'; import { toTreeSync } from './print'; import { ERRSTR, FLAGS } from './node/constants'; import { getDefaultOpts, getDefaultOptsAndCb, getMkdirOptions, getOptions, getReadFileOptions, getReaddirOptions, getReaddirOptsAndCb, getRmOptsAndCb, getRmdirOptions, optsAndCbGenerator, getAppendFileOptsAndCb, getAppendFileOpts, getStatOptsAndCb, getStatOptions, getRealpathOptsAndCb, getRealpathOptions, getWriteFileOptions, writeFileDefaults, getOpendirOptsAndCb, getOpendirOptions, } from './node/options'; import { validateCallback, modeToNumber, pathToFilename, nullCheck, createError, genRndStr6, flagsToNumber, validateFd, isFd, isWin, dataToBuffer, getWriteArgs, bufferToEncoding, getWriteSyncArgs, unixify, } from './node/util'; import { Dir } from './Dir'; const resolveCrossPlatform = pathModule.resolve; const { O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_EXCL, O_TRUNC, O_APPEND, O_DIRECTORY, O_SYMLINK, F_OK, COPYFILE_EXCL, COPYFILE_FICLONE_FORCE, } = constants; const { sep, relative, join, dirname } = pathModule.posix ? pathModule.posix : pathModule; // ---------------------------------------- Constants const kMinPoolSpace = 128; // ---------------------------------------- Error messages const EPERM = 'EPERM'; const ENOENT = 'ENOENT'; const EBADF = 'EBADF'; const EINVAL = 'EINVAL'; 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'; const ERR_OUT_OF_RANGE = 'ERR_OUT_OF_RANGE'; let resolve = (filename, base = process.cwd()) => resolveCrossPlatform(base, filename); if (isWin) { const _resolve = resolve; resolve = (filename, base) => unixify(_resolve(filename, base)); } export function filenameToSteps(filename, base) { const fullPath = resolve(filename, base); const fullPathSansSlash = fullPath.substring(1); if (!fullPathSansSlash) return []; return fullPathSansSlash.split(sep); } export function pathToSteps(path) { return filenameToSteps(pathToFilename(path)); } export function dataToStr(data, encoding = ENCODING_UTF8) { if (Buffer.isBuffer(data)) return data.toString(encoding); else if (data instanceof Uint8Array) return bufferFrom(data).toString(encoding); else return String(data); } // converts Date or number to a fractional UNIX timestamp export 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); } 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' || contentOrNode instanceof Buffer) { 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; } const notImplemented = () => { throw new Error('Not implemented'); }; /** * `Volume` represents a file system. */ export class Volume { 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; } /** * Global file descriptor counter. UNIX file descriptors start from 0 and go sequentially * up, so here, in order not to conflict with them, we choose some big number and descrease * the file descriptor of every new opened file. * @type {number} * @todo This should not be static, right? */ static fd = 0x7fffffff; // Constructor function used to create new nodes. // NodeClass: new (...args) => TNode = Node as new (...args) => TNode; // Hard link to the root of this volume. // root: Node = new (this.NodeClass)(null, '', true); root; // I-node number counter. ino = 0; // A mapping for i-node numbers to i-nodes (`Node`); inodes = {}; // List of released i-node numbers, for reuse. releasedInos = []; // A mapping for file descriptors to `File`s. fds = {}; // A list of reusable (opened and closed) file descriptors, that should be // used first before creating a new file descriptor. releasedFds = []; // Max number of open files. maxFiles = 10000; // Current number of open files. openFiles = 0; StatWatcher; ReadStream; WriteStream; FSWatcher; realpath; realpathSync; props; // @ts-expect-error promisesApi = new FsPromises(this, FileHandle); get promises() { if (this.promisesApi === null) throw new Error('Promise is not supported in this environment.'); return this.promisesApi; } constructor(props = {}) { this.props = Object.assign({ Node, Link, File }, props); const root = this.createLink(); root.setNode(this.createNode(constants.S_IFDIR | 0o777)); 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; const realpathImpl = (path, a, b) => { const [opts, callback] = getRealpathOptsAndCb(a, b); const pathFilename = pathToFilename(path); self.wrapAsync(self.realpathBase, [pathFilename, opts.encoding], callback); }; const realpathSyncImpl = (path, options) => { return self.realpathBase(pathToFilename(path), getRealpathOptions(options).encoding); }; this.realpath = realpathImpl; this.realpath.native = realpathImpl; this.realpathSync = realpathSyncImpl; this.realpathSync.native = realpathSyncImpl; } createLink(parent, name, isDirectory = false, mode) { if (!parent) { return new this.props.Link(this, null, ''); } if (!name) { throw new Error('createLink: name cannot be empty'); } // If no explicit permission is provided, use defaults based on type const finalPerm = mode ?? (isDirectory ? 0o777 : 0o666); // To prevent making a breaking change, `mode` can also just be a permission number // and the file type is set based on `isDirectory` const hasFileType = mode && mode & constants.S_IFMT; const modeType = hasFileType ? mode & constants.S_IFMT : isDirectory ? constants.S_IFDIR : constants.S_IFREG; const finalMode = (finalPerm & ~constants.S_IFMT) | modeType; return parent.createChild(name, this.createNode(finalMode)); } 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(mode) { const node = new this.props.Node(this.newInoNumber(), mode); this.inodes[node.ino] = node; return node; } deleteNode(node) { node.del(); delete this.inodes[node.ino]; this.releasedInos.push(node.ino); } walk(stepsOrFilenameOrLink, resolveSymlinks = false, checkExistence = false, checkAccess = false, funcName) { let steps; let filename; if (stepsOrFilenameOrLink instanceof Link) { steps = stepsOrFilenameOrLink.steps; filename = sep + steps.join(sep); } else if (typeof stepsOrFilenameOrLink === 'string') { steps = filenameToSteps(stepsOrFilenameOrLink); filename = stepsOrFilenameOrLink; } else { steps = stepsOrFilenameOrLink; filename = sep + steps.join(sep); } let curr = this.root; let i = 0; while (i < steps.length) { let node = curr.getNode(); // Check access permissions if current link is a directory if (node.isDirectory()) { if (checkAccess && !node.canExecute()) { throw createError(EACCES, funcName, filename); } } else { if (i < steps.length - 1) throw createError(ENOTDIR, funcName, filename); } curr = curr.getChild(steps[i]) ?? null; // Check existence of current link if (!curr) if (checkExistence) throw createError(ENOENT, funcName, filename); else return null; node = curr?.getNode(); // Resolve symlink if (resolveSymlinks && node.isSymlink()) { const resolvedPath = pathModule.isAbsolute(node.symlink) ? node.symlink : join(pathModule.dirname(curr.getPath()), node.symlink); // Relative to symlink's parent steps = filenameToSteps(resolvedPath).concat(steps.slice(i + 1)); curr = this.root; i = 0; continue; } i++; } return curr; } // Returns a `Link` (hard link) referenced by path "split" into steps. getLink(steps) { return this.walk(steps, false, false, false); } // Just link `getLink`, but throws a correct user error, if link to found. getLinkOrThrow(filename, funcName) { return this.walk(filename, false, true, true, funcName); } // Just like `getLink`, but also dereference/resolves symbolic links. getResolvedLink(filenameOrSteps) { return this.walk(filenameOrSteps, true, false, false); } // Just like `getLinkOrThrow`, but also dereference/resolves symbolic links. getResolvedLinkOrThrow(filename, funcName) { return this.walk(filename, true, true, true, funcName); } resolveSymlinks(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.getLink(steps.slice(0, -1)); } getLinkParentAsDirOrThrow(filenameOrSteps, funcName) { const steps = (filenameOrSteps instanceof Array ? filenameOrSteps : filenameToSteps(filenameOrSteps)).slice(0, -1); const filename = sep + steps.join(sep); const link = this.getLinkOrThrow(filename, funcName); if (!link.getNode().isDirectory()) throw createError(ENOTDIR, funcName, filename); 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); Promise.resolve().then(() => { let result; try { result = method.apply(this, args); } catch (err) { callback(err); return; } callback(null, result); }); } _toJSON(link = this.root, json = {}, path, asBuffer) { let isEmpty = true; let children = link.children; if (link.getNode().isFile()) { children = new Map([[link.getName(), link.parent.getChild(link.getName())]]); link = link.parent; } for (const name of children.keys()) { if (name === '.' || name === '..') { continue; } 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] = asBuffer ? node.getBuffer() : node.getString(); } else if (node.isDirectory()) { this._toJSON(child, json, path, asBuffer); } } let dirPath = link.getPath(); if (path) dirPath = relative(path, dirPath); if (dirPath && isEmpty) { json[dirPath] = null; } return json; } toJSON(paths, json = {}, isRelative = false, asBuffer = false) { const links = []; if (paths) { if (!Array.isArray(paths)) 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() : '', asBuffer); return json; } // TODO: `cwd` should probably not invoke `process.cwd()`. fromJSON(json, cwd = process.cwd()) { for (let filename in json) { const data = json[filename]; filename = resolve(filename, cwd); if (typeof data === 'string' || data instanceof Buffer) { 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); } toTree(opts = { separator: sep }) { return toTreeSync(this, opts); } reset() { this.ino = 0; this.inodes = {}; this.releasedInos = []; this.fds = {}; this.releasedFds = []; this.openFiles = 0; this.root = this.createLink(); this.root.setNode(this.createNode(constants.S_IFDIR | 0o777)); } // 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. // // @TODO: This should be superfluous. This method is only ever called by openFile(), which does its own symlink resolution // prior to calling. let realLink = link; if (resolveSymlinks) realLink = this.getResolvedLinkOrThrow(link.getPath(), 'open'); 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_RDONLY)) { if (!node.canWrite()) { throw createError(EACCES, 'open', link.getPath()); } } 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; try { link = resolveSymlinks ? this.getResolvedLinkOrThrow(filename, 'open') : this.getLinkOrThrow(filename, 'open'); // Check if file already existed when trying to create it exclusively (O_CREAT and O_EXCL flags are set). // This is an error, see https://pubs.opengroup.org/onlinepubs/009695399/functions/open.html: // "If O_CREAT and O_EXCL are set, open() shall fail if the file exists." if (link && flagsNum & O_CREAT && flagsNum & O_EXCL) throw createError(EEXIST, 'open', filename); } catch (err) { // Try creating a new file, if it does not exist and O_CREAT flag is set. // Note that this will still throw if the ENOENT came from one of the // intermediate directories instead of the file itself. if (err.code === ENOENT && flagsNum & O_CREAT) { const dirname = pathModule.dirname(filename); const dirLink = this.getResolvedLinkOrThrow(dirname); const dirNode = dirLink.getNode(); // Check that the place we create the new file is actually a directory and that we are allowed to do so: if (!dirNode.isDirectory()) throw createError(ENOTDIR, 'open', filename); if (!dirNode.canExecute() || !dirNode.canWrite()) throw createError(EACCES, 'open', filename); // This is a difference to the original implementation, which would simply not create a file unless modeNum was specified. // However, current Node versions will default to 0o666. modeNum ??= 0o666; link = this.createLink(dirLink, steps[steps.length - 1], false, modeNum); } else throw err; } 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, !(flagsNum & O_SYMLINK)); } 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, !(flagsNum & O_SYMLINK)], 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); const file = this.getFileByFdOrThrow(fd, 'close'); // NOTE: not calling closeSync because we can reset in between close and closeSync this.wrapAsync(this.closeFile, [file], 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) { if (buffer.byteLength < length) { throw createError(ERR_OUT_OF_RANGE, 'read', undefined, undefined, RangeError); } const file = this.getFileByFdOrThrow(fd); if (file.node.isSymlink()) { throw createError(EPERM, 'read', file.link.getPath()); } return file.read(buffer, Number(offset), Number(length), position === -1 || typeof position !== 'number' ? undefined : 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 queueMicrotask(() => { if (callback) callback(null, 0, buffer); }); } Promise.resolve().then(() => { try { const bytes = this.readBase(fd, buffer, offset, length, position); callback(null, bytes, buffer); } catch (err) { callback(err); } }); } readvBase(fd, buffers, position) { const file = this.getFileByFdOrThrow(fd); let p = position ?? undefined; if (p === -1) { p = undefined; } let bytesRead = 0; for (const buffer of buffers) { const bytes = file.read(buffer, 0, buffer.byteLength, p); p = undefined; bytesRead += bytes; if (bytes < buffer.byteLength) break; } return bytesRead; } readv(fd, buffers, a, b) { let position = a; let callback = b; if (typeof a === 'function') { position = null; callback = a; } validateCallback(callback); Promise.resolve().then(() => { try { const bytes = this.readvBase(fd, buffers, position); callback(null, bytes, buffers); } catch (err) { callback(err); } }); } readvSync(fd, buffers, position) { validateFd(fd); return this.readvBase(fd, buffers, position); } 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 link = this.getResolvedLinkOrThrow(filename, 'open'); 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'); if (file.node.isSymlink()) { throw createError(EBADF, 'write', file.link.getPath()); } return file.write(buf, offset, length, position === -1 || typeof position !== 'number' ? undefined : position); } writeSync(fd, a, b, c, d) { const [, buf, offset, length, position] = getWriteSyncArgs(fd, a, b, c, d); return this.writeBase(fd, buf, offset, length, position); } write(fd, a, b, c, d, e) { const [, asStr, buf, offset, length, position, cb] = getWriteArgs(fd, a, b, c, d, e); Promise.resolve().then(() => { try { const bytes = this.writeBase(fd, buf, offset, length, position); if (!asStr) { cb(null, bytes, buf); } else { cb(null, bytes, a); } } catch (err) { cb(err); } }); } writevBase(fd, buffers, position) { const file = this.getFileByFdOrThrow(fd); let p = position ?? undefined; if (p === -1) { p = undefined; } let bytesWritten = 0; for (const buffer of buffers) { const nodeBuf = Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength); const bytes = file.write(nodeBuf, 0, nodeBuf.byteLength, p); p = undefined; bytesWritten += bytes; if (bytes < nodeBuf.byteLength) break; } return bytesWritten; } writev(fd, buffers, a, b) { let position = a; let callback = b; if (typeof a === 'function') { position = null; callback = a; } validateCallback(callback); Promise.resolve().then(() => { try { const bytes = this.writevBase(fd, buffers, position); callback(null, bytes, buffers); } catch (err) { callback(err); } }); } writevSync(fd, buffers, position) { validateFd(fd); return this.writevBase(fd, buffers, position); } 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) { let link1; try { link1 = this.getLinkOrThrow(filename1, 'link'); } catch (err) { // Augment error with filename2 if (err.code) err = createError(err.code, 'link', filename1, filename2); throw err; } const dirname2 = pathModule.dirname(filename2); let dir2; try { dir2 = this.getLinkOrThrow(dirname2, 'link'); } catch (err) { // Augment error with filename1 if (err.code) err = createError(err.code, 'link', filename1, filename2); throw err; } const name = pathModule.basename(filename2); // 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); } cpSyncBase(src, dest, options) { // Apply filter if provided if (options.filter && !options.filter(src, dest)) { return; } const srcStat = options.dereference ? this.statSync(src) : this.lstatSync(src); let destStat = null; try { destStat = this.lstatSync(dest); } catch (err) { if (err.code !== 'ENOENT') { throw err; } } // Check if src and dest are the same (both exist and have same inode) if (destStat && this.areIdentical(srcStat, destStat)) { throw createError(EINVAL, 'cp', src, dest); } // Check type compatibility if (destStat) { if (srcStat.isDirectory() && !destStat.isDirectory()) { throw createError(EISDIR, 'cp', src, dest); } if (!srcStat.isDirectory() && destStat.isDirectory()) { throw createError(ENOTDIR, 'cp', src, dest); } } // Check if trying to copy directory to subdirectory of itself if (srcStat.isDirectory() && this.isSrcSubdir(src, dest)) { throw createError(EINVAL, 'cp', src, dest); } // Ensure parent directory exists this.ensureParentDir(dest); // Handle different file types if (srcStat.isDirectory()) { if (!options.recursive) { throw createError(EISDIR, 'cp', src); } this.cpDirSync(srcStat, destStat, src, dest, options); } else if (srcStat.isFile() || srcStat.isCharacterDevice() || srcStat.isBlockDevice()) { this.cpFileSync(srcStat, destStat, src, dest, options); } else if (srcStat.isSymbolicLink() && !options.dereference) { // Only handle as symlink if not dereferencing this.cpSymlinkSync(destStat, src, dest, options); } else { throw createError(EINVAL, 'cp', src); } } areIdentical(srcStat, destStat) { return srcStat.ino === destStat.ino && srcStat.dev === destStat.dev; } isSrcSubdir(src, dest) { // Use Node.js path utilities for cross-platform compatibility const { resolve, relative, normalize } = pathModule; try { // Normalize paths using Node.js utilities // Convert to absolute paths to ensure consistent comparison const normalizedSrc = normalize(src.startsWith('/') ? src : '/' + src); const normalizedDest = normalize(dest.startsWith('/') ? dest : '/' + dest); // Check if paths are identical if (normalizedSrc === normalizedDest) { return true; } // Check if dest is under src by using relative path // If dest is under src, the relative path from src to dest won't start with '..' const relativePath = relative(normalizedSrc, normalizedDest); // If relative path is empty or doesn't start with '..', dest is under src return relativePath === '' || (!relativePath.startsWith('..') && !pathModule.isAbsolute(relativePath)); } catch (error) { // If path operations fail, assume it's safe (don't block the copy) return false; } } ensureParentDir(dest) { const parent = dirname(dest); if (!this.existsSync(parent)) { this.mkdirSync(parent, { recursive: true }); } } cpFileSync(srcStat, destStat, src, dest, options) { if (destStat) { if (options.errorOnExist) { throw createError(EEXIST, 'cp', dest); } if (!options.force) { return; } this.unlinkSync(dest); } // Copy the file this.copyFileSync(src, dest, options.mode); // Preserve timestamps if requested if (options.preserveTimestamps) { this.utimesSync(dest, srcStat.atime, srcStat.mtime); } // Set file mode this.chmodSync(dest, Number(srcStat.mode)); } cpDirSync(srcStat, destStat, src, dest, options) { if (!destStat) { this.mkdirSync(dest); } // Read directory contents const entries = this.readdirSync(src); for (const entry of entries) { const srcItem = join(src, entry); const destItem = join(dest, entry); // Apply filter to each item if (options.filter && !options.filter(srcItem, destItem)) { continue; } this.cpSyncBase(srcItem, destItem, options); } // Set directory mode this.chmodSync(dest, Number(srcStat.mode)); } cpSymlinkSync(destStat, src, dest, options) { let linkTarget = String(this.readlinkSync(src)); if (!options.verbatimSymlinks && !pathModule.isAbsolute(linkTarget)) { linkTarget = resolveCrossPlatform(dirname(src), linkTarget); } if (destStat) { this.unlinkSync(dest); } this.symlinkSync(linkTarget, dest); } 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 link = this.getLinkOrThrow(filename, 'unlink'); // 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. let dirLink; try { dirLink = this.getLinkParentAsDirOrThrow(pathSteps); } catch (err) { // Catch error to populate with the correct fields - getLinkParentAsDirOrThrow won't be aware of the second path if (err.code) err = createError(err.code, 'symlink', targetFilename, pathFilename); throw err; } const name = pathSteps[pathSteps.length - 1]; // Check if new file already exists. if (dirLink.getChild(name)) throw createError(EEXIST, 'symlink', targetFilename, pathFilename); // Check permissions on the path where we are creating the symlink. // Note we're not checking permissions on the target path: It is not an error to create a symlink to a // non-existent or inaccessible target const node = dirLink.getNode(); if (!node.canExecute() || !node.canWrite()) throw createError(EACCES, 'symlink', targetFilename, pathFilename); // Create symlink. const symlink = dirLink.createChild(name); symlink.getNode().makeSymlink(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 realLink = this.getResolvedLinkOrThrow(filename, 'realpath'); return strToEncoding(realLink.getPath() || '/', encoding); } lstatBase(filename, bigint = false, throwIfNoEntry = false) { let link; try { link = this.getLinkOrThrow(filename, 'lstat'); } catch (err) { if (err.code === ENOENT && !throwIfNoEntry) return undefined; else throw err; } return Stats.build(link.getNode(), bigint); } 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) { let link; try { link = this.getResolvedLinkOrThrow(filename, 'stat'); } catch (err) { if (err.code === ENOENT && !throwIfNoEntry) return undefined; else throw err; } return Stats.build(link.getNode(), bigint); } 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.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) { let link; try { link = this.getResolvedLinkOrThrow(oldPathFilename); } catch (err) { // Augment err with newPathFilename if (err.code) err = createError(err.code, 'rename', oldPathFilename, newPathFilename); throw err; } // TODO: Check if it is directory, if non-empty, we cannot move it, right? // Check directory exists for the new location. let newPathDirLink; try { newPathDirLink = this.getLinkParentAsDirOrThrow(newPathFilename); } catch (err) { // Augment error with oldPathFilename if (err.code) err = createError(err.code, 'rename', oldPathFilename, newPathFilename); throw err; } // 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; // Check we have access and write permissions in both places const oldParentNode = oldLinkParent.getNode(); const newPathDirNode = newPathDirLink.getNode(); if (!oldParentNode.canExecute() || !oldParentNode.canWrite() || !newPathDirNode.canExecute() || !newPathDirNode.canWrite()) { throw createError(EACCES, 'rename', oldPathFilename, newPathFilename); } oldLinkParent.deleteChild(link); // Rename should overwrite the new path, if that exists. const name = pathModule.basename(newPathFilename); 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); Promise.resolve().then(() => { try { callback(this.existsBase(filename)); } catch (err) { callback(false); } }); } accessBase(filename, mode) { const link = this.getLinkOrThrow(filename, 'access'); } 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) { 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.getResolvedLinkOrThrow(filename, 'scandir'); const node = link.getNode(); if (!node.isDirectory()) throw createError(ENOTDIR, 'scandir', filename); // Check we have permissions if (!node.canRead()) throw createError(EACCES, 'scandir', filename); const list = []; // output list for (const name of link.children.keys()) { const child = link.getChild(name); if (!child || name === '.' || name === '..') continue; list.push(Dirent.build(child, options.encoding)); // recursion if (options.recursive && child.children.size) { const recurseOptions = { ...options, recursive: true, withFileTypes: true }; const childList = this.readdirBase(child.getPath(), recurseOptions); list.push(...childList); } } if (!isWin && options.encoding !== 'buffer') list.sort((a, b) => { if (a.name < b.name) return -1; if (a.name >