"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LinuxTargetHelper = exports.installPrefix = void 0;
const builder_util_1 = require("builder-util");
const builder_util_runtime_1 = require("builder-util-runtime");
const fs_extra_1 = require("fs-extra");
const lazy_val_1 = require("lazy-val");
const path_1 = require("path");
const semver = require("semver");
const core24_1 = require("./snap/core24");
const coreCustom_1 = require("./snap/coreCustom");
const coreLegacy_1 = require("./snap/coreLegacy");
/**
 * Escape a string value for use in a freedesktop .desktop file string field
 * (Name, Comment, StartupWMClass, etc.).
 *
 * The freedesktop Desktop Entry Specification requires that the following
 * characters be escaped in string / localestring values:
 *   \n  →  \\n      (newline — would inject new key=value lines otherwise)
 *   \r  →  \\r
 *   \t  →  \\t
 *   \\  →  \\\\
 *
 * Without escaping, a productName or description containing a literal newline
 * can inject arbitrary key=value pairs into the generated .desktop file,
 * potentially overriding the Exec key.
 *
 * @see https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#value-types
 */
function desktopStringEscape(value) {
    return value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
}
/**
 * Characters that require an Exec argument to be double-quoted per the
 * freedesktop Desktop Entry Specification.  Plain alphanumeric args and
 * field codes must NOT be wrapped in quotes.
 *
 * @see https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables
 */
