@ezdevlol/memfs
Version:
In-memory file-system with Node's fs API.
1,312 lines (1,310 loc) • 85.6 kB
JavaScript
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 >