#!/usr/bin/bash
#
# Copyright (C) 2026  fehlix <fehlix@mxlinux.org>
#                     MX Linux Development Team <https://mxlinux.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
# shellcheck disable=SC2034
# FLAG_*/CMD_MAN/ENV_* and other placeholders are read only via
# eval_gettext's envsubst substitution, which shellcheck can't trace.
#
# Public entry point (usr/bin/); verify-iso-sig-gui.sh lives in
# usr/lib/verify-iso-sig/. Also dispatches: hands off to the GUI or
# handles the request itself.
#
# Dispatch rules, in order:
#   1. Bare -h/--help (no --cli/--gui) -> print a short preamble, then main --help.
#   2. --manage-keys (+ --cli/--gui prefix) -> trusted-keys manager GUI if a
#      display is available, else the equivalent CLI flags.
#   3. Leading --cli/--gui forces that mode for the rest of argv.
#   4. Args matching the picker GUI's own [--debug] [iso-file] [sig-file]
#      syntax + a display available -> picker GUI.
#   5. Everything else -> main() below.
set -euo pipefail

VERSION="2026.09.01"

# Literal flag/command/env-var names used inside translatable messages -
# translators see a movable ${PLACEHOLDER}, never literal English text.
FLAG_HELP="--help"
FLAG_MAN="--man"
FLAG_TRUST_KEY="--trust-key"
FLAG_ALLOW_UNKNOWN="--allow-unrecognized-key"
FLAG_CHECKSUM_FILE="--checksum-file"
FLAG_CHECKSUM_ALGO="--checksum-algo"
FLAG_KEEP_KEY="--keep-key"
FLAG_SHOW_KEY_DETAILS="--show-key-details"
FLAG_LIST_KNOWN_KEYS="--list-known-keys"
FLAG_GUI="--gui"
FLAG_CLI="--cli"
FLAG_MANAGE_KEYS="--manage-keys"
FLAG_DND="--drag-and-drop"
CMD_MAN="verify-iso-sig --man"
ENV_DISPLAY='$DISPLAY'
ENV_WAYLAND_DISPLAY='$WAYLAND_DISPLAY'

# SELF: this file's own real path (for the checksum-fallback subprocess
# self-re-exec). SCRIPT_DIR: usr/lib/verify-iso-sig/ in a real install,
# or a flat dev-checkout layout otherwise.
case "${BASH_SOURCE[0]}" in
    /*) INVOKED_PATH=${BASH_SOURCE[0]} ;;
    *)  INVOKED_PATH="$PWD/${BASH_SOURCE[0]}" ;;
esac
SELF=$(readlink -f -- "$INVOKED_PATH" 2>/dev/null) || SELF=$INVOKED_PATH
INVOKED_DIR=$(dirname "$INVOKED_PATH")
RESOLVED_DIR=$(readlink -f -- "$INVOKED_DIR" 2>/dev/null) || RESOLVED_DIR=$INVOKED_DIR
if [ "$(basename "$RESOLVED_DIR")" = "bin" ] && [ "$(basename "$(dirname "$RESOLVED_DIR")")" = "usr" ]; then
    USR_DIR=$(dirname "$RESOLVED_DIR")
    SCRIPT_DIR="$USR_DIR/lib/verify-iso-sig"
else
    SCRIPT_DIR=$(dirname "$SELF")
fi
GUI="$SCRIPT_DIR/verify-iso-sig-gui.sh"

# Don't clobber DISPLAY_NAME if verify-iso-sig-gui.sh already set it
# before sourcing this file.
DISPLAY_NAME=${DISPLAY_NAME:-$(basename "$SELF")}

# 0 = running directly as the CLI, die()/exit really exit. 1 = a GUI
# script sourced this file to call functions in-process - die() must
# return instead of exit, or it would kill the whole GUI process.
LIB_MODE=${LIB_MODE:-0}

SESSION_TMPDIR=${SESSION_TMPDIR:-$(mktemp -d "/tmp/verify-iso-sig-$UID.session.XXXXXXXXXX")}
# Safety net for an external kill mid-run (normal completion is handled
# by cleanup()'s own RETURN trap) - also stops any dirmngr/gpg-agent
# left running against a homedir under here.
session_tmpdir_cleanup() {
    local d
    for d in "$SESSION_TMPDIR"/gnupghome.*/; do
        [ -d "$d" ] && gpgconf --homedir "${d%/}" --kill all >/dev/null 2>&1
    done
    rm -rf "$SESSION_TMPDIR"
}
trap session_tmpdir_cleanup EXIT

. /usr/bin/gettext.sh
export TEXTDOMAIN="verify-iso-sig"
export TEXTDOMAINDIR="$SCRIPT_DIR/locale"

# ============================================================
# Configuration - add a new signing key, keyserver, or checksum-listing
# naming convention here.
# ============================================================

# Known MX/antiX ISO signing key fingerprints (fingerprint|label), sourced
# from https://mxlinux.org/wiki/system/signed-iso-files/. One row per
# PRIMARY key - a valid signing subkey of a listed primary is recognized
# automatically (see known_key_label_or_primary()), no separate row needed.
KNOWN_KEYS=(
    "F62EDEAA3AE70A9C99DAC4189B68A1E8B9B6375C|MX Xfce / Xfce-AHS - Dolphin Oracle <dolphinoracle@gmail.com>"
    "F27753A18E92E3937E6335E770938C780679EE98|MX KDE / monthly snapshots - Adrian <adrian@mxlinux.org>"
    "09DA59435EF8C739C8ED615613C74A22892C32F1|MX KDE (older) - Steven K Pusser <stevopusser@gmail.com>"
    "4EB6BDCFC6CA16AE8C3471C2409C71B3BCFDED0A|MX Fluxbox - Michael Pavletich <m_pav10-4@thepavs.net.nz>"
    "30AA418A0C723D937B50A986A80582E000067FDD|antiX - anticapitalista <anticapitalista@riseup.net>"
)

# Third-party MX/antiX-based respins, not on the official wiki page.
KNOWN_THIRD_PARTY_KEYS=(
    "81280128CAC09F2BBADB4A775DBC090C710C87B3|AV Linux - Glen MacArthur <info@bandshed.net>"
)
KNOWN_KEYS+=("${KNOWN_THIRD_PARTY_KEYS[@]}")

# Keyservers tried in order when a key needs fetching. Overridden by $KEYSERVER.
DEFAULT_KEYSERVERS=(
    hkps://keys.openpgp.org
    hkps://keyserver.ubuntu.com
    hkps://pgpkeys.eu
)

# Known checksum-listing filenames (fingerprint|hashcmd), strongest hash first.
CHECKSUM_FILES=(
    "SHA512SUMS|sha512sum"
    "sha512sum.txt|sha512sum"
    "SHA256SUMS|sha256sum"
    "sha256sum.txt|sha256sum"
    "SHA1SUMS|sha1sum"
    "sha1sum.txt|sha1sum"
    "MD5SUMS|md5sum"
    "md5sum.txt|md5sum"
)
# Filename patterns with no fixed hashcmd (e.g. Fedora's
# "<spin>-<version>-<arch>-CHECKSUM") - hashcmd resolved per-line instead
# (find_checksum_line()). Matched lowercase - keep entries lowercase.
CHECKSUM_FILE_GLOBS=("*checksum" "*checksums")
# Signature suffixes tried per checksum file.
CHECKSUM_SIG_EXTS=(sig asc gpg sign)

# Unsigned, per-ISO checksum suffixes some distros ship alongside a real
# direct signature (e.g. <iso>.sig plus a redundant <iso>.sha512).
PLAIN_CHECKSUM_SUFFIXES=(sha512 sha256 sha1 md5)

# Broken/not-reliable algorithms - still usable, but flagged.
WEAK_HASHCMDS=(md5sum sha1sum)
hashcmd_is_weak() {
    local h
    for h in "${WEAK_HASHCMDS[@]}"; do
        [ "$1" = "$h" ] && return 0
    done
    return 1
}

warn_if_weak_hashcmd() {
    hashcmd_is_weak "$1" || return 0
    local algo=$2
    warn "$(eval_gettext "\${algo} is a broken checksum algorithm - a matching hash doesn't prove the ISO is genuine. Prefer a real signature or a stronger checksum (SHA256/SHA512) when available.")"
    status_out "WEAK_CHECKSUM_ALGO $algo"
}

# BSD/tag-style algorithm name -> hash command, e.g. Fedora's
# "SHA256 (name) = hash" convention.
BSD_TAG_TO_HASHCMD=(
    "md5|md5sum"
    "sha1|sha1sum"
    "sha224|sha224sum"
    "sha256|sha256sum"
    "sha384|sha384sum"
    "sha512|sha512sum"
    "blake2b|b2sum"
)

# Hex-digest length (hash_len|hashcmd) - used by find_checksum_line() as
# a fallback when a line carries no algorithm name at all (GNU-style,
# e.g. "<hash>  <name>") or an unrecognized one (a BSD-tag name not in
# $BSD_TAG_TO_HASHCMD above). Add a line here for a new algorithm once a
# real checksum listing actually uses it untagged.
HASHCMD_BY_LENGTH=(
    "32|md5sum"
    "40|sha1sum"
    "56|sha224sum"
    "64|sha256sum"
    "96|sha384sum"
    "128|sha512sum"
)

# Short usage summary; the full reference lives in man-page.md/--man.
usage() {
    # $1: exit code (default 1). $2: optional reason, printed before the usage block.
    if [ -n "${2:-}" ]; then
        printf '%s %s\n\n' "$ERROR_PREFIX" "$2" >&2
    fi
    USAGE_LABEL=$(gettext "Usage:")
    # TRANSLATORS: keep the distro names (Debian, Ubuntu, Linux Mint) exactly as-is.
    DESCRIPTION=$(gettext "Verify the GPG signature of an ISO image (any distro - built-in recognition of the documented MX Linux/antiX signing keys, plus a checksum-listing convention for distros that sign a checksum listing instead of the ISO directly, e.g. Debian, Ubuntu, Linux Mint).")
    OPT_HELP=$(gettext "this help")
    OPT_VERSION=$(gettext "show version and exit")
    OPT_MAN=$(gettext "full manual: every option, recognition rules, the checksum-listing convention, and more")
    OPT_ALLOW_UNKNOWN=$(gettext "verify once even if the key isn't recognized")
    # TRANSLATORS: ${FLAG_ALLOW_UNKNOWN} is a literal flag name - keep the placeholder as-is.
    OPT_TRUST_KEY=$(eval_gettext "like \${FLAG_ALLOW_UNKNOWN}, and remembers the key too")
    OPT_LIST_KNOWN_KEYS=$(gettext "list this tool's built-in recognized signing keys")
    OPT_LIST_TRUSTED=$(gettext "list every key saved in trustedkeys.gpg")
    OPT_GUI_NOTE=$(gettext "Run with no arguments, or via a file manager's \"Open With\" (up to 3 files), to open a picker window instead.")
    # TRANSLATORS: ${FLAG_MAN} is a literal flag name - keep the placeholder as-is.
    OPT_SEE_MAN=$(eval_gettext "For every other option (scripting/automation flags) see \${FLAG_MAN}.")
    cat <<EOF
$USAGE_LABEL $DISPLAY_NAME [options] <iso-file> [sig-file] [checksum-file]

$DESCRIPTION

$OPT_GUI_NOTE

  -h, --help                $OPT_HELP
  -V, --version             $OPT_VERSION
  --man                     $OPT_MAN
  --allow-unrecognized-key  $OPT_ALLOW_UNKNOWN
  --trust-key               $OPT_TRUST_KEY
  --list-known-keys         $OPT_LIST_KNOWN_KEYS
  --list-trusted-keys       $OPT_LIST_TRUSTED

$OPT_SEE_MAN
EOF
    exit "${1:-1}"
}

# Shows the man page: a real installed page if present, else regenerates
# verify-iso-sig.1 from man-page.md via pandoc (dev-checkout fallback).
show_man() {
    local script_dir man_src man_out
    if man -w verify-iso-sig >/dev/null 2>&1; then
        # exec replaces this process outright, so the EXIT trap never
        # fires - clean up the (always-empty at this point) SESSION_TMPDIR
        # by hand first, or it leaks.
        session_tmpdir_cleanup
        exec man verify-iso-sig
    fi
    script_dir=$SCRIPT_DIR
    man_src="$script_dir/man-page.md"
    man_out="$script_dir/verify-iso-sig.1"
    if command -v pandoc >/dev/null 2>&1; then
        if [ ! -e "$man_out" ] || [ "$man_src" -nt "$man_out" ]; then
            pandoc -f markdown-smart -s -t man "$man_src" -o "$man_out" \
                || die "$(gettext "could not generate the man page (pandoc failed)")"
        fi
    elif [ ! -e "$man_out" ]; then
        # TRANSLATORS: ${man_src} is a file path, ${FLAG_MAN} a literal flag name.
        die "$(eval_gettext "man page not built yet and pandoc isn't installed - see \${man_src} directly, or install pandoc and re-run \${FLAG_MAN}")"
    fi
    man -l "$man_out"
}

debug_cmd() { [ "$DEBUG" -eq 1 ] && printf 'debug: %s\n' "$(printf '%q ' "$@")" >&2; true; }
WARNING_PREFIX=$(gettext "warning:")
INFO_PREFIX=$(gettext "info:")
ERROR_PREFIX=$(gettext "error:")
log() { [ "$QUIET" -eq 1 ] || printf '[*] %s\n' "$*" >&2; }
warn() { printf '%s %s\n' "$WARNING_PREFIX" "$*" >&2; }
# For a fact worth noting that isn't actually a problem - "warning:"
# implies something's wrong, which isn't true for every notable fact
# (e.g. an expired-but-still-good signature).
info() { printf '%s %s\n' "$INFO_PREFIX" "$*" >&2; }
# Exits directly (CLI), or returns 1 (LIB_MODE=1, a sourced GUI caller).
die() {
    printf '%s %s\n' "$ERROR_PREFIX" "$*" >&2
    [ "$LIB_MODE" -eq 1 ] && return 1
    exit 1
}

# Shared "--flag requires a value" error. $1: the flag's literal name.
require_value() {
    local flag=$1
    # TRANSLATORS: ${flag}/${FLAG_MAN} are literal names - keep placeholders as-is.
    die "$(eval_gettext "\${flag} requires a value (see \${FLAG_MAN})")"
}

# Emits a machine-readable tag on $STATUS_FD (opt-in via --status-fd); a no-op otherwise.
status_out() {
    [ -n "$STATUS_FD" ] || return 0
    printf '[VERIFY-ISO-SIG:] %s\n' "$*" >&"$STATUS_FD"
}

# Runs gpgv with --status-fd 3. Sets: rc, GPGV_OUTPUT (human-readable
# stderr), STATUS_LINES (machine-readable status lines).
run_gpgv_status() {
    local status_file="$GNUPG_HOMEDIR/status-fd.$$"
    set +e
    GPGV_OUTPUT=$(gpgv --status-fd 3 "$@" 2>&1 3>"$status_file")
    rc=$?
    set -e
    STATUS_LINES=$(cat "$status_file" 2>/dev/null)
    rm -f "$status_file"
}

# Echoes field(s) $2 of the first "[GNUPG:] $1 ..." line in $STATUS_LINES.
status_field() {
    printf '%s\n' "$STATUS_LINES" | grep -m1 "^\[GNUPG:\] $1 " | cut -d' ' -f"$2" || true
}

# Undoes gpg's own "%" -> "%25" escaping in a status-fd UID field.
percent_decode_uid() {
    printf '%s' "${1//%25/%}"
}

TRUSTED_GPG="$HOME/.gnupg/trustedkeys.gpg"
PUBRING_KBX="$HOME/.gnupg/pubring.kbx"
# Overridable only for tests; not a documented setting.
MX_GPG_KEYRING=${MX_GPG_KEYRING:-/usr/share/mx-gpg-keys/mx-gpg-keyring}
UNTRUSTED_KEYS_FILE="$HOME/.gnupg/verify-iso-sig-untrusted-keys"

# Sets every global the mode functions below depend on. Called from
# main() for direct CLI execution; a library caller (LIB_MODE=1) must
# call this itself, once, right after sourcing.
lib_init_defaults() {
    QUIET=0
    DEBUG=0
    KEEP=0
    ALLOW_UNKNOWN=0
    TRUST_KEY=0
    KEEP_KEY=0
    IS_CACHED=0
    LIST_TRUSTED=0
    LIST_TRUSTED_FPRS=()
    REFRESH_TRUSTED_KEYS=0
    LIST_KNOWN_KEYS=0
    LIST_KEYSERVERS=0
    KEYSERVER_OPT=""
    UNTRUST_KEY=""
    VERIFY_AS_CHECKSUM_FILE=0
    VERIFY_AS_CLEARSIGNED_CHECKSUM_FILE=0
    EXTRACT_PLAINTEXT_TO=""
    NO_CHECKSUM_FALLBACK=0
    CHECKSUM_FILE_OVERRIDE=""
    CHECKSUM_ALGO_OVERRIDE=""
    EXPORT_KEY_TO=""
    FROM_RING_OVERRIDE=""
    EXPORT_TRUSTED_KEYS=""
    EXPORT_TRUSTED_KEYS_FPRS=()
    SHOW_KEY_DETAILS=0
    SHOW_KEY_DETAILS_FPRS=()
    INSPECT_KEY_FILE=""
    IMPORT_TRUSTED_KEYS=""
    IMPORT_TRUSTED_KEYS_FPRS=()
    STATUS_FD=""
}

# ============================================================
# Dispatch layer - decides whether an invocation is handled here or
# handed off to verify-iso-sig-gui.sh.
# ============================================================

# True if a graphical session (X11 or Wayland) is available.
have_gui_session() {
    [ -n "${DISPLAY:-}" ] || [ -n "${WAYLAND_DISPLAY:-}" ]
}

# True if "$@" matches the GUI picker's own [--debug] [iso-file]
# [sig-file] syntax. -h/--help/-V/--version always go to main() instead.
# A leading --debug is stripped first, then the same "0/1/2 bare files"
# check applies uniformly - --debug must not change the expected
# positional-arg count, or it silently drops to CLI mode.
looks_like_gui_args() {
    if [ "$#" -ge 1 ] && [ "$1" = "--debug" ]; then
        shift
    fi
    case "$#" in
        0) return 0 ;;
        1)
            case "$1" in
                -h|--help|-V|--version) return 1 ;;
                -*) return 1 ;;
                *) return 0 ;;
            esac
            ;;
        2)
            case "$1" in -*) return 1 ;; esac
            case "$2" in -*) return 1 ;; esac
            return 0
            ;;
        3)
            case "$1" in -*) return 1 ;; esac
            case "$2" in -*) return 1 ;; esac
            case "$3" in -*) return 1 ;; esac
            return 0
            ;;
        *) return 1 ;;
    esac
}

# Respects a --cli/--gui prefix (set via $FORCE_MODE by the dispatch
# gate at the bottom of this file); falls back to have_gui_session otherwise.
want_gui() {
    case "${FORCE_MODE:-}" in
        cli) return 1 ;;
        gui) return 0 ;;
        *) have_gui_session ;;
    esac
}

# Only reached for --manage-keys with no graphical session (real or
# forced via --cli) - prints the equivalent CLI flags instead.
print_manage_keys_cli_usage() {
    if [ "${FORCE_MODE:-}" = "cli" ]; then
        # --cli was requested explicitly - the GUI not opening is exactly
        # what was asked for, no explanation needed at all.
        MK_LEAD=""
        MK_INTRO=$(gettext "Manage trusted keys from the command line:")
    else
        # TRANSLATORS: ${ENV_DISPLAY}/${ENV_WAYLAND_DISPLAY} are literal env-var names - keep placeholders as-is.
        MK_REASON=$(eval_gettext "no graphical session detected (neither \${ENV_DISPLAY} nor \${ENV_WAYLAND_DISPLAY} is set) - the trusted-keys manager needs a desktop to show its window.")
        printf -v MK_LEAD '%s %s\n\n' "$ERROR_PREFIX" "$MK_REASON"
        MK_INTRO=$(gettext "Manage trusted keys from the command line instead:")
    fi
    MK_LIST_TRUSTED=$(gettext "list key(s) saved in trustedkeys.gpg (all if none given)")
    MK_REFRESH_TRUSTED_KEYS=$(gettext "check every trusted key against pubring.kbx, mx-gpg-keyring, and a keyserver for updates (renewed expiry, new subkeys, or a revocation)")
    MK_SHOW_KEY_DETAILS=$(gettext "show fingerprint/algorithm/dates/subkeys for trusted key(s)")
    MK_UNTRUST_KEY=$(gettext "remove <fpr> from trustedkeys.gpg")
    MK_EXPORT_TRUSTED_KEYS=$(gettext "export trusted key(s) to <path> (all if none given)")
    MK_INSPECT_KEY_FILE=$(gettext "list the key(s) in an external key file, read-only")
    MK_IMPORT_TRUSTED_KEYS=$(gettext "import key(s) from an external key file (all if none given)")
    # TRANSLATORS: ${CMD_MAN} is a literal command - keep the placeholder as-is.
    MK_SEE_MAN=$(eval_gettext "See '\${CMD_MAN}' for the full manual.")
    cat >&2 <<EOF
${MK_LEAD}$MK_INTRO
  verify-iso-sig --list-trusted-keys [fpr...]             $MK_LIST_TRUSTED
  verify-iso-sig --refresh-trusted-keys                    $MK_REFRESH_TRUSTED_KEYS
  verify-iso-sig --show-key-details <fpr> [fpr...]        $MK_SHOW_KEY_DETAILS
  verify-iso-sig --untrust-key=<fpr>                      $MK_UNTRUST_KEY
  verify-iso-sig --export-trusted-keys=<path> [fpr...]    $MK_EXPORT_TRUSTED_KEYS
  verify-iso-sig --inspect-key-file=<path>                $MK_INSPECT_KEY_FILE
  verify-iso-sig --import-trusted-keys=<path> [fpr...]    $MK_IMPORT_TRUSTED_KEYS

$MK_SEE_MAN
EOF
}

# Shown before main()'s own --help, only for a plain unforced -h/--help -
# covers this dispatch layer's own extra flags/default behavior.
print_dispatcher_help_preamble() {
    # TRANSLATORS: ${FLAG_MAN} is a literal flag name - keep the placeholder as-is.
    PRE_INTRO=$(eval_gettext "verify-iso-sig - opens a window to pick and check an ISO or signature file by default when a graphical session is available; falls back to the plain command-line tool otherwise. This preamble covers this launcher's own behavior; a short command-line reference (used automatically whenever given arguments don't look like that window's own syntax, or no graphical session is available) follows below it - see \${FLAG_MAN} for the complete one.")
    PRE_NOARGS=$(gettext "open that window if a display is available (default), otherwise this same help")
    PRE_GUI=$(gettext "(default) the graphical main program")
    PRE_CLI=$(gettext "plain command-line mode")
    # TRANSLATORS: ${FLAG_HELP} is a literal flag name - keep the placeholder as-is.
    PRE_MANAGE_KEYS=$(eval_gettext "trusted-keys manager GUI (add \${FLAG_HELP} for its own flags), or with no display, print the CLI flags that cover the same ground")
    # TRANSLATORS: ${FLAG_GUI} is a literal flag name - keep the placeholder as-is.
    PRE_DND=$(eval_gettext "same as \${FLAG_GUI}, but also shows the drag-and-drop pane (most users won't need this - reachable from the app menu's own \"Drag & Drop\" entry)")
    cat <<EOF
$PRE_INTRO

  (no arguments)            $PRE_NOARGS
  $FLAG_GUI [file]              $PRE_GUI
  $FLAG_CLI ...                 $PRE_CLI
  $FLAG_MANAGE_KEYS             $PRE_MANAGE_KEYS
  $FLAG_DND           $PRE_DND

EOF
}