const EXEC_RESERVED_RE = /[\s"'`\\<>~|&;$*?#()]/;
/**
 * Quote a single argument for use in a .desktop file Exec key.
 *
 * Field codes (`%f`, `%u`, `%F`, `%U`, etc.) MUST be left unquoted — the
 * desktop launcher only expands them in unquoted token positions.  Wrapping
 * them in `"…"` causes the launcher to treat them as literal strings, which
 * breaks file-association / drag-and-drop functionality.
 *
 * For all other arguments, double-quoting is used when the argument contains
 * any character that would be misinterpreted by the launcher without quoting
 * (spaces, shell metacharacters, etc.).  Safe plain-word args are passed
 * through unchanged to keep the Exec line readable.
 *
 * @see https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables
 */
function desktopExecArgEscape(arg) {
    // Field codes (%f, %u, %F, %U, %i, %c, %k, …) must never be quoted.
    if (/^%[a-zA-Z]$/.test(arg)) {
        return arg;
    }
    // Only quote when the arg actually contains characters that need it.
    if (EXEC_RESERVED_RE.test(arg)) {
        return `"${arg.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
    }
    return arg;
}
function mapLinuxCompressionToSnap(level) {
    if (level === "store") {
        return "lzo";
    }
    if (level === "maximum") {
        return "xz";
    }
    return undefined;
}
exports.installPrefix = "/opt";
class LinuxTargetHelper {
    constructor(packager) {
        this.packager = packager;
        this.iconPromise = new lazy_val_1.Lazy(() => this.computeDesktopIcons());
        this.mimeTypeFilesPromise = new lazy_val_1.Lazy(() => this.computeMimeTypeFiles());
        this.maxIconPath = null;
    }
    get icons() {
        return this.iconPromise.value;
    }
    get mimeTypeFiles() {
        return this.mimeTypeFilesPromise.value;
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    getSnapCore() {
        const { snapcraft, snap: legacySnap } = this.packager.config;
        if (snapcraft != null && legacySnap != null) {
            builder_util_1.log.warn("Both `snapcraft` and `snap` configurations are present. `snapcraft` takes precedence; please remove the `snap` key to silence this warning.");
        }
        // Merge linux-level options (category, description, mimeTypes, etc.) as the base so they
        // propagate into the generated snapcraft.yaml and .desktop file without requiring users to
        // duplicate them under core24/core18/etc. Per-core options always win for conflicts.
        // linux.compression is a CompressionLevel ("store"/"normal"/"maximum"); snap compression is an
        // algorithm ("xz"/"lzo"). Map the level to the nearest algorithm; per-core options override.
        const { compression: linuxCompression, ...linuxOptions } = this.packager.platformSpecificBuildOptions;
        const snapLinuxOptions = { ...linuxOptions, compression: mapLinuxCompressionToSnap(linuxCompression) };
        if (snapcraft != null) {
            const core = snapcraft.base;
            const options = snapcraft[core] || {};
            switch (core) {
                case "core18":
                case "core20":
                case "core22":
                    if (!this.isElectronVersionGreaterOrEqualThan("4.0.0")) {
                        if (!this.isElectronVersionGreaterOrEqualThan("2.0.0-beta.1")) {
                            throw new builder_util_1.InvalidConfigurationError("Electron 2 and higher is required to build Snap with core18/core20/core22");
                        }
                        builder_util_1.log.warn(null, "electron 4 and higher is highly recommended for Snap with core18/core20/core22");
                    }
                    return new coreLegacy_1.SnapCoreLegacy(this.packager, this, (0, builder_util_runtime_1.deepAssign)({}, snapLinuxOptions, { base: core, ...options }));
                case "core24":
                    if (!this.isElectronVersionGreaterOrEqualThan("28.0.0")) {
                        if (!this.isElectronVersionGreaterOrEqualThan("25.0.0")) {
                            throw new builder_util_1.InvalidConfigurationError("Electron 25 and higher is required to build Snap with core24");
                        }
                        builder_util_1.log.warn(null, "electron 28 and higher is highly recommended for Snap with core24");
                    }
                    return new core24_1.SnapCore24(this.packager, this, (0, builder_util_runtime_1.deepAssign)({}, snapLinuxOptions, options));
                case "custom":
                    // Pass-through: do not inject linux options into user-supplied yaml
                    return new coreCustom_1.SnapCoreCustom(this.packager, this, snapcraft.custom || {});
            }
        }
        if (legacySnap != null) {
            builder_util_1.log.warn({
                reason: "`snap` configuration is deprecated",
                docs: "https://www.electron.build/snapcraft",
            }, "please consider migrating `snap` configuration to `snapcraft.<core>` and remove `snap` configuration");
        }
        return new coreLegacy_1.SnapCoreLegacy(this.packager, this, (0, builder_util_runtime_1.deepAssign)({}, snapLinuxOptions, legacySnap !== null && legacySnap !== void 0 ? legacySnap : {}));
    }
    isElectronVersionGreaterOrEqualThan(version, fallback) {
        const electronVersion = this.packager.config.electronVersion;
        if (!electronVersion) {
            return fallback ? semver.gte(fallback, version) : true;
        }
        return semver.gte(electronVersion, version);
    }
    async computeMimeTypeFiles() {
        const items = [];
        for (const fileAssociation of this.packager.fileAssociations) {
            if (!fileAssociation.mimeType) {
                continue;
            }
            const data = `<mime-type type="${fileAssociation.mimeType}">
  <glob pattern="*.${fileAssociation.ext}"/>
    ${fileAssociation.description ? `<comment>${fileAssociation.description}</comment>` : ""}
  <icon name="x-office-document" />
</mime-type>`;
            items.push(data);
        }
        if (items.length === 0) {
            return null;
        }
        const file = await this.packager.getTempFile(".xml");
        await (0, fs_extra_1.outputFile)(file, '<?xml version="1.0" encoding="utf-8"?>\n<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">\n' + items.join("\n") + "\n</mime-info>");
        return file;
    }
    // must be name without spaces and other special characters, but not product name used
    async computeDesktopIcons() {
        var _a, _b, _c;
        const packager = this.packager;
        const { platformSpecificBuildOptions, config } = packager;
        const sources = [platformSpecificBuildOptions.icon, (_b = (_a = config.mac) === null || _a === void 0 ? void 0 : _a.icon) !== null && _b !== void 0 ? _b : config.icon].filter(str => !!str);
        // If no explicit sources are defined, fallback to buildResources directory, then default framework icon
        let fallbackSources = [...(0, builder_util_1.asArray)(packager.getDefaultFrameworkIcon())];
        const buildResources = (_c = config.directories) === null || _c === void 0 ? void 0 : _c.buildResources;
        if (buildResources && (await (0, builder_util_1.exists)((0, path_1.join)(buildResources, "icons")))) {
            fallbackSources = [buildResources, ...fallbackSources];
        }
        // need to put here and not as default because need to resolve image size
        const result = await packager.resolveIcon(sources, fallbackSources, "set");
        this.maxIconPath = result[result.length - 1].file;
        // Ignore .icon files for linux (they are exclusive for macOS)
        return result.filter(icon => !icon.file.endsWith(".icon"));
    }
    getDescription(options) {
        return options.description || this.packager.appInfo.description;
    }
    getSanitizedVersion(target) {
        const { appInfo: { version }, } = this.packager;
        switch (target) {
            case "pacman":
                return version.replace(/-/g, "_");
            case "rpm":
            case "deb":
                return version.replace(/-/g, "~");
            default:
                return version;
        }
    }
    async writeDesktopEntry(targetSpecificOptions, exec, destination, extra) {
        const data = await this.computeDesktopEntry(targetSpecificOptions, exec, extra);
        const file = destination || (await this.packager.getTempFile(`${this.packager.appInfo.productFilename}.desktop`));
        await (0, fs_extra_1.outputFile)(file, data);
        return file;
    }
    computeDesktopEntry(targetSpecificOptions, exec, extra) {
        var _a, _b, _c, _d, _e, _f, _g;
        if (exec != null && exec.length === 0) {
            throw new Error("Specified exec is empty");
        }
        // https://github.com/electron-userland/electron-builder/issues/3418
        if ((_b = (_a = targetSpecificOptions.desktop) === null || _a === void 0 ? void 0 : _a.entry) === null || _b === void 0 ? void 0 : _b.Exec) {
            throw new Error("Please specify executable name as linux.executableName instead of linux.desktop.Exec");
        }
        const packager = this.packager;
        const appInfo = packager.appInfo;
        const executableArgs = targetSpecificOptions.executableArgs;
        if (exec == null) {
            exec = `${exports.installPrefix}/${appInfo.sanitizedProductName}/${packager.executableName}`;
            if (!/^[/0-9A-Za-z._-]+$/.test(exec)) {
                exec = `"${exec}"`;
            }
            if (executableArgs) {
                exec += " ";
                // Each arg is double-quoted per the freedesktop Exec key spec so that
                // spaces, $, ;, & and other reserved characters are not misinterpreted.
                exec += executableArgs.map(desktopExecArgEscape).join(" ");
            }
            // https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables
            const execCodes = ["%f", "%u", "%F", "%U"];
            if (executableArgs == null || executableArgs.findIndex(arg => execCodes.includes(arg)) === -1) {
                exec += " %U";
            }
        }
        // https://github.com/electron-userland/electron-builder/issues/9103
        // Electron derives app_id from desktopName in package.json; StartupWMClass must match.
        // https://github.com/electron/electron/blob/9a7b73b5334f1d72c08e2d5e94106706ed751186/lib/browser/init.ts#L128-L133
        const trimmedDesktopName = (_c = packager.info.metadata.desktopName) === null || _c === void 0 ? void 0 : _c.trim();
        const wmClass = !(0, builder_util_1.isEmptyOrSpaces)(trimmedDesktopName) ? trimmedDesktopName.replace(/\.desktop$/, "") : appInfo.productName;
        const desktopMeta = (0, builder_util_runtime_1.deepAssign)({
            // String values are escaped per the freedesktop spec (\\, \n, \r, \t)
            // so that a product name containing a newline cannot inject new key=value
            // pairs into the .desktop file (e.g. overriding the Exec key).
            Name: desktopStringEscape(appInfo.productName),
            Exec: exec,
            Terminal: "false",
            Type: "Application",
            Icon: packager.executableName,
            // https://askubuntu.com/questions/367396/what-represent-the-startupwmclass-field-of-a-desktop-file
            // Set to desktopName (minus .desktop suffix) when provided, so it matches Electron's
            // app_id and desktop environments can associate running windows with this entry.
            // Falls back to productName for apps that don't set desktopName.
            // to get WM_CLASS of running window: xprop WM_CLASS
            // StartupWMClass doesn't work for unicode
            // https://github.com/electron/electron/blob/2-0-x/atom/browser/native_window_views.cc#L226
            StartupWMClass: desktopStringEscape(wmClass),
        }, extra, (_e = (_d = targetSpecificOptions.desktop) === null || _d === void 0 ? void 0 : _d.entry) !== null && _e !== void 0 ? _e : {});
        const description = this.getDescription(targetSpecificOptions);
        if (!(0, builder_util_1.isEmptyOrSpaces)(description)) {
            desktopMeta.Comment = desktopStringEscape(description);
        }
        const mimeTypes = (0, builder_util_1.asArray)(targetSpecificOptions.mimeTypes);
        for (const fileAssociation of packager.fileAssociations) {
            if (fileAssociation.mimeType != null) {
                mimeTypes.push(fileAssociation.mimeType);
            }
        }
        for (const protocol of (0, builder_util_1.asArray)(packager.config.protocols).concat((0, builder_util_1.asArray)(packager.platformSpecificBuildOptions.protocols))) {
            for (const scheme of (0, builder_util_1.asArray)(protocol.schemes)) {
                mimeTypes.push(`x-scheme-handler/${scheme}`);
            }
        }
        if (mimeTypes.length !== 0) {
            desktopMeta.MimeType = mimeTypes.join(";") + ";";
        }
        let category = targetSpecificOptions.category;
        if ((0, builder_util_1.isEmptyOrSpaces)(category)) {
            const macCategory = (packager.config.mac || {}).category;
            if (macCategory != null) {
                category = macToLinuxCategory[macCategory];
            }
            if (category == null) {
                // https://github.com/develar/onshape-desktop-shell/issues/48
                if (macCategory != null) {
                    builder_util_1.log.warn({ macCategory }, "cannot map macOS category to Linux. If possible mapping is known for you, please file issue to add it.");
                }
                builder_util_1.log.warn({
                    reason: "linux.category is not set and cannot map from macOS",
                    docs: "https://www.electron.build/linux",
                }, 'application Linux category is set to default "Utility"');
                category = "Utility";
            }
        }
        desktopMeta.Categories = `${category}${category.endsWith(";") ? "" : ";"}`;
        let data = `[Desktop Entry]`;
        for (const name of Object.keys(desktopMeta)) {
            data += `\n${name}=${desktopMeta[name]}`;
        }
        data += "\n";
        const desktopActions = (_g = (_f = targetSpecificOptions.desktop) === null || _f === void 0 ? void 0 : _f.desktopActions) !== null && _g !== void 0 ? _g : {};
        for (const [actionName, config] of Object.entries(desktopActions)) {
            if (!Object.keys(config !== null && config !== void 0 ? config : {}).length) {
                continue;
            }
            data += `\n[Desktop Action ${actionName}]`;
            for (const [key, value] of Object.entries(config !== null && config !== void 0 ? config : {})) {
                data += `\n${key}=${value}`;
            }
            data += "\n";
        }
        return Promise.resolve(data);
    }
}
exports.LinuxTargetHelper = LinuxTargetHelper;
const macToLinuxCategory = {
    "public.app-category.graphics-design": "Graphics",
    "public.app-category.developer-tools": "Development",
    "public.app-category.education": "Education",
    "public.app-category.games": "Game",
    "public.app-category.video": "Video;AudioVideo",
    "public.app-category.utilities": "Utility",
    "public.app-category.social-networking": "Network;Chat",
    "public.app-category.finance": "Office;Finance",
    "public.app-category.music": "Audio;AudioVideo",
};
//# sourceMappingURL=LinuxTargetHelper.js.map