"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getCacheDirectory = getCacheDirectory;
exports.extractArchive = extractArchive;
exports.downloadBuilderToolset = downloadBuilderToolset;
exports.downloadElectronArtifactZip = downloadElectronArtifactZip;
exports.downloadElectronArtifact = downloadElectronArtifact;
exports.getBinariesMirrorUrl = getBinariesMirrorUrl;
const get = require("@electron/get");
const get_1 = require("@electron/get");
const builder_util_1 = require("builder-util");
const multiProgress_1 = require("electron-publish/out/multiProgress");
const fs_1 = require("fs");
const fs = require("fs/promises");
const crypto = require("crypto");
const os = require("os");
const path = require("path");
const lockfile = require("proper-lockfile");
const promises_1 = require("stream/promises");
const tar = require("tar");
const unzipper = require("unzipper");
const builder_util_runtime_1 = require("builder-util-runtime");
const cacheState_1 = require("./cacheState");
function hashUrlSafe(input, length = 6) {
    let hash = 5381;
    for (let i = 0; i < input.length; i++) {
        hash = ((hash << 5) + hash) ^ input.charCodeAt(i);
    }
    hash >>>= 0;
    const out = hash.toString(36);
    return out.length >= length ? out.slice(0, length) : out.padStart(length, "0");
}
function getCacheDirectory(options) {
    var _a, _b, _c, _d, _e;
    const { isAvoidSystemOnWindows = true, allowEnvVarOverride } = options;
    const env = (_a = process.env.ELECTRON_BUILDER_CACHE) === null || _a === void 0 ? void 0 : _a.trim();
    if (allowEnvVarOverride && env && path.parse(env).root) {
        return env;
    }
    const appName = "electron-builder";
    const platform = os.platform();
    const homeDir = os.homedir();
    if (platform === "darwin") {
        return path.join(homeDir, "Library", "Caches", appName);
    }
    if (platform === "win32") {
        const localAppData = (_b = process.env.LOCALAPPDATA) === null || _b === void 0 ? void 0 : _b.trim();
        const username = (_d = (_c = process.env.USERNAME) === null || _c === void 0 ? void 0 : _c.trim()) === null || _d === void 0 ? void 0 : _d.toLowerCase();
        // https://github.com/electron-userland/electron-builder/issues/1164
        const isSystemUser = isAvoidSystemOnWindows && (((_e = localAppData === null || localAppData === void 0 ? void 0 : localAppData.toLowerCase()) === null || _e === void 0 ? void 0 : _e.includes("\\windows\\system32\\")) || username === "system");
        if (!localAppData || isSystemUser) {
            return path.join(os.tmpdir(), `${appName}-cache`);
        }
        return path.join(localAppData, appName, "Cache");
    }
    const xdgCache = process.env.XDG_CACHE_HOME;
    return xdgCache && path.parse(xdgCache).root ? path.join(xdgCache, appName) : path.join(homeDir, ".cache", appName);
}
function resolveCacheMode() {
    var _a;
    const varName = "ELECTRON_DOWNLOAD_CACHE_MODE";
    const cacheOverride = (_a = process.env[varName]) === null || _a === void 0 ? void 0 : _a.trim();
    if (cacheOverride && Number(cacheOverride) in get_1.ElectronDownloadCacheMode) {
        const mode = Number(cacheOverride);
        builder_util_1.log.debug({ mode }, `cache mode overridden via env var ${varName}`);
        return mode;
    }
    return get_1.ElectronDownloadCacheMode.ReadWrite;
}
async function extractZipStreaming(file, dir) {
    var _a;
    // Pass 1: read central directory once to collect Unix modes (one seek to EOF)
    const zipDir = await unzipper.Open.file(file);
    const entryModes = new Map();
    for (const entry of zipDir.files) {
        const mode = (entry.externalFileAttributes >> 16) & 0xffff;
        if (mode > 0) {
            entryModes.set(entry.path, mode);
        }
    }
    const isSymlink = (mode) => (mode & 0o170000) === 0o120000;
    // Pass 2: stream from byte 0 — no per-entry seeks, no Docker hang
    const entries = (0, fs_1.createReadStream)(file).pipe(unzipper.Parse({ forceStream: true }));
    for await (const entry of entries) {
        const destPath = path.resolve(dir, entry.path);
        if (!destPath.startsWith(dir + path.sep) && destPath !== dir) {
            throw new Error(`Path traversal blocked: ${entry.path}`);
        }
        const mode = (_a = entryModes.get(entry.path)) !== null && _a !== void 0 ? _a : 0;
        if (mode > 0 && isSymlink(mode)) {
            const target = (await entry.buffer()).toString();
            if (path.isAbsolute(target)) {
                throw new Error(`Absolute symlink target blocked: ${target}`);
            }
            const resolvedTarget = path.resolve(path.dirname(destPath), target);
            if (!resolvedTarget.startsWith(dir + path.sep) && resolvedTarget !== dir) {
                throw new Error(`Symlink target escapes extraction dir: ${target}`);
            }
            await fs.mkdir(path.dirname(destPath), { recursive: true });
            await fs.symlink(target, destPath);
        }
        else if (entry.type === "Directory") {
            await fs.mkdir(destPath, { recursive: true });
            entry.autodrain();
        }
        else {
            await fs.mkdir(path.dirname(destPath), { recursive: true });
            await (0, promises_1.pipeline)(entry, (0, fs_1.createWriteStream)(destPath));
            if (mode > 0) {
                await fs.chmod(destPath, mode & 0o7777);
            }
        }
    }
}
async function extractArchive(file, dir) {
    const tmpDir = `${dir}.tmp`;
    await fs.mkdir(tmpDir, { recursive: true });
    const release = await lockfile.lock(tmpDir, {
        retries: { retries: 15, minTimeout: 1000, maxTimeout: 5000 },
        stale: 120000, // Increased from 60s to allow long-running extractions
    });
    try {
        // rm + mkdir happen AFTER acquiring the lock so no concurrent caller can clear our tmpDir mid-extraction
        await fs.rm(tmpDir, { recursive: true, force: true });
        await fs.mkdir(tmpDir, { recursive: true });
        // Guard against the transient window in @electron/get's non-atomic putFileInCache (remove → move).
        // A concurrent worker may have deleted and not yet replaced the source archive; wait briefly for it.
        for (let i = 0; !(await (0, builder_util_1.exists)(file)); i++) {
            if (i >= 4) {
                throw Object.assign(new Error(`Source archive not found after retries: ${file}`), { code: "ENOENT", path: file });
            }
            builder_util_1.log.warn({ file, attempt: i + 1 }, "source archive transiently missing, retrying");
            await new Promise(r => setTimeout(r, 300 * (i + 1)));
        }
        if (file.endsWith(".tar.gz") || file.endsWith(".tgz")) {
            await tar.extract({ file, cwd: tmpDir, strip: 1 });
        }
        else if (file.endsWith(".zip")) {
            await extractZipStreaming(file, tmpDir);
        }
        else if (file.endsWith(".7z")) {
            const cmd7za = await (0, builder_util_1.getPath7za)();
            try {
                await (0, builder_util_1.exec)(cmd7za, ["x", "-bd", file, (0, builder_util_1.to7zaOutputSwitch)((0, builder_util_1.sanitizeDirPath)(tmpDir)), "-y"]);
            }
            catch (e) {
                // Check if extraction actually failed or just had benign warnings
                const files = await fs.readdir(tmpDir);
                if (files.length === 0) {
                    builder_util_1.log.warn({ file, tmpDir, error: e.message }, "7z extraction produced no output");
                    throw new Error(`7z extraction failed for ${file}: ${e.message}`);
                }
                // If files were extracted despite the error, log and continue
                builder_util_1.log.warn({ error: e.message, filesExtracted: files.length }, "7z reported error but extracted files");
            }
        }
        else {
            throw new Error(`Unsupported archive format: ${path.basename(file)}`);
        }
        // Verify extraction produced files
        const extractedFiles = await fs.readdir(tmpDir);
        if (extractedFiles.length === 0) {
            throw new Error(`Extraction of ${path.basename(file)} produced no files`);
        }
        await fs.rm(dir, { recursive: true, force: true });
        await fs.rename(tmpDir, dir);
    }
    finally {
        await release().catch(err => builder_util_1.log.warn({ err }, "failed to release lockfile"));
    }
}
async function downloadArtifactToFile(config, label) {
    var _a, _b, _c, _d;
    // Serialize concurrent downloads of the same artifact across vitest workers to prevent @electron/get's
    // non-atomic putFileInCache (remove + move) from racing with a concurrent reader.
    const artifactLockKey = crypto
        .createHash("sha256")
        .update(JSON.stringify({ v: config.version, p: config.platform, a: config.arch, n: config.artifactName }))
        .digest("hex")
        .slice(0, 20);
    const artifactLockPath = path.join(os.tmpdir(), `eb-dl-${artifactLockKey}.lock`);
    const releaseArtifactLock = await lockfile.lock(artifactLockPath, {
        retries: { retries: 30, minTimeout: 500, maxTimeout: 5000 },
        stale: 600000,
        realpath: false,
    });
    let lastLoggedMilestone = -1;
    const state = { bar: undefined };
    const downloadOptions = {
        timeout: { request: 10 * 60 * 1000 }, // prevent indefinite hang on stalled connections
        ...config.downloadOptions,
        agent: (_b = (_a = config.downloadOptions) === null || _a === void 0 ? void 0 : _a.agent) !== null && _b !== void 0 ? _b : (0, builder_util_1.buildGotProxyAgent)(),
        getProgressCallback: info => {
            // @electron/get passes downloadOptions (including this callback) to its internal
            // SHASUMS256.txt validation download. That file is tiny (<1 MB) and fires at 100%
            // immediately, producing a spurious bar even when the artifact itself is cached.
            // Skip progress display for any download whose total size is known and small.
            if (info.total && info.total < 1000000) {
                return Promise.resolve();
            }
            if (!state.bar && process.stdout.isTTY) {
                builder_util_1.log.info({ label }, "downloading");
                state.bar = new multiProgress_1.MultiProgress().createBar(`${" ".repeat(builder_util_1.PADDING + 2)}[:bar] :percent | ${label}`, { total: 100 });
            }
            const pct = info.percent != null ? Math.floor(info.percent * 100) : 0;
            if (state.bar) {
                state.bar.update(pct);
            }
            else {
                // log every 25% milestone for non-TTY environments (e.g. CI logs) to provide some visibility into download progress without overwhelming logs with too many updates
                const percentCompleted = info.transferred && info.total ? Math.floor((info.transferred / info.total) * 100) : Math.floor(pct / 25) * 25;
                if (percentCompleted > lastLoggedMilestone && percentCompleted > 0) {
                    lastLoggedMilestone = percentCompleted;
                    builder_util_1.log.info({ label, progress: `${percentCompleted}%` }, percentCompleted !== 100 ? "downloading" : "downloaded");
                }
            }
            return Promise.resolve();
        },
    };
    const configWithProgress = { ...config, downloadOptions };
    try {
        let filePath;
        try {
            filePath = await (0, builder_util_runtime_1.retry)(() => get.downloadArtifact(configWithProgress), {
                retries: 3,
                interval: 2000,
                backoff: 2000,
                shouldRetry: (e) => e instanceof builder_util_runtime_1.HttpError ? e.isServerError() : typeof (e === null || e === void 0 ? void 0 : e.code) === "string" && ["ENOTFOUND", "ETIMEDOUT", "ECONNRESET", "EPIPE", "ENOENT"].includes(e.code),
            });
        }
        catch (err) {
            if (typeof (err === null || err === void 0 ? void 0 : err.message) === "string" && err.message.includes("dest already exists")) {
                filePath = await get.downloadArtifact(configWithProgress);
            }
            else {
                throw err;
            }
        }
        if (!(await (0, builder_util_1.exists)(filePath))) {
            builder_util_1.log.warn({ filePath, label }, "cached artifact missing from disk; retrying with cache write");
            filePath = await get.downloadArtifact({ ...configWithProgress, cacheMode: get_1.ElectronDownloadCacheMode.WriteOnly });
        }
        if (!state.bar && lastLoggedMilestone === -1) {
            builder_util_1.log.info({ label }, "using cached artifact");
        }
        return filePath;
    }
    finally {
        (_c = state.bar) === null || _c === void 0 ? void 0 : _c.update(100);
        (_d = state.bar) === null || _d === void 0 ? void 0 : _d.terminate();
        await releaseArtifactLock().catch(err => builder_util_1.log.warn({ err }, "failed to release artifact download lock"));
    }
}
/**
 * Core download + extract engine. Handles lockfile, cache-hit short-circuit,
 * progress bar, extraction (.zip or .tar.gz), and completion marker.
 * Both public download functions delegate here after building their respective configs.
 */