# True if $1's basename is a known checksum-listing name: a literal
# $CHECKSUM_FILES entry, or a $CHECKSUM_FILE_GLOBS pattern match (case-
# insensitive, e.g. Fedora's "<spin>-<version>-<arch>-CHECKSUM").
looks_like_checksum_listing_name() {
    local base entry glob
    base=$(basename "$1")
    for entry in "${CHECKSUM_FILES[@]}"; do
        [ "$base" = "${entry%%|*}" ] && return 0
    done
    for glob in "${CHECKSUM_FILE_GLOBS[@]}"; do
        # shellcheck disable=SC2254
        # Unquoted on purpose: $glob must expand as a glob pattern here
        # (e.g. "*checksum"), not match literally. ${base,,}: $glob is
        # already lowercase, matched case-insensitively this way.
        case "${base,,}" in $glob) return 0 ;; esac
    done
    return 1
}

# True if $1's first line is a clearsign header (RFC 4880 SS7) - the
# file is itself an OpenPGP-signed message, needing no separate detached
# signature. "head -c 40", not "read -r": the latter scans the whole
# file when it has no newline, which a large binary file (e.g. an ISO)
# passed here by mistake could easily be.
is_clearsigned_file() {
    local f=$1 head_bytes
    [ -r "$f" ] || return 1
    # tr -d strips a NUL byte before "$(...)" - bash itself warns "NUL
    # byte ignored" otherwise, likely for real binary data here.
    head_bytes=$(head -c 40 "$f" 2>/dev/null | tr -d '\0')
    case "$head_bytes" in
        "-----BEGIN PGP SIGNED MESSAGE-----"*) return 0 ;;
        *) return 1 ;;
    esac
}

# True if ISO path $1 has a "<iso>.<suffix>.<sigext>" clearsigned
# per-ISO checksum next to it (e.g. "<iso>.sha512.asc"). Checked ahead
# of a direct ISO signature during single-argument auto-discovery. Sets
# CLEARSIGNED_PLAIN_CHECKSUM to the matched path (strongest hash first).
find_clearsigned_plain_checksum() {
    local iso=$1 suffix sigext candidate
    for suffix in "${PLAIN_CHECKSUM_SUFFIXES[@]}"; do
        for sigext in "${CHECKSUM_SIG_EXTS[@]}"; do
            candidate="${iso}.${suffix}.${sigext}"
            if [ -r "$candidate" ] && is_clearsigned_file "$candidate"; then
                CLEARSIGNED_PLAIN_CHECKSUM=$candidate
                return 0
            fi
        done
    done
    return 1
}

# True if $1's content is confidently something OTHER than a signature,
# via "file -b" alone - no gpg parse. Ambiguous cases ("data", or any
# "-----BEGIN PGP..." header) are left as false rather than guessed.
looks_obviously_not_a_signature() {
    local f=$1 head_bytes type
    [ -r "$f" ] || return 1
    head_bytes=$(head -c 40 "$f" 2>/dev/null | tr -d '\0')
    case "$head_bytes" in
        "-----BEGIN PGP"*) return 1 ;;
    esac
    type=$(file -b "$f" 2>/dev/null) || return 1
    [ "$type" != "data" ]
}

# True if $1 parses as OpenPGP data whose first packet is a signature
# packet (tag 2), regardless of its name or wrapper (binary, --armor,
# or --enarmor). No keyring/network needed. Content-based fallback only
# - used when a file's extension isn't a recognized signature suffix.
is_detached_signature_file() {
    local f=$1 head_bytes first_packet size
    [ -r "$f" ] || return 1
    head_bytes=$(head -c 40 "$f" 2>/dev/null | tr -d '\0')
    case "$head_bytes" in
        "-----BEGIN PGP SIGNATURE-----"*) return 0 ;;
    esac
    # A real signature is always tiny - skip the gpg parse for anything
    # over 1 MiB (e.g. an ISO with an unrecognized extension).
    size=$(stat -c%s "$f" 2>/dev/null) || return 1
    [ "$size" -le 1048576 ] || return 1
    debug_cmd gpg --list-packets "$f"
    first_packet=$(gpg --list-packets "$f" 2>/dev/null | grep -m1 -o ':[a-z ]*packet[a-z ]*:') || true
    [ "$first_packet" = ":signature packet:" ]
}

# Classifies a checksum-listing filename (one recognized by
# looks_like_checksum_listing_name()) paired with an ISO, order-
# independent. Sets ISO, CHECKSUM_FILE_OVERRIDE, and CHECKSUM_SIG_PREVIEW
# (best-effort guess at the listing's own detached signature, display-
# only, left empty for an inline-signed listing - it has none). Returns 1
# if neither argument is a known listing name.
classify_checksum_listing_pair() {
    local a=$1 b=$2 a_is_listing=0 b_is_listing=0 sigext
    looks_like_checksum_listing_name "$a" && a_is_listing=1
    looks_like_checksum_listing_name "$b" && b_is_listing=1
    if [ "$a_is_listing" -eq 1 ] && [ "$b_is_listing" -eq 0 ]; then
        CHECKSUM_FILE_OVERRIDE=$a
        ISO=$b
        SIG=""
    elif [ "$b_is_listing" -eq 1 ] && [ "$a_is_listing" -eq 0 ]; then
        CHECKSUM_FILE_OVERRIDE=$b
        ISO=$a
        SIG=""
    else
        return 1
    fi
    CHECKSUM_SIG_PREVIEW=""
    for sigext in "${CHECKSUM_SIG_EXTS[@]}"; do
        if [ -r "${CHECKSUM_FILE_OVERRIDE}.${sigext}" ]; then
            CHECKSUM_SIG_PREVIEW="${CHECKSUM_FILE_OVERRIDE}.${sigext}"
            break
        fi
    done
    return 0
}

# Classifies a checksum-listing's own detached signature file (e.g.
# "SHA256SUMS.gpg") paired with the ISO it indirectly (via the listing
# it signs) describes. Sets ISO, CHECKSUM_FILE_OVERRIDE (the listing's
# expected path, not the signature file itself), and CHECKSUM_SIG_PREVIEW
# (the matched signature argument). Returns 1 if neither argument matches
# a known listing name + one of $CHECKSUM_SIG_EXTS.
classify_checksum_signature_pair() {
    local a=$1 b=$2 a_listing="" b_listing="" entry sigext base
    for entry in "${CHECKSUM_FILES[@]}"; do
        base=${entry%%|*}
        for sigext in "${CHECKSUM_SIG_EXTS[@]}"; do
            [ "$(basename "$a")" = "${base}.${sigext}" ] && a_listing=$base
            [ "$(basename "$b")" = "${base}.${sigext}" ] && b_listing=$base
        done
    done
    if [ -n "$a_listing" ] && [ -z "$b_listing" ]; then
        CHECKSUM_FILE_OVERRIDE="$(dirname "$a")/$a_listing"
        CHECKSUM_SIG_PREVIEW=$a
        ISO=$b
        SIG=""
        return 0
    elif [ -n "$b_listing" ] && [ -z "$a_listing" ]; then
        CHECKSUM_FILE_OVERRIDE="$(dirname "$b")/$b_listing"
        CHECKSUM_SIG_PREVIEW=$b
        ISO=$a
        SIG=""
        return 0
    fi
    return 1
}

# Classifies a "<iso-basename>.<suffix>.<sigext>" argument paired with
# the ISO it describes - either a detached signature over a separate
# "<iso>.<suffix>" sibling (openSUSE), or a clearsigned checksum in its
# own right (e.g. "<iso>.sha512.asc", no sibling). Sets ISO, CHECKSUM_
# FILE_OVERRIDE, CHECKSUM_ALGO_OVERRIDE, and CHECKSUM_SIG_PREVIEW (empty
# for the clearsigned case). Returns 1 if no argument matches this shape.
classify_plain_checksum_signature_pair() {
    local a=$1 b=$2 suffix sigext a_base b_base match=""
    a_base=$(basename "$a")
    b_base=$(basename "$b")
    for suffix in "${PLAIN_CHECKSUM_SUFFIXES[@]}"; do
        for sigext in "${CHECKSUM_SIG_EXTS[@]}"; do
            if [ "$a_base" = "${b_base}.${suffix}.${sigext}" ]; then
                match=$a
                ISO=$b
            elif [ "$b_base" = "${a_base}.${suffix}.${sigext}" ]; then
                match=$b
                ISO=$a
            else
                continue
            fi
            CHECKSUM_ALGO_OVERRIDE="${suffix}sum"
            SIG=""
            if is_clearsigned_file "$match"; then
                CHECKSUM_FILE_OVERRIDE=$match
                CHECKSUM_SIG_PREVIEW=""
            else
                CHECKSUM_FILE_OVERRIDE="$(dirname "$match")/$(basename "$ISO").${suffix}"
                CHECKSUM_SIG_PREVIEW=$match
            fi
            return 0
        done
    done
    return 1
}

# Classifies an unsigned per-file checksum (one of $PLAIN_CHECKSUM_
# SUFFIXES, e.g. "manjaro-....iso.sha512") paired with the ISO it
# describes. Sets ISO, and either SIG (a real direct ISO signature) or
# CHECKSUM_FILE_OVERRIDE/CHECKSUM_ALGO_OVERRIDE/CHECKSUM_SIG_PREVIEW
# (the checksum file itself, if the ISO has no direct signature).
# NO_DIRECT_SIG mirrors which of the two happened. Returns 1 if neither
# basename has a known plain-checksum suffix, or both do.
classify_plain_checksum_pair() {
    local a=$1 b=$2 a_is_plain=0 b_is_plain=0 suffix sigext checksum_file
    for suffix in "${PLAIN_CHECKSUM_SUFFIXES[@]}"; do
        case "$(basename "$a")" in *".$suffix") a_is_plain=1 ;; esac
        case "$(basename "$b")" in *".$suffix") b_is_plain=1 ;; esac
    done
    if [ "$a_is_plain" -eq 1 ] && [ "$b_is_plain" -eq 0 ]; then
        ISO=$b
        checksum_file=$a
    elif [ "$b_is_plain" -eq 1 ] && [ "$a_is_plain" -eq 0 ]; then
        ISO=$a
        checksum_file=$b
    else
        return 1
    fi
    NO_DIRECT_SIG=0
    CHECKSUM_FILE_OVERRIDE=""
    CHECKSUM_ALGO_OVERRIDE=""
    CHECKSUM_SIG_PREVIEW=""
    for sigext in sig asc gpg; do
        if [ -r "${ISO}.${sigext}" ]; then
            SIG="${ISO}.${sigext}"
            return 0
        fi
    done
    NO_DIRECT_SIG=1
    SIG="${ISO}.sig"
    CHECKSUM_FILE_OVERRIDE=$checksum_file
    for suffix in "${PLAIN_CHECKSUM_SUFFIXES[@]}"; do
        case "$(basename "$checksum_file")" in
            *".$suffix") CHECKSUM_ALGO_OVERRIDE="${suffix}sum"; break ;;
        esac
    done
    for sigext in "${CHECKSUM_SIG_EXTS[@]}"; do
        if [ -r "${checksum_file}.${sigext}" ]; then
            CHECKSUM_SIG_PREVIEW="${checksum_file}.${sigext}"
            break
        fi
    done
    return 0
}

# Classifies two explicit file paths into ISO/SIG, order-independent:
# whichever looks like a signature (.sig/.asc/.gpg/.sign) is SIG, the
# other is ISO. Falls back to content (is_detached_signature_file()),
# then to positional order (first=ISO, second=SIG), setting
# NEITHER_LOOKS_LIKE_SIGNATURE=1 so a caller can refuse instead of
# guessing. Callers should try classify_checksum_listing_pair()/
# classify_plain_checksum_pair() first.
classify_iso_sig_pair() {
    local a=$1 b=$2 a_is_sig=0 b_is_sig=0 a_named_sig=0 b_named_sig=0
    case "$a" in *.sig|*.asc|*.gpg|*.sign) a_is_sig=1; a_named_sig=1 ;; esac
    case "$b" in *.sig|*.asc|*.gpg|*.sign) b_is_sig=1; b_named_sig=1 ;; esac
    # A recognized extension is trusted unless a cheap check (no gpg
    # parse) confidently shows the content is something else entirely.
    [ "$a_is_sig" -eq 1 ] && looks_obviously_not_a_signature "$a" && a_is_sig=0
    [ "$b_is_sig" -eq 1 ] && looks_obviously_not_a_signature "$b" && b_is_sig=0
    if [ "$a_is_sig" -eq 0 ] && [ "$b_is_sig" -eq 0 ]; then
        # Only tried when the name alone didn't decide it - an ordinary
        # .sig/.asc/.gpg/.sign pair never pays this extra gpg call.
        is_detached_signature_file "$a" && a_is_sig=1
        is_detached_signature_file "$b" && b_is_sig=1
    fi
    NEITHER_LOOKS_LIKE_SIGNATURE=0
    # SIG_NAMED_BUT_NOT_SIGNATURE: which file (if any) matched by name
    # but got demoted or never confirmed by content - lets a caller say
    # "this one looks right by name, but its content isn't a real
    # signature" instead of the more generic "neither looks like a
    # signature at all", which would be misleading here.
    SIG_NAMED_BUT_NOT_SIGNATURE=""
    if [ "$a_is_sig" -eq 0 ] && [ "$b_is_sig" -eq 0 ]; then
        NEITHER_LOOKS_LIKE_SIGNATURE=1
        [ "$a_named_sig" -eq 1 ] && SIG_NAMED_BUT_NOT_SIGNATURE=$a
        [ "$b_named_sig" -eq 1 ] && SIG_NAMED_BUT_NOT_SIGNATURE=$b
    fi
    if [ "$a_is_sig" -eq 1 ] && [ "$b_is_sig" -eq 0 ]; then
        SIG=$a
        ISO=$b
    else
        ISO=$a
        SIG=$b
    fi
    if [ -r "$SIG" ] && is_clearsigned_file "$SIG"; then
        CHECKSUM_FILE_OVERRIDE=$SIG
        SIG=""
    fi
}

# Classifies three explicit file paths, order-independent, as an ISO
# plus its checksum/signature files. Checksum file recognized by a
# shared-listing name or a plain per-file suffix
# ($PLAIN_CHECKSUM_SUFFIXES); for the plain-suffix shape, prefers the
# ISO's own direct signature over the checksum file's own signature,
# same priority as classify_plain_checksum_pair(). Returns 1 unless
# exactly one file is the checksum file and exactly one of the
# remaining two is "<that name>.<sigext>".
classify_iso_checksum_signature_triple() {
    local files=("$1" "$2" "$3") i chk_i=-1 chk_n=0 sig_i=-1 sig_n=0 \
        iso_i=-1 chk_base sigext suffix plain_algo="" p q iso_base_try
    for i in 0 1 2; do
        looks_like_checksum_listing_name "${files[$i]}" && { chk_i=$i; chk_n=$((chk_n + 1)); }
    done
    if [ "$chk_n" -eq 0 ]; then
        for i in 0 1 2; do
            for suffix in "${PLAIN_CHECKSUM_SUFFIXES[@]}"; do
                case "$(basename "${files[$i]}")" in
                    *".$suffix") chk_i=$i; chk_n=$((chk_n + 1)); plain_algo="${suffix}sum" ;;
                esac
            done
        done
    fi
    [ "$chk_n" -eq 1 ] || return 1
    if [ -n "$plain_algo" ]; then
        # Try both remaining files as "the ISO" - if the other one is
        # "<that ISO>.<sigext>", it's a real direct signature (tried
        # before the checksum file's own signature, below).
        for p in 0 1 2; do
            [ "$p" -eq "$chk_i" ] && continue
            for q in 0 1 2; do
                [ "$q" -eq "$chk_i" ] || [ "$q" -eq "$p" ] && continue
                iso_base_try=$(basename "${files[$p]}")
                for sigext in sig asc gpg; do
                    if [ "$(basename "${files[$q]}")" = "${iso_base_try}.${sigext}" ]; then
                        ISO=${files[$p]}
                        SIG=${files[$q]}
                        CHECKSUM_FILE_OVERRIDE=""
                        CHECKSUM_ALGO_OVERRIDE=""
                        CHECKSUM_SIG_PREVIEW=""
                        return 0
                    fi
                done
            done
        done
    fi
    chk_base=$(basename "${files[$chk_i]}")
    for i in 0 1 2; do
        [ "$i" -eq "$chk_i" ] && continue
        for sigext in "${CHECKSUM_SIG_EXTS[@]}"; do
            [ "$(basename "${files[$i]}")" = "${chk_base}.${sigext}" ] && { sig_i=$i; sig_n=$((sig_n + 1)); }
        done
    done
    [ "$sig_n" -eq 1 ] || return 1
    for i in 0 1 2; do
        [ "$i" != "$chk_i" ] && [ "$i" != "$sig_i" ] && iso_i=$i
    done
    ISO=${files[$iso_i]}
    SIG=""
    CHECKSUM_FILE_OVERRIDE=${files[$chk_i]}
    CHECKSUM_ALGO_OVERRIDE=$plain_algo
    CHECKSUM_SIG_PREVIEW=${files[$sig_i]}
}

# True if checksum-listing file $1 names $2's exact basename - guards
# classify_pair_any_shape() against pairing a real listing with an
# unrelated third file during three-explicit-file disambiguation.
checksum_listing_names_iso() {
    local listing=$1 iso=$2
    [ -r "$listing" ] || return 1
    find_checksum_line "$listing" "$(basename "$iso")"
}

# True if $1's basename is exactly "<$2's basename>.<sigext>"
# (sig/asc/gpg/sign) - a tie-break preference signal for
# classify_iso_triple_ignoring_one(), not a hard requirement (a real
# signature file's name has no required relation to its target).
sig_names_iso() {
    local sig_base iso_base sigext
    sig_base=$(basename "$1")
    iso_base=$(basename "$2")
    for sigext in sig asc gpg sign; do
        [ "$sig_base" = "${iso_base}.${sigext}" ] && return 0
    done
    return 1
}

# True if $1's basename has a $PLAIN_CHECKSUM_SUFFIXES extension (e.g.
# "manjaro-....iso.sha512") - a real ISO is never named this way itself.
looks_like_plain_checksum_name() {
    local suffix
    for suffix in "${PLAIN_CHECKSUM_SUFFIXES[@]}"; do
        case "$(basename "$1")" in *".$suffix") return 0 ;; esac
    done
    return 1
}

# True if the global $ISO a classify_*_pair() call just produced
# plausibly is a real ISO, not itself a signature or checksum file.
iso_result_plausible() {
    case "$(basename "$ISO")" in
        *.sig|*.asc|*.gpg|*.sign) return 1 ;;
    esac
    looks_like_checksum_listing_name "$ISO" && return 1
    ! looks_like_plain_checksum_name "$ISO"
}

# Classifies two file paths via the same classify_*_pair() chain
# main()'s two-argument dispatch uses. Resets CHECKSUM_FILE_OVERRIDE/
# CHECKSUM_SIG_PREVIEW/CHECKSUM_ALGO_OVERRIDE/NO_DIRECT_SIG first, so a
# caller trying several candidate pairs never sees a stale value.
# Returns 1 unless real crypto material was actually found, not just
# "some classifier returned success" (classify_plain_checksum_pair()
# alone succeeds on a recognized suffix with nothing to verify).
classify_pair_any_shape() {
    local x=$1 y=$2
    CHECKSUM_FILE_OVERRIDE=""
    CHECKSUM_SIG_PREVIEW=""
    CHECKSUM_ALGO_OVERRIDE=""
    NO_DIRECT_SIG=0
    NEITHER_LOOKS_LIKE_SIGNATURE=0
    if classify_checksum_listing_pair "$x" "$y"; then
        :
    elif classify_checksum_signature_pair "$x" "$y"; then
        :
    elif classify_plain_checksum_signature_pair "$x" "$y"; then
        :
    elif classify_plain_checksum_pair "$x" "$y"; then
        :
    else
        classify_iso_sig_pair "$x" "$y"
        [ "$NEITHER_LOOKS_LIKE_SIGNATURE" -eq 1 ] && return 1
    fi
    iso_result_plausible || return 1
    # $SIG must be $x or $y themselves, not some other path -
    # classify_plain_checksum_pair()'s auto-discovery of
    # "${ISO}.sigext" is correct for its normal two-argument use, but
    # here any other file in the directory must never affect the
    # outcome, whatever its content. looks_obviously_not_a_signature()
    # still guards the rarer case where $x/$y aren't a real signature.
    if [ -n "$SIG" ] && { [ "$SIG" = "$x" ] || [ "$SIG" = "$y" ]; } \
       && [ -f "$SIG" ] && [ -r "$SIG" ] && ! looks_obviously_not_a_signature "$SIG"; then
        return 0
    fi
    if [ -n "$CHECKSUM_FILE_OVERRIDE" ]; then
        if [ -z "$CHECKSUM_ALGO_OVERRIDE" ]; then
            checksum_listing_names_iso "$CHECKSUM_FILE_OVERRIDE" "$ISO" || return 1
        fi
        [ -n "$CHECKSUM_SIG_PREVIEW" ] && return 0
        is_clearsigned_file "$CHECKSUM_FILE_OVERRIDE" && return 0
    fi
    return 1
}

# Fallback for classify_iso_checksum_signature_triple()'s failure: try
# treating the three given files as an ISO+SIG pair, ignoring whichever
# third file isn't needed. Tries all three 2-of-3 sub-pairs via
# classify_pair_any_shape(); succeeds only if EXACTLY ONE is meaningful.
classify_iso_triple_ignoring_one() {
    local files=("$1" "$2" "$3") i j matched=0 match_i=-1 match_j=-1 \
        strong_matched=0 strong_i=-1 strong_j=-1
    local -a midx_count=(0 0 0)
    for i in 0 1 2; do
        for j in 0 1 2; do
            [ "$j" -le "$i" ] && continue
            if classify_pair_any_shape "${files[$i]}" "${files[$j]}"; then
                matched=$((matched + 1))
                match_i=$i
                match_j=$j
                midx_count[$i]=$((midx_count[$i] + 1))
                midx_count[$j]=$((midx_count[$j] + 1))
                if [ -z "$SIG" ] || sig_names_iso "$SIG" "$ISO"; then
                    strong_matched=$((strong_matched + 1))
                    strong_i=$i
                    strong_j=$j
                fi
            fi
        done
    done
    # A signature with no naming correlation to its "ISO" candidate is
    # weaker evidence than any other match shape - prefer a lone strong
    # match over reporting a false ambiguity against a weaker one.
    if [ "$matched" -gt 1 ] && [ "$strong_matched" -eq 1 ]; then
        matched=1
        match_i=$strong_i
        match_j=$strong_j
    fi
    # TRIPLE_AMBIGUOUS: more than one pairing is independently valid -
    # distinct from none matching, so a caller can give an accurate
    # reason. With exactly two valid pairings (only 3 possible pairs
    # exist), they always share one common file (the ISO, in every
    # realistic shape) - ANCHOR/CANDIDATE1/CANDIDATE2 name it and the
    # two competing files, found via which index appears twice vs. once.
    # Left empty for the rarer all-three-valid case (caller falls back
    # to listing all three files generically).
    TRIPLE_AMBIGUOUS=0
    TRIPLE_AMBIGUOUS_ANCHOR=""
    TRIPLE_AMBIGUOUS_CANDIDATE1=""
    TRIPLE_AMBIGUOUS_CANDIDATE2=""
    if [ "$matched" -eq 2 ]; then
        TRIPLE_AMBIGUOUS=1
        for i in 0 1 2; do
            if [ "${midx_count[$i]}" -eq 2 ]; then
                TRIPLE_AMBIGUOUS_ANCHOR=${files[$i]}
            elif [ -z "$TRIPLE_AMBIGUOUS_CANDIDATE1" ]; then
                TRIPLE_AMBIGUOUS_CANDIDATE1=${files[$i]}
            else
                TRIPLE_AMBIGUOUS_CANDIDATE2=${files[$i]}
            fi
        done
    elif [ "$matched" -gt 2 ]; then
        TRIPLE_AMBIGUOUS=1
    fi
    [ "$matched" -eq 1 ] || return 1
    classify_pair_any_shape "${files[$match_i]}" "${files[$match_j]}"
}

