"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BasePortableFakeFS = exports.FakeFS = void 0;
exports.normalizeLineEndings = normalizeLineEndings;
const crypto_1 = require("crypto");
const os_1 = require("os");
const copyPromise_1 = require("./algorithms/copyPromise");
const path_1 = require("./path");
class FakeFS {
    constructor(pathUtils) {
        this.pathUtils = pathUtils;
    }
    async *genTraversePromise(init, { stableSort = false } = {}) {
        const stack = [init];
        while (stack.length > 0) {
            const p = stack.shift();
            const entry = await this.lstatPromise(p);
            if (entry.isDirectory()) {
                const entries = await this.readdirPromise(p);
                if (stableSort) {
                    for (const entry of entries.sort()) {
                        stack.push(this.pathUtils.join(p, entry));
                    }
                }
                else {
                    throw new Error(`Not supported`);
                }
            }
            else {
                yield p;
            }
        }
    }
    async checksumFilePromise(path, { algorithm = `sha512` } = {}) {
        const fd = await this.openPromise(path, `r`);
        try {
            const CHUNK_SIZE = 65536;
            const chunk = Buffer.allocUnsafeSlow(CHUNK_SIZE);
            const hash = (0, crypto_1.createHash)(algorithm);
            let bytesRead = 0;
            while ((bytesRead = await this.readPromise(fd, chunk, 0, CHUNK_SIZE)) !== 0)
                hash.update(bytesRead === CHUNK_SIZE ? chunk : chunk.slice(0, bytesRead));
            return hash.digest(`hex`);
        }
        finally {
            await this.closePromise(fd);
        }
    }
    async removePromise(p, { recursive = true, maxRetries = 5 } = {}) {
        let stat;
        try {
            stat = await this.lstatPromise(p);
        }
        catch (error) {
            if (error.code === `ENOENT`) {
                return;
            }
            else {
                throw error;
            }
        }
        if (stat.isDirectory()) {
            if (recursive) {
                const entries = await this.readdirPromise(p);
                await Promise.all(entries.map(entry => {
                    return this.removePromise(this.pathUtils.resolve(p, entry));
                }));
            }
            // 5 gives 1s worth of retries at worst
            for (let t = 0; t <= maxRetries; t++) {
                try {
                    await this.rmdirPromise(p);
                    break;
                }
                catch (error) {
                    if (error.code !== `EBUSY` && error.code !== `ENOTEMPTY`) {
                        throw error;
                    }
                    else if (t < maxRetries) {
                        await new Promise(resolve => setTimeout(resolve, t * 100));
                    }
                }
            }
        }
        else {
            await this.unlinkPromise(p);
        }
    }
    removeSync(p, { recursive = true } = {}) {
        let stat;
        try {
            stat = this.lstatSync(p);
        }
        catch (error) {
            if (error.code === `ENOENT`) {
                return;
            }
            else {
                throw error;
            }
        }
        if (stat.isDirectory()) {
            if (recursive)
                for (const entry of this.readdirSync(p))
                    this.removeSync(this.pathUtils.resolve(p, entry));
            this.rmdirSync(p);
        }
        else {
            this.unlinkSync(p);
        }
    }
    async mkdirpPromise(p, { chmod, utimes } = {}) {
        p = this.resolve(p);
        if (p === this.pathUtils.dirname(p))
            return undefined;
        const parts = p.split(this.pathUtils.sep);
        let createdDirectory;
        for (let u = 2; u <= parts.length; ++u) {
            const subPath = parts.slice(0, u).join(this.pathUtils.sep);
            if (!this.existsSync(subPath)) {
                try {
                    await this.mkdirPromise(subPath);
                }
                catch (error) {
                    if (error.code === `EEXIST`) {
                        continue;
                    }
                    else {
                        throw error;
                    }
                }
                createdDirectory ??= subPath;
                if (chmod != null)
                    await this.chmodPromise(subPath, chmod);
                if (utimes != null) {
                    await this.utimesPromise(subPath, utimes[0], utimes[1]);
                }
                else {
                    const parentStat = await this.statPromise(this.pathUtils.dirname(subPath));
                    await this.utimesPromise(subPath, parentStat.atime, parentStat.mtime);
                }
            }
        }
        return createdDirectory;
    }
    mkdirpSync(p, { chmod, utimes } = {}) {
        p = this.resolve(p);
        if (p === this.pathUtils.dirname(p))
            return undefined;
        const parts = p.split(this.pathUtils.sep);
        let createdDirectory;
        for (let u = 2; u <= parts.length; ++u) {
            const subPath = parts.slice(0, u).join(this.pathUtils.sep);
            if (!this.existsSync(subPath)) {
                try {
                    this.mkdirSync(subPath);
                }
                catch (error) {
                    if (error.code === `EEXIST`) {
                        continue;
                    }
                    else {
                        throw error;
                    }
                }
                createdDirectory ??= subPath;
                if (chmod != null)
                    this.chmodSync(subPath, chmod);
                if (utimes != null) {
                    this.utimesSync(subPath, utimes[0], utimes[1]);
                }
                else {
                    const parentStat = this.statSync(this.pathUtils.dirname(subPath));
                    this.utimesSync(subPath, parentStat.atime, parentStat.mtime);
                }
            }
        }
        return createdDirectory;
    }
    async copyPromise(destination, source, { baseFs = this, overwrite = true, stableSort = false, stableTime = false, linkStrategy = null } = {}) {
        return await (0, copyPromise_1.copyPromise)(this, destination, baseFs, source, { overwrite, stableSort, stableTime, linkStrategy });
    }
    copySync(destination, source, { baseFs = this, overwrite = true } = {}) {
        const stat = baseFs.lstatSync(source);
        const exists = this.existsSync(destination);
        if (stat.isDirectory()) {
            this.mkdirpSync(destination);
            const directoryListing = baseFs.readdirSync(source);
            for (const entry of directoryListing) {
                this.copySync(this.pathUtils.join(destination, entry), baseFs.pathUtils.join(source, entry), { baseFs, overwrite });
            }
        }
        else if (stat.isFile()) {
            if (!exists || overwrite) {
                if (exists)
                    this.removeSync(destination);
                const content = baseFs.readFileSync(source);
                this.writeFileSync(destination, content);
            }
        }
        else if (stat.isSymbolicLink()) {
            if (!exists || overwrite) {
                if (exists)
                    this.removeSync(destination);
                const target = baseFs.readlinkSync(source);
                this.symlinkSync((0, path_1.convertPath)(this.pathUtils, target), destination);
            }
        }
        else {
            throw new Error(`Unsupported file type (file: ${source}, mode: 0o${stat.mode.toString(8).padStart(6, `0`)})`);
        }
        const mode = stat.mode & 0o777;
        this.chmodSync(destination, mode);
    }
    async changeFilePromise(p, content, opts = {}) {
        if (Buffer.isBuffer(content)) {
            return this.changeFileBufferPromise(p, content, opts);
        }
        else {
            return this.changeFileTextPromise(p, content, opts);
        }
    }
    async changeFileBufferPromise(p, content, { mode } = {}) {
        let current = Buffer.alloc(0);
        try {
            current = await this.readFilePromise(p);
        }
        catch {
            // ignore errors, no big deal
        }
        if (Buffer.compare(current, content) === 0)
            return;
        await this.writeFilePromise(p, content, { mode });
    }
    async changeFileTextPromise(p, content, { automaticNewlines, mode } = {}) {
        let current = ``;
        try {
            current = await this.readFilePromise(p, `utf8`);
        }
        catch {
            // ignore errors, no big deal
        }
        const normalizedContent = automaticNewlines
            ? normalizeLineEndings(current, content)
            : content;
        if (current === normalizedContent)
            return;
        await this.writeFilePromise(p, normalizedContent, { mode });
    }
    changeFileSync(p, content, opts = {}) {
        if (Buffer.isBuffer(content)) {
            return this.changeFileBufferSync(p, content, opts);
        }
        else {
            return this.changeFileTextSync(p, content, opts);
        }
    }
    changeFileBufferSync(p, content, { mode } = {}) {
        let current = Buffer.alloc(0);
        try {
            current = this.readFileSync(p);
        }
        catch {
            // ignore errors, no big deal
        }
        if (Buffer.compare(current, content) === 0)
            return;
        this.writeFileSync(p, content, { mode });
    }
    changeFileTextSync(p, content, { automaticNewlines = false, mode } = {}) {
        let current = ``;
        try {
            current = this.readFileSync(p, `utf8`);
        }
        catch {
            // ignore errors, no big deal
        }
        const normalizedContent = automaticNewlines
            ? normalizeLineEndings(current, content)
            : content;
        if (current === normalizedContent)
            return;
        this.writeFileSync(p, normalizedContent, { mode });
    }
    async movePromise(fromP, toP) {
        try {
            await this.renamePromise(fromP, toP);
        }
        catch (error) {
            if (error.code === `EXDEV`) {
                await this.copyPromise(toP, fromP);
                await this.removePromise(fromP);
            }
            else {
                throw error;
            }
        }
    }
    moveSync(fromP, toP) {
        try {
            this.renameSync(fromP, toP);
        }
        catch (error) {
            if (error.code === `EXDEV`) {
                this.copySync(toP, fromP);
                this.removeSync(fromP);
            }
            else {
                throw error;
            }
        }
    }
    async lockPromise(affectedPath, callback) {
        const lockPath = `${affectedPath}.flock`;
        const interval = 1000 / 60;
        const startTime = Date.now();
        let fd = null;
        // Even when we detect that a lock file exists, we still look inside to see
        // whether the pid that created it is still alive. It's not foolproof
        // (there are false positive), but there are no false negative and that's
        // all that matters in 99% of the cases.
        const isAlive = async () => {
            let pid;
            try {
                ([pid] = await this.readJsonPromise(lockPath));
            }
            catch {
                // If we can't read the file repeatedly, we assume the process was
                // aborted before even finishing writing the payload.
                return Date.now() - startTime < 500;
            }
            try {
                // "As a special case, a signal of 0 can be used to test for the
                // existence of a process" - so we check whether it's alive.
                process.kill(pid, 0);
                return true;
            }
            catch {
                return false;
            }
        };
        while (fd === null) {
            try {
                fd = await this.openPromise(lockPath, `wx`);
            }
            catch (error) {
                if (error.code === `EEXIST`) {
                    if (!await isAlive()) {
                        try {
                            await this.unlinkPromise(lockPath);
                            continue;
                        }
                        catch {
                            // No big deal if we can't remove it. Just fallback to wait for
                            // it to be eventually released by its owner.
                        }
                    }
                    if (Date.now() - startTime < 60 * 1000) {
                        await new Promise(resolve => setTimeout(resolve, interval));
                    }
                    else {
                        throw new Error(`Couldn't acquire a lock in a reasonable time (via ${lockPath})`);
                    }
                }
                else {
                    throw error;
                }
            }
        }
        await this.writePromise(fd, JSON.stringify([process.pid]));
        try {
            return await callback();
        }
        finally {
            try {
                // closePromise needs to come before unlinkPromise otherwise another process can attempt
                // to get the file handle after the unlink but before close resuling in
                // EPERM: operation not permitted, open
                await this.closePromise(fd);
                await this.unlinkPromise(lockPath);
            }
            catch {
                // noop
            }
        }
    }
    async readJsonPromise(p) {
        const content = await this.readFilePromise(p, `utf8`);
        try {
            return JSON.parse(content);
        }
        catch (error) {
            error.message += ` (in ${p})`;
            throw error;
        }
    }
    readJsonSync(p) {
        const content = this.readFileSync(p, `utf8`);
        try {
            return JSON.parse(content);
        }
        catch (error) {
            error.message += ` (in ${p})`;
            throw error;
        }
    }
    async writeJsonPromise(p, data, { compact = false } = {}) {
        const space = compact
            ? 0
            : 2;
        return await this.writeFilePromise(p, `${JSON.stringify(data, null, space)}\n`);
    }
    writeJsonSync(p, data, { compact = false } = {}) {
        const space = compact
            ? 0
            : 2;
        return this.writeFileSync(p, `${JSON.stringify(data, null, space)}\n`);
    }
    async preserveTimePromise(p, cb) {
        const stat = await this.lstatPromise(p);
        const result = await cb();
        if (typeof result !== `undefined`)
            p = result;
        await this.lutimesPromise(p, stat.atime, stat.mtime);
    }
    async preserveTimeSync(p, cb) {
        const stat = this.lstatSync(p);
        const result = cb();
        if (typeof result !== `undefined`)
            p = result;
        this.lutimesSync(p, stat.atime, stat.mtime);
    }
}
exports.FakeFS = FakeFS;
class BasePortableFakeFS extends FakeFS {
    constructor() {
        super(path_1.ppath);
    }
}
exports.BasePortableFakeFS = BasePortableFakeFS;
function getEndOfLine(content) {
    const matches = content.match(/\r?\n/g);
    if (matches === null)
        return os_1.EOL;
    const crlf = matches.filter(nl => nl === `\r\n`).length;
    const lf = matches.length - crlf;
    return crlf > lf ? `\r\n` : `\n`;
}
function normalizeLineEndings(originalContent, newContent) {
    return newContent.replace(/\r?\n/g, getEndOfLine(originalContent));
}
