"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SnapshotZipFS = void 0;
const fslib_1 = require("@yarnpkg/fslib");
const libzip_1 = require("@yarnpkg/libzip");
const path_1 = require("path");
const fs_1 = require("fs");
const uniqBy = (arr, pick) => {
    const seen = new Set();
    return arr.filter((x) => {
        const key = pick(x);
        if (seen.has(key))
            return false;
        seen.add(key);
        return true;
    });
};
function uniqReaddir(arr) {
    return uniqBy(arr, (s) => (typeof s === 'string' ? s : s.name));
}
class SnapshotZipFS extends fslib_1.BasePortableFakeFS {
    constructor(opts) {
        super();
        this.fdMap = new Map();
        this.nextFd = 3;
        this.accessPromise = libzip_1.ZipOpenFS.prototype.accessPromise;
        this.accessSync = libzip_1.ZipOpenFS.prototype.accessSync;
        this.appendFilePromise = libzip_1.ZipOpenFS.prototype.appendFilePromise;
        this.appendFileSync = libzip_1.ZipOpenFS.prototype.appendFileSync;
        this.chmodPromise = libzip_1.ZipOpenFS.prototype.chmodPromise;
        this.chmodSync = libzip_1.ZipOpenFS.prototype.chmodSync;
        this.fchmodPromise = libzip_1.ZipOpenFS.prototype.fchmodPromise;
        this.fchmodSync = libzip_1.ZipOpenFS.prototype.fchmodSync;
        this.chownPromise = libzip_1.ZipOpenFS.prototype.chownPromise;
        this.chownSync = libzip_1.ZipOpenFS.prototype.chownSync;
        this.fchownPromise = libzip_1.ZipOpenFS.prototype.fchownPromise;
        this.fchownSync = libzip_1.ZipOpenFS.prototype.fchownSync;
        this.closePromise = libzip_1.ZipOpenFS.prototype.closePromise;
        this.closeSync = libzip_1.ZipOpenFS.prototype.closeSync;
        this.createReadStream = libzip_1.ZipOpenFS.prototype.createReadStream;
        this.createWriteStream = libzip_1.ZipOpenFS.prototype.createWriteStream;
        this.existsPromise = libzip_1.ZipOpenFS.prototype.existsPromise;
        this.existsSync = libzip_1.ZipOpenFS.prototype.existsSync;
        this.fstatPromise = libzip_1.ZipOpenFS.prototype.fstatPromise;
        this.fstatSync = libzip_1.ZipOpenFS.prototype.fstatSync;
        this.getExtractHint = libzip_1.ZipOpenFS.prototype.getExtractHint;
        this.getRealPath = libzip_1.ZipOpenFS.prototype.getRealPath;
        this.linkPromise = libzip_1.ZipOpenFS.prototype.linkPromise;
        this.linkSync = libzip_1.ZipOpenFS.prototype.linkSync;
        this.lstatPromise = libzip_1.ZipOpenFS.prototype.lstatPromise;
        this.lstatSync = libzip_1.ZipOpenFS.prototype.lstatSync;
        this.openPromise = libzip_1.ZipOpenFS.prototype.openPromise;
        this.openSync = libzip_1.ZipOpenFS.prototype.openSync;
        this.readFilePromise = libzip_1.ZipOpenFS.prototype.readFilePromise;
        this.readFileSync = libzip_1.ZipOpenFS.prototype.readFileSync;
        this.readlinkPromise = libzip_1.ZipOpenFS.prototype.readlinkPromise;
        this.readlinkSync = libzip_1.ZipOpenFS.prototype.readlinkSync;
        this.readPromise = libzip_1.ZipOpenFS.prototype.readPromise;
        this.readSync = libzip_1.ZipOpenFS.prototype.readSync;
        this.renamePromise = libzip_1.ZipOpenFS.prototype.renamePromise;
        this.renameSync = libzip_1.ZipOpenFS.prototype.renameSync;
        this.resolve = libzip_1.ZipOpenFS.prototype.resolve;
        this.statPromise = libzip_1.ZipOpenFS.prototype.statPromise;
        this.statSync = libzip_1.ZipOpenFS.prototype.statSync;
        this.symlinkPromise = libzip_1.ZipOpenFS.prototype.symlinkPromise;
        this.symlinkSync = libzip_1.ZipOpenFS.prototype.symlinkSync;
        this.truncatePromise = libzip_1.ZipOpenFS.prototype.truncatePromise;
        this.truncateSync = libzip_1.ZipOpenFS.prototype.truncateSync;
        this.ftruncatePromise = libzip_1.ZipOpenFS.prototype.ftruncatePromise;
        this.ftruncateSync = libzip_1.ZipOpenFS.prototype.ftruncateSync;
        this.unlinkPromise = libzip_1.ZipOpenFS.prototype.unlinkPromise;
        this.unlinkSync = libzip_1.ZipOpenFS.prototype.unlinkSync;
        this.unwatchFile = libzip_1.ZipOpenFS.prototype.unwatchFile;
        this.utimesPromise = libzip_1.ZipOpenFS.prototype.utimesPromise;
        this.utimesSync = libzip_1.ZipOpenFS.prototype.utimesSync;
        this.lutimesPromise = libzip_1.ZipOpenFS.prototype.utimesPromise;
        this.lutimesSync = libzip_1.ZipOpenFS.prototype.utimesSync;
        this.watch = libzip_1.ZipOpenFS.prototype.watch;
        this.watchFile = libzip_1.ZipOpenFS.prototype.watchFile;
        this.writeFilePromise = libzip_1.ZipOpenFS.prototype.writeFilePromise;
        this.writeFileSync = libzip_1.ZipOpenFS.prototype.writeFileSync;
        this.writePromise = libzip_1.ZipOpenFS.prototype.writePromise;
        this.writeSync = libzip_1.ZipOpenFS.prototype.writeSync;
        // @ts-ignore
        this.remapFd = libzip_1.ZipOpenFS.prototype.remapFd;
        this.zipFs = opts.zipFs;
        this.baseFs = opts.baseFs;
        this.root = opts.root;
        this.magic = 0x2a << 24;
    }
    makeCallPromise(p, discard, accept, { requireSubpath = true } = {}) {
        return __awaiter(this, void 0, void 0, function* () {
            if (typeof p !== 'string')
                return yield discard();
            const normalizedP = this.resolve(p);
            const zipInfo = this.findZip(normalizedP);
            if (!zipInfo)
                return yield discard();
            if (requireSubpath && zipInfo.subPath === '/')
                return yield discard();
            return yield accept(this.zipFs, zipInfo);
        });
    }
    makeCallSync(p, discard, accept, { requireSubpath = true } = {}) {
        if (typeof p !== 'string')
            return discard();
        const normalizedP = this.resolve(p);
        const zipInfo = this.findZip(normalizedP);
        if (!zipInfo)
            return discard();
        if (requireSubpath && zipInfo.subPath === '/')
            return discard();
        return accept(this.zipFs, Object.assign({ archivePath: '' }, zipInfo));
    }
    realpathPromise(p) {
        return __awaiter(this, void 0, void 0, function* () {
            return yield this.realpathSync(p);
        });
    }
    realpathSync(p) {
        return this.makeCallSync(p, () => this.baseFs.realpathSync(p), (zipFs, { subPath }) => {
            if (zipFs.lstatSync(subPath).isSymbolicLink()) {
                return zipFs.realpathSync(subPath);
            }
            else {
                // return the original path in case it wasn't under /snapshot, e.g. if it was for a node module - otherwise the node module parent path is the wrong one (and other things resolve relative to that)
                return p;
            }
        });
    }
    findZip(p) {
        p = this.resolve(p);
        const snapshotPP = fslib_1.npath.toPortablePath('/snapshot');
        const pathsToTry = Array.from(new Set([
            p,
            (0, path_1.resolve)('/snapshot', p),
            fslib_1.ppath.resolve(snapshotPP, fslib_1.npath.toPortablePath(fslib_1.npath.relative(fslib_1.npath.fromPortablePath(this.root), fslib_1.npath.fromPortablePath(p)))),
            fslib_1.ppath.resolve(snapshotPP, fslib_1.npath.toPortablePath(fslib_1.npath.relative((0, path_1.toNamespacedPath)(fslib_1.npath.fromPortablePath(this.root)), (0, path_1.toNamespacedPath)(fslib_1.npath.fromPortablePath(p))))),
            fslib_1.ppath.resolve(snapshotPP, fslib_1.npath.toPortablePath(fslib_1.npath.relative(fslib_1.npath.fromPortablePath(process.cwd()), fslib_1.npath.fromPortablePath(p)))),
            fslib_1.ppath.resolve(snapshotPP, fslib_1.npath.toPortablePath(fslib_1.npath.relative((0, path_1.toNamespacedPath)(fslib_1.npath.fromPortablePath(process.cwd())), (0, path_1.toNamespacedPath)(fslib_1.npath.fromPortablePath(p))))),
        ]));
        for (const path of pathsToTry) {
            const portablePath = fslib_1.npath.toPortablePath(path);
            if (this.zipFs.existsSync(portablePath)) {
                return {
                    subPath: portablePath,
                };
            }
        }
    }
    copyFilePromise(sourceP, destP, flags = 0) {
        return __awaiter(this, void 0, void 0, function* () {
            const fallback = (sourceFs, sourceP, destFs, destP) => __awaiter(this, void 0, void 0, function* () {
                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 && (yield this.existsPromise(sourceP)))
                    throw Object.assign(new Error(`EEXIST: file already exists, copyfile '${sourceP}' -> '${destP}'`), { code: `EEXIST` });
                let content;
                try {
                    content = yield sourceFs.readFilePromise(sourceP);
                }
                catch (error) {
                    throw Object.assign(new Error(`EINVAL: invalid argument, copyfile '${sourceP}' -> '${destP}'`), { code: `EINVAL` });
                }
                yield destFs.writeFilePromise(destP, content);
            });
            return yield this.makeCallPromise(sourceP, () => __awaiter(this, void 0, void 0, function* () {
                return yield this.baseFs.copyFilePromise(sourceP, destP, flags);
            }), (zipFsS, { subPath: subPathS }) => __awaiter(this, void 0, void 0, function* () {
                return yield fallback(zipFsS, subPathS, this.baseFs, destP);
            }));
        });
    }
    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 (error) {
                throw Object.assign(new Error(`EINVAL: invalid argument, copyfile '${sourceP}' -> '${destP}'`), { code: `EINVAL` });
            }
            destFs.writeFileSync(destP, content);
        };
        return this.makeCallSync(sourceP, () => {
            return this.baseFs.copyFileSync(sourceP, destP, flags);
        }, (zipFsS, { subPath: subPathS }) => {
            return fallback(zipFsS, subPathS, this.baseFs, destP);
        });
    }
    readdirPromise(p, opts) {
        return __awaiter(this, void 0, void 0, function* () {
            const fallback = () => __awaiter(this, void 0, void 0, function* () {
                return yield this.baseFs.readdirPromise(p, opts);
            });
            return yield this.makeCallPromise(p, fallback, (zipFs, { subPath }) => __awaiter(this, void 0, void 0, function* () {
                const fallbackPaths = yield fallback().catch(() => []);
                return Promise.resolve(uniqReaddir(fallbackPaths.concat(yield zipFs.readdirPromise(subPath, opts))));
            }), {
                requireSubpath: false,
            });
        });
    }
    readdirSync(p, opts) {
        const fallback = () => {
            return this.baseFs.readdirSync(p, opts);
        };
        return this.makeCallSync(p, fallback, (zipFs, { subPath }) => {
            let fallbackPaths = [];
            try {
                fallbackPaths = fallback();
            }
            catch (e) { }
            return fallbackPaths.concat(uniqReaddir(zipFs.readdirSync(subPath, opts)));
        }, {
            requireSubpath: false,
        });
    }
    mkdirPromise(p, opts) {
        return __awaiter(this, void 0, void 0, function* () {
            return yield this.baseFs.mkdirPromise(p, opts);
        });
    }
    mkdirSync(p, opts) {
        return this.baseFs.mkdirSync(p, opts);
    }
    rmdirPromise(p, opts) {
        return __awaiter(this, void 0, void 0, function* () {
            return yield this.baseFs.rmdirPromise(p, opts);
        });
    }
    rmdirSync(p, opts) {
        return this.baseFs.rmdirSync(p, opts);
    }
    opendirPromise(p, opts) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.opendirSync(p, opts);
        });
    }
    opendirSync(p, opts) {
        const zipInfo = this.findZip(p);
        let zipFsDir = null;
        if (zipInfo) {
            zipFsDir = this.zipFs.opendirSync(zipInfo.subPath);
        }
        let realFsDir = null;
        try {
            realFsDir = this.baseFs.opendirSync(p);
        }
        catch (e) {
            if (!zipFsDir)
                throw e;
        }
        const seen = new Set();
        const nextDirent = () => {
            const entry = (realFsDir === null || realFsDir === void 0 ? void 0 : realFsDir.readSync()) || (zipFsDir === null || zipFsDir === void 0 ? void 0 : zipFsDir.readSync());
            if (entry && !seen.has(entry.name)) {
                seen.add(entry.name);
                return entry;
            }
            return null;
        };
        const onClose = () => {
            zipFsDir === null || zipFsDir === void 0 ? void 0 : zipFsDir.closeSync();
            realFsDir === null || realFsDir === void 0 ? void 0 : realFsDir.closeSync();
        };
        return new fslib_1.CustomDir(p, nextDirent, { onClose });
    }
}
exports.SnapshotZipFS = SnapshotZipFS;