main() {
ORIGINAL_ARGS=("$@")
lib_init_defaults
# -h/--help/-V/--version/--man always win, wherever they appear in "$@",
# checked in one pass before the value-taking-flag parsing below (else
# e.g. `--untrust-key --help` would be rejected as a missing value
# instead of recognized). Stops at a literal "--".
for arg in "$@"; do
    case "$arg" in
        --) break ;;
        -h|--help) usage 0 ;;
        -V|--version) printf '%s %s\n' "$DISPLAY_NAME" "$VERSION"; exit 0 ;;
        --man) show_man; exit 0 ;;
    esac
done
# Shift-capable loop: every value-taking option accepts both
# "--option=value" and "--option value"; the "-*" catch-all rejects any
# unrecognized flag instead of treating it as a filename.
ARGS=()
while [ "$#" -gt 0 ]; do
    case "$1" in
        --quiet) QUIET=1 ;;
        --debug) DEBUG=1 ;;
        --keep) KEEP=1 ;;
        --allow-unrecognized-key) ALLOW_UNKNOWN=1 ;;
        --trust-key) ALLOW_UNKNOWN=1; TRUST_KEY=1 ;;
        --keep-key) KEEP_KEY=1 ;;
        --show-key-details) SHOW_KEY_DETAILS=1 ;;
        --is-cached) IS_CACHED=1 ;;
        --list-trusted-keys) LIST_TRUSTED=1 ;;
        --refresh-trusted-keys) REFRESH_TRUSTED_KEYS=1 ;;
        --list-known-keys) LIST_KNOWN_KEYS=1 ;;
        --list-keyservers | --list-keyserver) LIST_KEYSERVERS=1 ;;
        --keyserver=*) KEYSERVER_OPT=${1#*=} ;;
        --keyserver)
            { [ "$#" -ge 2 ] && [[ "$2" != -* ]]; } || require_value --keyserver
            KEYSERVER_OPT=$2
            shift
            ;;
        --untrust-key=*) UNTRUST_KEY=${1#*=} ;;
        --untrust-key)
            { [ "$#" -ge 2 ] && [[ "$2" != -* ]]; } || require_value --untrust-key
            UNTRUST_KEY=$2
            shift
            ;;
        # Takes an optional trailing fingerprint list (zero = every key) -
        # once the path is captured, the rest of "$@" is that list.
        --export-trusted-keys=*)
            EXPORT_TRUSTED_KEYS=${1#*=}
            shift
            EXPORT_TRUSTED_KEYS_FPRS=("$@")
            break
            ;;
        --export-trusted-keys)
            { [ "$#" -ge 2 ] && [[ "$2" != -* ]]; } || require_value --export-trusted-keys
            EXPORT_TRUSTED_KEYS=$2
            shift 2
            EXPORT_TRUSTED_KEYS_FPRS=("$@")
            break
            ;;
        --inspect-key-file=*) INSPECT_KEY_FILE=${1#*=} ;;
        --inspect-key-file)
            { [ "$#" -ge 2 ] && [[ "$2" != -* ]]; } || require_value --inspect-key-file
            INSPECT_KEY_FILE=$2
            shift
            ;;
        --import-trusted-keys=*)
            IMPORT_TRUSTED_KEYS=${1#*=}
            shift
            IMPORT_TRUSTED_KEYS_FPRS=("$@")
            break
            ;;
        --import-trusted-keys)
            { [ "$#" -ge 2 ] && [[ "$2" != -* ]]; } || require_value --import-trusted-keys
            IMPORT_TRUSTED_KEYS=$2
            shift 2
            IMPORT_TRUSTED_KEYS_FPRS=("$@")
            break
            ;;
        --verify-as-checksum-file) VERIFY_AS_CHECKSUM_FILE=1 ;;
        --verify-as-clearsigned-checksum-file) VERIFY_AS_CLEARSIGNED_CHECKSUM_FILE=1 ;;
        --no-checksum-fallback) NO_CHECKSUM_FALLBACK=1 ;;
        --extract-plaintext-to=*) EXTRACT_PLAINTEXT_TO=${1#*=} ;;
        --extract-plaintext-to)
            { [ "$#" -ge 2 ] && [[ "$2" != -* ]]; } || require_value --extract-plaintext-to
            EXTRACT_PLAINTEXT_TO=$2
            shift
            ;;
        --checksum-file=*) CHECKSUM_FILE_OVERRIDE=${1#*=} ;;
        --checksum-file)
            { [ "$#" -ge 2 ] && [[ "$2" != -* ]]; } || require_value --checksum-file
            CHECKSUM_FILE_OVERRIDE=$2
            shift
            ;;
        --export-key-to=*) EXPORT_KEY_TO=${1#*=} ;;
        --export-key-to)
            { [ "$#" -ge 2 ] && [[ "$2" != -* ]]; } || require_value --export-key-to
            EXPORT_KEY_TO=$2
            shift
            ;;
        # For the GUI scripts: opens the status_out() machine-tag channel.
        --status-fd=*) STATUS_FD=${1#*=} ;;
        --status-fd)
            { [ "$#" -ge 2 ] && [[ "$2" != -* ]]; } || require_value --status-fd
            STATUS_FD=$2
            shift
            ;;
        --from-ring=*) FROM_RING_OVERRIDE=${1#*=} ;;
        --from-ring)
            { [ "$#" -ge 2 ] && [[ "$2" != -* ]]; } || require_value --from-ring
            FROM_RING_OVERRIDE=$2
            shift
            ;;
        --checksum-algo=*|--checksum-algo)
            if [ "$1" = "--checksum-algo" ]; then
                { [ "$#" -ge 2 ] && [[ "$2" != -* ]]; } || require_value --checksum-algo
                algo=$2
                shift
            else
                algo=${1#*=}
            fi
            case "$algo" in
                sha256) CHECKSUM_ALGO_OVERRIDE=sha256sum ;;
                sha512) CHECKSUM_ALGO_OVERRIDE=sha512sum ;;
                sha1)   CHECKSUM_ALGO_OVERRIDE=sha1sum ;;
                md5)    CHECKSUM_ALGO_OVERRIDE=md5sum ;;
                # TRANSLATORS: ${FLAG_CHECKSUM_ALGO}/${algo} are literal - keep placeholders as-is.
                *) die "$(eval_gettext "unknown \${FLAG_CHECKSUM_ALGO} '\${algo}' (expected sha256, sha512, sha1, or md5)")" ;;
            esac
            ;;
        -h|--help) usage 0 ;;
        -V|--version) printf '%s %s\n' "$DISPLAY_NAME" "$VERSION"; exit 0 ;;
        --man) show_man; exit 0 ;;
        --) shift; while [ "$#" -gt 0 ]; do ARGS+=("$1"); shift; done; break ;;
        # TRANSLATORS: ${opt}/${FLAG_MAN} are literal - keep placeholders as-is.
        -*) opt=$1; die "$(eval_gettext "unknown option '\${opt}' (see \${FLAG_MAN})")" ;;
        *) ARGS+=("$1") ;;
    esac
    shift
done
set -- "${ARGS[@]}"

debug_cmd "$0" "${ORIGINAL_ARGS[@]}"

if [ "$KEEP_KEY" -eq 1 ]; then
    # TRANSLATORS: ${FLAG_KEEP_KEY} is a literal flag name - keep the placeholder as-is.
    [ "$#" -eq 1 ] || usage 1 "$(eval_gettext "exactly one fingerprint argument is required for \${FLAG_KEEP_KEY}")"
    FPR=$1
    # TRANSLATORS: ${FLAG_KEEP_KEY} is a literal flag name - keep the placeholder as-is.
    is_full_fingerprint "$FPR" || usage 1 "$(eval_gettext "\${FLAG_KEEP_KEY} needs a full 40-character (or 64-character) hex fingerprint, not '\${FPR}'")"
elif [ "$SHOW_KEY_DETAILS" -eq 1 ]; then
    # TRANSLATORS: ${FLAG_SHOW_KEY_DETAILS} is a literal flag name - keep the placeholder as-is.
    [ "$#" -ge 1 ] || usage 1 "$(eval_gettext "at least one fingerprint argument is required for \${FLAG_SHOW_KEY_DETAILS}")"
    SHOW_KEY_DETAILS_FPRS=("$@")
    for fpr in "${SHOW_KEY_DETAILS_FPRS[@]}"; do
        # TRANSLATORS: ${FLAG_SHOW_KEY_DETAILS} is a literal flag name - keep the placeholder as-is.
        is_full_fingerprint "$fpr" || usage 1 "$(eval_gettext "\${FLAG_SHOW_KEY_DETAILS} needs a full 40-character (or 64-character) hex fingerprint, not '\${fpr}'")"
    done
elif [ "$LIST_TRUSTED" -eq 1 ]; then
    # Zero fingerprints (the default) means every key.
    LIST_TRUSTED_FPRS=("$@")
elif [ "$REFRESH_TRUSTED_KEYS" -eq 1 ] || [ "$LIST_KNOWN_KEYS" -eq 1 ] || [ "$LIST_KEYSERVERS" -eq 1 ] \
     || [ -n "$UNTRUST_KEY" ] \
     || [ -n "$EXPORT_TRUSTED_KEYS" ] \
     || [ -n "$INSPECT_KEY_FILE" ] || [ -n "$IMPORT_TRUSTED_KEYS" ]; then
    [ "$#" -eq 0 ] || usage 1 "$(gettext "this option takes no extra arguments")"
else
    [ "$#" -ge 1 ] && [ "$#" -le 3 ] || usage 1 "$(gettext "expected an ISO or signature file to check")"
    NO_DIRECT_SIG=0
    ISO_DERIVED_FROM_SIG=0
    if [ -n "${3:-}" ]; then
        # Three explicit arguments - an ISO plus its checksum/signature
        # files. Falls back to treating any two as an ISO+SIG pair
        # (ignoring the third) when the triple doesn't match a known shape.
        if ! classify_iso_checksum_signature_triple "$1" "$2" "$3" \
            && ! classify_iso_triple_ignoring_one "$1" "$2" "$3"; then
            triple_base1=$(basename "$1")
            triple_base2=$(basename "$2")
            triple_base3=$(basename "$3")
            if [ "$TRIPLE_AMBIGUOUS" -eq 1 ]; then
                if [ -n "$TRIPLE_AMBIGUOUS_ANCHOR" ]; then
                    anchor_base=$(basename "$TRIPLE_AMBIGUOUS_ANCHOR")
                    cand1_base=$(basename "$TRIPLE_AMBIGUOUS_CANDIDATE1")
                    cand2_base=$(basename "$TRIPLE_AMBIGUOUS_CANDIDATE2")
                    die "$(eval_gettext "cannot detect what '\${anchor_base}' belongs to: '\${cand1_base}', '\${cand2_base}'.")"
                fi
                die "$(eval_gettext "cannot detect how these three files belong together: '\${triple_base1}', '\${triple_base2}', '\${triple_base3}'.")"
            fi
            die "$(eval_gettext "cannot detect an ISO, a checksum listing, and its signature among these three files: '\${triple_base1}', '\${triple_base2}', '\${triple_base3}'.")"
        fi
        [ -d "$ISO" ] && die "$(eval_gettext "'\${ISO}' is a directory, not a file - point this at the actual .iso file (or its .sig/.asc/.gpg/.sign signature file) directly.")"
    elif [ -n "${2:-}" ]; then
        # Two explicit arguments - order-independent via, in order,
        # classify_checksum_listing_pair()/classify_checksum_signature_
        # pair()/classify_plain_checksum_signature_pair()/classify_plain_
        # checksum_pair()/classify_iso_sig_pair(). && iso_result_plausible:
        # an implausible match falls through to the next classifier.
        #
        # The two checksum-listing classifiers are skipped in
        # --verify-as-checksum-file mode (try_checksum_fallback()'s own
        # internal re-exec) - its pair already IS one of those shapes and
        # must not be reinterpreted, or it recurses into itself.
        if [ "$VERIFY_AS_CHECKSUM_FILE" -eq 1 ]; then
            classify_iso_sig_pair "$1" "$2"
        elif classify_checksum_listing_pair "$1" "$2" && iso_result_plausible; then
            :
        elif classify_checksum_signature_pair "$1" "$2" && iso_result_plausible; then
            :
        elif classify_plain_checksum_signature_pair "$1" "$2" && iso_result_plausible; then
            :
        elif classify_plain_checksum_pair "$1" "$2" && iso_result_plausible; then
            :
        else
            classify_iso_sig_pair "$1" "$2"
            if [ "$NEITHER_LOOKS_LIKE_SIGNATURE" -eq 1 ]; then
                if [ -n "$SIG_NAMED_BUT_NOT_SIGNATURE" ]; then
                    named_base=$(basename "$SIG_NAMED_BUT_NOT_SIGNATURE")
                    die "$(eval_gettext "'\${named_base}' looks like a signature file by its name, but its content doesn't look like a real one - point this tool at the actual signature file, or a checksum listing.")"
                fi
                iso_base=$(basename "$ISO")
                sig_base=$(basename "$SIG")
                die "$(eval_gettext "neither '\${iso_base}' nor '\${sig_base}' looks like a real ISO signature file, by name or content - point this tool at the actual ISO and its .sig/.asc/.gpg/.sign file, or a checksum listing.")"
            elif ! iso_result_plausible; then
                iso_base=$(basename "$ISO")
                sig_base=$(basename "$SIG")
                die "$(eval_gettext "'\${iso_base}' and '\${sig_base}' both look like signature or checksum-listing files - point this tool at the actual ISO, plus its checksum listing or signature file.")"
            fi
        fi
        [ -d "$ISO" ] && die "$(eval_gettext "'\${ISO}' is a directory, not a file - point this at the actual .iso file (or its .sig/.asc/.gpg/.sign signature file) directly.")"
        [ -d "$SIG" ] && die "$(eval_gettext "'\${SIG}' is a directory, not a file - point this at the actual signature file directly.")"
    else
        ISO=$1
        # Placeholder, overwritten below by whichever branch finds a real
        # direct signature - kept as a default so a "cannot read
        # signature file" error still names a path if nothing else does.
        SIG="${ISO}.sig"
        [ -d "$ISO" ] && die "$(eval_gettext "'\${ISO}' is a directory, not a file - point this at the actual .iso file (or its .sig/.asc/.gpg/.sign signature file) directly.")"
        if is_clearsigned_file "$ISO"; then
            # A full clearsigned message needs no separate signature -
            # left exactly as given.
            :
        elif case "$ISO" in *.sig|*.asc|*.gpg|*.sign) true ;; *) false ;; esac; then
            # A genuine (non-clearsigned) signature file - derive the ISO
            # name by stripping the extension. ISO_DERIVED_FROM_SIG: this
            # name is only a guess, not user-given.
            SIG=$ISO
            ISO_DERIVED_FROM_SIG=1
            case "$SIG" in
                *.sig)  ISO=${SIG%.sig}  ;;
                *.asc)  ISO=${SIG%.asc}  ;;
                *.gpg)  ISO=${SIG%.gpg}  ;;
                *.sign) ISO=${SIG%.sign} ;;
            esac
        elif find_clearsigned_plain_checksum "$ISO"; then
            CHECKSUM_FILE_OVERRIDE=$CLEARSIGNED_PLAIN_CHECKSUM
        elif [ -r "${ISO}.sig" ]; then
            # A same-named ".sig" can itself be a full clearsigned
            # checksum listing rather than the ISO's own signature.
            if is_clearsigned_file "${ISO}.sig"; then
                CHECKSUM_FILE_OVERRIDE="${ISO}.sig"
            else
                SIG="${ISO}.sig"
            fi
        elif [ -r "${ISO}.asc" ]; then
            if is_clearsigned_file "${ISO}.asc"; then
                CHECKSUM_FILE_OVERRIDE="${ISO}.asc"
            else
                SIG="${ISO}.asc"
            fi
        elif [ -r "${ISO}.gpg" ]; then
            if is_clearsigned_file "${ISO}.gpg"; then
                CHECKSUM_FILE_OVERRIDE="${ISO}.gpg"
            else
                SIG="${ISO}.gpg"
            fi
        else
            # No direct signature - let the checksum-file fallback try
            # (try_checksum_fallback below); keep the .sig default so the
            # "cannot read signature file" error still names a path if not.
            NO_DIRECT_SIG=1
            SIG="${ISO}.sig"
        fi
    fi
fi

# Dispatch to the mode selected above - one function call per mode,
# matching the flag classification just above one-for-one.
if [ "$KEEP_KEY" -eq 1 ]; then
    verify_iso
elif [ "$SHOW_KEY_DETAILS" -eq 1 ]; then
    show_key_details
elif [ "$LIST_TRUSTED" -eq 1 ]; then
    list_trusted
elif [ "$REFRESH_TRUSTED_KEYS" -eq 1 ]; then
    refresh_trusted_keys
elif [ "$LIST_KNOWN_KEYS" -eq 1 ]; then
    list_known_keys
elif [ "$LIST_KEYSERVERS" -eq 1 ]; then
    list_keyservers
elif [ -n "$UNTRUST_KEY" ]; then
    untrust_key
elif [ -n "$EXPORT_TRUSTED_KEYS" ]; then
    export_trusted_keys
elif [ -n "$INSPECT_KEY_FILE" ]; then
    inspect_key_file
elif [ -n "$IMPORT_TRUSTED_KEYS" ]; then
    import_trusted_keys
else
    verify_iso
fi
}

# LC_CTYPE only, not the message language: gpg's --status-fd protocol
# (never localized) is what this script parses, so gpg/gpgv's own text
# output is left in whatever language the environment provides. C.UTF-8
# keeps a non-ASCII signer UID rendering correctly with no locale set.
export LC_CTYPE=C.UTF-8

# Every gpg/gpgv call runs against a throwaway, isolated GNUPGHOME - the
# real keyrings (trustedkeys.gpg, pubring.kbx) are still addressed by
# absolute path via --keyring. Each mode function calls this itself,
# then its own `trap cleanup RETURN` (fires on the function containing
# the trap, not a shared helper) - not a script-wide EXIT trap, which
# would clobber a sourcing GUI's own.
setup_gnupg_homedir() {
    TMP_RING=""
    GNUPG_HOMEDIR=$(mktemp -d "$SESSION_TMPDIR/gnupghome.XXXXXXXXXX")
    chmod 700 "$GNUPG_HOMEDIR"
    export GNUPGHOME="$GNUPG_HOMEDIR"
}
cleanup() {
    [ -n "$TMP_RING" ] && rm -f "$TMP_RING" "${TMP_RING}~"
    gpgconf --homedir "$GNUPG_HOMEDIR" --kill all >/dev/null 2>&1
    rm -rf "$GNUPG_HOMEDIR"
    true
}

# True if $1 is a full OpenPGP fingerprint: 40-hex (v4) or 64-hex (v5/v6,
# RFC 9580). Never a prefix/substring match, unlike gpg's own
# --list-keys/--recv-keys search-term handling - callers rely on this to
# reject non-fingerprint input (e.g. the "-" placeholder gpgv reports for
# a v3 signature) before it reaches gpg as a search term.
is_full_fingerprint() {
    [[ $1 =~ ^[0-9A-Fa-f]{40}$ ]] || [[ $1 =~ ^[0-9A-Fa-f]{64}$ ]]
}

# echoes the label for fingerprint $1 if it is one of the KNOWN_KEYS, else
# returns non-zero
known_key_label() {
    local fpr=$1 entry
    for entry in "${KNOWN_KEYS[@]}"; do
        if [ "${entry%%|*}" = "$fpr" ]; then
            printf '%s' "${entry#*|}"
            return 0
        fi
    done
    return 1
}

# Recognizes $1 either directly (an exact KNOWN_KEYS entry) or via its
# PRIMARY key, resolved out of whichever given "ring|prefix" pair
# actually has $1 on file - a listed primary's valid signing subkeys
# inherit its recognition automatically.
known_key_label_or_primary() {
    local fpr=$1; shift
    local label primary ring_prefix ring prefix
    label=$(known_key_label "$fpr") && { printf '%s' "$label"; return 0; }
    is_full_fingerprint "$fpr" || return 1
    for ring_prefix in "$@"; do
        ring=${ring_prefix%%|*}
        prefix=${ring_prefix#*|}
        primary=$(find_primary_fpr_of "$fpr" "$ring" "$prefix") || continue
        [ "$primary" != "$fpr" ] || continue
        label=$(known_key_label "$primary") && { printf '%s' "$label"; return 0; }
    done
    return 1
}

# Echoes "missing"/"valid"/"expired"/"revoked" for $FPR in keyring $1
# ($2: optional prefix, e.g. "gnupg-ring:" to force the legacy format).
key_status() {
    local ring=$1 prefix=$2 line validity
    [ -f "$ring" ] || { echo missing; return; }
    # $FPR must look like a real full fingerprint before it's ever handed
    # to gpg as a search term - anything shorter/non-hex (e.g. the literal
    # "-" placeholder gpgv's own ERRSIG line reports when a v3 signature
    # packet carries no full fingerprint) makes gpg fall back to a
    # UID-substring search instead of "no such key", which can spuriously
    # match an unrelated key and report ITS validity as if it were the
    # queried key's own.
    is_full_fingerprint "$FPR" || { echo missing; return; }
    debug_cmd gpg --batch --no-default-keyring --keyring "${prefix}${ring}" --with-colons --list-keys "$FPR"
    line=$(gpg --batch --no-default-keyring --keyring "${prefix}${ring}" \
               --with-colons --list-keys "$FPR" 2>/dev/null | grep '^pub:' | head -n1) || true
    [ -n "$line" ] || { echo missing; return; }
    validity=$(printf '%s' "$line" | cut -d: -f2)
    case "$validity" in
        e) echo expired ;;
        r) echo revoked ;;
        *) echo valid ;;
    esac
}

# Echoes $FPR's UID from keyring $1 (prefix $2), or nothing (rc 1).
ring_key_uid() {
    local ring=$1 prefix=$2 line
    line=$(gpg --batch --no-default-keyring --keyring "${prefix}${ring}" \
               --with-colons --list-keys "$FPR" 2>/dev/null \
           | parse_colon_key_listing | head -n1) || true
    [ -n "$line" ] || return 1
    printf '%s' "$line" | cut -d'|' -f3
}

# True if a key_status() result ($1) is good enough to attempt a gpgv
# check with. "expired" qualifies (a lifecycle signal, not a
# cryptographic weakness - gpgv itself still verifies fine with an
# expired key); "revoked" never does, regardless of context.
key_material_usable() {
    case "$1" in
        valid|expired) return 0 ;;
        *) return 1 ;;
    esac
}

# True if $FPR is revoked per ANY of these three ring statuses - used
# right after a fresh key_status()/refresh_trusted_key_status() read,
# so a revoked key is refused consistently regardless of which ring
# reveals it first.
any_status_revoked() {
    [ "$1" = revoked ] || [ "$2" = revoked ] || [ "$3" = revoked ]
}

