"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZipFS = exports.makeEmptyArchive = exports.DEFAULT_COMPRESSION_LEVEL = void 0;
const tslib_1 = require("tslib");
const fs_1 = require("fs");
const stream_1 = require("stream");
const util_1 = require("util");
const zlib_1 = tslib_1.__importDefault(require("zlib"));
const FakeFS_1 = require("./FakeFS");
const NodeFS_1 = require("./NodeFS");
const opendir_1 = require("./algorithms/opendir");
const watchFile_1 = require("./algorithms/watchFile");
const constants_1 = require("./constants");
const errors = tslib_1.__importStar(require("./errors"));
const path_1 = require("./path");
const statUtils = tslib_1.__importStar(require("./statUtils"));
exports.DEFAULT_COMPRESSION_LEVEL = `mixed`;
function toUnixTimestamp(time) {
    if (typeof time === `string` && String(+time) === time)
        return +time;
    if (typeof time === `number` && Number.isFinite(time)) {
        if (time < 0) {
            return Date.now() / 1000;
        }
        else {
            return time;
        }
    }
    // convert to 123.456 UNIX timestamp
    if (util_1.types.isDate(time))
        return time.getTime() / 1000;
    throw new Error(`Invalid time`);
}
function makeEmptyArchive() {
    return Buffer.from([
        0x50, 0x4B, 0x05, 0x06,
        0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00,
        0x00, 0x00,
    ]);
}
exports.makeEmptyArchive = makeEmptyArchive;
class ZipFS extends FakeFS_1.BasePortableFakeFS {
    constructor(source, opts) {
        super();
        this.lzSource = null;
        this.listings = new Map();
        this.entries = new Map();
        /**
         * A cache of indices mapped to file sources.
         * Populated by `setFileSource` calls.
         * Required for supporting read after write.
         */
        this.fileSources = new Map();
        this.fds = new Map();
        this.nextFd = 0;
        this.ready = false;
        this.readOnly = false;
        this.libzip = opts.libzip;
        const pathOptions = opts;
        this.level = typeof pathOptions.level !== `undefined`
            ? pathOptions.level
            : exports.DEFAULT_COMPRESSION_LEVEL;
        source !== null && source !== void 0 ? source : (source = makeEmptyArchive());
        if (typeof source === `string`) {
            const { baseFs = new NodeFS_1.NodeFS() } = pathOptions;
            this.baseFs = baseFs;
            this.path = source;
        }
        else {
            this.path = null;
            this.baseFs = null;
        }
        if (opts.stats) {
            this.stats = opts.stats;
        }
        else {
            if (typeof source === `string`) {
                try {
                    this.stats = this.baseFs.statSync(source);
                }
                catch (error) {
                    if (error.code === `ENOENT` && pathOptions.create) {
                        this.stats = statUtils.makeDefaultStats();
                    }
                    else {
                        throw error;
                    }
                }
            }
            else {
                this.stats = statUtils.makeDefaultStats();
            }
        }
        const errPtr = this.libzip.malloc(4);
        try {
            let flags = 0;
            if (typeof source === `string` && pathOptions.create)
                flags |= this.libzip.ZIP_CREATE | this.libzip.ZIP_TRUNCATE;
            if (opts.readOnly) {
                flags |= this.libzip.ZIP_RDONLY;
                this.readOnly = true;
            }
            if (typeof source === `string`) {
                this.zip = this.libzip.open(path_1.npath.fromPortablePath(source), flags, errPtr);
            }
            else {
                const lzSource = this.allocateUnattachedSource(source);
                try {
                    this.zip = this.libzip.openFromSource(lzSource, flags, errPtr);
                    this.lzSource = lzSource;
                }
                catch (error) {
                    this.libzip.source.free(lzSource);
                    throw error;
                }
            }
            if (this.zip === 0) {
                const error = this.libzip.struct.errorS();
                this.libzip.error.initWithCode(error, this.libzip.getValue(errPtr, `i32`));
                throw this.makeLibzipError(error);
            }
        }
        finally {
            this.libzip.free(errPtr);
        }
        this.listings.set(path_1.PortablePath.root, new Set());
        const entryCount = this.libzip.getNumEntries(this.zip, 0);
        for (let t = 0; t < entryCount; ++t) {
            const raw = this.libzip.getName(this.zip, t, 0);
            if (path_1.ppath.isAbsolute(raw))
                continue;
            const p = path_1.ppath.resolve(path_1.PortablePath.root, raw);
            this.registerEntry(p, t);
            // If the raw path is a directory, register it
            // to prevent empty folder being skipped
            if (raw.endsWith(`/`)) {
                this.registerListing(p);
            }
        }
        this.symlinkCount = this.libzip.ext.countSymlinks(this.zip);
        if (this.symlinkCount === -1)
            throw this.makeLibzipError(this.libzip.getError(this.zip));
        this.ready = true;
    }
    makeLibzipError(error) {
        const errorCode = this.libzip.struct.errorCodeZip(error);
        const strerror = this.libzip.error.strerror(error);
        const libzipError = new errors.LibzipError(strerror, this.libzip.errors[errorCode]);
        // This error should never come up because of the file source cache
        if (errorCode === this.libzip.errors.ZIP_ER_CHANGED)
            throw new Error(`Assertion failed: Unexpected libzip error: ${libzipError.message}`);
        return libzipError;
    }
    getExtractHint(hints) {
        for (const fileName of this.entries.keys()) {
            const ext = this.pathUtils.extname(fileName);
            if (hints.relevantExtensions.has(ext)) {
                return true;
            }
        }
        return false;
    }
    getAllFiles() {
        return Array.from(this.entries.keys());
    }
    getRealPath() {
        if (!this.path)
            throw new Error(`ZipFS don't have real paths when loaded from a buffer`);
        return this.path;
    }
    getBufferAndClose() {
        this.prepareClose();
        if (!this.lzSource)
            throw new Error(`ZipFS was not created from a Buffer`);
        // zip_source_open on an unlink-after-write empty archive fails with "Entry has been deleted"
        if (this.entries.size === 0) {
            this.discardAndClose();
            return makeEmptyArchive();
        }
        try {
            // Prevent close from cleaning up the source
            this.libzip.source.keep(this.lzSource);
            // Close the zip archive
            if (this.libzip.close(this.zip) === -1)
                throw this.makeLibzipError(this.libzip.getError(this.zip));
            // Open the source for reading
            if (this.libzip.source.open(this.lzSource) === -1)
                throw this.makeLibzipError(this.libzip.source.error(this.lzSource));
            // Move to the end of source
            if (this.libzip.source.seek(this.lzSource, 0, 0, this.libzip.SEEK_END) === -1)
                throw this.makeLibzipError(this.libzip.source.error(this.lzSource));
            // Get the size of source
            const size = this.libzip.source.tell(this.lzSource);
            if (size === -1)
                throw this.makeLibzipError(this.libzip.source.error(this.lzSource));
            // Move to the start of source
            if (this.libzip.source.seek(this.lzSource, 0, 0, this.libzip.SEEK_SET) === -1)
                throw this.makeLibzipError(this.libzip.source.error(this.lzSource));
            const buffer = this.libzip.malloc(size);
            if (!buffer)
                throw new Error(`Couldn't allocate enough memory`);
            try {
                const rc = this.libzip.source.read(this.lzSource, buffer, size);
                if (rc === -1)
                    throw this.makeLibzipError(this.libzip.source.error(this.lzSource));
                else if (rc < size)
                    throw new Error(`Incomplete read`);
                else if (rc > size)
                    throw new Error(`Overread`);
                const memory = this.libzip.HEAPU8.subarray(buffer, buffer + size);
                return Buffer.from(memory);
            }
            finally {
                this.libzip.free(buffer);
            }
        }
        finally {
            this.libzip.source.close(this.lzSource);
            this.libzip.source.free(this.lzSource);
            this.ready = false;
        }
    }
    prepareClose() {
        if (!this.ready)
            throw errors.EBUSY(`archive closed, close`);
        (0, watchFile_1.unwatchAllFiles)(this);
    }
    saveAndClose() {
        if (!this.path || !this.baseFs)
            throw new Error(`ZipFS cannot be saved and must be discarded when loaded from a buffer`);
        this.prepareClose();
        if (this.readOnly) {
            this.discardAndClose();
            return;
        }
        const newMode = this.baseFs.existsSync(this.path) || this.stats.mode === statUtils.DEFAULT_MODE
            ? undefined
            : this.stats.mode;
        // zip_close doesn't persist empty archives
        if (this.entries.size === 0) {
            this.discardAndClose();
            this.baseFs.writeFileSync(this.path, makeEmptyArchive(), { mode: newMode });
        }
        else {
            const rc = this.libzip.close(this.zip);
            if (rc === -1)
                throw this.makeLibzipError(this.libzip.getError(this.zip));
            if (typeof newMode !== `undefined`) {
                this.baseFs.chmodSync(this.path, newMode);
            }
        }
        this.ready = false;
    }
    discardAndClose() {
        this.prepareClose();
        this.libzip.discard(this.zip);
        this.ready = false;
    }
    resolve(p) {
        return path_1.ppath.resolve(path_1.PortablePath.root, p);
    }
    async openPromise(p, flags, mode) {
        return this.openSync(p, flags, mode);
    }
    openSync(p, flags, mode) {
        const fd = this.nextFd++;
        this.fds.set(fd, { cursor: 0, p });
        return fd;
    }
    hasOpenFileHandles() {
        return !!this.fds.size;
    }
    async opendirPromise(p, opts) {
        return this.opendirSync(p, opts);
    }
    opendirSync(p, opts = {}) {
        const resolvedP = this.resolveFilename(`opendir '${p}'`, p);
        if (!this.entries.has(resolvedP) && !this.listings.has(resolvedP))
            throw errors.ENOENT(`opendir '${p}'`);
        const directoryListing = this.listings.get(resolvedP);
        if (!directoryListing)
            throw errors.ENOTDIR(`opendir '${p}'`);
        const entries = [...directoryListing];
        const fd = this.openSync(resolvedP, `r`);
        const onClose = () => {
            this.closeSync(fd);
        };
        return (0, opendir_1.opendir)(this, resolvedP, entries, { onClose });
    }
    async readPromise(fd, buffer, offset, length, position) {
        return this.readSync(fd, buffer, offset, length, position);
    }
    readSync(fd, buffer, offset = 0, length = buffer.byteLength, position = -1) {
        const entry = this.fds.get(fd);
        if (typeof entry === `undefined`)
            throw errors.EBADF(`read`);
        const realPosition = position === -1 || position === null
            ? entry.cursor
            : position;
        const source = this.readFileSync(entry.p);
        source.copy(buffer, offset, realPosition, realPosition + length);
        const bytesRead = Math.max(0, Math.min(source.length - realPosition, length));
        if (position === -1 || position === null)
            entry.cursor += bytesRead;
        return bytesRead;
    }
    async writePromise(fd, buffer, offset, length, position) {
        if (typeof buffer === `string`) {
            return this.writeSync(fd, buffer, position);
        }
        else {
            return this.writeSync(fd, buffer, offset, length, position);
        }
    }
    writeSync(fd, buffer, offset, length, position) {
        const entry = this.fds.get(fd);
        if (typeof entry === `undefined`)
            throw errors.EBADF(`read`);
        throw new Error(`Unimplemented`);
    }
    async closePromise(fd) {
        return this.closeSync(fd);
    }
    closeSync(fd) {
        const entry = this.fds.get(fd);
        if (typeof entry === `undefined`)
            throw errors.EBADF(`read`);
        this.fds.delete(fd);
    }
    createReadStream(p, { encoding } = {}) {
        if (p === null)
            throw new Error(`Unimplemented`);
        const fd = this.openSync(p, `r`);
        const stream = Object.assign(new stream_1.PassThrough({
            emitClose: true,
            autoDestroy: true,
            destroy: (error, callback) => {
                clearImmediate(immediate);
                this.closeSync(fd);
                callback(error);
            },
        }), {
            close() {
                stream.destroy();
            },
            bytesRead: 0,
            path: p,
            // "This property is `true` if the underlying file has not been opened yet"
            pending: false,
        });
        const immediate = setImmediate(async () => {
            try {
                const data = await this.readFilePromise(p, encoding);
                stream.bytesRead = data.length;
                stream.end(data);
            }
            catch (error) {
                stream.destroy(error);
            }
        });
        return stream;
    }
    createWriteStream(p, { encoding } = {}) {
        if (this.readOnly)
            throw errors.EROFS(`open '${p}'`);
        if (p === null)
            throw new Error(`Unimplemented`);
        const chunks = [];
        const fd = this.openSync(p, `w`);
        const stream = Object.assign(new stream_1.PassThrough({
            autoDestroy: true,
            emitClose: true,
            destroy: (error, callback) => {
                try {
                    if (error) {
                        callback(error);
                    }
                    else {
                        this.writeFileSync(p, Buffer.concat(chunks), encoding);
                        callback(null);
                    }
                }
                catch (err) {
                    callback(err);
                }
                finally {
                    this.closeSync(fd);
                }
            },
        }), {
            close() {
                stream.destroy();
            },
            bytesWritten: 0,
            path: p,
            // "This property is `true` if the underlying file has not been opened yet"
            pending: false,
        });
        stream.on(`data`, chunk => {
            const chunkBuffer = Buffer.from(chunk);
            stream.bytesWritten += chunkBuffer.length;
            chunks.push(chunkBuffer);
        });
        return stream;
    }
    async realpathPromise(p) {
        return this.realpathSync(p);
    }
    realpathSync(p) {
        const resolvedP = this.resolveFilename(`lstat '${p}'`, p);
        if (!this.entries.has(resolvedP) && !this.listings.has(resolvedP))
            throw errors.ENOENT(`lstat '${p}'`);
        return resolvedP;
    }
    async existsPromise(p) {
        return this.existsSync(p);
    }
    existsSync(p) {
        if (!this.ready)
            throw errors.EBUSY(`archive closed, existsSync '${p}'`);
        if (this.symlinkCount === 0) {
            const resolvedP = path_1.ppath.resolve(path_1.PortablePath.root, p);
            return this.entries.has(resolvedP) || this.listings.has(resolvedP);
        }
        let resolvedP;
        try {
            resolvedP = this.resolveFilename(`stat '${p}'`, p, undefined, false);
        }
        catch (error) {
            return false;
        }
        if (resolvedP === undefined)
            return false;
        return this.entries.has(resolvedP) || this.listings.has(resolvedP);
    }
    async accessPromise(p, mode) {
        return this.accessSync(p, mode);
    }
    accessSync(p, mode = fs_1.constants.F_OK) {
        const resolvedP = this.resolveFilename(`access '${p}'`, p);
        if (!this.entries.has(resolvedP) && !this.listings.has(resolvedP))
            throw errors.ENOENT(`access '${p}'`);
        if (this.readOnly && (mode & fs_1.constants.W_OK)) {
            throw errors.EROFS(`access '${p}'`);
        }
    }
    async statPromise(p, opts = { bigint: false }) {
        if (opts.bigint)
            return this.statSync(p, { bigint: true });
        return this.statSync(p);
    }
    statSync(p, opts = { bigint: false, throwIfNoEntry: true }) {
        const resolvedP = this.resolveFilename(`stat '${p}'`, p, undefined, opts.throwIfNoEntry);
        if (resolvedP === undefined)
            return undefined;
        if (!this.entries.has(resolvedP) && !this.listings.has(resolvedP)) {
            if (opts.throwIfNoEntry === false)
                return undefined;
            throw errors.ENOENT(`stat '${p}'`);
        }
        if (p[p.length - 1] === `/` && !this.listings.has(resolvedP))
            throw errors.ENOTDIR(`stat '${p}'`);
        return this.statImpl(`stat '${p}'`, resolvedP, opts);
    }
    async fstatPromise(fd, opts) {
        return this.fstatSync(fd, opts);
    }
    fstatSync(fd, opts) {
        const entry = this.fds.get(fd);
        if (typeof entry === `undefined`)
            throw errors.EBADF(`fstatSync`);
        const { p } = entry;
        const resolvedP = this.resolveFilename(`stat '${p}'`, p);
        if (!this.entries.has(resolvedP) && !this.listings.has(resolvedP))
            throw errors.ENOENT(`stat '${p}'`);
        if (p[p.length - 1] === `/` && !this.listings.has(resolvedP))
            throw errors.ENOTDIR(`stat '${p}'`);
        return this.statImpl(`fstat '${p}'`, resolvedP, opts);
    }
    async lstatPromise(p, opts = { bigint: false }) {
        if (opts.bigint)
            return this.lstatSync(p, { bigint: true });
        return this.lstatSync(p);
    }
    lstatSync(p, opts = { bigint: false, throwIfNoEntry: true }) {
        const resolvedP = this.resolveFilename(`lstat '${p}'`, p, false, opts.throwIfNoEntry);
        if (resolvedP === undefined)
            return undefined;
        if (!this.entries.has(resolvedP) && !this.listings.has(resolvedP)) {
            if (opts.throwIfNoEntry === false)
                return undefined;
            throw errors.ENOENT(`lstat '${p}'`);
        }
        if (p[p.length - 1] === `/` && !this.listings.has(resolvedP))
            throw errors.ENOTDIR(`lstat '${p}'`);
        return this.statImpl(`lstat '${p}'`, resolvedP, opts);
    }
    statImpl(reason, p, opts = {}) {
        const entry = this.entries.get(p);
        // File, or explicit directory
        if (typeof entry !== `undefined`) {
            const stat = this.libzip.struct.statS();
            const rc = this.libzip.statIndex(this.zip, entry, 0, 0, stat);
            if (rc === -1)
                throw this.makeLibzipError(this.libzip.getError(this.zip));
            const uid = this.stats.uid;
            const gid = this.stats.gid;
            const size = (this.libzip.struct.statSize(stat) >>> 0);
            const blksize = 512;
            const blocks = Math.ceil(size / blksize);
            const mtimeMs = (this.libzip.struct.statMtime(stat) >>> 0) * 1000;
            const atimeMs = mtimeMs;
            const birthtimeMs = mtimeMs;
            const ctimeMs = mtimeMs;
            const atime = new Date(atimeMs);
            const birthtime = new Date(birthtimeMs);
            const ctime = new Date(ctimeMs);
            const mtime = new Date(mtimeMs);
            const type = this.listings.has(p)
                ? constants_1.S_IFDIR
                : this.isSymbolicLink(entry)
                    ? constants_1.S_IFLNK
                    : constants_1.S_IFREG;
            const defaultMode = type === constants_1.S_IFDIR
                ? 0o755
                : 0o644;
            const mode = type | (this.getUnixMode(entry, defaultMode) & 0o777);
            const crc = this.libzip.struct.statCrc(stat);
            const statInstance = Object.assign(new statUtils.StatEntry(), { uid, gid, size, blksize, blocks, atime, birthtime, ctime, mtime, atimeMs, birthtimeMs, ctimeMs, mtimeMs, mode, crc });
            return opts.bigint === true ? statUtils.convertToBigIntStats(statInstance) : statInstance;
        }
        // Implicit directory
        if (this.listings.has(p)) {
            const uid = this.stats.uid;
            const gid = this.stats.gid;
            const size = 0;
            const blksize = 512;
            const blocks = 0;
            const atimeMs = this.stats.mtimeMs;
            const birthtimeMs = this.stats.mtimeMs;
            const ctimeMs = this.stats.mtimeMs;
            const mtimeMs = this.stats.mtimeMs;
            const atime = new Date(atimeMs);
            const birthtime = new Date(birthtimeMs);
            const ctime = new Date(ctimeMs);
            const mtime = new Date(mtimeMs);
            const mode = constants_1.S_IFDIR | 0o755;
            const crc = 0;
            const statInstance = Object.assign(new statUtils.StatEntry(), { uid, gid, size, blksize, blocks, atime, birthtime, ctime, mtime, atimeMs, birthtimeMs, ctimeMs, mtimeMs, mode, crc });
            return opts.bigint === true ? statUtils.convertToBigIntStats(statInstance) : statInstance;
        }
        throw new Error(`Unreachable`);
    }
    getUnixMode(index, defaultMode) {
        const rc = this.libzip.file.getExternalAttributes(this.zip, index, 0, 0, this.libzip.uint08S, this.libzip.uint32S);
        if (rc === -1)
            throw this.makeLibzipError(this.libzip.getError(this.zip));
        const opsys = this.libzip.getValue(this.libzip.uint08S, `i8`) >>> 0;
        if (opsys !== this.libzip.ZIP_OPSYS_UNIX)
            return defaultMode;
        return this.libzip.getValue(this.libzip.uint32S, `i32`) >>> 16;
    }
    registerListing(p) {
        const existingListing = this.listings.get(p);
        if (existingListing)
            return existingListing;
        const parentListing = this.registerListing(path_1.ppath.dirname(p));
        parentListing.add(path_1.ppath.basename(p));
        const newListing = new Set();
        this.listings.set(p, newListing);
        return newListing;
    }
    registerEntry(p, index) {
        const parentListing = this.registerListing(path_1.ppath.dirname(p));
        parentListing.add(path_1.ppath.basename(p));
        this.entries.set(p, index);
    }
    unregisterListing(p) {
        this.listings.delete(p);
        const parentListing = this.listings.get(path_1.ppath.dirname(p));
        parentListing === null || parentListing === void 0 ? void 0 : parentListing.delete(path_1.ppath.basename(p));
    }
    unregisterEntry(p) {
        this.unregisterListing(p);
        const entry = this.entries.get(p);
        this.entries.delete(p);
        if (typeof entry === `undefined`)
            return;
        this.fileSources.delete(entry);
        if (this.isSymbolicLink(entry)) {
            this.symlinkCount--;
        }
    }
    deleteEntry(p, index) {
        this.unregisterEntry(p);
        const rc = this.libzip.delete(this.zip, index);
        if (rc === -1) {
            throw this.makeLibzipError(this.libzip.getError(this.zip));
        }
    }
    resolveFilename(reason, p, resolveLastComponent = true, throwIfNoEntry = true) {
        if (!this.ready)
            throw errors.EBUSY(`archive closed, ${reason}`);
        let resolvedP = path_1.ppath.resolve(path_1.PortablePath.root, p);
        if (resolvedP === `/`)
            return path_1.PortablePath.root;
        const fileIndex = this.entries.get(resolvedP);
        if (resolveLastComponent && fileIndex !== undefined) {
            if (this.symlinkCount !== 0 && this.isSymbolicLink(fileIndex)) {
                const target = this.getFileSource(fileIndex).toString();
                return this.resolveFilename(reason, path_1.ppath.resolve(path_1.ppath.dirname(resolvedP), target), true, throwIfNoEntry);
            }
            else {
                return resolvedP;
            }
        }
        while (true) {
            const parentP = this.resolveFilename(reason, path_1.ppath.dirname(resolvedP), true, throwIfNoEntry);
            if (parentP === undefined)
                return parentP;
            const isDir = this.listings.has(parentP);
            const doesExist = this.entries.has(parentP);
            if (!isDir && !doesExist) {
                if (throwIfNoEntry === false)
                    return undefined;
                throw errors.ENOENT(reason);
            }
            if (!isDir)
                throw errors.ENOTDIR(reason);
            resolvedP = path_1.ppath.resolve(parentP, path_1.ppath.basename(resolvedP));
            if (!resolveLastComponent || this.symlinkCount === 0)
                break;
            const index = this.libzip.name.locate(this.zip, resolvedP.slice(1), 0);
            if (index === -1)
                break;
            if (this.isSymbolicLink(index)) {
                const target = this.getFileSource(index).toString();
                resolvedP = path_1.ppath.resolve(path_1.ppath.dirname(resolvedP), target);
            }
            else {
                break;
            }
        }
        return resolvedP;
    }
    allocateBuffer(content) {
        if (!Buffer.isBuffer(content))
            content = Buffer.from(content);
        const buffer = this.libzip.malloc(content.byteLength);
        if (!buffer)
            throw new Error(`Couldn't allocate enough memory`);
        // Copy the file into the Emscripten heap
        const heap = new Uint8Array(this.libzip.HEAPU8.buffer, buffer, content.byteLength);
        heap.set(content);
        return { buffer, byteLength: content.byteLength };
    }
    allocateUnattachedSource(content) {
        const error = this.libzip.struct.errorS();
        const { buffer, byteLength } = this.allocateBuffer(content);
        const source = this.libzip.source.fromUnattachedBuffer(buffer, byteLength, 0, 1, error);
        if (source === 0) {
            this.libzip.free(error);
            throw this.makeLibzipError(error);
        }
        return source;
    }
    allocateSource(content) {
        const { buffer, byteLength } = this.allocateBuffer(content);
        const source = this.libzip.source.fromBuffer(this.zip, buffer, byteLength, 0, 1);
        if (source === 0) {
            this.libzip.free(buffer);
            throw this.makeLibzipError(this.libzip.getError(this.zip));
        }
        return source;
    }
    setFileSource(p, content) {
        const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content);
        const target = path_1.ppath.relative(path_1.PortablePath.root, p);
        const lzSource = this.allocateSource(content);
        try {
            const newIndex = this.libzip.file.add(this.zip, target, lzSource, this.libzip.ZIP_FL_OVERWRITE);
            if (newIndex === -1)
                throw this.makeLibzipError(this.libzip.getError(this.zip));
            if (this.level !== `mixed`) {
                // Use store for level 0, and deflate for 1..9
                const method = this.level === 0
                    ? this.libzip.ZIP_CM_STORE
                    : this.libzip.ZIP_CM_DEFLATE;
                const rc = this.libzip.file.setCompression(this.zip, newIndex, 0, method, this.level);
                if (rc === -1) {
                    throw this.makeLibzipError(this.libzip.getError(this.zip));
                }
            }
            this.fileSources.set(newIndex, buffer);
            return newIndex;
        }
        catch (error) {
            this.libzip.source.free(lzSource);
            throw error;
        }
    }
    isSymbolicLink(index) {
        if (this.symlinkCount === 0)
            return false;
        const attrs = this.libzip.file.getExternalAttributes(this.zip, index, 0, 0, this.libzip.uint08S, this.libzip.uint32S);
        if (attrs === -1)
            throw this.makeLibzipError(this.libzip.getError(this.zip));
        const opsys = this.libzip.getValue(this.libzip.uint08S, `i8`) >>> 0;
        if (opsys !== this.libzip.ZIP_OPSYS_UNIX)
            return false;
        const attributes = this.libzip.getValue(this.libzip.uint32S, `i32`) >>> 16;
        return (attributes & constants_1.S_IFMT) === constants_1.S_IFLNK;
    }
    getFileSource(index, opts = { asyncDecompress: false }) {
        const cachedFileSource = this.fileSources.get(index);
        if (typeof cachedFileSource !== `undefined`)
            return cachedFileSource;
        const stat = this.libzip.struct.statS();
        const rc = this.libzip.statIndex(this.zip, index, 0, 0, stat);
        if (rc === -1)
            throw this.makeLibzipError(this.libzip.getError(this.zip));
        const size = this.libzip.struct.statCompSize(stat);
        const compressionMethod = this.libzip.struct.statCompMethod(stat);
        const buffer = this.libzip.malloc(size);
        try {
            const file = this.libzip.fopenIndex(this.zip, index, 0, this.libzip.ZIP_FL_COMPRESSED);
            if (file === 0)
                throw this.makeLibzipError(this.libzip.getError(this.zip));
            try {
                const rc = this.libzip.fread(file, buffer, size, 0);
                if (rc === -1)
                    throw this.makeLibzipError(this.libzip.file.getError(file));
                else if (rc < size)
                    throw new Error(`Incomplete read`);
                else if (rc > size)
                    throw new Error(`Overread`);
                const memory = this.libzip.HEAPU8.subarray(buffer, buffer + size);
                const data = Buffer.from(memory);
                if (compressionMethod === 0) {
                    this.fileSources.set(index, data);
                    return data;
                }
                else if (opts.asyncDecompress) {
                    return new Promise((resolve, reject) => {
                        zlib_1.default.inflateRaw(data, (error, result) => {
                            if (error) {
                                reject(error);
                            }
                            else {
                                this.fileSources.set(index, result);
                                resolve(result);
                            }
                        });
                    });
                }
                else {
                    const decompressedData = zlib_1.default.inflateRawSync(data);
                    this.fileSources.set(index, decompressedData);
                    return decompressedData;
                }
            }
            finally {
                this.libzip.fclose(file);
            }
        }
        finally {
            this.libzip.free(buffer);
        }
    }
    async fchmodPromise(fd, mask) {
        return this.chmodPromise(this.fdToPath(fd, `fchmod`), mask);
    }
    fchmodSync(fd, mask) {
        return this.chmodSync(this.fdToPath(fd, `fchmodSync`), mask);
    }
    async chmodPromise(p, mask) {
        return this.chmodSync(p, mask);
    }
    chmodSync(p, mask) {
        if (this.readOnly)
            throw errors.EROFS(`chmod '${p}'`);
        // We don't allow to make the extracted entries group-writable
        mask &= 0o755;
        const resolvedP = this.resolveFilename(`chmod '${p}'`, p, false);
        const entry = this.entries.get(resolvedP);
        if (typeof entry === `undefined`)
            throw new Error(`Assertion failed: The entry should have been registered (${resolvedP})`);
        const oldMod = this.getUnixMode(entry, constants_1.S_IFREG | 0o000);
        const newMod = oldMod & (~0o777) | mask;
        const rc = this.libzip.file.setExternalAttributes(this.zip, entry, 0, 0, this.libzip.ZIP_OPSYS_UNIX, newMod << 16);
        if (rc === -1) {
            throw this.makeLibzipError(this.libzip.getError(this.zip));
        }
    }
    async fchownPromise(fd, uid, gid) {
        return this.chownPromise(this.fdToPath(fd, `fchown`), uid, gid);
    }
    fchownSync(fd, uid, gid) {
        return this.chownSync(this.fdToPath(fd, `fchownSync`), uid, gid);
    }
    async chownPromise(p, uid, gid) {
        return this.chownSync(p, uid, gid);
    }
    chownSync(p, uid, gid) {
        throw new Error(`Unimplemented`);
    }
    async renamePromise(oldP, newP) {
        return this.renameSync(oldP, newP);
    }
    renameSync(oldP, newP) {
        throw new Error(`Unimplemented`);
    }
    async copyFilePromise(sourceP, destP, flags) {
        const { indexSource, indexDest, resolvedDestP } = this.prepareCopyFile(sourceP, destP, flags);
        const source = await this.getFileSource(indexSource, { asyncDecompress: true });
        const newIndex = this.setFileSource(resolvedDestP, source);
        if (newIndex !== indexDest) {
            this.registerEntry(resolvedDestP, newIndex);
        }
    }
    copyFileSync(sourceP, destP, flags = 0) {
        const { indexSource, indexDest, resolvedDestP } = this.prepareCopyFile(sourceP, destP, flags);
        const source = this.getFileSource(indexSource);
        const newIndex = this.setFileSource(resolvedDestP, source);
        if (newIndex !== indexDest) {
            this.registerEntry(resolvedDestP, newIndex);
        }
    }
    prepareCopyFile(sourceP, destP, flags = 0) {
        if (this.readOnly)
            throw errors.EROFS(`copyfile '${sourceP} -> '${destP}'`);
        if ((flags & fs_1.constants.COPYFILE_FICLONE_FORCE) !== 0)
            throw errors.ENOSYS(`unsupported clone operation`, `copyfile '${sourceP}' -> ${destP}'`);
        const resolvedSourceP = this.resolveFilename(`copyfile '${sourceP} -> ${destP}'`, sourceP);
        const indexSource = this.entries.get(resolvedSourceP);
        if (typeof indexSource === `undefined`)
            throw errors.EINVAL(`copyfile '${sourceP}' -> '${destP}'`);
        const resolvedDestP = this.resolveFilename(`copyfile '${sourceP}' -> ${destP}'`, destP);
        const indexDest = this.entries.get(resolvedDestP);
        if ((flags & (fs_1.constants.COPYFILE_EXCL | fs_1.constants.COPYFILE_FICLONE_FORCE)) !== 0 && typeof indexDest !== `undefined`)
            throw errors.EEXIST(`copyfile '${sourceP}' -> '${destP}'`);
        return {
            indexSource,
            resolvedDestP,
            indexDest,
        };
    }
    async appendFilePromise(p, content, opts) {
        if (this.readOnly)
            throw errors.EROFS(`open '${p}'`);
        if (typeof opts === `undefined`)
            opts = { flag: `a` };
        else if (typeof opts === `string`)
            opts = { flag: `a`, encoding: opts };
        else if (typeof opts.flag === `undefined`)
            opts = { flag: `a`, ...opts };
        return this.writeFilePromise(p, content, opts);
    }
    appendFileSync(p, content, opts = {}) {
        if (this.readOnly)
            throw errors.EROFS(`open '${p}'`);
        if (typeof opts === `undefined`)
            opts = { flag: `a` };
        else if (typeof opts === `string`)
            opts = { flag: `a`, encoding: opts };
        else if (typeof opts.flag === `undefined`)
            opts = { flag: `a`, ...opts };
        return this.writeFileSync(p, content, opts);
    }
    fdToPath(fd, reason) {
        var _a;
        const path = (_a = this.fds.get(fd)) === null || _a === void 0 ? void 0 : _a.p;
        if (typeof path === `undefined`)
            throw errors.EBADF(reason);
        return path;
    }
    async writeFilePromise(p, content, opts) {
        const { encoding, mode, index, resolvedP } = this.prepareWriteFile(p, opts);
        if (index !== undefined && typeof opts === `object` && opts.flag && opts.flag.includes(`a`))
            content = Buffer.concat([await this.getFileSource(index, { asyncDecompress: true }), Buffer.from(content)]);
        if (encoding !== null)
            // @ts-expect-error: toString ignores unneeded arguments
            content = content.toString(encoding);
        const newIndex = this.setFileSource(resolvedP, content);
        if (newIndex !== index)
            this.registerEntry(resolvedP, newIndex);
        if (mode !== null) {
            await this.chmodPromise(resolvedP, mode);
        }
    }
    writeFileSync(p, content, opts) {
        const { encoding, mode, index, resolvedP } = this.prepareWriteFile(p, opts);
        if (index !== undefined && typeof opts === `object` && opts.flag && opts.flag.includes(`a`))
            content = Buffer.concat([this.getFileSource(index), Buffer.from(content)]);
        if (encoding !== null)
            // @ts-expect-error: toString ignores unneeded arguments
            content = content.toString(encoding);
        const newIndex = this.setFileSource(resolvedP, content);
        if (newIndex !== index)
            this.registerEntry(resolvedP, newIndex);
        if (mode !== null) {
            this.chmodSync(resolvedP, mode);
        }
    }
    prepareWriteFile(p, opts) {
        if (typeof p === `number`)
            p = this.fdToPath(p, `read`);
        if (this.readOnly)
            throw errors.EROFS(`open '${p}'`);
        const resolvedP = this.resolveFilename(`open '${p}'`, p);
        if (this.listings.has(resolvedP))
            throw errors.EISDIR(`open '${p}'`);
        let encoding = null, mode = null;
        if (typeof opts === `string`) {
            encoding = opts;
        }
        else if (typeof opts === `object`) {
            ({
                encoding = null,
                mode = null,
            } = opts);
        }
        const index = this.entries.get(resolvedP);
        return {
            encoding,
            mode,
            resolvedP,
            index,
        };
    }
    async unlinkPromise(p) {
        return this.unlinkSync(p);
    }
    unlinkSync(p) {
        if (this.readOnly)
            throw errors.EROFS(`unlink '${p}'`);
        const resolvedP = this.resolveFilename(`unlink '${p}'`, p);
        if (this.listings.has(resolvedP))
            throw errors.EISDIR(`unlink '${p}'`);
        const index = this.entries.get(resolvedP);
        if (typeof index === `undefined`)
            throw errors.EINVAL(`unlink '${p}'`);
        this.deleteEntry(resolvedP, index);
    }
    async utimesPromise(p, atime, mtime) {
        return this.utimesSync(p, atime, mtime);
    }
    utimesSync(p, atime, mtime) {
        if (this.readOnly)
            throw errors.EROFS(`utimes '${p}'`);
        const resolvedP = this.resolveFilename(`utimes '${p}'`, p);
        this.utimesImpl(resolvedP, mtime);
    }
    async lutimesPromise(p, atime, mtime) {
        return this.lutimesSync(p, atime, mtime);
    }
    lutimesSync(p, atime, mtime) {
        if (this.readOnly)
            throw errors.EROFS(`lutimes '${p}'`);
        const resolvedP = this.resolveFilename(`utimes '${p}'`, p, false);
        this.utimesImpl(resolvedP, mtime);
    }
    utimesImpl(resolvedP, mtime) {
        if (this.listings.has(resolvedP))
            if (!this.entries.has(resolvedP))
                this.hydrateDirectory(resolvedP);
        const entry = this.entries.get(resolvedP);
        if (entry === undefined)
            throw new Error(`Unreachable`);
        const rc = this.libzip.file.setMtime(this.zip, entry, 0, toUnixTimestamp(mtime), 0);
        if (rc === -1) {
            throw this.makeLibzipError(this.libzip.getError(this.zip));
        }
    }
    async mkdirPromise(p, opts) {
        return this.mkdirSync(p, opts);
    }
    mkdirSync(p, { mode = 0o755, recursive = false } = {}) {
        if (recursive)
            return this.mkdirpSync(p, { chmod: mode });
        if (this.readOnly)
            throw errors.EROFS(`mkdir '${p}'`);
        const resolvedP = this.resolveFilename(`mkdir '${p}'`, p);
        if (this.entries.has(resolvedP) || this.listings.has(resolvedP))
            throw errors.EEXIST(`mkdir '${p}'`);
        this.hydrateDirectory(resolvedP);
        this.chmodSync(resolvedP, mode);
        return undefined;
    }
    async rmdirPromise(p, opts) {
        return this.rmdirSync(p, opts);
    }
    rmdirSync(p, { recursive = false } = {}) {
        if (this.readOnly)
            throw errors.EROFS(`rmdir '${p}'`);
        if (recursive) {
            this.removeSync(p);
            return;
        }
        const resolvedP = this.resolveFilename(`rmdir '${p}'`, p);
        const directoryListing = this.listings.get(resolvedP);
        if (!directoryListing)
            throw errors.ENOTDIR(`rmdir '${p}'`);
        if (directoryListing.size > 0)
            throw errors.ENOTEMPTY(`rmdir '${p}'`);
        const index = this.entries.get(resolvedP);
        if (typeof index === `undefined`)
            throw errors.EINVAL(`rmdir '${p}'`);
        this.deleteEntry(p, index);
    }
    hydrateDirectory(resolvedP) {
        const index = this.libzip.dir.add(this.zip, path_1.ppath.relative(path_1.PortablePath.root, resolvedP));
        if (index === -1)
            throw this.makeLibzipError(this.libzip.getError(this.zip));
        this.registerListing(resolvedP);
        this.registerEntry(resolvedP, index);
        return index;
    }
    async linkPromise(existingP, newP) {
        return this.linkSync(existingP, newP);
    }
    linkSync(existingP, newP) {
        // Zip archives don't support hard links:
        // https://stackoverflow.com/questions/8859616/are-hard-links-possible-within-a-zip-archive
        throw errors.EOPNOTSUPP(`link '${existingP}' -> '${newP}'`);
    }
    async symlinkPromise(target, p) {
        return this.symlinkSync(target, p);
    }
    symlinkSync(target, p) {
        if (this.readOnly)
            throw errors.EROFS(`symlink '${target}' -> '${p}'`);
        const resolvedP = this.resolveFilename(`symlink '${target}' -> '${p}'`, p);
        if (this.listings.has(resolvedP))
            throw errors.EISDIR(`symlink '${target}' -> '${p}'`);
        if (this.entries.has(resolvedP))
            throw errors.EEXIST(`symlink '${target}' -> '${p}'`);
        const index = this.setFileSource(resolvedP, target);
        this.registerEntry(resolvedP, index);
        const rc = this.libzip.file.setExternalAttributes(this.zip, index, 0, 0, this.libzip.ZIP_OPSYS_UNIX, (constants_1.S_IFLNK | 0o777) << 16);
        if (rc === -1)
            throw this.makeLibzipError(this.libzip.getError(this.zip));
        this.symlinkCount += 1;
    }
    async readFilePromise(p, encoding) {
        // This is messed up regarding the TS signatures
        if (typeof encoding === `object`)
            // @ts-expect-error
            encoding = encoding ? encoding.encoding : undefined;
        const data = await this.readFileBuffer(p, { asyncDecompress: true });
        return encoding ? data.toString(encoding) : data;
    }
    readFileSync(p, encoding) {
        // This is messed up regarding the TS signatures
        if (typeof encoding === `object`)
            // @ts-expect-error
            encoding = encoding ? encoding.encoding : undefined;
        const data = this.readFileBuffer(p);
        return encoding ? data.toString(encoding) : data;
    }
    readFileBuffer(p, opts = { asyncDecompress: false }) {
        if (typeof p === `number`)
            p = this.fdToPath(p, `read`);
        const resolvedP = this.resolveFilename(`open '${p}'`, p);
        if (!this.entries.has(resolvedP) && !this.listings.has(resolvedP))
            throw errors.ENOENT(`open '${p}'`);
        // Ensures that the last component is a directory, if the user said so (even if it is we'll throw right after with EISDIR anyway)
        if (p[p.length - 1] === `/` && !this.listings.has(resolvedP))
            throw errors.ENOTDIR(`open '${p}'`);
        if (this.listings.has(resolvedP))
            throw errors.EISDIR(`read`);
        const entry = this.entries.get(resolvedP);
        if (entry === undefined)
            throw new Error(`Unreachable`);
        return this.getFileSource(entry, opts);
    }
    async readdirPromise(p, opts) {
        return this.readdirSync(p, opts);
    }
    readdirSync(p, opts) {
        const resolvedP = this.resolveFilename(`scandir '${p}'`, p);
        if (!this.entries.has(resolvedP) && !this.listings.has(resolvedP))
            throw errors.ENOENT(`scandir '${p}'`);
        const directoryListing = this.listings.get(resolvedP);
        if (!directoryListing)
            throw errors.ENOTDIR(`scandir '${p}'`);
        const entries = [...directoryListing];
        if (!(opts === null || opts === void 0 ? void 0 : opts.withFileTypes))
            return entries;
        return entries.map(name => {
            return Object.assign(this.statImpl(`lstat`, path_1.ppath.join(p, name)), {
                name,
            });
        });
    }
    async readlinkPromise(p) {
        const entry = this.prepareReadlink(p);
        return (await this.getFileSource(entry, { asyncDecompress: true })).toString();
    }
    readlinkSync(p) {
        const entry = this.prepareReadlink(p);
        return this.getFileSource(entry).toString();
    }
    prepareReadlink(p) {
        const resolvedP = this.resolveFilename(`readlink '${p}'`, p, false);
        if (!this.entries.has(resolvedP) && !this.listings.has(resolvedP))
            throw errors.ENOENT(`readlink '${p}'`);
        // Ensure that the last component is a directory (if it is we'll throw right after with EISDIR anyway)
        if (p[p.length - 1] === `/` && !this.listings.has(resolvedP))
            throw errors.ENOTDIR(`open '${p}'`);
        if (this.listings.has(resolvedP))
            throw errors.EINVAL(`readlink '${p}'`);
        const entry = this.entries.get(resolvedP);
        if (entry === undefined)
            throw new Error(`Unreachable`);
        if (!this.isSymbolicLink(entry))
            throw errors.EINVAL(`readlink '${p}'`);
        return entry;
    }
    async truncatePromise(p, len = 0) {
        const resolvedP = this.resolveFilename(`open '${p}'`, p);
        const index = this.entries.get(resolvedP);
        if (typeof index === `undefined`)
            throw errors.EINVAL(`open '${p}'`);
        const source = await this.getFileSource(index, { asyncDecompress: true });
        const truncated = Buffer.alloc(len, 0x00);
        source.copy(truncated);
        return await this.writeFilePromise(p, truncated);
    }
    truncateSync(p, len = 0) {
        const resolvedP = this.resolveFilename(`open '${p}'`, p);
        const index = this.entries.get(resolvedP);
        if (typeof index === `undefined`)
            throw errors.EINVAL(`open '${p}'`);
        const source = this.getFileSource(index);
        const truncated = Buffer.alloc(len, 0x00);
        source.copy(truncated);
        return this.writeFileSync(p, truncated);
    }
    async ftruncatePromise(fd, len) {
        return this.truncatePromise(this.fdToPath(fd, `ftruncate`), len);
    }
    ftruncateSync(fd, len) {
        return this.truncateSync(this.fdToPath(fd, `ftruncateSync`), len);
    }
    watch(p, a, b) {
        let persistent;
        switch (typeof a) {
            case `function`:
            case `string`:
            case `undefined`:
                {
                    persistent = true;
                }
                break;
            default:
                {
                    ({ persistent = true } = a);
                }
                break;
        }
        if (!persistent)
            return { on: () => { }, close: () => { } };
        const interval = setInterval(() => { }, 24 * 60 * 60 * 1000);
        return { on: () => { }, close: () => {
                clearInterval(interval);
            } };
    }
    watchFile(p, a, b) {
        const resolvedP = path_1.ppath.resolve(path_1.PortablePath.root, p);
        return (0, watchFile_1.watchFile)(this, resolvedP, a, b);
    }
    unwatchFile(p, cb) {
        const resolvedP = path_1.ppath.resolve(path_1.PortablePath.root, p);
        return (0, watchFile_1.unwatchFile)(this, resolvedP, cb);
    }
}
exports.ZipFS = ZipFS;