async function downloadAndExtract(config, extractDir, label) {
    await fs.mkdir(extractDir, { recursive: true });
    // Pre-lock fast path: only short-circuit for a definitively complete and valid cache.
    // Do NOT clean up here — concurrent processes could race to delete under each other.
    const stateData = await (0, cacheState_1.readCacheStateFile)(extractDir);
    if ((stateData === null || stateData === void 0 ? void 0 : stateData.state) === cacheState_1.CacheState.complete) {
        const isValid = await (0, cacheState_1.validateCacheDirectory)(extractDir, stateData.fileCount);
        if (isValid) {
            builder_util_1.log.debug({ file: label, path: extractDir }, "using cached artifact - cache valid");
            return extractDir;
        }
    }
    const release = await lockfile.lock(extractDir, {
        retries: { retries: 15, minTimeout: 1000, maxTimeout: 5000 },
        stale: 120000,
    });
    let downloadedFile = null;
    try {
        // Re-check after acquiring lock: another worker may have completed while we waited.
        const stateDataAfterLock = await (0, cacheState_1.readCacheStateFile)(extractDir);
        if ((stateDataAfterLock === null || stateDataAfterLock === void 0 ? void 0 : stateDataAfterLock.state) === cacheState_1.CacheState.complete) {
            const isValid = await (0, cacheState_1.validateCacheDirectory)(extractDir, stateDataAfterLock.fileCount);
            if (isValid) {
                builder_util_1.log.debug({ file: label, path: extractDir }, "using cached artifact - skipping download/extract");
                return extractDir;
            }
            builder_util_1.log.warn({ extractDir }, "Cache marked complete but files invalid, clearing");
        }
        else if ((stateDataAfterLock === null || stateDataAfterLock === void 0 ? void 0 : stateDataAfterLock.state) === cacheState_1.CacheState.corrupted) {
            builder_util_1.log.warn({ extractDir }, "Corrupted cache detected, cleaning up and re-extracting");
        }
        // Cleanup inside the lock — skip lock files since we currently hold them
        await (0, cacheState_1.cleanupCacheDirectory)(extractDir, { skipLockFiles: true });
        await fs.mkdir(extractDir, { recursive: true });
        builder_util_1.log.debug({ file: label }, "downloading");
        downloadedFile = await downloadArtifactToFile(config, label);
        if (!downloadedFile) {
            throw new Error(`Failed to download artifact: ${label}`);
        }
        await (0, cacheState_1.writeCacheState)(extractDir, cacheState_1.CacheState.downloaded);
        // Mark extracting so stale-lock detection can recover a crashed mid-extraction process
        await (0, cacheState_1.writeCacheState)(extractDir, cacheState_1.CacheState.extracting);
        // Extract while holding the lock to prevent concurrent interference
        await extractArchive(downloadedFile, extractDir);
        // Compute metadata with a full recursive walk now that tmpDir has been renamed to extractDir
        const { fileCount, extractedSize } = await (0, cacheState_1.computeCacheMetadata)(extractDir);
        // Write complete state with metadata BEFORE releasing the lock; throwOnError=true so a
        // failed write (disk full, permissions) propagates instead of silently producing a no-op cache
        await (0, cacheState_1.writeCacheState)(extractDir, cacheState_1.CacheState.complete, { fileCount, extractedSize }, true);
    }
    finally {
        await release().catch(err => builder_util_1.log.warn({ err }, "failed to release lockfile"));
    }
    return extractDir;
}
// ─── Public API ───────────────────────────────────────────────────────────────
/**
 * Downloads a generic artifact (.tar.gz or .zip) from a GitHub release.
 * Used for electron-builder-binaries tools (appimage, etc.).
 */