# Detects $MX_GPG_KEYRING's format and sets MX_GPG_KEYRING_RING/
# MX_GPG_KEYRING_PREFIX (ring, prefix), empty if missing, unreadable, or
# dearmor fails. Detects the format itself (KBXf magic byte) rather than
# relying on gpg's own --keyring auto-detection.
resolve_mx_gpg_keyring() {
    MX_GPG_KEYRING_RING=""
    MX_GPG_KEYRING_PREFIX=""
    [ -r "$MX_GPG_KEYRING" ] || return 0
    local head_bytes magic tmp out
    head_bytes=$(head -c 40 "$MX_GPG_KEYRING" 2>/dev/null | tr -d '\0')
    case "$head_bytes" in
        "-----BEGIN PGP PUBLIC KEY BLOCK-----"*)
            # gpg --keyring can't read armored data - dearmor to a tmp file first.
            tmp=$(mktemp "$SESSION_TMPDIR/mx-gpg-keyring-dearmored.XXXXXXXXXX")
            debug_cmd gpg --batch --yes -o "$tmp" --dearmor "$MX_GPG_KEYRING"
            if out=$(gpg --batch --yes -o "$tmp" --dearmor "$MX_GPG_KEYRING" 2>&1); then
                MX_GPG_KEYRING_RING=$tmp
                MX_GPG_KEYRING_PREFIX="gnupg-ring:"
            else
                log "$(eval_gettext "warning: could not read \${MX_GPG_KEYRING}: \${out}")"
                rm -f "$tmp"
            fi
            return 0
            ;;
    esac
    magic=$(head -c 12 "$MX_GPG_KEYRING" 2>/dev/null | tail -c 4 | tr -d '\0')
    MX_GPG_KEYRING_RING=$MX_GPG_KEYRING
    if [ "$magic" = "KBXf" ]; then
        MX_GPG_KEYRING_PREFIX="gnupg-kbx:"
    else
        MX_GPG_KEYRING_PREFIX="gnupg-ring:"
    fi
}

# Transparency note for an already-accepted expired key (see
# key_material_usable()). $1/$2: same ring/prefix the caller's
# key_status() just used - re-queried once to surface the actual
# expiry date.
warn_key_expired() {
    local ring=$1 prefix=$2 line epoch date_str=""
    line=$(gpg --batch --no-default-keyring --keyring "${prefix}${ring}" \
               --with-colons --list-keys "$FPR" 2>/dev/null | grep '^pub:' | head -n1) || true
    epoch=$(printf '%s' "$line" | cut -d: -f7)
    [ -n "$epoch" ] && date_str=$(date -d "@$epoch" +%Y-%m-%d 2>/dev/null) || true
    if [ -n "$date_str" ]; then
        info "$(eval_gettext "this key expired on \${date_str} - this does not affect the signature check")"
    else
        info "$(gettext "this key has expired - this does not affect the signature check")"
    fi
    status_out "KEY_EXPIRED${date_str:+ $date_str}"
}

# Revoked key - refuses outright, no "verify anyway" escape hatch (see
# key_material_usable()'s own doc comment: revoked material is never
# usable here, whatever the reason).
warn_key_revoked() {
    status_out "KEY_REVOKED"
    die "$(gettext "this signing key has been revoked - refusing to use it, whatever the reason (a real key compromise, or the owner deliberately retiring it)")" || return 1
}

# Keeps $TRUSTED_GPG's copy of $FPR fresh against $PUBRING_KBX and
# mx-gpg-keyring (local, no network) on every call - $1/$2/$3 are the
# caller's already-known tg_status/pk_status/mx_status. A local
# revocation is merged in regardless of the cached copy's own status.
# Only when $1 is "expired" does this also try a keyserver, for a
# renewal or a revocation neither local source knows about - no
# keyserver check for an already-"valid" cached key (see
# --refresh-trusted-keys for that, on demand only).
refresh_trusted_key_status() {
    local orig_status=$1 pk_status=$2 mx_status=$3 ks tmp_ring renew_args renew_out fetched
    [ "$orig_status" = missing ] && return 1

    if [ "$pk_status" = revoked ] && export_to_tmp_ring "$PUBRING_KBX" ""; then
        if keep_in_trusted_gpg "$TMP_RING" "gnupg-ring:"; then
            rm -f "$TMP_RING" "${TMP_RING}~"
            TMP_RING=""
            VERIFY_RING=$TRUSTED_GPG
            log "$(gettext "this key was found revoked locally - updated the local copy automatically")"
            return 0
        fi
        rm -f "$TMP_RING" "${TMP_RING}~"
        TMP_RING=""
        VERIFY_RING=$TRUSTED_GPG
    elif [ "$mx_status" = revoked ] && export_to_tmp_ring "$MX_GPG_KEYRING_RING" "$MX_GPG_KEYRING_PREFIX"; then
        if keep_in_trusted_gpg "$TMP_RING" "gnupg-ring:"; then
            rm -f "$TMP_RING" "${TMP_RING}~"
            TMP_RING=""
            VERIFY_RING=$TRUSTED_GPG
            log "$(gettext "this key was found revoked locally - updated the local copy automatically")"
            return 0
        fi
        rm -f "$TMP_RING" "${TMP_RING}~"
        TMP_RING=""
        VERIFY_RING=$TRUSTED_GPG
    elif [ "$orig_status" = expired ] && [ "$pk_status" = valid ] && export_to_tmp_ring "$PUBRING_KBX" ""; then
        if keep_in_trusted_gpg "$TMP_RING" "gnupg-ring:"; then
            rm -f "$TMP_RING" "${TMP_RING}~"
            TMP_RING=""
            VERIFY_RING=$TRUSTED_GPG
            log "$(gettext "this key's expiry was renewed - updated local copy automatically")"
            status_out "KEY_RENEWED"
            return 0
        fi
        rm -f "$TMP_RING" "${TMP_RING}~"
        TMP_RING=""
        VERIFY_RING=$TRUSTED_GPG
    fi

    [ "$orig_status" = expired ] || return 1
    for ks in "${KEYSERVERS[@]}"; do
        log "$(eval_gettext "checking \${ks} for a renewed copy of this key...")"
        tmp_ring=$(mktemp "$SESSION_TMPDIR/gnupg-ring.XXXXXXXXXX.gpg")
        renew_args=(
            --batch
            --no-default-keyring
            --keyring "gnupg-ring:${tmp_ring}"
            # 5s, not the mandatory fetch loop's own 15s - this check is
            # optional, so the worst-case no-network delay is capped lower.
            --keyserver-options timeout=5,import-clean,import-minimal
            --keyserver "$ks"
            # "0x" prefix: unambiguous Key-ID/fingerprint marker per HKP.
            --receive-keys "0x$FPR"
        )
        debug_cmd gpg "${renew_args[@]}"
        set +e
        renew_out=$(gpg "${renew_args[@]}" 2>&1)
        set -e
        [ "$DEBUG" -eq 1 ] && printf '%s\n' "$renew_out" >&2
        fetched=$(key_status "$tmp_ring" "gnupg-ring:")
        if [ "$fetched" = valid ] && keep_in_trusted_gpg "$tmp_ring" "gnupg-ring:"; then
            rm -f "$tmp_ring" "${tmp_ring}~"
            log "$(gettext "this key's expiry was renewed - updated local copy automatically")"
            status_out "KEY_RENEWED"
            return 0
        elif [ "$fetched" = revoked ] && keep_in_trusted_gpg "$tmp_ring" "gnupg-ring:"; then
            rm -f "$tmp_ring" "${tmp_ring}~"
            log "$(eval_gettext "\${ks} reports this key as revoked - updated the local copy automatically")"
            return 0
        fi
        rm -f "$tmp_ring" "${tmp_ring}~"
    done
    log "$(gettext "no renewal found - continuing with the existing (expired) key")"
    return 1
}

# Turns gpg --with-colons output (pub:/fpr:/uid: records) into one line
# per primary key: FPR|VALIDITY|UID|EXPIRE (EXPIRE is a raw Unix epoch,
# empty if unset). Shared by --list-trusted-keys and --inspect-key-file.
parse_colon_key_listing() {
    awk -F: '
        $1 == "pub" {
            if (fpr != "") print fpr "|" vstr "|" uid "|" expire
            fpr=""; uid=""
            if ($2 == "e") vstr = "expired"
            else if ($2 == "r") vstr = "revoked"
            else vstr = "valid"
            expire = $7
            next
        }
        $1 == "fpr" && fpr == "" { fpr = $10 }
        $1 == "uid" && uid == "" { uid = $10 }
        END { if (fpr != "") print fpr "|" vstr "|" uid "|" expire }
    '
}

# When $1 is a bare 16-hex Key ID (not a full fingerprint), looks up
# whether keyring $2 (prefix $3) already contains a key whose fingerprint
# ends in it - a Key ID is defined (RFC 4880) as the low-order 64 bits of
# the fingerprint, so a suffix match is exact, not heuristic. Reuses
# parse_colon_key_listing() (never a risky "gpg --list-keys <term>"
# search - a full, unfiltered listing matched by exact string comparison,
# the same already-safe pattern export_trusted_keys()/import_trusted_keys()
# use). Echoes that key's real fingerprint and returns 0 only if EXACTLY
# one match is found; returns 1 (no output) for zero or more-than-one
# match - ambiguity here just means "couldn't confirm a safe local
# upgrade," falling through to the normal recognized/fetch flow rather
# than guessing which key is the real one.
find_fpr_by_keyid() {
    local keyid=$1 ring=$2 prefix=$3 matches count
    [ -f "$ring" ] || return 1
    matches=$(gpg --batch --no-default-keyring --keyring "${prefix}${ring}" \
                   --with-colons --list-keys 2>/dev/null \
              | parse_colon_key_listing \
              | awk -F'|' -v kid="$keyid" '$1 ~ kid"$" {print $1}') || true
    # grep -c exits 1 (not just an empty count) when nothing matches - see
    # the identical guard in verify_iso()'s own post-fetch re-derivation.
    count=$(printf '%s\n' "$matches" | grep -c .) || true
    [ "$count" -eq 1 ] || return 1
    printf '%s' "$matches"
}

# Reads FPR|VALIDITY|UID|EXPIRE (parse_colon_key_listing's output) and
# re-prints as FPR|VALIDITY|UID|KNOWN|EXPIRE, KNOWN="known" if on
# KNOWN_KEYS, else empty. Shared by --list-trusted-keys and --inspect-key-file.
add_known_flag() {
    while IFS='|' read -r line_fpr line_vstr line_uid line_expire; do
        if known_key_label "$line_fpr" >/dev/null 2>&1; then
            printf '%s|%s|%s|known|%s\n' "$line_fpr" "$line_vstr" "$line_uid" "$line_expire"
        else
            printf '%s|%s|%s||%s\n' "$line_fpr" "$line_vstr" "$line_uid" "$line_expire"
        fi
    done
}

# True if a gpg --with-colons stream (from --show-keys) contains a
# secret-key packet (sec:/ssb: record) - used by --inspect-key-file and
# --import-trusted-keys to refuse a file before running gpg --import on
# it. Anchored to field 1, not a substring grep - a UID is free text and
# could itself contain "sec:"/"ssb:".
colon_stream_has_secret_key() {
    awk -F: '$1=="sec"||$1=="ssb"{found=1} END{exit !found}'
}

# Reads a gpg --with-colons stream (pub:/fpr:/sub: records) and prints
# "FPR|PRIMARY_FPR" for every fingerprint seen - a primary key maps to
# itself, a subkey maps to its primary key. Lets a caller resolve a
# subkey fingerprint passed where a primary key one was meant.
build_fpr_primary_map() {
    awk -F: '
        $1 == "pub" { primary = "" }
        $1 == "fpr" && primary == "" { primary = $10; print $10 "|" $10; next }
        $1 == "fpr" { print $10 "|" primary }
    '
}

# Resolves $1 (a fingerprint, primary or subkey) to its PRIMARY key's own
# fingerprint, if $1 is present in ring $2 (prefix $3) - empty/failure if
# $1 isn't there at all.
find_primary_fpr_of() {
    local fpr=$1 ring=$2 prefix=$3 map resolved
    [ -f "$ring" ] || return 1
    is_full_fingerprint "$fpr" || return 1
    map=$(gpg --batch --no-default-keyring --keyring "${prefix}${ring}" \
              --with-colons --list-keys "$fpr" 2>/dev/null | build_fpr_primary_map) || true
    resolved=$(awk -F'|' -v want="$fpr" '$1==want{print $2; exit}' <<< "$map")
    [ -n "$resolved" ] || return 1
    printf '%s' "$resolved"
}

# Resolves the keyserver list into $KEYSERVERS: --keyserver overrides
# $KEYSERVER, which overrides DEFAULT_KEYSERVERS. Accepts a comma-
# and/or space-separated list; an entry with no "scheme://" is assumed hkps://.
resolve_keyservers() {
    local raw
    if [ -n "$KEYSERVER_OPT" ]; then
        raw=$KEYSERVER_OPT
    elif [ -n "${KEYSERVER:-}" ]; then
        raw=$KEYSERVER
    else
        raw=""
    fi
    if [ -n "$raw" ]; then
        IFS=$' \t\n' read -r -a KEYSERVERS <<< "${raw//,/ }"
        for i in "${!KEYSERVERS[@]}"; do
            case "${KEYSERVERS[$i]}" in
                *://*) ;;
                *) KEYSERVERS[$i]="hkps://${KEYSERVERS[$i]}" ;;
            esac
        done
    else
        KEYSERVERS=("${DEFAULT_KEYSERVERS[@]}")
    fi
}

# --list-trusted-keys [FPR...]: one line per primary key in
# $TRUSTED_GPG - FPR|VALIDITY|UID|KNOWN|EXPIRE (KNOWN="known" if also on
# the hardcoded KNOWN_KEYS list). Zero fingerprints means every key; a
# given fingerprint resolves to its primary first, same as
# --export-trusted-keys/--import-trusted-keys. No $TRUSTED_GPG at all ->
# no output (an empty list, not an error).
list_trusted() {
    local FPR_MAP WANTED_FPRS raw fpr resolved sub_fpr primary_fpr
    setup_gnupg_homedir
    trap cleanup RETURN
    [ -f "$TRUSTED_GPG" ] || return 0
    debug_cmd gpg --batch --no-default-keyring --keyring "gnupg-ring:${TRUSTED_GPG}" --with-colons --list-keys
    if [ "${#LIST_TRUSTED_FPRS[@]}" -eq 0 ]; then
        gpg --batch --no-default-keyring --keyring "gnupg-ring:${TRUSTED_GPG}" \
            --with-colons --list-keys 2>/dev/null | parse_colon_key_listing | add_known_flag | sort -t'|' -k3,3f
        return 0
    fi
    FPR_MAP=$(gpg --batch --no-default-keyring --keyring "gnupg-ring:${TRUSTED_GPG}" \
        --with-colons --list-keys 2>/dev/null | build_fpr_primary_map)
    WANTED_FPRS=()
    for raw in "${LIST_TRUSTED_FPRS[@]}"; do
        fpr=$(printf '%s' "$raw" | tr -d '[:space:]' | tr '[:lower:]' '[:upper:]')
        resolved=$(awk -F'|' -v want="$fpr" '$1==want{print $2; exit}' <<< "$FPR_MAP")
        if [ -z "$resolved" ]; then
            log "$(eval_gettext "warning: fingerprint '\${fpr}' not found in \${TRUSTED_GPG}, skipping")"
        else
            [ "$resolved" != "$fpr" ] \
                && log "$(eval_gettext "note: '\${fpr}' is a subkey - resolved to its primary key '\${resolved}'")"
            if ! printf '%s\n' "${WANTED_FPRS[@]-}" | grep -qxF "$resolved"; then
                WANTED_FPRS+=("$resolved")
            fi
        fi
    done
    [ "${#WANTED_FPRS[@]}" -gt 0 ] || return 0
    gpg --batch --no-default-keyring --keyring "gnupg-ring:${TRUSTED_GPG}" \
        --with-colons --list-keys "${WANTED_FPRS[@]}" 2>/dev/null | parse_colon_key_listing | add_known_flag | sort -t'|' -k3,3f
    return 0
}

