"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildStaticRuntimeAppImage = buildStaticRuntimeAppImage;
exports.buildLegacyFuse2AppImage = buildLegacyFuse2AppImage;
exports.validateCriticalPathString = validateCriticalPathString;
exports.generateAppRunScript = generateAppRunScript;
const builder_util_1 = require("builder-util");
const fs = require("fs-extra");
const path = require("path");
const linux_1 = require("../../toolsets/linux");
const appLauncher_1 = require("./appLauncher");
const differentialUpdateInfoBuilder_1 = require("../differentialUpdateInfoBuilder");
const AppImageTarget_1 = require("./AppImageTarget");
async function buildStaticRuntimeAppImage(appimageToolVersion, opts) {
    const { stageDir, output, appDir, options, arch } = opts;
    try {
        await fs.remove(output);
        // Write AppRun launcher and related files
        await writeAppLauncherAndRelatedFiles(opts);
        const { runtimeLibraries: libraries, runtime, mksquashfs } = await (0, linux_1.getAppImageTools)(appimageToolVersion, arch);
        await (0, builder_util_1.copyDir)(libraries, path.join(stageDir, "usr", "lib"));
        // Copy app directory to stage
        // mksquashfs doesn't support merging, so we copy the entire app dir
        await (0, builder_util_1.copyDir)(appDir, stageDir);
        const runtimeData = await fs.readFile(runtime);
        // Create squashfs with offset for runtime
        const args = [stageDir, output, "-offset", runtimeData.length.toString(), "-all-root", "-noappend", "-no-progress", "-quiet", "-no-xattrs", "-no-fragments"];
        if (options.compression) {
            args.push("-comp", options.compression);
        }
        await (0, builder_util_1.exec)(mksquashfs, args, {
            cwd: stageDir,
        });
        // Write runtime data at the beginning of the file
        await writeRuntimeData(output, runtimeData);
        // Make executable
        await fs.chmod(output, 0o755);
        // Append blockmap inside try block to ensure cleanup on failure
        const updateInfo = await (0, differentialUpdateInfoBuilder_1.appendBlockmap)(output);
        return updateInfo;
    }
    catch (error) {
        // Clean up partial build on failure
        await fs.remove(output).catch(() => { });
        throw error;
    }
}
async function buildLegacyFuse2AppImage(opts) {
    const { stageDir, output, appDir, options, arch } = opts;
    try {
        await fs.remove(output);
        await writeAppLauncherAndRelatedFiles(opts);
        const { runtime, mksquashfs, runtimeLibraries } = await (0, linux_1.getAppImageTools)("0.0.0", arch);
        // Mirror the app-builder-lib Go implementation: bundle lib/<arch> into usr/lib for x64 and ia32.
        // arm targets don't have a dedicated lib dir in the FUSE2 toolset.
        if (arch === builder_util_1.Arch.x64 || arch === builder_util_1.Arch.ia32) {
            await (0, builder_util_1.copyDir)(runtimeLibraries, path.join(stageDir, "usr", "lib"));
        }
        await (0, builder_util_1.copyDir)(appDir, stageDir);
        const runtimeData = await fs.readFile(runtime);
        const args = [stageDir, output, "-offset", runtimeData.length.toString(), "-all-root", "-noappend", "-no-progress", "-quiet", "-no-xattrs", "-no-fragments"];
        if (options.compression) {
            args.push("-comp", options.compression);
            if (options.compression === "xz") {
                // Match the dictionary/block-size settings used by the original app-builder Go implementation
                args.push("-Xdict-size", "100%", "-b", "1048576");
            }
        }
        await (0, builder_util_1.exec)(mksquashfs, args, { cwd: stageDir });
        await writeRuntimeData(output, runtimeData);
        await fs.chmod(output, 0o755);
        const updateInfo = await (0, differentialUpdateInfoBuilder_1.appendBlockmap)(output);
        return updateInfo;
    }
    catch (error) {
        await fs.remove(output).catch(() => { });
        throw error;
    }
}
async function writeRuntimeData(filePath, runtimeData) {
    const fd = await fs.open(filePath, "r+");
    try {
        await fs.write(fd, runtimeData, 0, runtimeData.length, 0);
    }
    finally {
        try {
            await fs.close(fd);
        }
        catch (closeError) {
            // Log but don't throw - preserve original error if any
            builder_util_1.log.warn({ message: closeError.message, file: filePath }, `failed to close file descriptor`);
        }
    }
}
/**
 * Escapes a string for safe use in shell scripts by wrapping in single quotes
 * and escaping any single quotes within the string.
 *
 * This allows strings with spaces, special characters, etc. to be safely used.
 */
function escapeShellString(str) {
    // Escape single quotes by replacing ' with '\''
    // Then wrap the whole string in single quotes
    return `'${str.replace(/'/g, "'\\''")}'`;
}
/**
 * Validates that critical path fields (executable name, product filename, license filename)
 * contain only characters that are safe for use in filesystem paths and embedded bash strings.
 * Allowed: Unicode letters, digits, dots, underscores, hyphens, and spaces.
 */