async function downloadBuilderToolset(options) {
    const { releaseName, filenameWithExt, checksums, githubOrgRepo = "electron-userland/electron-builder-binaries", overrideUrl } = options;
    const baseUrl = getBinariesMirrorUrl(githubOrgRepo);
    const fullUrl = overrideUrl ? `${overrideUrl}/${filenameWithExt}` : `${baseUrl}${releaseName}/${filenameWithExt}`;
    const suffix = hashUrlSafe(fullUrl, 5);
    const folderName = `${filenameWithExt.replace(/\.(tar\.gz|tgz|zip|7z)$/, "")}-${suffix}`;
    const extractDir = path.join(getCacheDirectory({ allowEnvVarOverride: true }), releaseName, folderName);
    // Use resolveAssetURL so @electron/get's ELECTRON_MIRROR env var check cannot override
    // the builder-binaries URL we've already resolved (see getArtifactRemoteURL in @electron/get).
    const mirrorOptions = {
        resolveAssetURL: async () => Promise.resolve(fullUrl),
    };
    const config = {
        version: "9.9.9", // must be >1.3.2 to bypass @electron/get validation shortcut
        artifactName: filenameWithExt,
        cacheRoot: path.resolve(getCacheDirectory({ allowEnvVarOverride: true }), "downloads"),
        cacheMode: resolveCacheMode(),
        ...(checksums != null ? { checksums } : { unsafelyDisableChecksums: true }),
        mirrorOptions,
        isGeneric: true,
    };
    return downloadAndExtract(config, extractDir, filenameWithExt);
}
// Keys present in ElectronGetOptions but absent from ElectronDownloadOptions.
// Used to discriminate between the two config shapes at runtime.
const ELECTRON_GET_EXCLUSIVE_KEYS = ["mirrorOptions", "force", "unsafelyDisableChecksums"];
function isElectronGetOptions(dl) {
    return ELECTRON_GET_EXCLUSIVE_KEYS.some(k => Object.hasOwnProperty.call(dl, k));
}
/**
 * Downloads and extracts an electron platform artifact (e.g. ffmpeg) using @electron/get.
 * Deduplicates concurrent calls for the same artifact within the same process.
 */