# --refresh-trusted-keys: a separate, explicitly-invoked mode - checks
# every fingerprint currently in $TRUSTED_GPG against $PUBRING_KBX,
# mx-gpg-keyring, AND every configured keyserver for ANY change (a
# later expiry, a new subkey/UID, or a revocation) not yet reflected
# locally, and merges in whatever's found. On-demand only, by design -
# a normal verify run never touches the network for an already-valid
# cached key, and never sweeps every OTHER cached key's local sources
# either.
refresh_trusted_keys() {
    local fpr vstr uid expire checked=0 updated=0 revoked_found=0 unreachable=0
    local ks tmp_ring args fetch_rc before after new_status src_status ks_out key_uncertain
    # A library caller (LIB_MODE=1, e.g. the GUI's background sweep)
    # needs status_out()'s tag readable even with no explicit --status-fd.
    [ "$LIB_MODE" -eq 1 ] && [ -z "$STATUS_FD" ] && STATUS_FD=2
    setup_gnupg_homedir
    trap cleanup RETURN
    resolve_keyservers
    resolve_mx_gpg_keyring
    if [ ! -f "$TRUSTED_GPG" ]; then
        log "$(eval_gettext "no trusted keys saved yet (\${TRUSTED_GPG} doesn't exist)")"
        status_out "REFRESH_TRUSTED_KEYS_DONE 0 0 0 0"
        return 0
    fi
    while IFS='|' read -r fpr vstr uid expire; do
        [ -n "$fpr" ] || continue
        checked=$((checked + 1))
        before=$(FPR=$fpr gpg --batch --no-default-keyring --keyring "gnupg-ring:${TRUSTED_GPG}" \
                     --export-options export-clean --export "$fpr" 2>/dev/null | sha256sum)

        # Local sources first - free, no network; keep_in_trusted_gpg()'s
        # import is additive/idempotent, safe to attempt unconditionally.
        src_status=$(FPR=$fpr key_status "$PUBRING_KBX" "")
        [ "$src_status" = missing ] || FPR=$fpr keep_in_trusted_gpg "$PUBRING_KBX" "" >/dev/null 2>&1 || true
        src_status=$(FPR=$fpr key_status "$MX_GPG_KEYRING_RING" "$MX_GPG_KEYRING_PREFIX")
        [ "$src_status" = missing ] || FPR=$fpr keep_in_trusted_gpg "$MX_GPG_KEYRING_RING" "$MX_GPG_KEYRING_PREFIX" >/dev/null 2>&1 || true

        # Then every configured keyserver. [GNUPG:] FAILURE recv-keys
        # <code>'s low 16 bits = 58 (GPG_ERR_NO_DATA) means reachable but
        # not there (harmless); anything else means the result may be stale.
        key_uncertain=0
        for ks in "${KEYSERVERS[@]}"; do
            tmp_ring=$(mktemp "$SESSION_TMPDIR/gnupg-ring.XXXXXXXXXX.gpg")
            ks_status_file=$(mktemp "$SESSION_TMPDIR/refresh-recv-status.XXXXXXXXXX")
            args=(
                --batch --no-default-keyring --keyring "gnupg-ring:${tmp_ring}"
                --status-fd 3
                --keyserver-options timeout=5,import-clean,import-minimal
                --keyserver "$ks" --receive-keys "0x$fpr"
            )
            debug_cmd gpg "${args[@]}"
            set +e; gpg "${args[@]}" 3>"$ks_status_file" >/dev/null 2>&1; fetch_rc=$?; set -e
            [ "$DEBUG" -eq 1 ] && cat "$ks_status_file" >&2
            if [ "$fetch_rc" -eq 0 ] && [ "$(FPR=$fpr key_status "$tmp_ring" "gnupg-ring:")" != missing ]; then
                FPR=$fpr keep_in_trusted_gpg "$tmp_ring" "gnupg-ring:" || true
                rm -f "$tmp_ring" "${tmp_ring}~" "$ks_status_file"
                key_uncertain=0
                break
            fi
            if [ "$fetch_rc" -ne 0 ]; then
                failure_code=$(grep -m1 '^\[GNUPG:\] FAILURE recv-keys ' "$ks_status_file" | awk '{print $NF}')
                if [ -n "$failure_code" ] && [ "$((failure_code & 0xFFFF))" -eq 58 ]; then
                    : # GPG_ERR_NO_DATA - not on this keyserver, not a fetch problem.
                else
                    key_uncertain=1
                fi
            fi
            rm -f "$tmp_ring" "${tmp_ring}~" "$ks_status_file"
        done

        after=$(FPR=$fpr gpg --batch --no-default-keyring --keyring "gnupg-ring:${TRUSTED_GPG}" \
                    --export-options export-clean --export "$fpr" 2>/dev/null | sha256sum)
        if [ "$before" != "$after" ]; then
            new_status=$(FPR=$fpr key_status "$TRUSTED_GPG" "gnupg-ring:")
            if [ "$new_status" = revoked ]; then
                log "$(eval_gettext "\${fpr} (\${uid}) is revoked - updated \${TRUSTED_GPG}")"
                revoked_found=$((revoked_found + 1))
            else
                log "$(eval_gettext "\${fpr} (\${uid}) has newer key data - updated \${TRUSTED_GPG}")"
                updated=$((updated + 1))
            fi
        elif [ "$key_uncertain" -eq 1 ]; then
            unreachable=$((unreachable + 1))
        fi
    done < <(gpg --batch --no-default-keyring --keyring "gnupg-ring:${TRUSTED_GPG}" \
                  --with-colons --list-keys 2>/dev/null | parse_colon_key_listing)
    log "$(eval_gettext "checked \${checked} key(s): \${updated} updated, \${revoked_found} revoked")"
    if [ "$unreachable" -gt 0 ]; then
        warn "$(eval_gettext "\${unreachable} of \${checked} key(s) could not be checked against any keyserver - results for those may be incomplete")"
    fi
    status_out "REFRESH_TRUSTED_KEYS_DONE $checked $updated $revoked_found $unreachable"
    return 0
}

# --show-key-details <fpr> [fpr...]: gpg's own human-readable --list-keys
# output (algorithm, dates, capabilities, each subkey's own fingerprint)
# for keys already in $TRUSTED_GPG. A fingerprint not found is a
# warning, not fatal for the rest of the batch. The uid line's own
# "[ trustlevel ]" is stripped - always "unknown" here (a fresh
# throwaway trustdb), not real information. show-unusable-subkeys/-uids:
# gpg hides an expired/revoked subkey or UID by default otherwise.
show_key_details() {
    local fpr out err_file rc=0
    setup_gnupg_homedir
    trap cleanup RETURN
    [ -f "$TRUSTED_GPG" ] || die "$(eval_gettext "no trusted keys saved yet (\${TRUSTED_GPG} doesn't exist)")" || return 1
    for fpr in "${SHOW_KEY_DETAILS_FPRS[@]}"; do
        debug_cmd gpg --batch --no-default-keyring --keyring "gnupg-ring:${TRUSTED_GPG}" --list-options show-unusable-subkeys,show-unusable-uids --list-keys --keyid-format=long --with-subkey-fingerprint "$fpr"
        err_file=$(mktemp "$SESSION_TMPDIR/show-key-details-err.XXXXXXXXXX")
        if out=$(gpg --batch --no-default-keyring --keyring "gnupg-ring:${TRUSTED_GPG}" \
                      --list-options show-unusable-subkeys,show-unusable-uids --list-keys --keyid-format=long --with-subkey-fingerprint "$fpr" 2>"$err_file"); then
            printf '%s\n\n' "$out" | sed -E '/^uid/ s/\[[^]]*\][[:space:]]*//'
        else
            out=$(cat "$err_file" 2>/dev/null)
            log "$(eval_gettext "warning: could not read details for '\${fpr}': \${out}")"
            rc=1
        fi
        rm -f "$err_file"
    done
    return "$rc"
}

# --list-known-keys: dumps the hardcoded KNOWN_KEYS allow-list, offline,
# independent of $TRUSTED_GPG.
list_known_keys() {
    printf '%s\n' "${KNOWN_KEYS[@]}"
    return 0
}

# --list-keyservers: prints the resolved KEYSERVERS list this run would try.
list_keyservers() {
    resolve_keyservers
    printf '%s\n' "${KEYSERVERS[@]}"
    return 0
}

# --untrust-key=<fpr>: removes it from $TRUSTED_GPG if present, and
# always records it in $UNTRUSTED_KEYS_FILE, blocking every recognition
# path (KNOWN_KEYS, $TRUSTED_GPG, pubring.kbx trust). Doesn't die if the
# key wasn't cached yet.
untrust_key() {
    local backup_tmp
    setup_gnupg_homedir
    trap cleanup RETURN
    FPR=$UNTRUST_KEY
    if [ -f "$TRUSTED_GPG" ] && [ "$(key_status "$TRUSTED_GPG" "gnupg-ring:")" != missing ]; then
        # Pre-change backup: temp file next to $TRUSTED_GPG (same
        # filesystem, for an atomic "mv"), moved into place only after
        # gpg finishes so gpg's own "~" write can't clobber it.
        backup_tmp=$(mktemp "$(dirname "$TRUSTED_GPG")/trustedkeys-backup.XXXXXXXXXX")
        trap 'rm -f "$backup_tmp"' INT TERM HUP
        cp -pf "$TRUSTED_GPG" "$backup_tmp" 2>/dev/null || true
        debug_cmd gpg --batch --no-default-keyring --keyring "gnupg-ring:${TRUSTED_GPG}" --delete-key --yes "$UNTRUST_KEY"
        if out=$(gpg --batch --no-default-keyring --keyring "gnupg-ring:${TRUSTED_GPG}" --delete-key --yes "$UNTRUST_KEY" 2>&1); then
            trap - INT TERM HUP
            mv -f "$backup_tmp" "${TRUSTED_GPG}~" 2>/dev/null || true
            log "$(eval_gettext "removed \${UNTRUST_KEY} from \${TRUSTED_GPG}")"
        else
            trap - INT TERM HUP
            rm -f "$backup_tmp"
            # `|| return 1`: under `set -e`, a failing die() (LIB_MODE=1) would
            # otherwise abort the whole process instead of unwinding via return.
            die "$(eval_gettext "could not remove '\${UNTRUST_KEY}' from \${TRUSTED_GPG}: \${out}")" || return 1
        fi
    fi
    add_untrusted_override "$UNTRUST_KEY"
    return 0
}

# --export-trusted-keys=<path> [FPR...]: exports keys from $TRUSTED_GPG
# to <path>, ASCII-armored. Zero fingerprints means every key. A
# requested fingerprint not found is a warning, not a hard failure -
# only an empty final set dies. Overwrites <path> unconditionally.
export_trusted_keys() {
    local FPR_MAP WANTED_FPRS raw fpr resolved sub_fpr primary_fpr ERR_FILE ERR EXPORTED_COUNT
    setup_gnupg_homedir
    trap cleanup RETURN
    [ -f "$TRUSTED_GPG" ] || die "$(eval_gettext "no trusted keys saved yet (\${TRUSTED_GPG} doesn't exist)")" || return 1
    FPR_MAP=$(gpg --batch --no-default-keyring --keyring "gnupg-ring:${TRUSTED_GPG}" \
        --with-colons --list-keys 2>/dev/null | build_fpr_primary_map)
    WANTED_FPRS=()
    if [ "${#EXPORT_TRUSTED_KEYS_FPRS[@]}" -eq 0 ]; then
        # No fingerprints given - every primary key, deduplicated.
        while IFS='|' read -r sub_fpr primary_fpr; do
            [ -n "$sub_fpr" ] && [ "$sub_fpr" = "$primary_fpr" ] && WANTED_FPRS+=("$primary_fpr")
        done <<< "$FPR_MAP"
    else
        for raw in "${EXPORT_TRUSTED_KEYS_FPRS[@]}"; do
            fpr=$(printf '%s' "$raw" | tr -d '[:space:]' | tr '[:lower:]' '[:upper:]')
            resolved=$(awk -F'|' -v want="$fpr" '$1==want{print $2; exit}' <<< "$FPR_MAP")
            if [ -z "$resolved" ]; then
                log "$(eval_gettext "warning: fingerprint '\${fpr}' not found in \${TRUSTED_GPG}, skipping")"
            else
                [ "$resolved" != "$fpr" ] \
                    && log "$(eval_gettext "note: '\${fpr}' is a subkey - resolved to its primary key '\${resolved}'")"
                # Dedup: same primary may be requested via its own fpr and a subkey's.
                if ! printf '%s\n' "${WANTED_FPRS[@]-}" | grep -qxF "$resolved"; then
                    WANTED_FPRS+=("$resolved")
                fi
            fi
        done
    fi
    [ "${#WANTED_FPRS[@]}" -gt 0 ] || die "$(gettext "no matching trusted key(s) to export")" || return 1
    debug_cmd gpg --batch --no-default-keyring --keyring "gnupg-ring:${TRUSTED_GPG}" \
        --armor --export-options export-clean --export "${WANTED_FPRS[@]}"
    ERR_FILE=$(mktemp "$SESSION_TMPDIR/export-err.XXXXXXXXXX")
    if ! gpg --batch --no-default-keyring --keyring "gnupg-ring:${TRUSTED_GPG}" \
             --armor --export-options export-clean --export "${WANTED_FPRS[@]}" \
             >"$EXPORT_TRUSTED_KEYS" 2>"$ERR_FILE"; then
        ERR=$(cat "$ERR_FILE")
        rm -f "$ERR_FILE"
        die "$(eval_gettext "could not export to '\${EXPORT_TRUSTED_KEYS}': \${ERR}")" || return 1
    fi
    rm -f "$ERR_FILE"
    EXPORTED_COUNT=${#WANTED_FPRS[@]}
    log "$(eval_gettext "exported \${EXPORTED_COUNT} key(s) to \${EXPORT_TRUSTED_KEYS}")"
    return 0
}

# --inspect-key-file=<path>: read-only inspection of an arbitrary
# external key file via gpg --show-keys - never imports, never touches
# any keyring/agent. One output line per primary key found; refuses if
# the file contains secret key material.
inspect_key_file() {
    local SHOW_OUT
    { [ -f "$INSPECT_KEY_FILE" ] && [ -r "$INSPECT_KEY_FILE" ]; } \
        || die "$(eval_gettext "cannot read '\${INSPECT_KEY_FILE}'")" || return 1
    debug_cmd gpg --batch --with-colons --show-keys "$INSPECT_KEY_FILE"
    if ! SHOW_OUT=$(gpg --batch --with-colons --show-keys "$INSPECT_KEY_FILE" 2>&1); then
        # Same fallback as import_trusted_keys(): a real keyring/keybox
        # container isn't a plain OpenPGP message, so --show-keys rejects
        # it - --keyring's own loader auto-detects keybox vs legacy-ring
        # from a bare path instead.
        debug_cmd gpg --batch --no-default-keyring --keyring "$INSPECT_KEY_FILE" --with-colons --list-keys
        SHOW_OUT=$(gpg --batch --no-default-keyring --keyring "$INSPECT_KEY_FILE" --with-colons --list-keys 2>&1) \
            || die "$(eval_gettext "'\${INSPECT_KEY_FILE}' does not look like a valid OpenPGP key file: \${SHOW_OUT}")" || return 1
    fi
    printf '%s\n' "$SHOW_OUT" | colon_stream_has_secret_key \
        && { die "$(eval_gettext "'\${INSPECT_KEY_FILE}' contains private/secret key material - refusing to treat it as a public trusted-signing key")" || return 1; }
    printf '%s\n' "$SHOW_OUT" | parse_colon_key_listing | add_known_flag | sort -t'|' -k3,3f
    return 0
}

# True if $FPR has marginal/full/ultimate ownertrust in pubring.kbx.
# Ownertrust lives in trustdb.gpg, scoped to the whole GNUPGHOME, so this
# deliberately queries the real ~/.gnupg (read-only) rather than our
# isolated one. Merely being present in pubring.kbx does NOT count - only
# an explicit trust assignment (Seahorse, or `gpg --edit-key ... trust`) does.
pubring_key_explicitly_trusted() {
    local line trust
    [ -f "$PUBRING_KBX" ] || return 1
    debug_cmd env "GNUPGHOME=$HOME/.gnupg" gpg --batch --no-default-keyring --keyring "$PUBRING_KBX" --with-colons --list-keys "$FPR"
    line=$(env "GNUPGHOME=$HOME/.gnupg" gpg --batch --no-default-keyring \
               --keyring "$PUBRING_KBX" --with-colons --list-keys "$FPR" \
               2>/dev/null | grep '^pub:' | head -n1) || true
    [ -n "$line" ] || return 1
    trust=$(printf '%s' "$line" | cut -d: -f9)
    case "$trust" in
        m|f|u) return 0 ;;
        *) return 1 ;;
    esac
}

# True if $FPR is on the user's own "never auto-trust this again" list -
# checked directly, or via $FPR's own primary key if resolvable against a
# local ring, so untrusting a primary also blocks a not-yet-seen subkey.
key_explicitly_untrusted() {
    [ -f "$UNTRUSTED_KEYS_FILE" ] || return 1
    grep -qxF "$FPR" "$UNTRUSTED_KEYS_FILE" 2>/dev/null && return 0
    local primary=""
    primary=$(find_primary_fpr_of "$FPR" "$TRUSTED_GPG" "gnupg-ring:") || primary=""
    [ -n "$primary" ] || primary=$(find_primary_fpr_of "$FPR" "$MX_GPG_KEYRING_RING" "$MX_GPG_KEYRING_PREFIX") || primary=""
    [ -n "$primary" ] || primary=$(find_primary_fpr_of "$FPR" "$PUBRING_KBX" "") || primary=""
    [ -n "$primary" ] && [ "$primary" != "$FPR" ] || return 1
    grep -qxF "$primary" "$UNTRUSTED_KEYS_FILE" 2>/dev/null
}

# Records $1 (idempotent).
add_untrusted_override() {
    local fpr=$1
    mkdir -p "$(dirname "$UNTRUSTED_KEYS_FILE")"
    grep -qxF "$fpr" "$UNTRUSTED_KEYS_FILE" 2>/dev/null && return 0
    printf '%s\n' "$fpr" >> "$UNTRUSTED_KEYS_FILE"
    chmod 600 "$UNTRUSTED_KEYS_FILE"
}

# Clears $1 from the override list, if present.
remove_untrusted_override() {
    local fpr=$1 tmp
    [ -f "$UNTRUSTED_KEYS_FILE" ] || return 0
    grep -qxF "$fpr" "$UNTRUSTED_KEYS_FILE" 2>/dev/null || return 0
    tmp=$(mktemp "$SESSION_TMPDIR/untrusted-keys.XXXXXXXXXX")
    grep -vxF "$fpr" "$UNTRUSTED_KEYS_FILE" > "$tmp" 2>/dev/null || true
    mv "$tmp" "$UNTRUSTED_KEYS_FILE"
    log "$(eval_gettext "\${fpr} removed from the explicitly-untrusted list - it can be auto-recognized again")"
}

# Exports $FPR out of keyring $1 (prefix $2, see key_status) into a fresh
# legacy-format keyring gpgv can read, pointing VERIFY_RING at it. Returns
# non-zero, leaving VERIFY_RING untouched, if the export/import fails, so
# the caller can fall back to the keyserver.
#
# --export-options is export-clean only, NOT export-clean,export-minimal:
# export-minimal drops an expired subkey entirely, even when its
# signature over the ISO predates its own expiry and would otherwise
# still verify fine.
export_to_tmp_ring() {
    local ring=$1 prefix=$2 tmp out
    tmp=$(mktemp "$SESSION_TMPDIR/gnupg-ring.XXXXXXXXXX.gpg")
    local EXPORT_ARGS=(
        --batch
        --no-default-keyring
        --keyring "${prefix}${ring}"
        --export-options export-clean
        --export "$FPR"
    )
    local IMPORT_ARGS=(
        --batch
        --no-default-keyring
        --keyring "gnupg-ring:${tmp}"
        --import-options import-clean,import-minimal
        --import
    )
    debug_cmd gpg "${EXPORT_ARGS[@]}"
    debug_cmd gpg "${IMPORT_ARGS[@]}"
    if out=$( { gpg "${EXPORT_ARGS[@]}" | gpg "${IMPORT_ARGS[@]}"; } 2>&1 ); then
        TMP_RING=$tmp
        VERIFY_RING=$tmp
        return 0
    fi
    log "$(eval_gettext "warning: could not export key from \${ring}: \${out}")"
    rm -f "$tmp" "${tmp}~"
    return 1
}

# Exports $FPR out of keyring $1 (prefix $2) into TRUSTED_GPG. Used both
# after a fresh fetch (--keep/--keep-key) and after --trust-key verifies
# an unrecognized key successfully. export-clean only - see
# export_to_tmp_ring()'s comment for why.
keep_in_trusted_gpg() {
    local ring=$1 prefix=$2 rc status_file backup_tmp=""
    local EXPORT_ARGS=(
        --batch
        --no-default-keyring
        --keyring "${prefix}${ring}"
        --export-options export-clean
        --export "$FPR"
    )
    status_file=$(mktemp "$SESSION_TMPDIR/import-status.XXXXXXXXXX")
    local IMPORT_ARGS=(
        --batch
        --no-default-keyring
        --keyring "gnupg-ring:${TRUSTED_GPG}"
        --status-fd 3
        --import-options import-clean,import-minimal
        --import
    )
    # Pre-change backup - see untrust_key()'s own copy of this comment.
    if [ -f "$TRUSTED_GPG" ]; then
        backup_tmp=$(mktemp "$(dirname "$TRUSTED_GPG")/trustedkeys-backup.XXXXXXXXXX")
        trap 'rm -f "$backup_tmp"' INT TERM HUP
        cp -pf "$TRUSTED_GPG" "$backup_tmp" 2>/dev/null || true
    fi
    debug_cmd gpg "${EXPORT_ARGS[@]}"
    debug_cmd gpg "${IMPORT_ARGS[@]}"
    out=$( { gpg "${EXPORT_ARGS[@]}" | gpg "${IMPORT_ARGS[@]}" 3>"$status_file"; } 2>&1 )
    rc=$?
    # IMPORT_RES status-fd fields (never localized, unlike gpg's human
    # text): count, no_user_id, imported, imported_rsa, unchanged, ...
    STATUS_LINES=$(cat "$status_file" 2>/dev/null)
    rm -f "$status_file"
    trap - INT TERM HUP
    if [ "$rc" -eq 0 ]; then
        if [ -n "$backup_tmp" ]; then
            mv -f "$backup_tmp" "${TRUSTED_GPG}~" 2>/dev/null || true
        fi
        remove_untrusted_override "$FPR"
    else
        if [ -n "$backup_tmp" ]; then
            rm -f "$backup_tmp"
        fi
    fi
    return "$rc"
}

# --import-trusted-keys=<path> [FPR...]: imports keys from an arbitrary
# external key file into $TRUSTED_GPG. Same trailing-fingerprint/
# normalization/subkey-resolution behavior as --export-trusted-keys.
#
# Secret key material is refused via --show-keys BEFORE any gpg --import
# call - --import of a secret key writes real private key material into
# $GNUPGHOME/private-keys-v1.d/ regardless of which --keyring the public
# parts targeted, which on a shared gpg-agent can leak it outside this
# script's own throwaway GNUPGHOME. --show-keys never touches any
# agent/keyring, so checking with it first keeps this safe.
#
# PROCESSED/IMPORTED/CHANGED/UNCHANGED_COUNT are real globals, not just
# status_out() tags - a sourcing caller reads them directly afterward.
import_trusted_keys() {
    local FPR_MAP WANTED_FPRS raw fpr resolved sub_fpr primary_fpr SHOW_OUT IMPORT_SOURCE
    setup_gnupg_homedir
    trap cleanup RETURN
    { [ -f "$IMPORT_TRUSTED_KEYS" ] && [ -r "$IMPORT_TRUSTED_KEYS" ]; } \
        || die "$(eval_gettext "cannot read '\${IMPORT_TRUSTED_KEYS}'")" || return 1
    debug_cmd gpg --batch --with-colons --show-keys "$IMPORT_TRUSTED_KEYS"
    IMPORT_SOURCE=$IMPORT_TRUSTED_KEYS
    if SHOW_OUT=$(gpg --batch --with-colons --show-keys "$IMPORT_TRUSTED_KEYS" 2>&1); then
        :
    else
        # Not a plain OpenPGP message - a real keyring/keybox CONTAINER
        # file (e.g. mx-gpg-keyring, or a copy of pubring.kbx) fails
        # --show-keys even though it's perfectly valid. Retry via
        # --keyring, whose loader auto-detects keybox vs legacy-ring from
        # a bare path, same as $PUBRING_KBX's usage elsewhere.
        debug_cmd gpg --batch --no-default-keyring --keyring "$IMPORT_TRUSTED_KEYS" --with-colons --list-keys
        if ! SHOW_OUT=$(gpg --batch --no-default-keyring --keyring "$IMPORT_TRUSTED_KEYS" --with-colons --list-keys 2>&1); then
            die "$(eval_gettext "'\${IMPORT_TRUSTED_KEYS}' does not look like a valid OpenPGP key file: \${SHOW_OUT}")" || return 1
        fi
        # A real keyring/keybox never embeds secret key material itself
        # (GnuPG keeps that separate, in private-keys-v1.d) - re-export
        # into a portable OpenPGP stream for the actual import step below.
        IMPORT_SOURCE=$(mktemp "$SESSION_TMPDIR/import-source.XXXXXXXXXX.gpg")
        gpg --batch --no-default-keyring --keyring "$IMPORT_TRUSTED_KEYS" --export >"$IMPORT_SOURCE" 2>/dev/null
    fi
    printf '%s\n' "$SHOW_OUT" | colon_stream_has_secret_key \
        && { die "$(eval_gettext "'\${IMPORT_TRUSTED_KEYS}' contains private/secret key material - refusing to import it as a public trusted-signing key")" || return 1; }
    FPR_MAP=$(printf '%s\n' "$SHOW_OUT" | build_fpr_primary_map)
    [ -n "$FPR_MAP" ] || die "$(eval_gettext "no OpenPGP keys found in '\${IMPORT_TRUSTED_KEYS}'")" || return 1
    # Only reached once the file is confirmed to contain no secret key material.
    TMP_RING=$(mktemp "$SESSION_TMPDIR/import-ring.XXXXXXXXXX.gpg")
    # import-clean only, not import-clean,import-minimal: this is raw
    # externally-supplied input, not gpg's own sanitized re-export.
    debug_cmd gpg --batch --no-default-keyring --keyring "gnupg-ring:${TMP_RING}" \
        --import-options import-clean --import "$IMPORT_SOURCE"
    gpg --batch --no-default-keyring --keyring "gnupg-ring:${TMP_RING}" \
        --import-options import-clean --import "$IMPORT_SOURCE" >/dev/null 2>&1 \
        || die "$(eval_gettext "could not import '\${IMPORT_TRUSTED_KEYS}' into a scratch keyring")" || return 1
    WANTED_FPRS=()
    if [ "${#IMPORT_TRUSTED_KEYS_FPRS[@]}" -eq 0 ]; then
        while IFS='|' read -r sub_fpr primary_fpr; do
            [ -n "$sub_fpr" ] && [ "$sub_fpr" = "$primary_fpr" ] && WANTED_FPRS+=("$primary_fpr")
        done <<< "$FPR_MAP"
    else
        for raw in "${IMPORT_TRUSTED_KEYS_FPRS[@]}"; do
            fpr=$(printf '%s' "$raw" | tr -d '[:space:]' | tr '[:lower:]' '[:upper:]')
            resolved=$(awk -F'|' -v want="$fpr" '$1==want{print $2; exit}' <<< "$FPR_MAP")
            if [ -z "$resolved" ]; then
                log "$(eval_gettext "warning: fingerprint '\${fpr}' not found in '\${IMPORT_TRUSTED_KEYS}', skipping")"
            else
                [ "$resolved" != "$fpr" ] \
                    && log "$(eval_gettext "note: '\${fpr}' is a subkey - resolved to its primary key '\${resolved}'")"
                if ! printf '%s\n' "${WANTED_FPRS[@]-}" | grep -qxF "$resolved"; then
                    WANTED_FPRS+=("$resolved")
                fi
            fi
        done
    fi
    [ "${#WANTED_FPRS[@]}" -gt 0 ] || die "$(gettext "no matching key(s) to import")" || return 1
    # Three outcomes per key, via IMPORT_RES's status-fd fields: field 5
    # ("imported")=1 only for a genuinely new primary key; field 7
    # ("unchanged")=1 only for a byte-identical re-import; both 0 means
    # the key already existed but gained something (new UID/subkey/sig).
    IMPORTED=0
    CHANGED=0
    UNCHANGED_COUNT=0
    for FPR in "${WANTED_FPRS[@]}"; do
        if keep_in_trusted_gpg "$TMP_RING" "gnupg-ring:"; then
            if [ "$(status_field IMPORT_RES 5)" = "1" ]; then
                IMPORTED=$((IMPORTED + 1))
            elif [ "$(status_field IMPORT_RES 7)" = "1" ]; then
                UNCHANGED_COUNT=$((UNCHANGED_COUNT + 1))
            else
                CHANGED=$((CHANGED + 1))
            fi
        else
            log "$(eval_gettext "warning: could not import '\${FPR}' into \${TRUSTED_GPG}")"
        fi
    done
    PROCESSED=$((IMPORTED + CHANGED + UNCHANGED_COUNT))
    [ "$PROCESSED" -gt 0 ] || die "$(eval_gettext "no key(s) were actually imported into \${TRUSTED_GPG}")" || return 1
    # TRANSLATORS: this exact line is also parsed by the GUI's own
    # --status-fd tag right after it (IMPORT_SUMMARY, never localized) -
    # translate freely, the numbers/word order can differ per language,
    # only this line's own display text changes.
    log "$(eval_gettext "\${PROCESSED} key(s) processed: \${IMPORTED} new, \${CHANGED} updated, \${UNCHANGED_COUNT} unchanged")"
    status_out "IMPORT_SUMMARY $PROCESSED $IMPORTED $CHANGED $UNCHANGED_COUNT"
    return 0
}

# Exports $FPR out of $VERIFY_RING into $1, classic keyring format - lets
# a caller (the GUI) pass that path back in as --from-ring later, reusing
# the fetched copy instead of hitting the keyserver again. Best-effort.
export_key_to() {
    local dest=$1 rc
    local EXPORT_ARGS=(
        --batch
        --no-default-keyring
        --keyring "gnupg-ring:${VERIFY_RING}"
        --export-options export-clean
        --export "$FPR"
    )
    local IMPORT_ARGS=(
        --batch
        --no-default-keyring
        --keyring "gnupg-ring:${dest}"
        --import-options import-clean,import-minimal
        --import
    )
    debug_cmd gpg "${EXPORT_ARGS[@]}"
    debug_cmd gpg "${IMPORT_ARGS[@]}"
    gpg "${EXPORT_ARGS[@]}" 2>/dev/null | gpg "${IMPORT_ARGS[@]}" >/dev/null 2>&1
    rc=$?
    rm -f "${dest}~"
    return "$rc"
}

# $ISO's basename ends in ".$1" (one of PLAIN_CHECKSUM_SUFFIXES). Two
# cases: A) the checksum file itself is signed - handed to try_
# checksum_fallback() via CHECKSUM_FILE_OVERRIDE/CHECKSUM_ALGO_OVERRIDE.
# B) unsigned, but the ISO has its own direct signature - use that
# instead (redirected to A if that signature is itself clearsigned).
# Neither found - refuses outright.
resolve_plain_checksum_as_iso() {
    local suffix=$1 candidate sigext iso_base candidate_base resolved_base candidate_sig_base \
        dir line hash rest mode fname iso_candidates=() candidates_joined clearsigned_base

    candidate=${ISO%."$suffix"}
    if [ ! -f "$candidate" ]; then
        # Filename-derived guess doesn't exist - fall back to content:
        # use the one named file that's actually present, same approach
        # as resolve_checksum_listing_as_iso()'s own content scan.
        dir=$(dirname "$ISO")
        while IFS= read -r line || [ -n "$line" ]; do
            hash=${line%% *}
            rest=${line#* }
            mode=${rest:0:1}
            fname=${rest:1}
            case "$mode" in
                ' '|'*') ;;
                *) continue ;;
            esac
            [ -r "$dir/$fname" ] && iso_candidates+=("$fname")
        done < "$ISO"
        case "${#iso_candidates[@]}" in
            1) candidate="$dir/${iso_candidates[0]}" ;;
            0)
                status_out "PLAIN_CHECKSUM_TARGET_MISSING"
                candidate_base=$(basename "$candidate")
                # Emitted even on this failure path so a caller like the GUI still
                # learns the intended ISO name, instead of the raw ".sha512"-suffixed one.
                status_out "RESOLVED_ISO $candidate_base"
                RESOLVED_ISO=$candidate_base
                die "$(eval_gettext "neither '\${candidate_base}' nor a signature file for it were found in this folder - nothing to check yet.")" || return 1
                ;;
            *)
                iso_base=$(basename "$ISO")
                candidates_joined="${iso_candidates[*]}"
                status_out "PLAIN_CHECKSUM_TARGET_AMBIGUOUS"
                die "$(eval_gettext "'\${iso_base}' names multiple files present in this folder: \${candidates_joined} - point this tool at the specific .iso file you want to check instead.")" || return 1
                ;;
        esac
    fi

    for sigext in "${CHECKSUM_SIG_EXTS[@]}"; do
        if [ -r "${ISO}.${sigext}" ]; then
            CHECKSUM_FILE_OVERRIDE=$ISO
            CHECKSUM_ALGO_OVERRIDE="${suffix}sum"
            ISO=$candidate
            NO_DIRECT_SIG=1
            resolved_base=$(basename "$ISO")
            log "$(eval_gettext "resolved ISO: \${resolved_base}")"
            status_out "RESOLVED_ISO $resolved_base"
            RESOLVED_ISO=$resolved_base
            return 0
        fi
    done

    # Same precedence as bare-ISO auto-discovery: clearsigned checksum
    # wins over a direct signature.
    if find_clearsigned_plain_checksum "$candidate"; then
        iso_base=$(basename "$ISO")
        clearsigned_base=$(basename "$CLEARSIGNED_PLAIN_CHECKSUM")
        log "$(eval_gettext "'\${iso_base}' is unsigned - not used for verification. Using '\${clearsigned_base}' instead, a self-contained clearsigned checksum for the ISO.")"
        CHECKSUM_FILE_OVERRIDE=$CLEARSIGNED_PLAIN_CHECKSUM
        ISO=$candidate
        NO_DIRECT_SIG=1
        resolved_base=$(basename "$ISO")
        log "$(eval_gettext "resolved ISO: \${resolved_base}")"
        status_out "RESOLVED_ISO $resolved_base"
        RESOLVED_ISO=$resolved_base
        return 0
    fi

    for sigext in "${CHECKSUM_SIG_EXTS[@]}"; do
        if [ -r "${candidate}.${sigext}" ]; then
            if is_clearsigned_file "${candidate}.${sigext}"; then
                # The ISO's would-be direct signature is actually a full
                # clearsigned checksum listing - use it as that instead.
                CHECKSUM_FILE_OVERRIDE="${candidate}.${sigext}"
                ISO=$candidate
                NO_DIRECT_SIG=1
                resolved_base=$(basename "$ISO")
                log "$(eval_gettext "resolved ISO: \${resolved_base}")"
                status_out "RESOLVED_ISO $resolved_base"
                RESOLVED_ISO=$resolved_base
                return 0
            fi
            iso_base=$(basename "$ISO")
            candidate_sig_base="$(basename "$candidate").${sigext}"
            log "$(eval_gettext "'\${iso_base}' is unsigned - not used for verification. Using '\${candidate_sig_base}' instead, the ISO's own direct signature.")"
            ISO=$candidate
            SIG="${candidate}.${sigext}"
            NO_DIRECT_SIG=0
            resolved_base=$(basename "$ISO")
            log "$(eval_gettext "resolved ISO: \${resolved_base}")"
            status_out "RESOLVED_ISO $resolved_base"
            RESOLVED_ISO=$resolved_base
            status_out "RESOLVED_SIG $candidate_sig_base"
            return 0
        fi
    done

    status_out "PLAIN_CHECKSUM_NOTHING_VERIFIABLE"
    iso_base=$(basename "$ISO")
    candidate_base=$(basename "$candidate")
    status_out "RESOLVED_ISO $candidate_base"
    RESOLVED_ISO=$candidate_base
    die "$(eval_gettext "'\${iso_base}' is an unsigned checksum file, and '\${candidate_base}' has no direct .sig/.asc/.gpg/.sign of its own, nor a self-contained clearsigned checksum (e.g. '.sha512.asc') either - nothing here can be cryptographically verified.")" || return 1
}