function validateCriticalPathString(str, fieldName) {
    if (!/^[\p{L}\p{N}._\- ]+$/u.test(str)) {
        throw new builder_util_1.InvalidConfigurationError(`${fieldName} contains characters that cannot be safely used in file paths: ${str}. Please use only letters, digits, hyphens, underscores, dots, and spaces.`);
    }
}
async function writeAppLauncherAndRelatedFiles(opts) {
    const { stageDir, options: { license, executableName, productFilename, productName, desktopEntry }, } = opts;
    // executableName and productFilename are embedded directly into double-quoted bash strings
    // and used as filesystem paths, so they must pass the whitelist.
    // productName is not validated here because it is only ever passed through escapeShellString().
    validateCriticalPathString(executableName, "executableName");
    validateCriticalPathString(productFilename, "productFilename");
    // Write desktop file
    const desktopFileName = `${executableName}.desktop`;
    await fs.writeFile(path.join(stageDir, desktopFileName), desktopEntry, { mode: 0o644 });
    await (0, appLauncher_1.copyIcons)(opts);
    const templateConfig = {
        DesktopFileName: desktopFileName,
        ExecutableName: executableName,
        ProductName: productName,
        ProductFilename: productFilename,
        ResourceName: `appimagekit-${executableName}`,
    };
    const mimeTypeFile = await (0, appLauncher_1.copyMimeTypes)(stageDir, opts.options);
    if (mimeTypeFile) {
        templateConfig.MimeTypeFile = mimeTypeFile;
    }
    let finalConfig = templateConfig;
    // Copy license file if provided
    if (license) {
        // Validate license file exists
        if (!(await (0, builder_util_1.exists)(license))) {
            throw new builder_util_1.InvalidConfigurationError(`License file not found: ${license}`);
        }
        const licenseBaseName = path.basename(license);
        const ext = path.extname(license).toLowerCase();
        // Validate license filename for path safety
        validateCriticalPathString(licenseBaseName, "licenseBaseName");
        // Validate extension
        if (![".txt", ".html"].includes(ext)) {
            builder_util_1.log.warn({ license, expected: ".txt or .html" }, `license file has unexpected extension`);
        }
        await (0, builder_util_1.copyFile)(license, path.join(stageDir, licenseBaseName));
        finalConfig = {
            ...templateConfig,
            EulaFile: licenseBaseName,
            IsHtmlEula: ext === ".html",
        };
    }
    const appRunContent = generateAppRunScript(finalConfig);
    await fs.writeFile(path.join(stageDir, AppImageTarget_1.APP_RUN_ENTRYPOINT), appRunContent, { mode: 0o755 });
}
function hasEula(config) {
    return "EulaFile" in config && typeof config.EulaFile === "string";
}
function generateAppRunScript(config) {
    const eulaEnabled = hasEula(config);
    return `#!/usr/bin/env bash
set -e

THIS="$0"
# http://stackoverflow.com/questions/3190818/
args=("$@")
NUMBER_OF_ARGS="$#"

if [ -z "$APPDIR" ] ; then
  # Find the AppDir. It is the directory that contains AppRun.
  # This assumes that this script resides inside the AppDir or a subdirectory.
  # If this script is run inside an AppImage, then the AppImage runtime likely has already set $APPDIR
  path="$(dirname "$(readlink -f "\${THIS}")")"
  while [[ "$path" != "" && ! -e "$path/${AppImageTarget_1.APP_RUN_ENTRYPOINT}" ]]; do
    path=\${path%/*}
  done
  APPDIR="$path"
fi

if [ -z "$APPDIR" ] ; then
  echo "ERROR: could not locate the AppDir. Ensure this script is run from within a properly structured AppImage." >&2
  exit 1
fi

export PATH="\${APPDIR}:\${APPDIR}/usr/sbin\${PATH:+:\${PATH}}"
export XDG_DATA_DIRS="\${APPDIR}/usr/share/\${XDG_DATA_DIRS:+:\${XDG_DATA_DIRS}}:/usr/share/gnome:/usr/local/share/:/usr/share/"
export LD_LIBRARY_PATH="\${APPDIR}/usr/lib\${LD_LIBRARY_PATH:+:\${LD_LIBRARY_PATH}}"
export GSETTINGS_SCHEMA_DIR="\${APPDIR}/usr/share/glib-2.0/schemas\${GSETTINGS_SCHEMA_DIR:+:\${GSETTINGS_SCHEMA_DIR}}"

BIN="$APPDIR/${config.ExecutableName}"

if [ -z "$APPIMAGE_EXIT_AFTER_INSTALL" ] ; then
  trap atexit EXIT
fi

isEulaAccepted=1

HAVE_NO_SANDBOX=0
for arg in "\${args[@]}" ; do
  if [ "$arg" = --no-sandbox ] ; then
    HAVE_NO_SANDBOX=1
    break
  fi
done
NO_SANDBOX=()
# Use 'unshare -Ur true' as a heuristic to detect whether user namespaces are available.
# Notes:
#   - When running as root, this check will always succeed even if the sandbox configuration
#     actually relies on unprivileged user namespaces. In practice, Chrome/Electron usually
#     disables or adjusts the sandbox separately when running as root, so this probe is mostly
#     a no-op in that scenario.
#   - On minimal systems (e.g. Alpine or stripped-down containers) 'unshare' may not exist.
#     In that case the shell will return exit code 127 ("command not found"), which will cause
#     us to add '--no-sandbox'. This is an intentional fail-safe: we prefer the app to start
#     without sandboxing rather than crash on startup.
if [ $HAVE_NO_SANDBOX -eq 0 ] && ! unshare -Ur true 2>/dev/null ; then
  NO_SANDBOX=(--no-sandbox)
fi

atexit()
{
  if [ $isEulaAccepted == 1 ] ; then
    if [ $NUMBER_OF_ARGS -eq 0 ] ; then
      exec "$BIN" "\${NO_SANDBOX[@]}"
    else
      exec "$BIN" "\${NO_SANDBOX[@]}" "\${args[@]}"
    fi
  fi
}

error()
{
  if [ -x /usr/bin/zenity ] ; then
    LD_LIBRARY_PATH="" zenity --error --text "\${1}" 2>/dev/null
  elif [ -x /usr/bin/kdialog ] ; then
    LD_LIBRARY_PATH="" kdialog --msgbox "\${1}" 2>/dev/null
  elif [ -x /usr/bin/Xdialog ] ; then
    LD_LIBRARY_PATH="" Xdialog --msgbox "\${1}" 2>/dev/null
  else
    echo "\${1}"
  fi
  exit 1
}

yesno()
{
  TITLE=$1
  TEXT=$2
  if [ -x /usr/bin/zenity ] ; then
    LD_LIBRARY_PATH="" zenity --question --title="$TITLE" --text="$TEXT" 2>/dev/null || exit 0
  elif [ -x /usr/bin/kdialog ] ; then
    LD_LIBRARY_PATH="" kdialog --title "$TITLE" --yesno "$TEXT" || exit 0
  elif [ -x /usr/bin/Xdialog ] ; then
    LD_LIBRARY_PATH="" Xdialog --title "$TITLE" --clear --yesno "$TEXT" 10 80 || exit 0
  else
    echo "zenity, kdialog, Xdialog missing. Skipping \${THIS}."
    exit 0
  fi
}

check_dep()
{
  DEP=$1
  if ! command -v "$DEP" &>/dev/null ; then
    echo "$DEP is missing. Skipping \${THIS}."
    exit 0
  fi
}

if [ -z "$APPIMAGE" ] ; then
  APPIMAGE="$APPDIR/${AppImageTarget_1.APP_RUN_ENTRYPOINT}"
  # not running from within an AppImage; hence using the AppRun for Exec=
fi

${eulaEnabled
        ? `if [ -z "$APPIMAGE_SILENT_INSTALL" ] ; then
  EULA_MARK_DIR="\${XDG_CONFIG_HOME:-$HOME/.config}/${config.ProductFilename}"
  EULA_MARK_FILE="$EULA_MARK_DIR/eulaAccepted"
  # show EULA only if desktop file doesn't exist
  if [ ! -e "$EULA_MARK_FILE" ] ; then
    if [ -x /usr/bin/zenity ] ; then
      # on cancel simply exits and our trap handler launches app, so, $isEulaAccepted is set here to 0 and then to 1 if EULA accepted
      isEulaAccepted=0
      LD_LIBRARY_PATH="" zenity --text-info --title=${escapeShellString(config.ProductName)} --filename="$APPDIR/${config.EulaFile}" --ok-label=Agree --cancel-label=Disagree ${config.IsHtmlEula ? "--html" : ""}
    elif [ -x /usr/bin/Xdialog ] ; then
      isEulaAccepted=0
      LD_LIBRARY_PATH="" Xdialog --title ${escapeShellString(config.ProductName)} --textbox "$APPDIR/${config.EulaFile}" 30 80 --ok-label Agree --cancel-label Disagree
    elif [ -x /usr/bin/kdialog ] ; then
      # cannot find any option to force Agree/Disagree buttons for kdialog. And official example exactly with OK button https://techbase.kde.org/Development/Tutorials/Shell_Scripting_with_KDE_Dialogs#Example_21._--textbox_dialog_box
      # in any case we pass labels text
      isEulaAccepted=0
      LD_LIBRARY_PATH="" kdialog --textbox "$APPDIR/${config.EulaFile}" --yes-label Agree --cancel-label "Disagree"
    fi

    case $? in
      0)
          isEulaAccepted=1
          echo "License accepted"
          mkdir -p "$EULA_MARK_DIR"
          touch "$EULA_MARK_FILE"
      ;;
        1)
          echo "License not accepted"
          exit 0
      ;;
        -1)
          echo "An unexpected error has occurred."
          isEulaAccepted=1
      ;;
    esac
  fi
fi`
        : ""}
`;
}
//# sourceMappingURL=appImageUtil.js.map