function buildElectronArtifactConfig(options) {
    const { electronDownload, arch, version, platformName: platform, artifactName, cacheDir: cacheRoot } = options;
    let artifactConfig = { cacheRoot, platform, arch, version, artifactName };
    if (electronDownload != null) {
        if (isElectronGetOptions(electronDownload)) {
            const { mirrorOptions, ...rest } = electronDownload;
            artifactConfig = { ...artifactConfig, ...rest, cacheRoot, mirrorOptions };
        }
        else {
            const { mirror, customDir, cache, customFilename, isVerifyChecksum, strictSSL, platform: overridePlatform, arch: overrideArch } = electronDownload;
            // strictSSL: false disables TLS certificate validation for all
            // electron/tool downloads.  This option exists only for air-gapped or
            // self-signed-cert environments; ensure your build network is fully trusted.
            if (strictSSL === false) {
                builder_util_1.log.warn({ option: "electronDownload.strictSSL" }, "strictSSL is false — TLS certificate validation is DISABLED for Electron downloads. Only use this option in a trusted, isolated build environment.");
            }
            artifactConfig = {
                ...artifactConfig,
                unsafelyDisableChecksums: isVerifyChecksum === false,
                cacheRoot: cache !== null && cache !== void 0 ? cache : cacheRoot,
                mirrorOptions: {
                    mirror: mirror || undefined,
                    customDir: customDir || undefined,
                    customFilename: customFilename || undefined,
                },
                ...(strictSSL === false ? { downloadOptions: { https: { rejectUnauthorized: false } } } : {}),
            };
            if (overridePlatform != null) {
                artifactConfig.platform = overridePlatform;
            }
            if (overrideArch != null) {
                artifactConfig.arch = overrideArch;
            }
        }
    }
    artifactConfig.cacheMode = resolveCacheMode();
    return artifactConfig;
}
/**
 * Downloads the electron artifact zip via @electron/get (with caching) and returns the zip file path.
 * Use when you need to extract the zip yourself (e.g. directly to appOutDir to preserve empty dirs and symlinks).
 */