# If $ISO is itself a known checksum-listing filename, gpgv would still
# "verify" it without ever checking an actual ISO's hash - redirect to
# the one ISO it mentions that's unambiguously present in the same
# directory, or refuse with specific guidance otherwise.
resolve_checksum_listing_as_iso() {
    local dir line hash rest mode fname iso_candidates=() hashcmd="" algo_entry sigfile sigext \
        iso_base algo sigfile_base resolved_base candidates_joined

    # Resolve algo/signature-file info up front for the two failure
    # branches below to log. Not logged in the success (1-candidate) case -
    # try_checksum_fallback logs the same two lines right after.
    for algo_entry in "${CHECKSUM_FILES[@]}"; do
        if [ "$(basename "$ISO")" = "${algo_entry%%|*}" ]; then
            hashcmd=${algo_entry#*|}
            break
        fi
    done
    sigfile=""
    for sigext in "${CHECKSUM_SIG_EXTS[@]}"; do
        [ -r "${ISO}.${sigext}" ] && { sigfile="${ISO}.${sigext}"; break; }
    done

    dir=$(dirname "$ISO")
    # A detached signature can exist without its listing (e.g. only
    # "SHA256SUMS.gpg" left after "SHA256SUMS" was deleted) - without
    # this check, the read-loop below would crash under set -e instead
    # of a translated die().
    if ! { [ -f "$ISO" ] && [ -r "$ISO" ]; }; then
        iso_base=$(basename "$ISO")
        # SIG_WITHOUT_ISO's sibling, one level up (a listing's own
        # signature, not a plain ISO's).
        if [ "${ISO_DERIVED_FROM_SIG:-0}" -eq 1 ]; then
            status_out "SIG_WITHOUT_CHECKSUM_LISTING"
            die "$(eval_gettext "this is a signature file for a checksum listing, but '\${iso_base}' isn't in the same folder")" || return 1
        fi
        status_out "NOTHING_TO_VERIFY"
        die "$(eval_gettext "cannot read checksum listing '\${iso_base}'")" || return 1
    fi
    while IFS= read -r line || [ -n "$line" ]; do
        # BSD/tag-style line (e.g. Fedora's "ALGO (name) = hash") - same
        # format find_checksum_line() recognizes. Opportunistically fills
        # in $hashcmd for the failure-branch logging below when this
        # listing's own name isn't one of the fixed $CHECKSUM_FILES
        # entries (so $hashcmd is still unset at this point).
        if [[ $line =~ ^([A-Za-z0-9-]+)\ \((.+)\)\ =\ ([0-9a-fA-F]+)$ ]]; then
            fname=${BASH_REMATCH[2]}
            [ -n "$hashcmd" ] || hashcmd="$(printf '%s' "${BASH_REMATCH[1]}" | tr '[:upper:]' '[:lower:]')sum"
            case "$fname" in
                *.iso) [ -r "$dir/$fname" ] && iso_candidates+=("$fname") ;;
            esac
            continue
        fi
        hash=${line%% *}
        rest=${line#* }
        mode=${rest:0:1}
        fname=${rest:1}
        case "$mode" in
            ' '|'*') ;;
            *) continue ;;
        esac
        case "$fname" in
            *.iso) [ -r "$dir/$fname" ] && iso_candidates+=("$fname") ;;
        esac
    done < "$ISO"

    case "${#iso_candidates[@]}" in
        0)
            iso_base=$(basename "$ISO")
            algo=${hashcmd%sum}
            log "$(eval_gettext "using checksum file: \${iso_base} (\${algo})")"
            status_out "CHECKSUM_FILE $iso_base ($algo)"
            CHECKSUM_INFO="$iso_base ($algo)"
            if [ -n "$sigfile" ]; then
                sigfile_base=$(basename "$sigfile")
                log "$(eval_gettext "checksum file signature: \${sigfile_base}")"
                status_out "CHECKSUM_SIG_INFO $sigfile_base"
                CHECKSUM_SIG_INFO=$sigfile_base
            fi
            status_out "CHECKSUM_LISTING_AMBIGUOUS"
            die "$(eval_gettext "'\${iso_base}' is a checksum listing (it can cover many ISOs), not a single ISO to verify directly - none of the .iso files it mentions are present in '\${dir}'. Point this tool at the actual .iso file instead (its checksum/signature will be found and checked automatically).")" || return 1
            ;;
        1)
            CHECKSUM_FILE_OVERRIDE=$ISO
            ISO="$dir/${iso_candidates[0]}"
            NO_DIRECT_SIG=1
            SIG="${ISO}.sig"
            # Lets the GUI show the actual resolved ISO instead of the
            # checksum listing's own name in its result heading.
            resolved_base=$(basename "$ISO")
            log "$(eval_gettext "resolved ISO: \${resolved_base}")"
            status_out "RESOLVED_ISO $resolved_base"
            RESOLVED_ISO=$resolved_base
            ;;
        *)
            iso_base=$(basename "$ISO")
            algo=${hashcmd%sum}
            log "$(eval_gettext "using checksum file: \${iso_base} (\${algo})")"
            status_out "CHECKSUM_FILE $iso_base ($algo)"
            CHECKSUM_INFO="$iso_base ($algo)"
            if [ -n "$sigfile" ]; then
                sigfile_base=$(basename "$sigfile")
                log "$(eval_gettext "checksum file signature: \${sigfile_base}")"
                status_out "CHECKSUM_SIG_INFO $sigfile_base"
                CHECKSUM_SIG_INFO=$sigfile_base
            fi
            status_out "CHECKSUM_LISTING_AMBIGUOUS"
            candidates_joined="${iso_candidates[*]}"
            die "$(eval_gettext "'\${iso_base}' is a checksum listing covering multiple ISOs present in '\${dir}': \${candidates_joined} - point this tool at the specific .iso file you want to check instead.")" || return 1
            ;;
    esac
}

# Looks $1 (a hex digest length) up in $HASHCMD_BY_LENGTH, echoing the
# matching hashcmd or nothing if the length matches none of them. Safe
# to call via "$(...)" - unlike find_checksum_line() below, this has no
# side effects.
hashcmd_by_length() {
    local len=$1 entry
    for entry in "${HASHCMD_BY_LENGTH[@]}"; do
        [ "$len" = "${entry%%|*}" ] && { printf '%s' "${entry#*|}"; return 0; }
    done
}

# Looks lowercased tag name $1 up in $BSD_TAG_TO_HASHCMD, echoing the
# matching hashcmd or nothing if the name isn't recognized. Safe to call
# via "$(...)" - no side effects.
hashcmd_by_tag() {
    local tag=$1 entry
    for entry in "${BSD_TAG_TO_HASHCMD[@]}"; do
        [ "$tag" = "${entry%%|*}" ] && { printf '%s' "${entry#*|}"; return 0; }
    done
    # BLAKE2b's variable-length tag ("BLAKE2b-N", bit count) - "b2sum -c"
    # reads the length from the tag itself, no --length flag needed back.
    case "$tag" in
        blake2b-[0-9]*) printf '%s' "b2sum" ;;
    esac
}

# Finds the line in checksum file $1 that names basename $2 exactly, or
# returns non-zero. Recognizes both GNU coreutils style ("<hash>  <name>"
# or "<hash> *<name>") and BSD/tag style ("ALGO (<name>) = <hash>", e.g.
# Fedora's convention) - real checksum files list decoy entries too (e.g.
# Debian's own "<iso>.log"/"<iso>.packages"/"<iso>.contents"). Sets
# MATCHED_LINE, FOUND_HASHCMD (the real command to run) and
# FOUND_HASH_LABEL (the fuller name to show a user, e.g. "blake2b-400"
# vs just "b2sum") - both left empty if the algorithm can't be
# resolved. Sets globals directly rather than echoing to stdout, so
# callers must NOT invoke this via "$(...)" command substitution - that
# forks a subshell and the side effects would be lost.
find_checksum_line() {
    local file=$1 target=$2 line hash rest mode fname tag_algo tag_cmd len_cmd len_algo hash_len
    while IFS= read -r line || [ -n "$line" ]; do
        if [[ $line =~ ^([A-Za-z0-9-]+)\ \((.+)\)\ =\ ([0-9a-fA-F]+)$ ]]; then
            if [ "${BASH_REMATCH[2]}" = "$target" ]; then
                tag_algo=$(printf '%s' "${BASH_REMATCH[1]}" | tr '[:upper:]' '[:lower:]')
                tag_cmd=$(hashcmd_by_tag "$tag_algo")
                if [ -n "$tag_cmd" ] && command -v "$tag_cmd" >/dev/null 2>&1; then
                    FOUND_HASHCMD=$tag_cmd
                    FOUND_HASH_LABEL=$tag_algo
                else
                    hash_len=${#BASH_REMATCH[3]}
                    len_cmd=$(hashcmd_by_length "$hash_len")
                    if [ -n "$len_cmd" ]; then
                        len_algo=${len_cmd%sum}
                        warn "$(eval_gettext "this line claims \${tag_algo}, but its hash is \${hash_len} characters long, which looks like \${len_algo} instead - using \${len_algo}")"
                        FOUND_HASHCMD=$len_cmd
                        FOUND_HASH_LABEL=$len_algo
                    else
                        FOUND_HASHCMD=""
                        FOUND_HASH_LABEL=""
                    fi
                fi
                MATCHED_LINE=$line
                return 0
            fi
            continue
        fi
        hash=${line%% *}
        rest=${line#* }
        mode=${rest:0:1}
        fname=${rest:1}
        case "$mode" in
            ' '|'*') ;;
            *) continue ;;
        esac
        if [ "$fname" = "$target" ]; then
            FOUND_HASHCMD=""
            if [[ $hash =~ ^[0-9a-fA-F]+$ ]]; then
                FOUND_HASHCMD=$(hashcmd_by_length "${#hash}")
            fi
            FOUND_HASH_LABEL=${FOUND_HASHCMD%sum}
            MATCHED_LINE=$line
            return 0
        fi
    done < "$file"
    return 1
}

