"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MountFS = void 0;
const tslib_1 = require("tslib");
const fs_1 = require("fs");
const FakeFS_1 = require("./FakeFS");
const NodeFS_1 = require("./NodeFS");
const watchFile_1 = require("./algorithms/watchFile");
const errors = tslib_1.__importStar(require("./errors"));
const path_1 = require("./path");
// Only file descriptors prefixed by those values will be forwarded to the MountFS
// instances. Note that the highest MOUNT_MAGIC bit MUST NOT be set, otherwise the
// resulting fd becomes a negative integer, which isn't supposed to happen per
// the unix rules (caused problems w/ Go).
//
// Those values must be synced with packages/yarnpkg-pnp/sources/esm-loader/fspatch.ts
//
const MOUNT_MASK = 0xff000000;
class MountFS extends FakeFS_1.BasePortableFakeFS {
    constructor({ baseFs = new NodeFS_1.NodeFS(), filter = null, magicByte = 0x2a, maxOpenFiles = Infinity, useCache = true, maxAge = 5000, typeCheck = fs_1.constants.S_IFREG, getMountPoint, factoryPromise, factorySync }) {
        if (Math.floor(magicByte) !== magicByte || !(magicByte > 1 && magicByte <= 127))
            throw new Error(`The magic byte must be set to a round value between 1 and 127 included`);
        super();
        this.fdMap = new Map();
        this.nextFd = 3;
        this.isMount = new Set();
        this.notMount = new Set();
        this.realPaths = new Map();
        this.limitOpenFilesTimeout = null;
        this.baseFs = baseFs;
        this.mountInstances = useCache ? new Map() : null;
        this.factoryPromise = factoryPromise;
        this.factorySync = factorySync;
        this.filter = filter;
        this.getMountPoint = getMountPoint;
        this.magic = magicByte << 24;
        this.maxAge = maxAge;
        this.maxOpenFiles = maxOpenFiles;
        this.typeCheck = typeCheck;
    }
    getExtractHint(hints) {
        return this.baseFs.getExtractHint(hints);
    }
    getRealPath() {
        return this.baseFs.getRealPath();
    }
    saveAndClose() {
        (0, watchFile_1.unwatchAllFiles)(this);
        if (this.mountInstances) {
            for (const [path, { childFs }] of this.mountInstances.entries()) {
                childFs.saveAndClose?.();
                this.mountInstances.delete(path);
            }
        }
    }
    discardAndClose() {
        (0, watchFile_1.unwatchAllFiles)(this);
        if (this.mountInstances) {
            for (const [path, { childFs }] of this.mountInstances.entries()) {
                childFs.discardAndClose?.();
                this.mountInstances.delete(path);
            }
        }
    }
    resolve(p) {
        return this.baseFs.resolve(p);
    }
    remapFd(mountFs, fd) {
        const remappedFd = this.nextFd++ | this.magic;
        this.fdMap.set(remappedFd, [mountFs, fd]);
        return remappedFd;
    }
    async openPromise(p, flags, mode) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.openPromise(p, flags, mode);
        }, async (mountFs, { subPath }) => {
            return this.remapFd(mountFs, await mountFs.openPromise(subPath, flags, mode));
        });
    }
    openSync(p, flags, mode) {
        return this.makeCallSync(p, () => {
            return this.baseFs.openSync(p, flags, mode);
        }, (mountFs, { subPath }) => {
            return this.remapFd(mountFs, mountFs.openSync(subPath, flags, mode));
        });
    }
    async opendirPromise(p, opts) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.opendirPromise(p, opts);
        }, async (mountFs, { subPath }) => {
            return await mountFs.opendirPromise(subPath, opts);
        }, {
            requireSubpath: false,
        });
    }
    opendirSync(p, opts) {
        return this.makeCallSync(p, () => {
            return this.baseFs.opendirSync(p, opts);
        }, (mountFs, { subPath }) => {
            return mountFs.opendirSync(subPath, opts);
        }, {
            requireSubpath: false,
        });
    }
    async readPromise(fd, buffer, offset, length, position) {
        if ((fd & MOUNT_MASK) !== this.magic)
            return await this.baseFs.readPromise(fd, buffer, offset, length, position);
        const entry = this.fdMap.get(fd);
        if (typeof entry === `undefined`)
            throw errors.EBADF(`read`);
        const [mountFs, realFd] = entry;
        return await mountFs.readPromise(realFd, buffer, offset, length, position);
    }
    readSync(fd, buffer, offset, length, position) {
        if ((fd & MOUNT_MASK) !== this.magic)
            return this.baseFs.readSync(fd, buffer, offset, length, position);
        const entry = this.fdMap.get(fd);
        if (typeof entry === `undefined`)
            throw errors.EBADF(`readSync`);
        const [mountFs, realFd] = entry;
        return mountFs.readSync(realFd, buffer, offset, length, position);
    }
    async writePromise(fd, buffer, offset, length, position) {
        if ((fd & MOUNT_MASK) !== this.magic) {
            if (typeof buffer === `string`) {
                return await this.baseFs.writePromise(fd, buffer, offset);
            }
            else {
                return await this.baseFs.writePromise(fd, buffer, offset, length, position);
            }
        }
        const entry = this.fdMap.get(fd);
        if (typeof entry === `undefined`)
            throw errors.EBADF(`write`);
        const [mountFs, realFd] = entry;
        if (typeof buffer === `string`) {
            return await mountFs.writePromise(realFd, buffer, offset);
        }
        else {
            return await mountFs.writePromise(realFd, buffer, offset, length, position);
        }
    }
    writeSync(fd, buffer, offset, length, position) {
        if ((fd & MOUNT_MASK) !== this.magic) {
            if (typeof buffer === `string`) {
                return this.baseFs.writeSync(fd, buffer, offset);
            }
            else {
                return this.baseFs.writeSync(fd, buffer, offset, length, position);
            }
        }
        const entry = this.fdMap.get(fd);
        if (typeof entry === `undefined`)
            throw errors.EBADF(`writeSync`);
        const [mountFs, realFd] = entry;
        if (typeof buffer === `string`) {
            return mountFs.writeSync(realFd, buffer, offset);
        }
        else {
            return mountFs.writeSync(realFd, buffer, offset, length, position);
        }
    }
    async closePromise(fd) {
        if ((fd & MOUNT_MASK) !== this.magic)
            return await this.baseFs.closePromise(fd);
        const entry = this.fdMap.get(fd);
        if (typeof entry === `undefined`)
            throw errors.EBADF(`close`);
        this.fdMap.delete(fd);
        const [mountFs, realFd] = entry;
        return await mountFs.closePromise(realFd);
    }
    closeSync(fd) {
        if ((fd & MOUNT_MASK) !== this.magic)
            return this.baseFs.closeSync(fd);
        const entry = this.fdMap.get(fd);
        if (typeof entry === `undefined`)
            throw errors.EBADF(`closeSync`);
        this.fdMap.delete(fd);
        const [mountFs, realFd] = entry;
        return mountFs.closeSync(realFd);
    }
    createReadStream(p, opts) {
        if (p === null)
            return this.baseFs.createReadStream(p, opts);
        return this.makeCallSync(p, () => {
            return this.baseFs.createReadStream(p, opts);
        }, (mountFs, { archivePath, subPath }) => {
            const stream = mountFs.createReadStream(subPath, opts);
            // This is a very hacky workaround. `MountOpenFS` shouldn't have to work with `NativePath`s.
            // Ref: https://github.com/yarnpkg/berry/pull/3774
            // TODO: think of a better solution
            stream.path = path_1.npath.fromPortablePath(this.pathUtils.join(archivePath, subPath));
            return stream;
        });
    }
    createWriteStream(p, opts) {
        if (p === null)
            return this.baseFs.createWriteStream(p, opts);
        return this.makeCallSync(p, () => {
            return this.baseFs.createWriteStream(p, opts);
        }, (mountFs, { subPath }) => {
            return mountFs.createWriteStream(subPath, opts);
        });
    }
    async realpathPromise(p) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.realpathPromise(p);
        }, async (mountFs, { archivePath, subPath }) => {
            let realArchivePath = this.realPaths.get(archivePath);
            if (typeof realArchivePath === `undefined`) {
                realArchivePath = await this.baseFs.realpathPromise(archivePath);
                this.realPaths.set(archivePath, realArchivePath);
            }
            return this.pathUtils.join(realArchivePath, this.pathUtils.relative(path_1.PortablePath.root, await mountFs.realpathPromise(subPath)));
        });
    }
    realpathSync(p) {
        return this.makeCallSync(p, () => {
            return this.baseFs.realpathSync(p);
        }, (mountFs, { archivePath, subPath }) => {
            let realArchivePath = this.realPaths.get(archivePath);
            if (typeof realArchivePath === `undefined`) {
                realArchivePath = this.baseFs.realpathSync(archivePath);
                this.realPaths.set(archivePath, realArchivePath);
            }
            return this.pathUtils.join(realArchivePath, this.pathUtils.relative(path_1.PortablePath.root, mountFs.realpathSync(subPath)));
        });
    }
    async existsPromise(p) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.existsPromise(p);
        }, async (mountFs, { subPath }) => {
            return await mountFs.existsPromise(subPath);
        });
    }
    existsSync(p) {
        return this.makeCallSync(p, () => {
            return this.baseFs.existsSync(p);
        }, (mountFs, { subPath }) => {
            return mountFs.existsSync(subPath);
        });
    }
    async accessPromise(p, mode) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.accessPromise(p, mode);
        }, async (mountFs, { subPath }) => {
            return await mountFs.accessPromise(subPath, mode);
        });
    }
    accessSync(p, mode) {
        return this.makeCallSync(p, () => {
            return this.baseFs.accessSync(p, mode);
        }, (mountFs, { subPath }) => {
            return mountFs.accessSync(subPath, mode);
        });
    }
    async statPromise(p, opts) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.statPromise(p, opts);
        }, async (mountFs, { subPath }) => {
            return await mountFs.statPromise(subPath, opts);
        });
    }
    statSync(p, opts) {
        return this.makeCallSync(p, () => {
            return this.baseFs.statSync(p, opts);
        }, (mountFs, { subPath }) => {
            return mountFs.statSync(subPath, opts);
        });
    }
    async fstatPromise(fd, opts) {
        if ((fd & MOUNT_MASK) !== this.magic)
            return this.baseFs.fstatPromise(fd, opts);
        const entry = this.fdMap.get(fd);
        if (typeof entry === `undefined`)
            throw errors.EBADF(`fstat`);
        const [mountFs, realFd] = entry;
        return mountFs.fstatPromise(realFd, opts);
    }
    fstatSync(fd, opts) {
        if ((fd & MOUNT_MASK) !== this.magic)
            return this.baseFs.fstatSync(fd, opts);
        const entry = this.fdMap.get(fd);
        if (typeof entry === `undefined`)
            throw errors.EBADF(`fstatSync`);
        const [mountFs, realFd] = entry;
        return mountFs.fstatSync(realFd, opts);
    }
    async lstatPromise(p, opts) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.lstatPromise(p, opts);
        }, async (mountFs, { subPath }) => {
            return await mountFs.lstatPromise(subPath, opts);
        });
    }
    lstatSync(p, opts) {
        return this.makeCallSync(p, () => {
            return this.baseFs.lstatSync(p, opts);
        }, (mountFs, { subPath }) => {
            return mountFs.lstatSync(subPath, opts);
        });
    }
    async fchmodPromise(fd, mask) {
        if ((fd & MOUNT_MASK) !== this.magic)
            return this.baseFs.fchmodPromise(fd, mask);
        const entry = this.fdMap.get(fd);
        if (typeof entry === `undefined`)
            throw errors.EBADF(`fchmod`);
        const [mountFs, realFd] = entry;
        return mountFs.fchmodPromise(realFd, mask);
    }
    fchmodSync(fd, mask) {
        if ((fd & MOUNT_MASK) !== this.magic)
            return this.baseFs.fchmodSync(fd, mask);
        const entry = this.fdMap.get(fd);
        if (typeof entry === `undefined`)
            throw errors.EBADF(`fchmodSync`);
        const [mountFs, realFd] = entry;
        return mountFs.fchmodSync(realFd, mask);
    }
    async chmodPromise(p, mask) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.chmodPromise(p, mask);
        }, async (mountFs, { subPath }) => {
            return await mountFs.chmodPromise(subPath, mask);
        });
    }
    chmodSync(p, mask) {
        return this.makeCallSync(p, () => {
            return this.baseFs.chmodSync(p, mask);
        }, (mountFs, { subPath }) => {
            return mountFs.chmodSync(subPath, mask);
        });
    }
    async fchownPromise(fd, uid, gid) {
        if ((fd & MOUNT_MASK) !== this.magic)
            return this.baseFs.fchownPromise(fd, uid, gid);
        const entry = this.fdMap.get(fd);
        if (typeof entry === `undefined`)
            throw errors.EBADF(`fchown`);
        const [zipFs, realFd] = entry;
        return zipFs.fchownPromise(realFd, uid, gid);
    }
    fchownSync(fd, uid, gid) {
        if ((fd & MOUNT_MASK) !== this.magic)
            return this.baseFs.fchownSync(fd, uid, gid);
        const entry = this.fdMap.get(fd);
        if (typeof entry === `undefined`)
            throw errors.EBADF(`fchownSync`);
        const [zipFs, realFd] = entry;
        return zipFs.fchownSync(realFd, uid, gid);
    }
    async chownPromise(p, uid, gid) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.chownPromise(p, uid, gid);
        }, async (mountFs, { subPath }) => {
            return await mountFs.chownPromise(subPath, uid, gid);
        });
    }
    chownSync(p, uid, gid) {
        return this.makeCallSync(p, () => {
            return this.baseFs.chownSync(p, uid, gid);
        }, (mountFs, { subPath }) => {
            return mountFs.chownSync(subPath, uid, gid);
        });
    }
    async renamePromise(oldP, newP) {
        return await this.makeCallPromise(oldP, async () => {
            return await this.makeCallPromise(newP, async () => {
                return await this.baseFs.renamePromise(oldP, newP);
            }, async () => {
                throw Object.assign(new Error(`EEXDEV: cross-device link not permitted`), { code: `EEXDEV` });
            });
        }, async (mountFsO, { subPath: subPathO }) => {
            return await this.makeCallPromise(newP, async () => {
                throw Object.assign(new Error(`EEXDEV: cross-device link not permitted`), { code: `EEXDEV` });
            }, async (mountFsN, { subPath: subPathN }) => {
                if (mountFsO !== mountFsN) {
                    throw Object.assign(new Error(`EEXDEV: cross-device link not permitted`), { code: `EEXDEV` });
                }
                else {
                    return await mountFsO.renamePromise(subPathO, subPathN);
                }
            });
        });
    }
    renameSync(oldP, newP) {
        return this.makeCallSync(oldP, () => {
            return this.makeCallSync(newP, () => {
                return this.baseFs.renameSync(oldP, newP);
            }, () => {
                throw Object.assign(new Error(`EEXDEV: cross-device link not permitted`), { code: `EEXDEV` });
            });
        }, (mountFsO, { subPath: subPathO }) => {
            return this.makeCallSync(newP, () => {
                throw Object.assign(new Error(`EEXDEV: cross-device link not permitted`), { code: `EEXDEV` });
            }, (mountFsN, { subPath: subPathN }) => {
                if (mountFsO !== mountFsN) {
                    throw Object.assign(new Error(`EEXDEV: cross-device link not permitted`), { code: `EEXDEV` });
                }
                else {
                    return mountFsO.renameSync(subPathO, subPathN);
                }
            });
        });
    }
    async copyFilePromise(sourceP, destP, flags = 0) {
        const fallback = async (sourceFs, sourceP, destFs, destP) => {
            if ((flags & fs_1.constants.COPYFILE_FICLONE_FORCE) !== 0)
                throw Object.assign(new Error(`EXDEV: cross-device clone not permitted, copyfile '${sourceP}' -> ${destP}'`), { code: `EXDEV` });
            if ((flags & fs_1.constants.COPYFILE_EXCL) && await this.existsPromise(sourceP))
                throw Object.assign(new Error(`EEXIST: file already exists, copyfile '${sourceP}' -> '${destP}'`), { code: `EEXIST` });
            let content;
            try {
                content = await sourceFs.readFilePromise(sourceP);
            }
            catch {
                throw Object.assign(new Error(`EINVAL: invalid argument, copyfile '${sourceP}' -> '${destP}'`), { code: `EINVAL` });
            }
            await destFs.writeFilePromise(destP, content);
        };
        return await this.makeCallPromise(sourceP, async () => {
            return await this.makeCallPromise(destP, async () => {
                return await this.baseFs.copyFilePromise(sourceP, destP, flags);
            }, async (mountFsD, { subPath: subPathD }) => {
                return await fallback(this.baseFs, sourceP, mountFsD, subPathD);
            });
        }, async (mountFsS, { subPath: subPathS }) => {
            return await this.makeCallPromise(destP, async () => {
                return await fallback(mountFsS, subPathS, this.baseFs, destP);
            }, async (mountFsD, { subPath: subPathD }) => {
                if (mountFsS !== mountFsD) {
                    return await fallback(mountFsS, subPathS, mountFsD, subPathD);
                }
                else {
                    return await mountFsS.copyFilePromise(subPathS, subPathD, flags);
                }
            });
        });
    }
    copyFileSync(sourceP, destP, flags = 0) {
        const fallback = (sourceFs, sourceP, destFs, destP) => {
            if ((flags & fs_1.constants.COPYFILE_FICLONE_FORCE) !== 0)
                throw Object.assign(new Error(`EXDEV: cross-device clone not permitted, copyfile '${sourceP}' -> ${destP}'`), { code: `EXDEV` });
            if ((flags & fs_1.constants.COPYFILE_EXCL) && this.existsSync(sourceP))
                throw Object.assign(new Error(`EEXIST: file already exists, copyfile '${sourceP}' -> '${destP}'`), { code: `EEXIST` });
            let content;
            try {
                content = sourceFs.readFileSync(sourceP);
            }
            catch {
                throw Object.assign(new Error(`EINVAL: invalid argument, copyfile '${sourceP}' -> '${destP}'`), { code: `EINVAL` });
            }
            destFs.writeFileSync(destP, content);
        };
        return this.makeCallSync(sourceP, () => {
            return this.makeCallSync(destP, () => {
                return this.baseFs.copyFileSync(sourceP, destP, flags);
            }, (mountFsD, { subPath: subPathD }) => {
                return fallback(this.baseFs, sourceP, mountFsD, subPathD);
            });
        }, (mountFsS, { subPath: subPathS }) => {
            return this.makeCallSync(destP, () => {
                return fallback(mountFsS, subPathS, this.baseFs, destP);
            }, (mountFsD, { subPath: subPathD }) => {
                if (mountFsS !== mountFsD) {
                    return fallback(mountFsS, subPathS, mountFsD, subPathD);
                }
                else {
                    return mountFsS.copyFileSync(subPathS, subPathD, flags);
                }
            });
        });
    }
    async appendFilePromise(p, content, opts) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.appendFilePromise(p, content, opts);
        }, async (mountFs, { subPath }) => {
            return await mountFs.appendFilePromise(subPath, content, opts);
        });
    }
    appendFileSync(p, content, opts) {
        return this.makeCallSync(p, () => {
            return this.baseFs.appendFileSync(p, content, opts);
        }, (mountFs, { subPath }) => {
            return mountFs.appendFileSync(subPath, content, opts);
        });
    }
    async writeFilePromise(p, content, opts) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.writeFilePromise(p, content, opts);
        }, async (mountFs, { subPath }) => {
            return await mountFs.writeFilePromise(subPath, content, opts);
        });
    }
    writeFileSync(p, content, opts) {
        return this.makeCallSync(p, () => {
            return this.baseFs.writeFileSync(p, content, opts);
        }, (mountFs, { subPath }) => {
            return mountFs.writeFileSync(subPath, content, opts);
        });
    }
    async unlinkPromise(p) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.unlinkPromise(p);
        }, async (mountFs, { subPath }) => {
            return await mountFs.unlinkPromise(subPath);
        });
    }
    unlinkSync(p) {
        return this.makeCallSync(p, () => {
            return this.baseFs.unlinkSync(p);
        }, (mountFs, { subPath }) => {
            return mountFs.unlinkSync(subPath);
        });
    }
    async utimesPromise(p, atime, mtime) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.utimesPromise(p, atime, mtime);
        }, async (mountFs, { subPath }) => {
            return await mountFs.utimesPromise(subPath, atime, mtime);
        });
    }
    utimesSync(p, atime, mtime) {
        return this.makeCallSync(p, () => {
            return this.baseFs.utimesSync(p, atime, mtime);
        }, (mountFs, { subPath }) => {
            return mountFs.utimesSync(subPath, atime, mtime);
        });
    }
    async lutimesPromise(p, atime, mtime) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.lutimesPromise(p, atime, mtime);
        }, async (mountFs, { subPath }) => {
            return await mountFs.lutimesPromise(subPath, atime, mtime);
        });
    }
    lutimesSync(p, atime, mtime) {
        return this.makeCallSync(p, () => {
            return this.baseFs.lutimesSync(p, atime, mtime);
        }, (mountFs, { subPath }) => {
            return mountFs.lutimesSync(subPath, atime, mtime);
        });
    }
    async mkdirPromise(p, opts) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.mkdirPromise(p, opts);
        }, async (mountFs, { subPath }) => {
            return await mountFs.mkdirPromise(subPath, opts);
        });
    }
    mkdirSync(p, opts) {
        return this.makeCallSync(p, () => {
            return this.baseFs.mkdirSync(p, opts);
        }, (mountFs, { subPath }) => {
            return mountFs.mkdirSync(subPath, opts);
        });
    }
    async rmdirPromise(p, opts) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.rmdirPromise(p, opts);
        }, async (mountFs, { subPath }) => {
            return await mountFs.rmdirPromise(subPath, opts);
        });
    }
    rmdirSync(p, opts) {
        return this.makeCallSync(p, () => {
            return this.baseFs.rmdirSync(p, opts);
        }, (mountFs, { subPath }) => {
            return mountFs.rmdirSync(subPath, opts);
        });
    }
    async rmPromise(p, opts) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.rmPromise(p, opts);
        }, async (mountFs, { subPath }) => {
            return await mountFs.rmPromise(subPath, opts);
        });
    }
    rmSync(p, opts) {
        return this.makeCallSync(p, () => {
            return this.baseFs.rmSync(p, opts);
        }, (mountFs, { subPath }) => {
            return mountFs.rmSync(subPath, opts);
        });
    }
    async linkPromise(existingP, newP) {
        return await this.makeCallPromise(newP, async () => {
            return await this.baseFs.linkPromise(existingP, newP);
        }, async (mountFs, { subPath }) => {
            return await mountFs.linkPromise(existingP, subPath);
        });
    }
    linkSync(existingP, newP) {
        return this.makeCallSync(newP, () => {
            return this.baseFs.linkSync(existingP, newP);
        }, (mountFs, { subPath }) => {
            return mountFs.linkSync(existingP, subPath);
        });
    }
    async symlinkPromise(target, p, type) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.symlinkPromise(target, p, type);
        }, async (mountFs, { subPath }) => {
            return await mountFs.symlinkPromise(target, subPath);
        });
    }
    symlinkSync(target, p, type) {
        return this.makeCallSync(p, () => {
            return this.baseFs.symlinkSync(target, p, type);
        }, (mountFs, { subPath }) => {
            return mountFs.symlinkSync(target, subPath);
        });
    }
    async readFilePromise(p, encoding) {
        return this.makeCallPromise(p, async () => {
            return await this.baseFs.readFilePromise(p, encoding);
        }, async (mountFs, { subPath }) => {
            return await mountFs.readFilePromise(subPath, encoding);
        });
    }
    readFileSync(p, encoding) {
        return this.makeCallSync(p, () => {
            return this.baseFs.readFileSync(p, encoding);
        }, (mountFs, { subPath }) => {
            return mountFs.readFileSync(subPath, encoding);
        });
    }
    async readdirPromise(p, opts) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.readdirPromise(p, opts);
        }, async (mountFs, { subPath }) => {
            return await mountFs.readdirPromise(subPath, opts);
        }, {
            requireSubpath: false,
        });
    }
    readdirSync(p, opts) {
        return this.makeCallSync(p, () => {
            return this.baseFs.readdirSync(p, opts);
        }, (mountFs, { subPath }) => {
            return mountFs.readdirSync(subPath, opts);
        }, {
            requireSubpath: false,
        });
    }
    async readlinkPromise(p) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.readlinkPromise(p);
        }, async (mountFs, { subPath }) => {
            return await mountFs.readlinkPromise(subPath);
        });
    }
    readlinkSync(p) {
        return this.makeCallSync(p, () => {
            return this.baseFs.readlinkSync(p);
        }, (mountFs, { subPath }) => {
            return mountFs.readlinkSync(subPath);
        });
    }
    async truncatePromise(p, len) {
        return await this.makeCallPromise(p, async () => {
            return await this.baseFs.truncatePromise(p, len);
        }, async (mountFs, { subPath }) => {
            return await mountFs.truncatePromise(subPath, len);
        });
    }
    truncateSync(p, len) {
        return this.makeCallSync(p, () => {
            return this.baseFs.truncateSync(p, len);
        }, (mountFs, { subPath }) => {
            return mountFs.truncateSync(subPath, len);
        });
    }
    async ftruncatePromise(fd, len) {
        if ((fd & MOUNT_MASK) !== this.magic)
            return this.baseFs.ftruncatePromise(fd, len);
        const entry = this.fdMap.get(fd);
        if (typeof entry === `undefined`)
            throw errors.EBADF(`ftruncate`);
        const [mountFs, realFd] = entry;
        return mountFs.ftruncatePromise(realFd, len);
    }
    ftruncateSync(fd, len) {
        if ((fd & MOUNT_MASK) !== this.magic)
            return this.baseFs.ftruncateSync(fd, len);
        const entry = this.fdMap.get(fd);
        if (typeof entry === `undefined`)
            throw errors.EBADF(`ftruncateSync`);
        const [mountFs, realFd] = entry;
        return mountFs.ftruncateSync(realFd, len);
    }
    watch(p, a, b) {
        return this.makeCallSync(p, () => {
            return this.baseFs.watch(p, 
            // @ts-expect-error - reason TBS
            a, b);
        }, (mountFs, { subPath }) => {
            return mountFs.watch(subPath, 
            // @ts-expect-error - reason TBS
            a, b);
        });
    }
    watchFile(p, a, b) {
        return this.makeCallSync(p, () => {
            return this.baseFs.watchFile(p, 
            // @ts-expect-error - reason TBS
            a, b);
        }, () => {
            return (0, watchFile_1.watchFile)(this, p, a, b);
        });
    }
    unwatchFile(p, cb) {
        return this.makeCallSync(p, () => {
            return this.baseFs.unwatchFile(p, cb);
        }, () => {
            return (0, watchFile_1.unwatchFile)(this, p, cb);
        });
    }
    async makeCallPromise(p, discard, accept, { requireSubpath = true } = {}) {
        if (typeof p !== `string`)
            return await discard();
        const normalizedP = this.resolve(p);
        const mountInfo = this.findMount(normalizedP);
        if (!mountInfo)
            return await discard();
        if (requireSubpath && mountInfo.subPath === `/`)
            return await discard();
        return await this.getMountPromise(mountInfo.archivePath, async (mountFs) => await accept(mountFs, mountInfo));
    }
    makeCallSync(p, discard, accept, { requireSubpath = true } = {}) {
        if (typeof p !== `string`)
            return discard();
        const normalizedP = this.resolve(p);
        const mountInfo = this.findMount(normalizedP);
        if (!mountInfo)
            return discard();
        if (requireSubpath && mountInfo.subPath === `/`)
            return discard();
        return this.getMountSync(mountInfo.archivePath, mountFs => accept(mountFs, mountInfo));
    }
    findMount(p) {
        if (this.filter && !this.filter.test(p))
            return null;
        let filePath = ``;
        while (true) {
            const pathPartWithArchive = p.substring(filePath.length);
            const mountPoint = this.getMountPoint(pathPartWithArchive, filePath);
            if (!mountPoint)
                return null;
            filePath = this.pathUtils.join(filePath, mountPoint);
            if (!this.isMount.has(filePath)) {
                if (this.notMount.has(filePath))
                    continue;
                try {
                    if (this.typeCheck !== null && (this.baseFs.statSync(filePath).mode & fs_1.constants.S_IFMT) !== this.typeCheck) {
                        this.notMount.add(filePath);
                        continue;
                    }
                }
                catch {
                    return null;
                }
                this.isMount.add(filePath);
            }
            return {
                archivePath: filePath,
                subPath: this.pathUtils.join(path_1.PortablePath.root, p.substring(filePath.length)),
            };
        }
    }
    limitOpenFiles(max) {
        if (this.mountInstances === null)
            return;
        const now = Date.now();
        let nextExpiresAt = now + this.maxAge;
        let closeCount = max === null ? 0 : this.mountInstances.size - max;
        for (const [path, { childFs, expiresAt, refCount }] of this.mountInstances.entries()) {
            if (refCount !== 0 || childFs.hasOpenFileHandles?.()) {
                continue;
            }
            else if (now >= expiresAt) {
                childFs.saveAndClose?.();
                this.mountInstances.delete(path);
                closeCount -= 1;
                continue;
            }
            else if (max === null || closeCount <= 0) {
                nextExpiresAt = expiresAt;
                break;
            }
            childFs.saveAndClose?.();
            this.mountInstances.delete(path);
            closeCount -= 1;
        }
        if (this.limitOpenFilesTimeout === null && ((max === null && this.mountInstances.size > 0) || max !== null) && isFinite(nextExpiresAt)) {
            this.limitOpenFilesTimeout = setTimeout(() => {
                this.limitOpenFilesTimeout = null;
                this.limitOpenFiles(null);
            }, nextExpiresAt - now).unref();
        }
    }
    async getMountPromise(p, accept) {
        if (this.mountInstances) {
            let cachedMountFs = this.mountInstances.get(p);
            if (!cachedMountFs) {
                const createFsInstance = await this.factoryPromise(this.baseFs, p);
                // We need to recheck because concurrent getMountPromise calls may
                // have instantiated the mount archive while we were waiting
                cachedMountFs = this.mountInstances.get(p);
                if (!cachedMountFs) {
                    cachedMountFs = {
                        childFs: createFsInstance(),
                        expiresAt: 0,
                        refCount: 0,
                    };
                }
            }
            // Removing then re-adding the field allows us to easily implement
            // a basic LRU garbage collection strategy
            this.mountInstances.delete(p);
            this.limitOpenFiles(this.maxOpenFiles - 1);
            this.mountInstances.set(p, cachedMountFs);
            cachedMountFs.expiresAt = Date.now() + this.maxAge;
            cachedMountFs.refCount += 1;
            try {
                return await accept(cachedMountFs.childFs);
            }
            finally {
                cachedMountFs.refCount -= 1;
            }
        }
        else {
            const mountFs = (await this.factoryPromise(this.baseFs, p))();
            try {
                return await accept(mountFs);
            }
            finally {
                mountFs.saveAndClose?.();
            }
        }
    }
    getMountSync(p, accept) {
        if (this.mountInstances) {
            let cachedMountFs = this.mountInstances.get(p);
            if (!cachedMountFs) {
                cachedMountFs = {
                    childFs: this.factorySync(this.baseFs, p),
                    expiresAt: 0,
                    refCount: 0,
                };
            }
            // Removing then re-adding the field allows us to easily implement
            // a basic LRU garbage collection strategy
            this.mountInstances.delete(p);
            this.limitOpenFiles(this.maxOpenFiles - 1);
            this.mountInstances.set(p, cachedMountFs);
            cachedMountFs.expiresAt = Date.now() + this.maxAge;
            return accept(cachedMountFs.childFs);
        }
        else {
            const childFs = this.factorySync(this.baseFs, p);
            try {
                return accept(childFs);
            }
            finally {
                childFs.saveAndClose?.();
            }
        }
    }
}
exports.MountFS = MountFS;