function downloadElectronArtifactZip(options) {
    const config = buildElectronArtifactConfig(options);
    return downloadArtifactToFile(config, config.artifactName);
}
async function downloadElectronArtifact(options) {
    const { arch, version, platformName: platform, artifactName } = options;
    const artifactConfig = buildElectronArtifactConfig(options);
    const suffix = hashUrlSafe(JSON.stringify(artifactConfig), 5);
    const folderName = `${artifactName}-v${version}-${platform}-${arch}-${suffix}`;
    const extractDir = path.join(getCacheDirectory({ allowEnvVarOverride: true }), `${artifactName}-v${version}`, folderName);
    return downloadAndExtract(artifactConfig, extractDir, artifactName);
}
/**
 * Get the binaries mirror URL from environment variables.
 * Supports various npm config formats and falls back to GitHub.
 */
function getBinariesMirrorUrl(githubOrgRepo) {
    for (const envVar of [
        "NPM_CONFIG_ELECTRON_BUILDER_BINARIES_MIRROR",
        "npm_config_electron_builder_binaries_mirror",
        "npm_package_config_electron_builder_binaries_mirror",
        "ELECTRON_BUILDER_BINARIES_MIRROR",
    ]) {
        const url = (0, builder_util_1.parseValidEnvVarUrl)(envVar);
        if (url) {
            return url.endsWith("/") ? url : `${url}/`;
        }
    }
    return `https://github.com/${githubOrgRepo}/releases/download/`;
}
//# sourceMappingURL=electronGet.js.map