# Searches $ISO's own directory for a known, signed checksum-listing
# file that names $ISO by its exact basename (or uses the
# --checksum-file/--checksum-algo override instead of searching),
# verifies that checksum file's own signature by re-invoking this same
# script as a subprocess, then verifies $ISO's own hash against the
# matched line. Exits directly on success or either failure mode;
# returns normally only if nothing applicable was found at all.
try_checksum_fallback() {
    local dir base candidates entry name hashcmd sigfile sigext glob \
        match_line child_rc hash_rc tried_names=() plain_file \
        name_base algo sigfile_base tried_joined checksum_sig_missing=0 \
        override_base unsigned_matches=() peek_file name_algo content_algo \
        override_is_glob nocaseglob_was candidates_i

    dir=$(dirname "$ISO")
    base=$(basename "$ISO")

    candidates=()
    if [ -n "$CHECKSUM_FILE_OVERRIDE" ]; then
        hashcmd="$CHECKSUM_ALGO_OVERRIDE"
        if [ -z "$hashcmd" ]; then
            for entry in "${CHECKSUM_FILES[@]}"; do
                if [ "$(basename "$CHECKSUM_FILE_OVERRIDE")" = "${entry%%|*}" ]; then
                    hashcmd=${entry#*|}
                    break
                fi
            done
        fi
        # Left empty for a clearsigned override or a $CHECKSUM_FILE_GLOBS
        # match (no fixed hashcmd) - resolved per-line further down instead.
        if [ -z "$hashcmd" ] && ! is_clearsigned_file "$CHECKSUM_FILE_OVERRIDE"; then
            override_is_glob=0
            override_base=$(basename "$CHECKSUM_FILE_OVERRIDE")
            for glob in "${CHECKSUM_FILE_GLOBS[@]}"; do
                # shellcheck disable=SC2254
                # Unquoted on purpose: $glob must expand as a glob
                # pattern here (e.g. "*checksum"), not match literally.
                case "${override_base,,}" in $glob) override_is_glob=1; break ;; esac
            done
            if [ "$override_is_glob" -eq 0 ]; then
                # TRANSLATORS: ${FLAG_CHECKSUM_ALGO}/${FLAG_CHECKSUM_FILE} are literal flag names - keep placeholders as-is.
                die "$(eval_gettext "\${FLAG_CHECKSUM_ALGO} is required when \${FLAG_CHECKSUM_FILE} names a file not in the built-in list (SHA512SUMS, SHA256SUMS, sha256sum.txt, ...)")" || return 1
            fi
        fi
        candidates=("$CHECKSUM_FILE_OVERRIDE|$hashcmd")
    else
        for entry in "${CHECKSUM_FILES[@]}"; do
            candidates+=("$dir/${entry%%|*}|${entry#*|}")
        done
        # Release-specific listing names (e.g. Fedora's "<spin>-<version>-
        # <arch>-CHECKSUM") - hashcmd left empty, resolved per-line below.
        # nocaseglob: this is real pathname expansion against files on
        # disk, not a case pattern match (case-insensitive there via
        # ${var,,} instead) - restored right after, scoped tightly.
        # shopt -p's own exit status reflects whether the option is
        # currently enabled, not whether the query itself succeeded -
        # "|| true" needed so a normally-disabled nocaseglob doesn't
        # trip errexit right here.
        nocaseglob_was=$(shopt -p nocaseglob) || true
        shopt -s nocaseglob
        for glob in "${CHECKSUM_FILE_GLOBS[@]}"; do
            for name in "$dir"/$glob; do
                [ -r "$name" ] && candidates+=("$name|")
            done
        done
        eval "$nocaseglob_was"
        # A plain per-ISO checksum with its own detached signature
        # (openSUSE's convention, e.g. "<iso>.sha256" + "<iso>.sha256.asc") -
        # tried last, after every shared-listing shape.
        for suffix in "${PLAIN_CHECKSUM_SUFFIXES[@]}"; do
            candidates+=("$dir/${base}.${suffix}|${suffix}sum")
        done
        # Clearsigned per-ISO checksum, e.g. "<iso>.sha512.asc" - only
        # added when actually clearsigned, so the detached case above
        # isn't double-added.
        for suffix in "${PLAIN_CHECKSUM_SUFFIXES[@]}"; do
            for sigext in "${CHECKSUM_SIG_EXTS[@]}"; do
                if [ -r "$dir/${base}.${suffix}.${sigext}" ] \
                   && is_clearsigned_file "$dir/${base}.${suffix}.${sigext}"; then
                    candidates+=("$dir/${base}.${suffix}.${sigext}|${suffix}sum")
                fi
            done
        done
    fi

    # Index-based, not "for entry in ${candidates[@]}" - a "for...in"
    # snapshots the array once, so a clearsigned sigfile appended as a
    # new candidate below would never be reached.
    candidates_i=0
    while [ "$candidates_i" -lt "${#candidates[@]}" ]; do
        entry=${candidates[$candidates_i]}
        candidates_i=$((candidates_i + 1))
        name=${entry%%|*}
        hashcmd=${entry#*|}
        [ -r "$name" ] || continue

        if is_clearsigned_file "$name"; then
            # Cheap relevance pre-check: gpgv writes a cleartext-signed
            # message's plaintext to --output even with an empty keyring
            # (data-format parsing, not crypto) - avoids a real keyserver
            # fetch for a candidate that turns out irrelevant. Never
            # trusted for anything else.
            peek_file=$(mktemp "$SESSION_TMPDIR/checksum-peek.XXXXXXXXXX")
            debug_cmd gpgv --keyring /dev/null --output "$peek_file" "$name"
            gpgv --keyring /dev/null --output "$peek_file" "$name" >/dev/null 2>&1 || true
            if ! find_checksum_line "$peek_file" "$base"; then
                rm -f "$peek_file"
                continue
            fi
            rm -f "$peek_file"

            tried_names+=("$(basename "$name")")

            # --is-cached: skip the real verify/child-re-exec below, just
            # report the signing key's cache status (same shortcut the
            # detached-checksum-listing branch further down already has).
            if [ "$IS_CACHED" -eq 1 ]; then
                debug_cmd gpgv --status-fd 3 "$name"
                run_gpgv_status "$name"
                FPR=$(status_field ERRSIG 9)
                [ -n "$FPR" ] || FPR=$(printf '%s' "$GPGV_OUTPUT" | grep -m1 -oE '[[:xdigit:]]{40}' || true)
                [ -n "$FPR" ] || return 1
                key_material_usable "$(key_status "$TRUSTED_GPG" "gnupg-ring:")" && return 0
                key_material_usable "$(key_status "$PUBRING_KBX" "")" && return 0
                key_material_usable "$(key_status "$MX_GPG_KEYRING_RING" "$MX_GPG_KEYRING_PREFIX")" && return 0
                return 1
            fi

            plain_file=$(mktemp "$SESSION_TMPDIR/checksum-plain.XXXXXXXXXX")

            CHILD_ARGS=(--verify-as-clearsigned-checksum-file --no-checksum-fallback "--extract-plaintext-to=$plain_file")
            [ "$QUIET" -eq 1 ] && CHILD_ARGS+=(--quiet)
            [ "$DEBUG" -eq 1 ] && CHILD_ARGS+=(--debug)
            [ "$KEEP" -eq 1 ] && CHILD_ARGS+=(--keep)
            [ -n "$EXPORT_KEY_TO" ] && CHILD_ARGS+=("--export-key-to=$EXPORT_KEY_TO")
            [ -n "$KEYSERVER_OPT" ] && CHILD_ARGS+=("--keyserver=$KEYSERVER_OPT")
            [ -n "$STATUS_FD" ] && CHILD_ARGS+=("--status-fd=$STATUS_FD")
            if [ "$TRUST_KEY" -eq 1 ]; then
                CHILD_ARGS+=(--trust-key)
            elif [ "$ALLOW_UNKNOWN" -eq 1 ]; then
                CHILD_ARGS+=(--allow-unrecognized-key)
            fi

            debug_cmd "$SELF" "${CHILD_ARGS[@]}" "$name"
            set +e
            CHILD_OUTPUT=$("$SELF" "${CHILD_ARGS[@]}" "$name" 2>&1)
            child_rc=$?
            set -e
            printf '%s\n' "$CHILD_OUTPUT" >&2

            if [ "$child_rc" -ne 0 ]; then
                rm -f "$plain_file"
                name_base=$(basename "$name")
                # The child's KEY_FETCH_FAILED tag (a genuine connectivity
                # problem, not just "key not found") deserves a distinct,
                # less alarming message than a bad signature.
                if printf '%s\n' "$CHILD_OUTPUT" | grep -q '^\[VERIFY-ISO-SIG:\] KEY_FETCH_FAILED$'; then
                    status_out "CHECKSUM_KEY_FETCH_FAILED"
                    die "$(eval_gettext "could not fetch the signing key for checksum file '\${name_base}' from any keyserver - nothing in it could be checked. This looks like a network problem, not necessarily a bad signature - try again once you have a working connection.")" || return 1
                fi
                status_out "CHECKSUM_SIG_FAILED"
                die "$(eval_gettext "the checksum file '\${name_base}' failed its own signature check - it cannot be trusted, so the ISO hash inside it cannot be trusted either. Re-download the ISO and the checksum/signature files, ideally from a different mirror.")" || return 1
            fi

            # Only ever read $plain_file once $child_rc confirms the
            # signature was GOOD - gpgv still writes tampered plaintext to
            # --output even on a BAD signature, so this order is load-bearing.
            find_checksum_line "$plain_file" "$base" || { rm -f "$plain_file"; continue; }
            match_line=$MATCHED_LINE
            rm -f "$plain_file"

            name_base=$(basename "$name")
            if [ -n "$hashcmd" ] && [ -n "$FOUND_HASHCMD" ] && [ "$hashcmd" != "$FOUND_HASHCMD" ]; then
                name_algo=${hashcmd%sum}
                content_algo=${FOUND_HASHCMD%sum}
                [ -z "$FOUND_HASH_LABEL" ] || content_algo=$FOUND_HASH_LABEL
                warn "$(eval_gettext "\${name_base} is named for \${name_algo}, but its content looks like \${content_algo} - using \${content_algo}")"
            fi
            [ -z "$FOUND_HASHCMD" ] || hashcmd=$FOUND_HASHCMD
            command -v "$hashcmd" >/dev/null 2>&1 || die "$(eval_gettext "checksum file '\${name_base}' uses an unsupported hash algorithm ('\${hashcmd}' not found)")" || return 1

            algo=${hashcmd%sum}
            [ -z "$FOUND_HASH_LABEL" ] || algo=$FOUND_HASH_LABEL
            log "$(eval_gettext "using checksum file: \${name_base} (\${algo})")"
            warn_if_weak_hashcmd "$hashcmd" "$algo"
            # "+sig" (status-tag only) flags an inline-signed listing, so
            # it doesn't look identical to an unsigned one.
            status_out "CHECKSUM_FILE $name_base ($algo+sig)"
            CHECKSUM_INFO="$name_base ($algo+sig)"
            # No separate signature file to name - this listing is its own signature.
            CHECKSUM_SIG_INFO=""

            set +e
            HASH_OUTPUT=$(cd "$dir" && printf '%s\n' "$match_line" | "$hashcmd" -c - 2>&1)
            hash_rc=$?
            set -e
            printf '%s\n' "$HASH_OUTPUT" >&2

            if [ "$hash_rc" -ne 0 ]; then
                status_out "CHECKSUM_HASH_MISMATCH"
                die "$(eval_gettext "'\${name_base}' is validly signed, but '\${base}' does not match the recorded \${algo} checksum - this download is likely corrupted, incomplete, or was tampered with after the checksum file was signed. Re-download the ISO, ideally from a different mirror. (The checksum/signature files themselves are fine - no need to re-fetch those.)")" || return 1
            fi

            log "$(gettext "OK - signature and checksum both verified")"
            return 0
        fi

        sigfile=""
        for sigext in "${CHECKSUM_SIG_EXTS[@]}"; do
            if [ -r "${name}.${sigext}" ]; then
                sigfile="${name}.${sigext}"
                break
            fi
        done
        # $sigfile can itself be a clearsigned checksum listing rather
        # than a real detached signature - add it as its own candidate.
        if [ -n "$sigfile" ] && is_clearsigned_file "$sigfile"; then
            candidates+=("$sigfile|")
            continue
        fi
        if [ -z "$sigfile" ]; then
            # Distinct from "found the sig, but ISO isn't listed" below.
            checksum_sig_missing=1
            # Read-only peek (never trusted/used for anything but this
            # message) - only worth naming this untrusted candidate below
            # if it's actually relevant to $ISO, not just because it
            # happens to match a known checksum-listing name/pattern.
            if find_checksum_line "$name" "$base"; then
                unsigned_matches+=("$(basename "$name")")
            fi
            continue
        fi
        checksum_sig_missing=0

        tried_names+=("$(basename "$name")")

        find_checksum_line "$name" "$base" || continue
        match_line=$MATCHED_LINE

        name_base=$(basename "$name")
        # Falls back to find_checksum_line()'s hash-length guess when
        # the filename gave no algorithm, and overrides a known-from-
        # filename algorithm too if the content disagrees (e.g. a
        # sha256sum.txt that actually holds sha512 digests). The
        # command -v check itself is deferred until after the
        # signature check below, so a tampered listing fails as an
        # invalid signature, not an unsupported-algorithm error.
        if [ -n "$hashcmd" ] && [ -n "$FOUND_HASHCMD" ] && [ "$hashcmd" != "$FOUND_HASHCMD" ]; then
            name_algo=${hashcmd%sum}
            content_algo=${FOUND_HASHCMD%sum}
            [ -z "$FOUND_HASH_LABEL" ] || content_algo=$FOUND_HASH_LABEL
            warn "$(eval_gettext "\${name_base} is named for \${name_algo}, but its content looks like \${content_algo} - using \${content_algo}")"
        fi
        [ -z "$FOUND_HASHCMD" ] || hashcmd=$FOUND_HASHCMD
        algo=${hashcmd%sum}
        [ -z "$FOUND_HASH_LABEL" ] || algo=$FOUND_HASH_LABEL
        log "$(eval_gettext "using checksum file: \${name_base} (\${algo})")"
        warn_if_weak_hashcmd "$hashcmd" "$algo"
        status_out "CHECKSUM_FILE $name_base ($algo)"
        CHECKSUM_INFO="$name_base ($algo)"
        sigfile_base=$(basename "$sigfile")
        log "$(eval_gettext "checksum file signature: \${sigfile_base}")"
        status_out "CHECKSUM_SIG_INFO $sigfile_base"
        CHECKSUM_SIG_INFO=$sigfile_base

        # --is-cached: skip the full verify/hash flow, just extract the
        # checksum file's own signing key and report its cache status.
        if [ "$IS_CACHED" -eq 1 ]; then
            debug_cmd gpgv --status-fd 3 "$sigfile" /dev/null
            run_gpgv_status "$sigfile" /dev/null
            FPR=$(status_field ERRSIG 9)
            [ -n "$FPR" ] || FPR=$(printf '%s' "$GPGV_OUTPUT" | grep -m1 -oE '[[:xdigit:]]{40}' || true)
            [ -n "$FPR" ] || return 1
            key_material_usable "$(key_status "$TRUSTED_GPG" "gnupg-ring:")" && return 0
            key_material_usable "$(key_status "$PUBRING_KBX" "")" && return 0
            key_material_usable "$(key_status "$MX_GPG_KEYRING_RING" "$MX_GPG_KEYRING_PREFIX")" && return 0
            return 1
        fi

        CHILD_ARGS=(--verify-as-checksum-file --no-checksum-fallback)
        [ "$QUIET" -eq 1 ] && CHILD_ARGS+=(--quiet)
        [ "$DEBUG" -eq 1 ] && CHILD_ARGS+=(--debug)
        [ "$KEEP" -eq 1 ] && CHILD_ARGS+=(--keep)
        [ -n "$EXPORT_KEY_TO" ] && CHILD_ARGS+=("--export-key-to=$EXPORT_KEY_TO")
        # Forward --keyserver: the child does its own independent fetch
        # for the checksum file's signing key (a different key than the
        # ISO's), so without this an override wouldn't reach it.
        # ($KEYSERVER env var reaches it via inheritance already.)
        [ -n "$KEYSERVER_OPT" ] && CHILD_ARGS+=("--keyserver=$KEYSERVER_OPT")
        # Forward so the child's own status_out() tags reach the parent's captured output.
        [ -n "$STATUS_FD" ] && CHILD_ARGS+=("--status-fd=$STATUS_FD")
        if [ "$TRUST_KEY" -eq 1 ]; then
            CHILD_ARGS+=(--trust-key)
        elif [ "$ALLOW_UNKNOWN" -eq 1 ]; then
            CHILD_ARGS+=(--allow-unrecognized-key)
        fi

        debug_cmd "$SELF" "${CHILD_ARGS[@]}" "$name" "$sigfile"
        set +e
        CHILD_OUTPUT=$("$SELF" "${CHILD_ARGS[@]}" "$name" "$sigfile" 2>&1)
        child_rc=$?
        set -e
        printf '%s\n' "$CHILD_OUTPUT" >&2

        if [ "$child_rc" -ne 0 ]; then
            name_base=$(basename "$name")
            # See the identical comment on the clearsigned-checksum
            # branch above.
            if printf '%s\n' "$CHILD_OUTPUT" | grep -q '^\[VERIFY-ISO-SIG:\] KEY_FETCH_FAILED$'; then
                status_out "CHECKSUM_KEY_FETCH_FAILED"
                die "$(eval_gettext "could not fetch the signing key for checksum file '\${name_base}' from any keyserver - nothing in it could be checked. This looks like a network problem, not necessarily a bad signature - try again once you have a working connection.")" || return 1
            fi
            status_out "CHECKSUM_SIG_FAILED"
            die "$(eval_gettext "the checksum file '\${name_base}' failed its own signature check - it cannot be trusted, so the ISO hash inside it cannot be trusted either. Re-download the ISO and the checksum/signature files, ideally from a different mirror.")" || return 1
        fi

        # Checked only after the signature check above, so a tampered
        # listing fails as an invalid signature, not an unsupported-
        # algorithm error.
        command -v "$hashcmd" >/dev/null 2>&1 || die "$(eval_gettext "checksum file '\${name_base}' uses an unsupported hash algorithm ('\${hashcmd}' not found)")" || return 1

        set +e
        HASH_OUTPUT=$(cd "$dir" && printf '%s\n' "$match_line" | "$hashcmd" -c - 2>&1)
        hash_rc=$?
        set -e
        printf '%s\n' "$HASH_OUTPUT" >&2

        if [ "$hash_rc" -ne 0 ]; then
            status_out "CHECKSUM_HASH_MISMATCH"
            name_base=$(basename "$name")
            algo=${hashcmd%sum}
            die "$(eval_gettext "'\${name_base}' is validly signed, but '\${base}' does not match the recorded \${algo} checksum - this download is likely corrupted, incomplete, or was tampered with after the checksum file was signed. Re-download the ISO, ideally from a different mirror. (The checksum/signature files themselves are fine - no need to re-fetch those.)")" || return 1
        fi

        log "$(gettext "OK - signature and checksum both verified")"
        return 0
    done

    if [ -n "$CHECKSUM_FILE_OVERRIDE" ]; then
        if [ "$checksum_sig_missing" -eq 1 ]; then
            override_base=$(basename "$CHECKSUM_FILE_OVERRIDE")
            status_out "CHECKSUM_LISTING_SIG_MISSING"
            die "$(eval_gettext "no signature file (.sig/.asc/.gpg/.sign) was found for '\${override_base}' - cannot verify it without one")" || return 1
        fi
        die "$(eval_gettext "'\${base}' is not listed in '\${CHECKSUM_FILE_OVERRIDE}'")" || return 1
    elif [ "${#unsigned_matches[@]}" -gt 0 ]; then
        # More actionable than the "no match" cases below - report even
        # if other, irrelevant signed listings were also found. Reuses
        # $override_base (not a new var): eval_gettext substitutes by
        # that literal name in the message text shared with the
        # CHECKSUM_FILE_OVERRIDE branch above.
        override_base=${unsigned_matches[0]}
        status_out "CHECKSUM_LISTING_FOUND_UNSIGNED $override_base"
        die "$(eval_gettext "no signature file (.sig/.asc/.gpg/.sign) was found for '\${override_base}' - cannot verify it without one")" || return 1
    elif [ "${#tried_names[@]}" -gt 0 ]; then
        status_out "NO_CHECKSUM_LISTING_MATCH"
        tried_joined="${tried_names[*]}"
        die "$(eval_gettext "no signature file found for '\${base}', and none of the checksum-listing files in its directory (\${tried_joined}) contain an entry for it either")" || return 1
    fi
    # Genuinely nothing nearby - $SIG is just a placeholder guess here,
    # not worth naming (e.g. Debian/Ubuntu never use ".iso.sig" at all).
    status_out "NOTHING_TO_VERIFY"
    die "$(eval_gettext "no signature file or checksum listing was found for '\${base}'")" || return 1
}

# Runs the whole checksum-resolution + key-recognition + keyserver-fetch
# + gpgv-verify flow, using $ISO/$SIG/$FPR and every CLI-flag-equivalent
# global exactly as CLI flag-parsing sets them. Handles three CLI usages
# in one flow: a normal verify, --is-cached (network-free precheck), and
# --keep-key (cache a key without verifying anything).
verify_iso() {
    local tcf_rc clearsigned_copy
    # Reset every call so a repeat call (the GUI's "Check Another File"
    # loop) never sees a stale value from an earlier one.
    CHECKSUM_INFO=""
    CHECKSUM_SIG_INFO=""
    RESOLVED_ISO=""
    CLAIMED_UID=""
    # A library caller (LIB_MODE=1) needs status_out()'s tags readable
    # even with no explicit --status-fd - default to fd 2. Never applied
    # to a plain CLI run.
    [ "$LIB_MODE" -eq 1 ] && [ -z "$STATUS_FD" ] && STATUS_FD=2
    # Validated here too (not just in main()'s CLI parsing), since a
    # direct library caller (LIB_MODE=1, e.g. the GUI's run_keep_key())
    # can set KEEP_KEY=1/$FPR itself and call verify_iso() straight,
    # bypassing that gate entirely.
    if [ "$KEEP_KEY" -eq 1 ]; then
        is_full_fingerprint "$FPR" || die "$(eval_gettext "\${FLAG_KEEP_KEY} needs a full 40-character (or 64-character) hex fingerprint, not '\${FPR}'")" || return 1
    fi
    setup_gnupg_homedir
    trap cleanup RETURN
    resolve_keyservers
    resolve_mx_gpg_keyring

    if [ "$KEEP_KEY" -ne 1 ]; then
        if [ "$VERIFY_AS_CHECKSUM_FILE" -ne 1 ] && [ "$VERIFY_AS_CLEARSIGNED_CHECKSUM_FILE" -ne 1 ]; then
            # is_clearsigned_file() catches a listing whose content gives
            # it away regardless of name (e.g. a .sig/.asc/.gpg/.sign
            # argument that's actually a full clearsigned message).
            if looks_like_checksum_listing_name "$ISO" || is_clearsigned_file "$ISO"; then
                resolve_checksum_listing_as_iso || return 1
            fi
            # $ISO may already have been reassigned to a real *.iso by the
            # loop above (a picked shared-listing file) - in that case none
            # of these suffixes match its new basename, so this is a safe
            # no-op, not a second, conflicting resolution attempt.
            for suffix in "${PLAIN_CHECKSUM_SUFFIXES[@]}"; do
                case "$(basename "$ISO")" in
                    *".$suffix") resolve_plain_checksum_as_iso "$suffix" || return 1; break ;;
                esac
            done
        fi
        # -f (not just -r) also rejects a FIFO/device/socket masquerading as a readable path.
        if ! { [ -f "$ISO" ] && [ -r "$ISO" ]; }; then
            # Not a bare "[ cond ] && status_out": under set -e that trips
            # errexit when cond is false, with no die() message at all.
            if [ "${ISO_DERIVED_FROM_SIG:-0}" -eq 1 ]; then
                status_out "SIG_WITHOUT_ISO"
            fi
            die "$(eval_gettext "cannot read ISO file '\${ISO}'")" || return 1
        fi
        if [ "$NO_CHECKSUM_FALLBACK" -ne 1 ] && { [ -n "$CHECKSUM_FILE_OVERRIDE" ] || [ "$NO_DIRECT_SIG" -eq 1 ]; }; then
            # 2 = "not applicable, keep going"; 0/1 = fully handled, propagate.
            # tcf_rc=0 + "|| tcf_rc=$?", not a bare call then "tcf_rc=$?" on
            # its own line: under set -e a bare non-zero return (including
            # the normal return-2 case) would kill the process right there.
            tcf_rc=0
            try_checksum_fallback || tcf_rc=$?
            [ "$tcf_rc" -eq 2 ] || return "$tcf_rc"
        fi
        [ "$VERIFY_AS_CLEARSIGNED_CHECKSUM_FILE" -eq 1 ] || { [ -f "$SIG" ] && [ -r "$SIG" ]; } || die "$(eval_gettext "cannot read signature file '\${SIG}'")" || return 1
    fi
    command -v gpgv >/dev/null || die "$(gettext "gpgv not found")" || return 1
    command -v gpg  >/dev/null || die "$(gettext "gpg not found")" || return 1

    RECOGNIZED_VIA_TRUSTED_GPG=0
    if [ "$KEEP_KEY" -ne 1 ]; then
        # 1. Pull the signing key's fingerprint from the signature packet -
        #    gpgv emits an ERRSIG status-fd line even with no keyring/match.
        #    A clearsigned checksum file carries its own signature inline -
        #    one positional arg (itself), not a separate SIG/data pair.
    if [ "$VERIFY_AS_CLEARSIGNED_CHECKSUM_FILE" -eq 1 ]; then
        debug_cmd gpgv --status-fd 3 "$ISO"
        run_gpgv_status "$ISO"
    else
        debug_cmd gpgv --status-fd 3 "$SIG" /dev/null
        run_gpgv_status "$SIG" /dev/null
    fi
    FPR=$(status_field ERRSIG 9)
    # Backstop for a gpg version/sig-packet shape that omits ERRSIG.
    [ -n "$FPR" ] || FPR=$(printf '%s' "$GPGV_OUTPUT" | grep -m1 -oE '[[:xdigit:]]{40}' || true)
    # A legacy v3 signature packet structurally can't carry a full
    # fingerprint - gpgv's ERRSIG reports the literal placeholder "-"
    # then. Fall back to the same line's Key ID field (field 3) so a
    # v3-signed file can still be looked up/fetched by Key ID - treated
    # as unrecognized the whole time (is_full_fingerprint()/key_status())
    # until verify_iso()'s post-fetch step re-derives the
    # real fingerprint.
    if ! is_full_fingerprint "$FPR"; then
        FPR=$(status_field ERRSIG 3)
        case "$FPR" in
            ""|0000000000000000) FPR="" ;;
        esac
    fi
    if [ -z "$FPR" ]; then
        if [ "$VERIFY_AS_CLEARSIGNED_CHECKSUM_FILE" -eq 1 ]; then
            die "$(eval_gettext "could not extract signing key fingerprint from '\${ISO}'")" || return 1
        elif is_clearsigned_file "$SIG"; then
            die "$(eval_gettext "'\${SIG}' looks like a full clearsigned message (e.g. a checksum listing), not a plain detached signature - point this tool at it on its own, or together with the ISO it checksums, not as a signature file.")" || return 1
        else
            die "$(eval_gettext "could not extract signing key fingerprint from '\${SIG}'")" || return 1
        fi
    fi
    log "$(eval_gettext "signature made with: \${FPR}")"
    # SIGNATURE_FPR is emitted later, not here: a v3-signed key's $FPR is
    # still just its Key ID at this point, and the GUI's trust dialog
    # expects a real fingerprint - wait until $FPR has had its chances to
    # be upgraded (local lookup below, then post-fetch re-derivation).

    # Upgrade $FPR now via a safe local lookup so
    # recognition, --is-cached, and the "already-usable copy" step all
    # work exactly as they would for a full-fingerprint (v4) signature -
    # no separate "recognized despite being v3" logic needed anywhere else.
    if ! is_full_fingerprint "$FPR"; then
        local_real_fpr=$(find_fpr_by_keyid "$FPR" "$TRUSTED_GPG" "gnupg-ring:") || local_real_fpr=""
        [ -n "$local_real_fpr" ] || local_real_fpr=$(find_fpr_by_keyid "$FPR" "$PUBRING_KBX" "") || local_real_fpr=""
        [ -n "$local_real_fpr" ] && FPR=$local_real_fpr
    fi

    if [ "$IS_CACHED" -eq 1 ]; then
        key_material_usable "$(key_status "$TRUSTED_GPG" "gnupg-ring:")" && return 0
        key_material_usable "$(key_status "$PUBRING_KBX" "")" && return 0
        key_material_usable "$(key_status "$MX_GPG_KEYRING_RING" "$MX_GPG_KEYRING_PREFIX")" && return 0
        return 1
    fi

    # Check the claimed fingerprint against KNOWN_KEYS, a key already
    # cached/usable in trustedkeys.gpg, or a pubring.kbx key explicitly
    # marked trusted - before ever fetching or trusting the key material
    # itself. "expired" qualifies here too (trustedkeys.gpg membership is
    # the durable trust signal); "revoked" never does.
    tg_status=$(key_status "$TRUSTED_GPG" "gnupg-ring:")
    pk_status=$(key_status "$PUBRING_KBX" "")
    mx_status=$(key_status "$MX_GPG_KEYRING_RING" "$MX_GPG_KEYRING_PREFIX")
    # Refresh BEFORE using these statuses - otherwise recognition below
    # warns about an expiry that's about to be silently fixed anyway
    # (confusing), or worse, recognizes a key that's actually since been
    # revoked locally.
    refresh_trusted_key_status "$tg_status" "$pk_status" "$mx_status" || true
    tg_status=$(key_status "$TRUSTED_GPG" "gnupg-ring:")
    if any_status_revoked "$tg_status" "$pk_status" "$mx_status"; then
        warn_key_revoked || return 1
    elif ! key_explicitly_untrusted && label=$(known_key_label "$FPR"); then
        log "$(eval_gettext "recognized signing key: \${label}")"
    elif ! key_explicitly_untrusted && key_material_usable "$tg_status"; then
        RECOGNIZED_VIA_TRUSTED_GPG=1
        log "$(eval_gettext "recognized: key already cached and trusted in \${TRUSTED_GPG}")"
        [ "$tg_status" = expired ] && warn_key_expired "$TRUSTED_GPG" "gnupg-ring:"
    elif ! key_explicitly_untrusted && key_material_usable "$mx_status"; then
        mx_uid=$(ring_key_uid "$MX_GPG_KEYRING_RING" "$MX_GPG_KEYRING_PREFIX") || mx_uid=""
        if [ -n "$mx_uid" ]; then
            log "$(eval_gettext "recognized: key found in mx-gpg-keyring - \${mx_uid}")"
        else
            log "$(gettext "recognized: key found in mx-gpg-keyring")"
        fi
        [ "$mx_status" = expired ] && warn_key_expired "$MX_GPG_KEYRING_RING" "$MX_GPG_KEYRING_PREFIX"
        # Trust is implicit for this source, like KNOWN_KEYS - no flag/prompt needed.
        if keep_in_trusted_gpg "$MX_GPG_KEYRING_RING" "$MX_GPG_KEYRING_PREFIX"; then
            log "$(eval_gettext "kept \${FPR} in \${TRUSTED_GPG} - future runs will recognize it automatically")"
            status_out "KEPT_IN_TRUSTED_GPG"
            # Already in $TRUSTED_GPG now - step 2 below won't repeat this message.
            RECOGNIZED_VIA_TRUSTED_GPG=1
        else
            log "$(eval_gettext "warning: could not save key into \${TRUSTED_GPG}: \${out}")"
        fi
    elif key_material_usable "$pk_status" && pubring_key_explicitly_trusted \
         && ! key_explicitly_untrusted; then
        log "$(eval_gettext "recognized: key already present in \${PUBRING_KBX} and explicitly marked trusted there")"
        [ "$pk_status" = expired ] && warn_key_expired "$PUBRING_KBX" ""
        # The trust decision was already made by the user - just moving it
        # into the keyring format gpgv reads. No flag/prompt needed.
        if keep_in_trusted_gpg "$PUBRING_KBX" ""; then
            log "$(eval_gettext "kept \${FPR} in \${TRUSTED_GPG} - future runs will recognize it automatically")"
            status_out "KEPT_IN_TRUSTED_GPG"
            # Just copied into $TRUSTED_GPG - the "already-usable copy"
            # step below will find it there and must not repeat either
            # message already shown above.
            RECOGNIZED_VIA_TRUSTED_GPG=1
        else
            log "$(eval_gettext "warning: could not save key into \${TRUSTED_GPG}: \${out}")"
        fi
    elif ! key_explicitly_untrusted && label=$(known_key_label_or_primary "$FPR" "$PUBRING_KBX|"); then
        log "$(eval_gettext "recognized signing key: \${label}")"
        # Trust comes from KNOWN_KEYS here, not from pubring.kbx's own
        # (absent) trust signal - pubring.kbx is just where the bytes
        # happened to be found. Persist directly, same as the
        # mx-gpg-keyring branch above.
        if keep_in_trusted_gpg "$PUBRING_KBX" ""; then
            log "$(eval_gettext "kept \${FPR} in \${TRUSTED_GPG} - future runs will recognize it automatically")"
            status_out "KEPT_IN_TRUSTED_GPG"
            RECOGNIZED_VIA_TRUSTED_GPG=1
        else
            log "$(eval_gettext "warning: could not save key into \${TRUSTED_GPG}: \${out}")"
        fi
    else
        # Brackets every info() line about this unrecognized key with
        # stable English tags, so the GUI can later strip the whole span
        # from its displayed technical body once the key is trusted -
        # matching the tags, not the (localized) info() text itself.
        status_out "UNRECOGNIZED_KEY_WARNINGS_BEGIN"
        if key_explicitly_untrusted; then
            info "$(eval_gettext "\${FPR} has been explicitly untrusted - you've told this tool not to auto-trust it anymore, whatever the reason it would otherwise be recognized. Pass \${FLAG_TRUST_KEY} again if you've changed your mind")"
            status_out "EXPLICITLY_UNTRUSTED"
        else
            info "$(eval_gettext "\${FPR} is not one of this tool's built-in recognized keys - verify its fingerprint independently before trusting it (e.g. against the distro's own official website or keyserver listing)")"
        fi
        # 0/1, not just a bare tag - the GUI needs to tell which of the two
        # differently-worded branches fired (direct ISO signature vs. some
        # kind of checksum-listing signature, detached or inline-signed).
        status_out "UNRECOGNIZED_KEY $(( VERIFY_AS_CHECKSUM_FILE || VERIFY_AS_CLEARSIGNED_CHECKSUM_FILE ))"
        if [ "$ALLOW_UNKNOWN" -ne 1 ]; then
            # TRANSLATORS: ${FLAG_ALLOW_UNKNOWN}/${FLAG_TRUST_KEY} are literal flag names - keep placeholders as-is.
            die "$(eval_gettext "refusing to verify against an unrecognized signing key (pass \${FLAG_ALLOW_UNKNOWN} to verify just this once, or \${FLAG_TRUST_KEY} to also remember it for next time)")" || return 1
        fi
        # TRANSLATORS: ${FLAG_TRUST_KEY} is a literal flag name - keep the placeholder as-is.
        info "$(eval_gettext "verifying anyway - if the result below is GOOD and you decide you trust this key, use \${FLAG_TRUST_KEY} next time (or mark it trusted in your own GPG keyring) so future checks recognize it automatically")"
        status_out "UNRECOGNIZED_KEY_WARNINGS_END"
    fi
