"use strict";
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseFileCache = void 0;
exports.statWithTimeout = statWithTimeout;
exports.getUncHost = getUncHost;
exports.isDirectory = isDirectory;
exports.readFileWithBusyRetry = readFileWithBusyRetry;
exports.watchDir = watchDir;
exports.hasDirTraversal = hasDirTraversal;
exports.escapeGlobPath = escapeGlobPath;
exports.unzip = unzip;
exports.ensureParentFolder = ensureParentFolder;
exports.createFileWithPath = createFileWithPath;
exports.createSafeWriteStream = createSafeWriteStream;
exports.isValidFileName = isValidFileName;
exports.exists = exists;
exports.loadFileCached = loadFileCached;
exports.parseFile = parseFile;
const promises_1 = require("fs/promises");
const cross_1 = require("./cross");
const config_1 = require("./config");
const fs_1 = require("fs");
const path_1 = require("path");
const fast_glob_1 = __importDefault(require("fast-glob"));
const const_1 = require("./const");
const promises_2 = require("stream/promises");
const stat_1 = require("./stat");
const unzipper_1 = __importDefault(require("unzipper"));
const fileTimeout = (0, config_1.defineConfig)('file_timeout', 3, x => x * 1000);
// a smart (and a bit arbitrary) way to decide if we need the stat-workers functionality. Without it, we may be a bit faster. We'll see with experience if we need a dedicated configuration.
const disableStatWorkers = Number(process.env.UV_THREADPOOL_SIZE) >= 10;
// some paths (mostly offline networked ones) can take 20+ seconds to fail
async function statWithTimeout(path) {
    // with just 4 requests we saturate the default UV_THREADPOOL_SIZE, blocking even local fs operations, so we move all UNC requests to worker threads
    const workerKey = !disableStatWorkers && const_1.IS_WINDOWS && getUncHost(path); // pool requests by server name
    const op = workerKey ? (0, stat_1.getStatWorker)(workerKey)(path) : (0, promises_1.stat)(path);
    return (0, cross_1.haveTimeout)(fileTimeout.compiled(), op);
}
function getUncHost(path) {
    return /^\\\\([^\\]+)\\/.exec(path)?.[1];
}
async function isDirectory(path) {
    try {
        return (await statWithTimeout(path)).isDirectory();
    }
    catch { }
}
async function readFileWithBusyRetry(path) {
    return (0, promises_1.readFile)(path, 'utf8').catch(e => {
        if (e?.code !== 'EBUSY')
            throw e;
        console.debug('Busy');
        return (0, cross_1.wait)(100).then(() => readFileWithBusyRetry(path));
    });
}
function watchDir(dir, cb, atStart = false) {
    let watcher;
    let paused = false;
    try {
        watcher = (0, fs_1.watch)(dir, controlledCb);
    }
    catch {
        // failing watching the content of the dir, we try to monitor its parent, but filtering events only for our target dir
        const base = (0, path_1.basename)(dir);
        try {
            watcher = (0, fs_1.watch)((0, path_1.dirname)(dir), (event, name) => {
                if (name !== base)
                    return;
                try {
                    watcher.close(); // if we succeed, we give up the parent watching
                    watcher = (0, fs_1.watch)(dir, controlledCb); // attempt at passing to a more specific watching
                }
                catch { }
                controlledCb();
            });
        }
        catch (e) {
            console.debug(String(e));
        }
    }
    if (atStart)
        controlledCb();
    return {
        working() { return Boolean(watcher); },
        stop() { watcher?.close(); },
        pause() { paused = true; },
        unpause() { paused = false; },
    };
    function controlledCb() {
        if (!paused)
            cb();
    }
}
function hasDirTraversal(s) {
    return s && /(^|[/\\])\.\.($|[/\\])/.test(s);
}
// apply this to paths that may contain \ as separator (not supported by fast-glob) and other special chars to be escaped (parenthesis)
function escapeGlobPath(path) {
    return fast_glob_1.default.escapePath(path.replace(/\\/g, '/'));
}
async function unzip(stream, cb) {
    const extracted = new Map();
    let chain = Promise.resolve();
    return new Promise((resolve, reject) => stream.pipe(unzipper_1.default.Parse())
        .on('close', () => chain.then(resolve, reject))
        .on('error', reject)
        .on('entry', (entry) => chain = chain.then(async () => {
        const { path, type } = entry;
        const dest = await (0, cross_1.try_)(() => cb(path), e => console.warn(String(e)));
        if (!dest || type !== 'File')
            return entry.autodrain().promise();
        extracted.set(path, dest);
        console.debug('Unzip', dest);
        // keep writes serialized so archive entries can't race while callers map paths asynchronously
        const thisFile = entry.pipe(await createSafeWriteStream(dest));
        await (0, promises_2.finished)(thisFile);
    }))
        // unix modes live in the central directory, so we reapply them after the file stream has been written
        .on('entryInCentral', (entry) => chain = chain.then(async () => {
        if (entry.type !== 'File')
            return;
        const dest = extracted.get(entry.path);
        if (dest && entry.unixAttrs)
            await (0, promises_1.chmod)(dest, entry.unixAttrs).catch(() => { });
    })));
}
async function ensureParentFolder(path, dirnameIt = true) {
    if (dirnameIt)
        path = (0, path_1.dirname)(path);
    if ((0, cross_1.isWindowsDrive)(path))
        return;
    try {
        await (0, promises_1.mkdir)(path, { recursive: true });
        return true;
    }
    catch {
        return false;
    }
}
function createFileWithPath(path, options) {
    const folder = (0, path_1.dirname)(path);
    if (!(0, cross_1.isWindowsDrive)(folder)) // can't use prepareFolder because it's async
        try {
            (0, fs_1.mkdirSync)(folder, { recursive: true });
        }
        catch {
            return;
        }
    return (0, fs_1.createWriteStream)(path, options);
}
async function createSafeWriteStream(path, options) {
    await ensureParentFolder(path);
    return new Promise((resolve, reject) => {
        const first = (0, fs_1.createWriteStream)(path, options)
            .on('open', () => resolve(first))
            .on('error', (e) => {
            if (!const_1.IS_WINDOWS || e.code !== 'EPERM') // Windows throws EPERM for hidden files with flags 'w' and 'a'
                return reject(e);
            if (typeof options === 'string')
                options = { encoding: options };
            else
                options ||= {};
            if (options.flags && options.flags !== 'w') // we only handle the 'w' case
                return reject(e);
            options.flags = 'r+';
            const second = (0, fs_1.createWriteStream)(path, options)
                .on('open', fd => (0, fs_1.ftruncate)(fd, 0, () => resolve(second)))
                .on('error', reject);
        });
    });
}
function isValidFileName(name, acceptUnreadable = false) {
    return name && name !== '.' && !(const_1.IS_WINDOWS ? /[/:"*?<>|\\]/ : /\//).test(name) && !hasDirTraversal(name)
        && (acceptUnreadable || !/[\u0000-\u001F\u007F]/.test(name));
}
function exists(path) {
    return (0, promises_1.access)(path).then(() => true, () => false);
}
// parse a file, caching unless timestamp has changed
exports.parseFileCache = new Map();
async function loadFileCached(path, loader, minInterval = 0) {
    const cached = exports.parseFileCache.get(path);
    const now = Date.now();
    if (cached && now - cached.lastCheck < minInterval)
        return cached.parsed;
    const ts = await statWithTimeout(path).then(x => x.mtime, e => {
        if (e?.message !== 'timeout')
            throw e;
        return cached?.ts || new Date(0); // on timeout (e.g. thread pool saturated), serve cache if any, or attempt the loader
    });
    if (cached)
        cached.lastCheck = now;
    if (cached && Number(ts) === Number(cached.ts))
        return cached.parsed;
    const parsed = loader(path);
    exports.parseFileCache.set(path, { ts, parsed, lastCheck: now });
    return parsed;
}
async function parseFile(path, parse, skipStatIfFresherThan = 0) {
    return loadFileCached(path, () => (0, promises_1.readFile)(path).then(parse), skipStatIfFresherThan);
}