fi

VERIFY_RING=""

# Shortcut for --keep-key: if the GUI already exported this exact key
# earlier in the same session (--export-key-to), reuse that copy instead
# of re-fetching. Falls through to the normal path if it doesn't actually
# contain $FPR.
if [ "$KEEP_KEY" -eq 1 ] && [ -n "$FROM_RING_OVERRIDE" ] && [ -r "$FROM_RING_OVERRIDE" ] \
   && keep_in_trusted_gpg "$FROM_RING_OVERRIDE" "gnupg-ring:"; then
    keepkey_status=$(key_status "$TRUSTED_GPG" "gnupg-ring:")
    if key_material_usable "$keepkey_status"; then
        [ "$keepkey_status" = expired ] && warn_key_expired "$TRUSTED_GPG" "gnupg-ring:"
        log "$(eval_gettext "kept \${FPR} in \${TRUSTED_GPG} - already fetched during verification, no second fetch needed")"
        status_out "KEPT_IN_TRUSTED_GPG"
        return 0
    fi
fi

# 2. Check for an already-usable copy of the key before reaching out to
#    a keyserver. Re-checks (rather than reuses) $TRUSTED_GPG's status
#    from step 1, since the pubring.kbx-explicit-trust path above can
#    itself have just copied the key into $TRUSTED_GPG in the meantime.
status=$(key_status "$TRUSTED_GPG" "gnupg-ring:")
pr_status=$(key_status "$PUBRING_KBX" "")
mx_status=$(key_status "$MX_GPG_KEYRING_RING" "$MX_GPG_KEYRING_PREFIX")
# Computed unconditionally (not just as a fallback further below) so
# this stays self-sufficient under --keep-key mode too, where the
# recognition chain above never ran at all - this is that mode's only
# freshness/revocation check.
refresh_trusted_key_status "$status" "$pr_status" "$mx_status" || true
status=$(key_status "$TRUSTED_GPG" "gnupg-ring:")
if any_status_revoked "$status" "$pr_status" "$mx_status"; then
    warn_key_revoked || return 1
elif key_material_usable "$status"; then
    # Both messages already fired above if RECOGNIZED_VIA_TRUSTED_GPG is
    # set (recognition itself now accepts "expired" too) - don't repeat
    # either one here.
    if [ "$RECOGNIZED_VIA_TRUSTED_GPG" -ne 1 ]; then
        if [ "$status" = expired ]; then
            warn_key_expired "$TRUSTED_GPG" "gnupg-ring:"
        else
            log "$(eval_gettext "key already present and valid in \${TRUSTED_GPG}")"
        fi
    fi
    status_out "ALREADY_IN_TRUSTED_GPG"
    VERIFY_RING=$TRUSTED_GPG
else
    [ "$status" = missing ] || log "$(eval_gettext "key in \${TRUSTED_GPG} is \${status}, ignoring cached copy")"
    if key_material_usable "$pr_status" && export_to_tmp_ring "$PUBRING_KBX" ""; then
        if [ "$pr_status" = expired ]; then
            warn_key_expired "$PUBRING_KBX" ""
        else
            log "$(eval_gettext "key already present and valid in \${PUBRING_KBX}")"
        fi
        status_out "ALREADY_IN_PUBRING"
        # Only persist if trust was already made explicit (same bar as
        # the recognition chain's own pubring.kbx branch) - unlike
        # mx-gpg-keyring, mere presence in pubring.kbx carries no trust
        # signal on its own.
        if ! key_explicitly_untrusted && pubring_key_explicitly_trusted; then
            if keep_in_trusted_gpg "$PUBRING_KBX" ""; then
                log "$(eval_gettext "kept \${FPR} in \${TRUSTED_GPG} - future runs will recognize it automatically")"
                status_out "KEPT_IN_TRUSTED_GPG"
            else
                log "$(eval_gettext "warning: could not save key into \${TRUSTED_GPG}: \${out}")"
            fi
        fi
    else
        [ "$pr_status" = missing ] || key_material_usable "$pr_status" || \
            log "$(eval_gettext "key in \${PUBRING_KBX} is \${pr_status}, ignoring cached copy")"
        if key_material_usable "$mx_status" && export_to_tmp_ring "$MX_GPG_KEYRING_RING" "$MX_GPG_KEYRING_PREFIX"; then
            if [ "$mx_status" = expired ]; then
                warn_key_expired "$MX_GPG_KEYRING_RING" "$MX_GPG_KEYRING_PREFIX"
            else
                log "$(gettext "key already present and valid in mx-gpg-keyring")"
            fi
            status_out "ALREADY_IN_MX_GPG_KEYRING"
            # Trust is implicit for this source (same as the recognition
            # chain's own mx-gpg-keyring branch) - persist it so it stays
            # usable even if the mx-gpg-keys package is ever removed later.
            if keep_in_trusted_gpg "$MX_GPG_KEYRING_RING" "$MX_GPG_KEYRING_PREFIX"; then
                log "$(eval_gettext "kept \${FPR} in \${TRUSTED_GPG} - future runs will recognize it automatically")"
                status_out "KEPT_IN_TRUSTED_GPG"
            else
                log "$(eval_gettext "warning: could not save key into \${TRUSTED_GPG}: \${out}")"
            fi
        fi
    fi
fi

if [ -z "$VERIFY_RING" ]; then
    FETCHED=0
    fetch_uncertain=0
    for ks in "${KEYSERVERS[@]}"; do
        log "$(eval_gettext "fetching \${FPR} from \${ks}")"
        status_out "FETCHING"
        TMP_RING=$(mktemp "$SESSION_TMPDIR/gnupg-ring.XXXXXXXXXX.gpg")
        fetch_status_file=$(mktemp "$SESSION_TMPDIR/fetch-status.XXXXXXXXXX")
        FETCH_ARGS=(
            --batch
            --no-default-keyring
            --keyring "gnupg-ring:${TMP_RING}"
            --status-fd 3
            --keyserver-options timeout=15,import-clean,import-minimal
            --keyserver "$ks"
            # "0x" prefix: see the identical comment in
            # refresh_trusted_key_status()'s own fetch construction.
            --receive-keys "0x$FPR"
        )
        debug_cmd gpg "${FETCH_ARGS[@]}"
        set +e
        out=$(gpg "${FETCH_ARGS[@]}" 3>"$fetch_status_file" 2>&1)
        fetch_rc=$?
        set -e
        # GPG_ERR_NO_DATA (low 16 bits of the [GNUPG:] FAILURE recv-keys
        # code = 58) means this keyserver is reachable, this key just
        # isn't there - common/harmless for a small/private signing key.
        # Anything else (no route, timeout, no keyserver available...)
        # is worth a more specific hint than "could not fetch" alone -
        # see the identical reasoning in refresh_trusted_keys().
        if [ "$fetch_rc" -ne 0 ]; then
            fetch_failure_code=$(grep -m1 '^\[GNUPG:\] FAILURE recv-keys ' "$fetch_status_file" | awk '{print $NF}')
            if [ -z "$fetch_failure_code" ] || [ "$((fetch_failure_code & 0xFFFF))" -ne 58 ]; then
                fetch_uncertain=1
            fi
        fi
        rm -f "$fetch_status_file"
        # $FPR may still be a bare Key ID here (v3-signature fallback) -
        # key_status() requires a full fingerprint, so re-derive it from
        # $TMP_RING's own freshly-imported material. Require exactly one
        # primary key - a Key-ID collision is unlikely but not
        # theoretical, and a wrong key silently entering $TRUSTED_GPG is
        # worse than a loud refusal.
        if [ "$fetch_rc" -eq 0 ] && ! is_full_fingerprint "$FPR"; then
            real_fprs=$(gpg --batch --no-default-keyring --keyring "gnupg-ring:${TMP_RING}" \
                            --with-colons --list-keys 2>/dev/null | parse_colon_key_listing) || true
            # grep -c exits 1 (not just an empty count) when nothing
            # matches - real_fprs is legitimately empty whenever the
            # keyserver's own reply carried no usable key material (see
            # the "without any usable key material" case just below), so
            # this must never be allowed to trip set -e via pipefail.
            real_fpr_count=$(printf '%s\n' "$real_fprs" | grep -c .) || true
            if [ "$real_fpr_count" -gt 1 ]; then
                die "$(eval_gettext "\${ks} returned more than one key for Key ID \${FPR} - refusing to guess which one is real")" || return 1
            elif [ "$real_fpr_count" -eq 1 ]; then
                FPR=$(printf '%s\n' "$real_fprs" | cut -d'|' -f1)
            fi
        fi
        # A keyserver can report success (exit 0) yet import nothing:
        # keys.openpgp.org withholds a key's user ID until confirmed, so
        # gpg keeps nothing and key_status() reports "missing". Only
        # trust this copy once key_status confirms it usable, falling
        # through to the next keyserver otherwise.
        fetched_status=""
        [ "$fetch_rc" -eq 0 ] && fetched_status=$(key_status "$TMP_RING" "gnupg-ring:")
        # A definitive, not a per-server-flaky, signal - refuse outright
        # rather than trying the next keyserver.
        if [ "$fetch_rc" -eq 0 ] && [ "$fetched_status" = revoked ]; then
            warn_key_revoked || return 1
        fi
        if [ "$fetch_rc" -eq 0 ] && key_material_usable "$fetched_status"; then
            # gpg's own import chatter is just plumbing, not the signature
            # check itself - only worth showing under --debug.
            [ "$DEBUG" -eq 1 ] && printf '%s\n' "$out" >&2
            [ "$fetched_status" = expired ] && warn_key_expired "$TMP_RING" "gnupg-ring:"
            VERIFY_RING=$TMP_RING
            FETCHED=1
            break
        fi
        printf '%s\n' "$out" >&2
        if [ "$fetch_rc" -eq 0 ]; then
            info "$(eval_gettext "\${ks} returned \${FPR} without any usable key material (likely a user ID that was never confirmed there) - trying next...")"
        else
            info "$(eval_gettext "could not fetch from \${ks}, trying next...")"
        fi
        rm -f "$TMP_RING" "${TMP_RING}~"
        TMP_RING=""
    done
    if [ "$FETCHED" -ne 1 ]; then
        TRIED_KEYSERVERS="${KEYSERVERS[*]}"
        if [ "$fetch_uncertain" -eq 1 ]; then
            status_out "KEY_FETCH_FAILED"
            die "$(eval_gettext "could not fetch key \${FPR} from any keyserver (tried: \${TRIED_KEYSERVERS}) - this looks like a network problem, not necessarily an invalid signature. Try again once you have a working connection.")" || return 1
        fi
        die "$(eval_gettext "could not fetch key \${FPR} from any keyserver (tried: \${TRIED_KEYSERVERS})")" || return 1
    fi

    if [ "$KEEP" -eq 1 ] || [ "$KEEP_KEY" -eq 1 ]; then
        if keep_in_trusted_gpg "$TMP_RING" "gnupg-ring:"; then
            log "$(eval_gettext "keeping fetched key in \${TRUSTED_GPG} for future runs")"
            rm -f "$TMP_RING" "${TMP_RING}~"
            TMP_RING=""
            VERIFY_RING=$TRUSTED_GPG
        else
            log "$(eval_gettext "warning: could not save key into \${TRUSTED_GPG} (\${out}), continuing with a throw-away keyring for this run")"
        fi
    fi
fi

# $FPR has now had every chance to be upgraded from a bare Key ID to a
# real fingerprint - emitted once, here (not right after the initial
# ERRSIG extraction), so the GUI's trust dialog always sees the best
# identifier available. Not for --keep-key (no signature involved).
[ "$KEEP_KEY" -ne 1 ] && status_out "SIGNATURE_FPR $FPR"

# Best-effort: hand a copy of the resolved key back to the caller, so a
# later --keep-key call can reuse it via --from-ring. Not for --keep-key itself.
if [ "$KEEP_KEY" -ne 1 ] && [ -n "$EXPORT_KEY_TO" ]; then
    export_key_to "$EXPORT_KEY_TO" || true
fi

if [ "$KEEP_KEY" -eq 1 ]; then
    if [ "$VERIFY_RING" != "$TRUSTED_GPG" ]; then
        if keep_in_trusted_gpg "$VERIFY_RING" "gnupg-ring:"; then
            log "$(eval_gettext "kept \${FPR} in \${TRUSTED_GPG} - future runs will recognize it automatically")"
            status_out "KEPT_IN_TRUSTED_GPG"
        else
            die "$(eval_gettext "could not save key into \${TRUSTED_GPG}: \${out}")" || return 1
        fi
    else
        log "$(eval_gettext "\${FPR} is already kept in \${TRUSTED_GPG}")"
        status_out "KEPT_IN_TRUSTED_GPG"
    fi
    return 0
fi

# 3. Verify. VERIFY_RING is always a classical OpenPGP-format keyring -
#    spell out gnupg-ring: explicitly rather than relying on auto-detection.
ISO_BASE=$(basename "$ISO")
if [ "$VERIFY_AS_CLEARSIGNED_CHECKSUM_FILE" -eq 1 ]; then
    log "$(eval_gettext "verifying inline-signed checksum file '\${ISO_BASE}'")"
    # A temp copy, not $ISO directly: gpgv itself prints "WARNING: not a
    # detached signature; file 'X' was NOT verified!" whenever a same-
    # directory sibling matching $ISO's basename minus a .sig/.asc/.gpg/
    # .sign suffix exists (harmless, but confusing to show). $SESSION_
    # TMPDIR has no such sibling and this temp name has no such suffix,
    # so the warning can never fire here.
    clearsigned_copy=$(mktemp "$SESSION_TMPDIR/clearsigned-verify.XXXXXXXXXX")
    cp "$ISO" "$clearsigned_copy"
    debug_cmd gpgv --status-fd 3 --keyring "gnupg-ring:${VERIFY_RING}" --output "$EXTRACT_PLAINTEXT_TO" "$clearsigned_copy"
    run_gpgv_status --keyring "gnupg-ring:${VERIFY_RING}" --output "$EXTRACT_PLAINTEXT_TO" "$clearsigned_copy"
    rm -f "$clearsigned_copy"
else
    SIG_BASE=$(basename "$SIG")
    log "$(eval_gettext "verifying '\${SIG_BASE}' against '\${ISO_BASE}'")"
    debug_cmd gpgv --status-fd 3 --keyring "gnupg-ring:${VERIFY_RING}" "$SIG" "$ISO"
    run_gpgv_status --keyring "gnupg-ring:${VERIFY_RING}" "$SIG" "$ISO"
fi
printf '%s\n' "$GPGV_OUTPUT" >&2

# gpgv reports a signature made by a revoked key as REVKEYSIG instead of
# GOODSIG/VALIDSIG, but its exit code doesn't reflect that (same
# advisory tier as EXPKEYSIG) - and key_status()'s own earlier
# revocation checks only ever see the PRIMARY key's overall status, not
# a specific subkey's. This is the only point that catches it.
if [ -n "$(status_field REVKEYSIG 2)" ]; then
    warn_key_revoked || return 1
fi

if [ "$rc" -eq 0 ]; then
    log "$(gettext "OK - signature verified")"
    # gpgv emits EXPKEYSIG instead of GOODSIG (same field layout) when
    # the signing key has expired - key_material_usable() accepts that.
    CLAIMED_UID=$(percent_decode_uid "$(status_field GOODSIG 4-)")
    [ -n "$CLAIMED_UID" ] || CLAIMED_UID=$(percent_decode_uid "$(status_field EXPKEYSIG 4-)")
    if [ -n "$CLAIMED_UID" ]; then
        log "$(eval_gettext "claimed identity: \${CLAIMED_UID}")"
        status_out "CLAIMED_IDENTITY $CLAIMED_UID"
    fi
    # VALIDSIG's last field: the primary key's fpr, if $FPR is a subkey.
    PRIMARY_FPR=$(status_field VALIDSIG 12)
    if [ -n "$PRIMARY_FPR" ] && [ "$PRIMARY_FPR" != "$FPR" ]; then
        log "$(eval_gettext "primary key: \${PRIMARY_FPR}")"
        status_out "PRIMARY_FPR $PRIMARY_FPR"
    fi
    # A key recognized via KNOWN_KEYS/pubring-explicit-trust but not yet
    # cached in $TRUSTED_GPG still needs saving here.
    if [ "$TRUST_KEY" -eq 1 ] && [ "$VERIFY_RING" != "$TRUSTED_GPG" ]; then
        if keep_in_trusted_gpg "$VERIFY_RING" "gnupg-ring:"; then
            log "$(eval_gettext "kept \${FPR} in \${TRUSTED_GPG} - future runs will recognize it automatically")"
            status_out "KEPT_IN_TRUSTED_GPG"
        else
            log "$(eval_gettext "warning: could not save key into \${TRUSTED_GPG}: \${out}")"
        fi
    fi
else
    log "$(gettext "FAILED - signature did NOT verify")"
fi
return "$rc"
}

# Only run the dispatch gate on direct execution - $0 differs from
# BASH_SOURCE[0] when another script sources this file instead (the GUI,
# with LIB_MODE=1), leaving that caller to invoke whichever functions it needs.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    # Refuses root outright: this tool reads/writes the real ~/.gnupg,
    # not its own throwaway per-run $GNUPGHOME - root/sudo risks
    # polluting it, or (under sudo -E/bare su) poisoning the invoking
    # user's own home with root-owned files.
    if [ "$EUID" -eq 0 ]; then
        die "$(gettext "refusing to run as root - run this as a regular user instead")"
    fi

    FORCE_MODE=""
    MANAGE_KEYS_DISPATCH=0
    DND_DISPATCH=0
    DISPATCH_ARGS=()
    # --cli/--gui/--manage-keys/--drag-and-drop are dispatcher-only flags,
    # recognized anywhere in the argument list, not just as the first
    # argument. --drag-and-drop implies --gui.
    for DISPATCH_ARG in "$@"; do
        case "$DISPATCH_ARG" in
            --cli) FORCE_MODE=cli ;;
            --gui) FORCE_MODE=gui ;;
            --manage-keys) MANAGE_KEYS_DISPATCH=1 ;;
            --drag-and-drop) DND_DISPATCH=1; FORCE_MODE=gui ;;
            *) DISPATCH_ARGS+=("$DISPATCH_ARG") ;;
        esac
    done
    set -- "${DISPATCH_ARGS[@]}"
    if [ -z "$FORCE_MODE" ] && [ "$MANAGE_KEYS_DISPATCH" -eq 0 ] && [ "$#" -eq 1 ] \
       && { [ "$1" = "-h" ] || [ "$1" = "--help" ]; }; then
        print_dispatcher_help_preamble
        main --help
    elif [ "$MANAGE_KEYS_DISPATCH" -eq 1 ]; then
        if want_gui; then
            export DISPLAY_NAME="verify-iso-sig"
            # exec replaces this process outright (no EXIT trap), which
            # would leak this always-empty SESSION_TMPDIR - the GUI
            # creates its own fresh one right after sourcing this file.
            session_tmpdir_cleanup
            exec "$GUI" --manage-keys "$@"
        else
            print_manage_keys_cli_usage
            exit 1
        fi
    elif [ "$FORCE_MODE" = "gui" ] \
         || { [ "$FORCE_MODE" != "cli" ] && have_gui_session \
              && looks_like_gui_args "$@"; }; then
        export DISPLAY_NAME="verify-iso-sig"
        # See the identical comment on the --manage-keys exec above.
        session_tmpdir_cleanup
        if [ "$DND_DISPATCH" -eq 1 ]; then
            exec "$GUI" --drag-and-drop "$@"
        else
            exec "$GUI" "$@"
        fi
    else
        main "$@"
    fi
    exit $?
fi
