#!/bin/bash
## vim:sw=2:sts=2:ts=2:et:spell:
##
## Copyright (C) 2023 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.
##########################
## BEGIN DEFAULT VALUES ##
##########################

## Declare global variables
declare version me start_time dialog_title license adrelanos_signify \
  hulahoop_signify oracle_pgp user_home_dir directory_prefix guest \
  hypervisor interface oracle_repo log_level guest_version socks_proxy onion \
  non_interactive dev noupdate noupgrade dry_run getversion getopt ci no_import \
  no_boot redownload import_only destroy_existing_guest testers allow_errors \
  mirror virtualbox_only target_user last_exit guest_pretty xtrace sucmd \
  url_version_domain run_background background_pid  \
  debian_derivative_detected ubuntu_derivative_detected \
  kali_derivative_detected fedora_derivative_detected \
  virtualbox_linux_user_group qubes_template_detected \
  install_pkg_fasttrack_extra_args_maybe user_warned_potential_startup_issue \
  vboxmanage_locale_english dist_installer_version_pretty \
  log_file_user log_file_debug hypervisor_pretty nested_virt_tool \
  fasttrack_backports_staging_suites_debsource secure_boot_dkms_key_enrolled \
  secure_boot_status_pretty secure_boot_status_short \
  secure_boot_mokutil_dkms_test_key kernel_modules_can_be_loaded \
  kernel_module_has_been_loaded kernel_module_signer \
  kernel_module_modprobe_output modules_signed_only console_write_command \
  package_manager_issue_extra_help_text pkg_mngr pkg_mngr_install \
  pkg_mngr_update pkg_mngr_check_installed pkg_mngr_upgrade_check \
  pkg_mngr_upgrade_install guest_full_vm_name_gateway \
  guest_full_vm_name_workstation guest_full_vm_name_kicksecure \
  workstation_exists gateway_exists vm_or_vms_already_existing_test_result \
  guest_file guest_file_ext debian_testing_or_unstable_detected \
  distro_codename_common_use use_deb822_sources oracle_found \
  oracle_file_debsource oracle_file_deb822source unstable_found \
  unstable_found_with_contrib unstable_file_debsource \
  unstable_file_deb822source backports_found backports_file_debsource \
  backports_file_deb822source fasttrack_found fasttrack_file_debsource \
  fasttrack_file_deb822source fasttrack_backports_staging_found \
  fasttrack_backports_staging_file_debsource \
  fasttrack_backports_staging_file_deb822source kicksecure_found \
  kicksecure_file_debsource kicksecure_file_deb822source kali_found \
  kali_found_with_contrib kali_file_debsource kali_file_deb822source \
  connection_type_debsource kicksecure_oneline_source \
  unstable_oneline_source fasttrack_oneline_source \
  fasttrack_backports_staging_oneline_source backports_oneline_source \
  kali_oneline_source kicksecure_deb822_source unstable_deb822_source \
  fasttrack_deb822_source fasttrack_backports_staging_deb822_source \
  backports_deb822_source kali_deb822_source oracle_oneline_source \
  oracle_deb822_source log_dir_cur virtualbox_qt_package_name \
  virtualbox_cache_version_cmd virtualbox_guest_additions_iso_package_name \
  use_rsync use_curl transfer_utility transfer_max_time_large_file \
  transfer_max_time_small_file transfer_size_test_connection \
  transfer_size_small_file transfer_size_large_file transfer_io_timeout_opt \
  transfer_connect_timeout_opt transfer_size_opt transfer_dryrun_opt \
  transfer_output_dir_opt transfer_output_file_opt transfer_verbosity_opt \
  transfer_speed_optimization_opt transfer_proxy_prefix \
  transfer_proxy_suffix proxy curl_transfer_proxy curl_opt_ssl \
  url_guest_file url_origin signify_key signify_signer url_domain \
  url_version download_flag checkhash interface_name \
  dist_installer_cli_was_sourced can_boot_virtualbox_guest_vms APTGETOPT \
  APTGETOPT_SERIALIZED

if [[ -n "${BASH_SOURCE[0]-}" && "${BASH_SOURCE[0]}" != "${0}" ]]; then
  ## Script was sourced.
  ## This is useful for other programs / scripts to be able to `source` the
  ## functions of this script for code re-use. dist-installer-gui will do this.
  dist_installer_cli_was_sourced="true"
else
  ## Script was executed.
  dist_installer_cli_was_sourced="false"
  set -o errexit
  set -o errtrace
  set -o nounset
  set -o pipefail
fi

set_globals() {
  local all_args
  ## Version is commit based: https://github.com/Whonix/usability-misc
  version="commit-hash-replace-me"
  me="${0##*/}"
  # shellcheck disable=SC2034
  all_args="${*}"
  start_time="$(date +%s)"

  dialog_title="License agreement (scroll with arrows)"
  license="
Please do NOT continue unless you understand everything!
 DISCLAIMER OF WARRANTY.
 .
 THE PROGRAM IS PROVIDED WITHOUT ANY WARRANTIES, WHETHER EXPRESSED OR IMPLIED,
 INCLUDING, WITHOUT LIMITATION, IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
 PURPOSE, NON-INFRINGEMENT, TITLE AND MERCHANTABILITY.  THE PROGRAM IS BEING
 DELIVERED OR MADE AVAILABLE 'AS IS', 'WITH ALL FAULTS' AND WITHOUT WARRANTY OR
 REPRESENTATION.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
 PROGRAM IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
 ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
 .
 LIMITATION OF LIABILITY.
 .
 UNDER NO CIRCUMSTANCES SHALL ANY COPYRIGHT HOLDER OR ITS AFFILIATES, OR ANY
 OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE
 LIABLE TO YOU, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, FOR ANY
 DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, DIRECT, INDIRECT, SPECIAL,
 INCIDENTAL, CONSEQUENTIAL OR PUNITIVE DAMAGES ARISING FROM, OUT OF OR IN
 CONNECTION WITH THE USE OR INABILITY TO USE THE PROGRAM OR OTHER DEALINGS WITH
 THE PROGRAM(INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
 INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE
 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), WHETHER OR NOT ANY COPYRIGHT HOLDER
 OR SUCH OTHER PARTY RECEIVES NOTICE OF ANY SUCH DAMAGES AND WHETHER OR NOT SUCH
 DAMAGES COULD HAVE BEEN FORESEEN.
 .
 INDEMNIFICATION.
 .
 IF YOU CONVEY A COVERED WORK AND AGREE WITH ANY RECIPIENT
 OF THAT COVERED WORK THAT YOU WILL ASSUME ANY LIABILITY FOR THAT COVERED WORK,
 YOU HEREBY AGREE TO INDEMNIFY, DEFEND AND HOLD HARMLESS THE OTHER LICENSORS AND
 AUTHORS OF THAT COVERED WORK FOR ANY DAMAGES, DEMANDS, CLAIMS, LOSSES, CAUSES OF
 ACTION, LAWSUITS, JUDGMENTS EXPENSES (INCLUDING WITHOUT LIMITATION REASONABLE
 ATTORNEYS' FEES AND EXPENSES) OR ANY OTHER LIABILITY ARISING FROM, RELATED TO OR
 IN CONNECTION WITH YOUR ASSUMPTIONS OF LIABILITY.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
"

  ## https://www.whonix.org/wiki/Main/Project_Signing_Key#cite_note-7
  adrelanos_signify="untrusted comment: Patrick Schleizer adrelanos@whonix.org signify public key
  RWQ6KRormNEETq+M8IysxRe/HAWlqZRlO8u7ACIiv5poAW0ztsirOjCQ"
  ## https://www.whonix.org/wiki/KVM/Project_Signing_Key#cite_note-4
  hulahoop_signify="untrusted comment: signify public key
  RWT2GZDQkp1NtTAC1IoQHUsyb/AQ2LIQF82cygQU+riOpPWSq730A/rq"

  ## https://www.virtualbox.org/wiki/Linux_Downloads
  oracle_pgp="-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1.4.12 (GNU/Linux)

mQINBFcZ9OEBEACSvycoAEIKJnyyIpZ9cZLCWa+rHjXJzPymndnPOwZr9lksZVYs
12YnsEy7Uj48rTB6mipbIuDDH9VBybJzpu3YjY7PFTkYAeW6WAPeJ8RcSGXsDvc0
fQ8c+7/2V1bpNofc9vDSdvcM/U8ULQcNeEa6DI4/wgy2sWLXpi1DYhuUOSU10I97
KHPwmpWQPsLtLHEeodeOTvnmSvLX1RRl32TPFLpLdjTpkEGS7XrOEXelqzMBQXau
VUwanUzQ2VkzKnh4WecmKFT7iekOFVHiW0355ErL2RZvEDfjMjeIOOa/lPmW7y4F
fHMU3a3sT3EzpD9ST/JGhrmaZ+r5yQD4s4hn1FheYFUtUN0dqHe9KgPDecUGgh4w
rGnm0nUQsmQLKGSFXskqt26IiERdRt1eXpR9C5yufCVZfYpSsoG/mIHAt9opXFqi
ryJqzx5pfQkOLTz9WErThHK1399jyXJwYGKLyddHFQEdy3u3ELM8Kfp7SZD/ERVq
t2oA8jsr24IOyL16cydzfSP2kAV1r30bsF/1Q4qq6ii/KfDLaI0KEliBLQuB9jrA
6XQ69VLtkNPgiWzVMclg+qW1pA8ptXqXLMxi4h5EmE5GOhsihuwkwhhBmFqGT1RJ
EGlc/uiHWQskOW3nhQ3Epd6xhCUImy8Eu83YRxS6QriH6K8z5LgRSdg9nwARAQAB
tElPcmFjbGUgQ29ycG9yYXRpb24gKFZpcnR1YWxCb3ggYXJjaGl2ZSBzaWduaW5n
IGtleSkgPGluZm9AdmlydHVhbGJveC5vcmc+iQI3BBMBCgAhBQJXGfThAhsDBQsJ
CAcDBRUKCQgLBRYDAgEAAh4BAheAAAoJEKL2g8UpgK7P49QP/39dH+lFqlD9ruCV
apBKVPmWTiwWbqmjxAV35PzG9reO7zHeZHil7vQ6UCb6FGMgZaYzcj4Sl9xVxfbH
Zk7lMgyLDuNMTTG4c6WUxQV9UH4i75E1IBm9lOJw64bpbpfEezUF/60PAFIiFBvD
34qUAoVKe49PbvuTy98er5Kw6Kea880emWxU6I1Q1ZA80+o2dFEEtQc+KCgfWFgd
O757WrqbTj6gjQjBAD5B4z5SwBYMg1/TiAYF0oa+a32LNhQIza/5H3Y+ufMfO3tY
B/z1jLj8ee5lhjrv0jWvvfUUeIlq5pNoOmtNYFS+TdkO0rsqEC6AD0JRTKsRHOBu
eSj7SLt8gmqy7eEzRCMlYIvoQEzt0/JuTQNJjHCuxH1scV13Q3bK6SmxqlY46tf5
Ljni9Z4lLJ7MB1BF2MkHuwQ7OcaEgUQBZSudzPkpRnY0AktiQYYP4Q1uDp+vfvFp
GTkY1pqz3z2XD66fLz0ea5WIBBb3X/uq9zdHu8BTwDCiZlWLaDR5eQoZWWe+u+5J
NUx1wcBpC1Hr2AnmuXBCRq+bzd8iaB8qxWfpCAFZBksSIW2aGhigSeYdx1jpjOob
xog4qbuo5w1IUh8YLHwQ6uM12CqwC1nZadLxG0Fj4KoYbvp0T5ryBM3XD+TVGjKB
m/QHLqabxZBbuJT0Cw2dRtW/ty5ZuQINBFcZ9OEBEADEY+YveerQjzzy5nA1FjQG
XSaPcjy4JlloRxrUyqlATA0AIuK7cwc7PVrpstV8mR9qb38fdeIoY1z1dD3wnQzJ
lbDfZhS5nGMzk9AANd6eJ2KcWI3qLeB//4fr2pPS0piOG4qyW4IhY4KeuCwusE6d
IyDBg2XEdpG1IesSDaqNsvLZjPFEBNiCIkqrC7XSmoPNwHkKGj5LeD1wAE914cn2
a04IlbXiT46V9jjJFnNem/Co0u+2e2J3oReNmHvbb62OC57rqeBxqBplXg9tvJk/
w0A3bXxUrfz83tY6vDYoFdwJDudaJJWQjvqpYnySXMJYT6KoE4Xgl5fNcbNIVUpU
k74BcWD9PZVadSMN7FWZzMfVsbTMmUA22tuDKD6hrF6ysCelex4YO44kSH7dhXu5
ANtZ2BFfRZvdjTQoblOI8C9cy/iX74vvG8OZarFG+u/kon3+xcAgY5KceUVbostO
0n3V8iK0gMQWH8sR8vXH+oV4GqHUEQURax2XM2Tt7Ra5XGcQaYDIkNPKSVVVtTk5
3OU/bNoBofAbwd4eOZOf9ag5ZVIIaoubMOEiveGYde4AEVE7krSNcYh/C48iCVKr
eOyS26AVA15dAvnKTAqxJqICUSQ9zjGfTp1obhXCkMAy0m+AxNVEfSzFznQLHtWK
zEGr+zCsvj1R8/qlMpHBXQARAQABiQIfBBgBCgAJBQJXGfThAhsMAAoJEKL2g8Up
gK7PKpQP+wY9zLgnJeqrvNowmd70afd8SVge9BvhLh60cdG+piM5ZuEV5ZmfTFoX
XPHzOo2dgt6VYTE9JO72Jv7MyzJj3zw3G/IcJQ6VuQwzfKkFTD+IeOiXX2I2lX1y
nFv24rs1MTZ4Px1NJai7fdyXLiCl3ToYBmLafFpfbsVEwJ8U9bCDrHE4KTVc9IXO
KQ5/86JaIPN+JJLHJoO2EBQC08Cw3oxTDFVcWZ/IWvEFeqyqRSyoFMoDkjLYsqHS
we1kEoMmM2qN20otpKYq8R+bIEI5KKuJvAts/1xKE2cHeRvwl5kcFw/S3QQjKj+b
LCVTSRZ6EgcDDmsAPKt7o01wmu+P3IjDoiyMZJQZpZIA2pYDxruY+OLXpcmw78Gq
lTXb4Q9Vf47sAE8HmHfkh/wrdDeEiY9TQErzCBCufYbQj7sgttGoxAt12N+pUepM
MBceAsnqkF6aEa4n8dUTdS2/nijnyUZ2rDVzikmKc0JlrZEKaw8orDzg8fXzfHIc
pTrXCmFLX5BzNQ4ezAlw0NZG/qvhSBCuAkFiibfQUal8KLYwswvGJFghuQHsVTkf
gF8Op7Br7loTNnp3yiI0jo2D+7DBFqtiSHCq1fIgktmKQoVLCfd3wlBJ/o9cguT4
Y3B83Y34PxuSIq2kokIGo8JhqfqPB/ohtTLHg/o9RhP8xmfvALRD
=Rv7/
-----END PGP PUBLIC KEY BLOCK-----"
}

# shellcheck source=../../../helper-scripts/usr/libexec/helper-scripts/wc-test.sh
source /usr/libexec/helper-scripts/wc-test.sh

# shellcheck source=../../../helper-scripts/usr/libexec/helper-scripts/get_colors.sh
source /usr/libexec/helper-scripts/get_colors.sh

# shellcheck source=../../../helper-scripts/usr/libexec/helper-scripts/package_installed_check.sh
source /usr/libexec/helper-scripts/package_installed_check.sh

########################
## END DEFAULT VALUES ##
########################

################
## BEGIN MISC ##
################

# shellcheck source=../../../helper-scripts/usr/libexec/helper-scripts/has.sh
source /usr/libexec/helper-scripts/has.sh
# shellcheck source=../../../helper-scripts/usr/libexec/helper-scripts/capitalize_first_char.sh
source /usr/libexec/helper-scripts/capitalize_first_char.sh
# shellcheck source=../../../helper-scripts/usr/libexec/helper-scripts/not_as_root.sh
source /usr/libexec/helper-scripts/not_as_root.sh
# shellcheck source=../../../helper-scripts/usr/libexec/helper-scripts/root_cmd.sh
source /usr/libexec/helper-scripts/root_cmd.sh
# shellcheck source=../../../helper-scripts/usr/libexec/helper-scripts/ip_syntax.sh
source /usr/libexec/helper-scripts/ip_syntax.sh
# shellcheck source=../../../helper-scripts/usr/libexec/helper-scripts/get_os.sh
source /usr/libexec/helper-scripts/get_os.sh

get_specifics() {
  distro_codename_real=$(lsb_release --short --codename)
  distro_codename_common_use="${distro_codename_real}"

  ## TODO: How to handle the default value for use_deb822_sources? If we
  ## default to 'yes', old releases might end up with deb822 sources
  ## incorrectly. If we default to 'no', new releases might end up with
  ## one-line sources incorrectly. Defaulting to 'no' for now.
  if [ "${ubuntu_derivative_detected}" = "1" ]; then
    if grep --quiet -- "^UBUNTU_CODENAME=\S\+=" -- /etc/os-release; then
      distro_codename_ubuntu="$(awk -F'=' '/^UBUNTU_CODENAME=/{print $2}' /etc/os-release)"
    else
      distro_codename_ubuntu="noble"
    fi
    case "${distro_codename_ubuntu}" in
      noble|plucky|questing)
        use_deb822_sources='yes'
        ;;
      *)
        use_deb822_sources='no'
        ;;
    esac
  elif [ "${debian_derivative_detected}" = "1" ]; then
    if [ "${kali_derivative_detected}" = "1" ]; then
      distro_codename_debian="forky"
    else
      distro_codename_debian="${distro_codename_common_use}"
    fi
    case "${distro_codename_debian}" in
      trixie|forky|sid)
        use_deb822_sources='yes'
        ;;
      *)
        use_deb822_sources='no'
        ;;
    esac
    case "${distro_codename_debian}" in
      trixie)
        log info "Install VirtualBox from Oracle repository because at the time of writing VirtualBox is not yet in Debian 'fasttrack' and Debian 'unstable' ('sid') is no longer compatible with 'trixie'."
        oracle_repo=1
        ;;
      *)
        ;;
    esac
  fi
}

get_installer_version() {
  dist_installer_version_pretty="${me} ${version}"

  get_installer_package_version
  log notice "dist-installer-cli version: '${dist_installer_version_pretty}'"
}


get_installer_package_version() {
  local dist_installer_package

  if [ ! "${debian_derivative_detected}" = "1" ]; then
    ## The usability-misc package is only available for Debian derivatives.
    ## The check using dpkg-query implemented below is only available on Debian and derivatives.
    true "INFO: No Debian derivative detected."
    return 0
  fi
  ## Do not use commit-hash replace-me as search string to avoid replacement script replacing this.
  if ! printf '%s' "${version}" | grep --fixed-strings -- "replace-me" >/dev/null ; then
    true "INFO: version variable does not match replace-me string."
    return 0
  fi
  if ! printf '%s' "${0}" | grep -- '^/usr/bin' >/dev/null ; then
    true "INFO: variable 0 does not match ^/usr/bin string. This means not for example /usr/bin/dist-installer-cli. So not packages dist-installer-cli version."
    return 0
  fi
  if ! printf '%s' "${me}" | grep --fixed-strings -- "installer-cli" >/dev/null ; then
    true "INFO: program name variable does not match installer-cli string."
    return 0
  fi

  dist_installer_package="usability-misc"
  ## Overwrite with '|| true' as this is not important enough to hard fail here.
  version="$(dpkg-query --show --showformat='${Version}' "${dist_installer_package}")" || true
  dist_installer_version_pretty="${dist_installer_package} ${version}"
  return 0
}


check_not_qubes_template() {
  if [ ! -f /run/qubes/this-is-templatevm ]; then
    true "INFO: Not running inside a Qubes Template."
    return 0
  fi

  qubes_template_detected=true
  user_warned_potential_startup_issue=true
  log warn "${underline}QubesOS Template Detection Test:${nounderline} 'Template detected'
- The installer has detected being run inside a Qubes Template."

  if [ "${virtualbox_only}" = "1" ]; then
    true "INFO: Running inside a Qubes Template. Continuing via '--virtualbox-only' option."
    return 0
  fi

  ## Downloading non-Qubes VM images in Qubes Template makes no sense. Not even for development purposes.
  ## Installing VirtualBox inside Qubes however makes sense for development purposes.
  ## For example, this permits building non-Qubes VM images inside Qubes VMs.
  die 1 "${underline}QubesOS Template Detection Test:${nounderline} Only virtualbox-installer-cli ('--virtualbox-only') allowed in Qubes Template."
}

##############
## END MISC ##
##############

##########################
## BEGIN OPTION PARSING ##
##########################

# shellcheck source=../../../helper-scripts/usr/libexec/helper-scripts/parse_opt.sh
source /usr/libexec/helper-scripts/parse_opt.sh

########################
## END OPTION PARSING ##
########################

###################
## BEGIN LOGGING ##
###################

# shellcheck source=../../../helper-scripts/usr/libexec/helper-scripts/log_run_die.sh
source /usr/libexec/helper-scripts/log_run_die.sh

## Wrapper to end the exit trap.
end_exit() {
  ## Reset exit trap.
  trap - EXIT HUP INT QUIT ABRT ALRM TERM
  ## Kill tail PID.
  if [ -n "${tail_pid:-}" ]; then
    ## Sleep less than a second so the file descriptors have enough time to
    ## output all the logs to the screen before the background job is killed.
    sleep 0.3
    if kill -0 -- "${tail_pid}"; then
      ## TODO: Check if this is really necessary.
      # shellcheck disable=SC2086
      kill -s sigterm -- "${tail_pid}" || true
    fi
  fi

  true "INFO: Show log file locations at end of xtrace."
  true "${log_file_user:-}"
  true "${log_file_debug:-}"

  ## Exit with desired exit code.
  exit "${last_exit}"
}


## Handle exit trap with line it failed and its exit code.
handle_exit() {
  local line_number line_above line_error line_below signal_code signal_caught
  true "BEGIN handle_exit() with args: $*"
  last_exit="${1}"
  line_number="${2:-0}"
  log_time
  ## Exit without errors.
  [ "${last_exit}" = "0" ] && end_exit
  ## Virtual Machine expected start issues.
  [ "${last_exit}" = "106" ] && end_exit
  ## Virtual Machine unexpected start issues.
  [ "${last_exit}" = "107" ] && end_exit
  ## Exit with errors.
  if [ -n "${BASH_COMMAND:-}" ]; then
    log notice "Executed script, function, command executed: '${0}' '${FUNCNAME[1]}' '${BASH_COMMAND}'"
  else
    log notice "Executed script: '${0}'"
  fi
  ## some shells have a bug that displays line 1 as LINENO
  if [ "${line_number}" -gt 2 ]; then
    log error "Installer aborted due to an error."
    log error "No need to panic. Nothing is broken. Just some rare condition has been hit."
    log error "A solution likely exists for this issue."
    log error "If the issue does not recur on retry, it can be safely ignored as transient."
    log error "Consult ${guest_pretty} News and the User Help Forum for assistance."
    log error "Please report this bug if it has not been already."
    printf '\n'
    log error "An error occurred at line: '${line_number}'"
    ## ideas from https://unix.stackexchange.com/questions/39623/trap-err-and-echoing-the-error-line
    ## Simple version that doesn't indicate the error line.
    # pr -tn "${0}" | tail -n+$((line_number - 3)) | head -n3
    ## Easy version for wasting resources and better readability.
    line_above="$(pr -tn "${0}" | tail -n+$((line_number - 4)) | head -n4)"
    line_error="$(pr -tn "${0}" | tail -n+$((line_number)) | head -n1)"
    line_below="$(pr -tn "${0}" | tail -n+$((line_number + 1)) | head -n4)"
    printf '%s\n*%s\n%s\n' "${line_above}" "${line_error}" "${line_below}"
    ## Too complex.
    # awk 'NR>L-4 && NR<L+4 { printf " %-5d%3s %s\n",NR,(NR==L?">>>":""),$0 }' L="${line_number}" "${0}" >&2
    printf '\n'
    log error "Please include the user log and the debug log in your bug report."
    log error "(For file locations where to find these logs, see above.)"
    printf '\n'
  else
    if [ "${last_exit}" -gt 128 ] && [ "${last_exit}" -lt 193 ]; then
      signal_code=$((last_exit-128))
      ## 'kill -l': Use short option name because 'kill' is a built-in and does not support long option name '--list'.
      ## 'kill -l' does not support end-of-options.
      signal_caught="$(kill -l "${signal_code}")"
      log error "Signal received: '${signal_caught}'"
    fi
  fi
  ## Print exit code.
  log error "Installer exited with code: '${last_exit}'"
  end_exit
}


#################
## END LOGGING ##
#################

###########################
## BEGIN SCRIPT SPECIFIC ##
###########################

claim_unsupported_distro() {
  local status distro
  status="${1}"
  distro="${2}"
  log error "At this time, your Operating System is unsupported by the '${guest_pretty}' Installer."
  log error "Visit the following URL to check support for manual installation:"
  log error "    ${url_version_domain}/wiki/Virtualbox"
  die 101 "${underline}Distribution Check:${nounderline} Unsupported ('${status}' '${distro}') system."
}

## Get necessary packages for your host system to be able to set the guest.
get_host_virtualizer_pkgs() {
  log notice "$hypervisor_pretty Installation: Required system virtualization packages are being installed... Please wait, this process may take a few minutes..."
  if [ "$ubuntu_derivative_detected" = "1" ]; then
    install_package_debian_common
    install_virtualbox_ubuntu
  elif [ "$debian_derivative_detected" = "1" ]; then
    install_package_debian_common
    install_virtualbox_debian
  elif [ "$fedora_derivative_detected" = "1" ]; then
    install_package_fedora_common
    install_virtualbox_fedora
  else
    die 1 "Operating system not found in 'get_host_virtualizer_pkgs'. Please report this bug. The debug log is required."
  fi
}


get_independent_host_pkgs() {
  ## Platform independent packages
  if has signify-openbsd; then
    ## Fix Debian's unconventional naming.
    run_signify() {
      ## Debian places signify-openbsd in the '/bin' folder instead of '/usr/bin'.
      run_as_target_user signify-openbsd "${@}"
    }
  else
    run_signify() {
      run_as_target_user signify "${@}"
    }
  fi

  has timeout || die 1 "${underline}Packages Installed Check:${nounderline} Timeout utility is missing."
  has curl || install_pkg curl
  has rsync || install_pkg rsync
  has mokutil || install_pkg mokutil

  ## Install openssl and ca-certificates.
  ## openssl is required:
  ## Otherwise there will be an error message by rsync-ssl:
  ## Failed to find on your path: openssl stunnel4 stunnel
  ## ca-certificates is required:
  ## Otherwise rsync-ssl will fail TLS verification.
  ## XXX: Strictly speaking packages openssl ca-certificates are only required
  ##      if using clearnet. I.e. not required when using --onion.
  install_pkg openssl ca-certificates

  while true; do
    has systemd-detect-virt && nested_virt_tool="systemd-detect-virt" && break
    test_pkg virt-what && nested_virt_tool="virt-what" && break
    install_pkg virt-what && nested_virt_tool="virt-what" && break
    break
  done
}


nested_virtualization_test() {
  #nested_virtualization_detected=""
  if [ -z "${nested_virt_tool:-}" ]; then
    ## No hard fail; not a requirement, only good to have.
    user_warned_potential_startup_issue=true
    log warn "${underline}Nested Virtualization Test:${nounderline} Detection tool for nested virtualization is missing."
  else
    if [ "${dry_run}" = "1" ]; then
      log notice "Nested Virtualization Test: Skipping test due to dry_run mode."
    else
      ## Check if we are a guest of virtualization.
      if root_cmd "${nested_virt_tool:-}" >/dev/null 2>&1; then
        #nested_virtualization_detected=true
        user_warned_potential_startup_issue=true
        log warn "${underline}Nested Virtualization Test:${nounderline} Nested virtualization detected.
  - This might be a user error.
  - This installer is designed to run on the host operating system.
  - This installer is not designed to be run inside virtual machines.
  - For more information about nested virtualization, refer to:
  ${url_version_domain}/wiki/Nested_Virtualization"
      fi
    fi
  fi

  if [ -f /usr/share/qubes/marker-vm ]; then
    #nested_virtualization_detected=true
    user_warned_potential_startup_issue=true
    log warn "${underline}QubesOS Detection Test:${nounderline} 'Qubes detected'
  - The installer is not designed for execution within Qubes.
  - Useful only for development purposes.
  - It is recommended to use Qubes-Whonix instead."
  fi
}


secure_boot_test() {
  local mokutil_output_sb_state
  secure_boot_dkms_key_enrolled=""
  if mokutil_output_sb_state=$(root_cmd mokutil --sb-state 2>&1) ; then
    if printf '%s' "$mokutil_output_sb_state" | grep --ignore-case --fixed-strings -- "enabled" >/dev/null; then
      secure_boot_status_pretty="'enabled' (mokutil_output_sb_state: '$mokutil_output_sb_state')"
      secure_boot_status_short='true'
      ## sudo mokutil --test-key /var/lib/dkms/mok.pub
      ## > /var/lib/dkms/mok.pub is already enrolled
      ## > zsh: exit 1     sudo mokutil --test-key /var/lib/dkms/mok.pub#
      ## Added "|| true" to handle the non-zero exit code.
      ## Redirection '2>&1' not necessary yet but that could be considered a minor bug in mokutil to write to stdout instead
      ## of writing to stderr.
      secure_boot_mokutil_dkms_test_key="$(root_cmd mokutil --test-key "/var/lib/dkms/mok.pub" 2>&1)" || true
      if printf '%s' "$secure_boot_mokutil_dkms_test_key" | grep --ignore-case --fixed-strings -- "already enrolled" >/dev/null ; then
        secure_boot_dkms_key_enrolled=true
      fi
    elif printf '%s' "$mokutil_output_sb_state" | grep --ignore-case --fixed-strings -- "disabled" >/dev/null ; then
      secure_boot_status_short='false'
      secure_boot_status_pretty="'disabled' (mokutil_output_sb_state: '$mokutil_output_sb_state')"
    else
      secure_boot_status_short='false'
      secure_boot_status_pretty="'unknown-please-report-this-minor-bug' (mokutil_output_sb_state: '$mokutil_output_sb_state')"
    fi
  else
    secure_boot_status_short='false'
    secure_boot_status_pretty="'disabled' (mokutil_output_sb_state: '$mokutil_output_sb_state')"
  fi
  log info "Secure Boot Check Result: $secure_boot_status_pretty Ok."
}


dkms_signing_key_enrollment() {
  if [ ! "$secure_boot_status_short" = "true" ]; then
    log info "Secure Boot DKMS Signing Key Enrollment Check Result: 'success' - Secure Boot is disabled, therefore DKMS signing key enrollment is unnecessary, ok."
    return 0
  fi
  if [ "$secure_boot_dkms_key_enrolled" = "true" ]; then
    log info "Secure Boot DKMS Signing Key Enrollment Check Result: 'success' - Secure Boot is enabled and DKMS signing key has already been enrolled, ok."
    return 0
  fi
  if ! [ "${hypervisor}" = "virtualbox" ]; then
    log info "Secure Boot DKMS Signing Key Enrollment Check Result: 'success' - Secure Boot is enabled, but the DKMS signing key is not enrolled. This is however unnecessary for hypervisor '${hypervisor}'."
    return 0
  fi

  log notice "secure_boot_mokutil_dkms_test_key: '$secure_boot_mokutil_dkms_test_key'"

  ## Not simple to automate, because it requires a password prompt.
  ## https://git.launchpad.net/~ubuntu-core-dev/shim/+git/shim-signed/tree/update-secureboot-policy
  #root_cmd mokutil --import /var/lib/dkms/mok.pub
  ##
  ## sudo mokutil --import /var/lib/dkms/mok.pub
  ## if already enrolled:
  ## > SKIP: /var/lib/dkms/mok.pub is already enrolled
  ##
  ## sudo mokutil --test-key /var/lib/dkms/mok.pub
  ## > /var/lib/dkms/mok.pub is not enrolled
  ## With exit code 0.
  ##
  ## sudo mokutil --root-pw --import /var/lib/dkms/mok.pub
  ## This can fail if the root account is locked or has no password, such as in Kicksecure.
  ## > Failed to get root password hash
  ##
  ## This is a Debian DKMS / shim-signed issue:
  ## - dkms Debian: enroll DKMS signing key / automate running "sudo mokutil --import /var/lib/dkms/mok.pub"
  ##   https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1076269
  ## - shim-signed Debian: please update the update-secureboot-policy script / add '--new-key' option
  ##   https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1076278
  ## - dkms upstream: automate running "`sudo mokutil --import /var/lib/dkms/mok.pub`"
  ##   https://github.com/dell/dkms/issues/429

die 1 "${underline}Secure Boot DKMS Signing Key Enrollment Check Result:${nounderline} ${red}${bold}'FAIL'${nobold}${nocolor}

- Secure Boot is enabled, but the DKMS signing key is not enrolled.
- VirtualBox kernel modules cannot be loaded due to this issue.
- For instructions on DKMS Signing Key Enrollment, visit:
  https://www.kicksecure.com/wiki/Secure_Boot
"
}


kernel_modules_load() {
  local modules_disabled
  ## Usually does not require root but might on some hardened systems.
  if ! modules_disabled=$(root_cmd_loglevel=null root_cmd cat -- /proc/sys/kernel/modules_disabled) ; then
    kernel_modules_can_be_loaded=maybe
    user_warned_potential_startup_issue=true
    log warn "${underline}Kernel Module Load Check Result:${nounderline} 'failed' (failed to cat -- /proc/sys/kernel/modules_disabled)"
    return 0
  fi
  if [ "$modules_disabled" = "0" ]; then
    kernel_modules_can_be_loaded=true
    log info "Kernel Module Load Check Result: 'yes' - modules can be loaded. (sysctl kernel.modules_disabled=0 is set.)"
    return 0
  fi
  kernel_modules_can_be_loaded=false
  log info "${underline}Kernel Module Load Check Result:${nounderline} 'no' - modules cannot be loaded. (sysctl kernel.modules_disabled=1 is set.)"
}


kernel_modules_signed_only() {
  local sig_enforce
  ## Usually does not require root but might on some hardened systems.
  if ! sig_enforce=$(root_cmd_loglevel="echo" root_cmd cat -- /sys/module/module/parameters/sig_enforce) ; then
    modules_signed_only=maybe
    user_warned_potential_startup_issue=true
    log warn "${underline}Kernel Module Signature Enforcement Check Result:${nounderline} 'failed' (failed to cat -- /sys/module/module/parameters/sig_enforce)"
    return 0
  fi
  if [ "$sig_enforce" = "Y" ]; then
    modules_signed_only=true
    log info "Kernel Module Signature Enforcement Check Result: 'yes' - only signed modules can be loaded. (kernel parameter module.sig_enforce=1 might be set.)"
    return 0
  fi
  modules_signed_only=false
  log info "Kernel Module Signature Enforcement Check Result: 'no' - unsigned modules can be loaded."
}


need_reboot_check_first() {
  ## Debian: /var/run/reboot-required
  ## Fedora: Does not have this.
  if [ ! -f /var/run/reboot-required ]; then
    log info "Reboot Check Result: File /var/run/reboot-required does not exist, good."
    return 0
  fi

  if [ "${ci}" = "1" ]; then
    true "INFO: Ignore need_reboot_check_first because running on CI."
    return 0
  fi

  ## Building for an old kernel might result in missing kernel headers for that kernel
  ## As a result, VirtualBox would fail to start.
  die 1 "${underline}Reboot Check Result:${nounderline} Your system reports that a reboot is required.

Please reboot your system and restart this installer.

Debugging information: file /var/run/reboot-required exists."
}


need_reboot_check_second() {
  true "fedora_derivative_detected: $fedora_derivative_detected"
  if [ ! "$fedora_derivative_detected" = "1" ]; then
    return 0
  fi

  ## Fedora 38: Part of dnf-utils. Not installed by default (on CI).
  ## Fedora 39: Part of dnf-plugins-core. Not installed by default (on CI).
  ## Therefore this can only run after install_package_fedora_common.
  if root_cmd /usr/bin/needs-restarting --reboothint ; then
    true "INFO: No reboot required."
    return 0
  fi

  if [ "${ci}" = "1" ]; then
    true "INFO: Ignore need_reboot_check_second because running on CI."
    return 0
  fi

  die 1 "${underline}Reboot Check Result:${nounderline} Your system reports that a reboot is required.

Please reboot your system and restart this installer.

Debugging information: needs-restarting reported that a reboot is required."
}


update_sources() {
  local update_output update_cmd

  log notice "Updating package list..."

  ## global console_write_command
  if tty >/dev/null >&1 ; then
    console_write_command=( 'tee' '/dev/tty' )
  else
    ## github actions CI issue:
    ## tee: /dev/tty: No such device or address
    ## https://github.com/actions/runner/issues/241
    console_write_command=( 'cat' )
  fi

  #root_cmd "${pkg_mngr_update[@]}" 2>&1 || true
  #update_sources_error
  ## result: OK: command_without_extraneous_spaces="sudo -- apt-get update --yes --error-on=any"
  ## But if root_cmd is run in a subshell using $(root_cmd ...) then command_without_extraneous_spaces
  ## won't be updated.

  if [ "${noupdate}" = "1" ]; then
    ## Too slow to run over and over again during testing.
    update_cmd=( 'true' "${pkg_mngr_update[@]}" )
    log warn "Package List Update: Simulate only, via '--noupdate' option."
  else
    update_cmd=( "${pkg_mngr_update[@]}" )
  fi

  if update_output=$(root_cmd_loglevel=notice root_cmd "${update_cmd[@]}" 2>&1 | "${console_write_command[@]}") ; then
    true "INFO: Exit code is zero but that does not guarantee in case of dnf that there is no error."
    if printf '%s' "$update_output" | grep --ignore-case -- "Error:" >/dev/null ; then
      log error "${underline}Package List Update:${nounderline} Exit code was 0 but 'Error:' found in output."
      if printf '%s' "$update_output" | grep --ignore-case -- "GPG signature verification error: Signing key not" >/dev/null; then
        log warn "${underline}Package List Update:${nounderline} Signing key not found. Skipping due to 'rpm --import' failing to import keys, but '--assumeyes' being used will import keys."
        return 0
      fi
      update_sources_error
    else
      true "INFO: No error found in update output, ok."
      return 0
    fi
  else
    log error "${underline}Package List Update:${nounderline} Non-zero exit code. Stop."
    update_sources_error
  fi
}


package_manager_issue_extra_help_text="The user is advised to attempt to debug this with the following steps:
1. Run above command as root (with sudo).
2. If there is an issue, use search engines, documentation and if needed
   contact the support of your operating system.
3. Once this has been fixed, re-run this installer."


update_sources_error() {
  die 1 "${underline}Package List Update:${nounderline} Could not update package lists.
  - This issue is most likely not caused by this installer.
  - This is most likely a package manager configuration or network issue.

This is the command which the installer has just run that failed:

${sucmd} ${pkg_mngr_update[*]}

${package_manager_issue_extra_help_text}"
}


check_upgrades_simulation() {
  local upgrade_simulate_output packages_upgradable upgrade_simulate_cmd

  upgrade_simulate_cmd=( "${pkg_mngr_upgrade_check[@]}" )
  [ "${#install_pkg_fasttrack_extra_args_maybe[@]}" != '0' ] \
    && upgrade_simulate_cmd+=( "${install_pkg_fasttrack_extra_args_maybe[@]}" )
  if upgrade_simulate_output=$(root_cmd_loglevel=notice root_cmd "${upgrade_simulate_cmd[@]}" 2>&1 | "${console_write_command[@]}") ; then
    true "INFO: Exit code is zero but that does not guarantee in case of dnf that there is no error."
    if printf '%s' "$upgrade_simulate_output" | grep --ignore-case -- "Error:" >/dev/null ; then
      log error "${underline}Package Upgrade Simulation:${nounderline} Exit code was 0 but 'Error:' found in output."
      upgrade_simulate_sources_error
    fi
  else
    log error "${underline}Package Upgrade Simulation:${nounderline} Non-zero exit code. Stop."
    upgrade_simulate_sources_error
  fi

  true "INFO: No error found in upgrade_simulate_output output, ok."
  if [ "${#install_pkg_fasttrack_extra_args_maybe[@]}" = '0' ]; then
    if pkg_mngr_upgradable_check >/dev/null ; then
      packages_upgradable=false
    else
      packages_upgradable=true
    fi
  else
    if pkg_mngr_upgradable_check "${install_pkg_fasttrack_extra_args_maybe[@]}" >/dev/null ; then
      packages_upgradable=false
    else
      packages_upgradable=true
    fi
  fi

  if [ "${dry_run}" = "1" ]; then
    packages_upgradable=false
  fi

  if [ "$packages_upgradable" = "false" ]; then
    log info "Package Upgrade Simulation: No packages require upgrading, ok."
    return 0
  fi

  if [ "${noupgrade}" = "1" ]; then
    log warn "Package Upgrade Simulation: Package upgrades available but proceeding anyhow via '--noupgrade' option."
    return 0
  fi

  die 1 "${underline}Package Upgrade Simulation:${nounderline} Package upgrades available.

Please upgrade your operating system first. Otherwise this installer cannot proceed.

${sucmd} ${pkg_mngr_upgrade_install[*]} ${install_pkg_fasttrack_extra_args_maybe[*]}

${package_manager_issue_extra_help_text}"

}

upgrade_simulate_sources_error() {
  die 1 "${underline}Package Upgrade Simulation:${nounderline} Failed.
  - This issue is most likely not caused by this installer.
  - This is most likely a package manager configuration or network issue.

This is the command which the installer has just run that failed:

${sucmd} ${pkg_mngr_upgrade_check[*]} ${install_pkg_fasttrack_extra_args_maybe[*]}

${package_manager_issue_extra_help_text}"
}


## Install package only if not installed already.
install_pkg() {
  local pkgs arg special_args pkg_not_installed

  pkgs="${*}"
  special_args=()
  pkg_not_installed=()

  for arg in ${pkgs}; do
    case "${arg}" in
      --target-release=*)
        special_args+=( "${arg}" )
        ;;
      *)
        ## Test if package exists as a binary or a library, using different tools.
        if "${pkg_mngr_check_installed[@]}" "${arg}" >/dev/null 2>&1; then
          log info "Package already installed: '${arg}'"
          continue
        fi
        if has "${arg}"; then
          log info "Program already installed: '${arg}'"
          continue
        fi

        pkg_not_installed+=( "${arg}" )
        ;;
    esac
  done

  if [ "${#pkg_not_installed[@]}" != '0' ]; then
    if [ "${dry_run}" = "1" ]; then
      log notice "Skipping installation of the following packages via '--dry-run' option: '${pkg_not_installed[*]}'"
      return 0
    fi

    log notice "Installing package(s): '${pkg_not_installed[*]}'"
    log info "special_args: '${special_args[*]}'"

    if ! root_cmd "${pkg_mngr_install[@]}" "${special_args[@]}" "${pkg_not_installed[@]}"; then
      if printf '%s' "${pkg_not_installed[*]}" | grep --ignore-case -- "virtualbox" >/dev/null; then
        virtualbox_installation_failure_debug
      fi
      die 1 "${underline}Package Installation:${nounderline} Failed to install package: '${pkg_not_installed[*]}'"
    fi

    ## Test if installation succeeded.
    test_pkg "${pkg_not_installed[@]}"
  fi
}


## Used to test for a 2nd time if packages exist or not, if not,
## install_pkg() failed above and best thing to do is abort because of missing
## dependencies.
test_pkg() {
  local pkgs pkg pkg_not_installed

  pkgs=( "$@" )
  pkg_not_installed=()
  for pkg in "${pkgs[@]}"; do
    if ! has "${pkg}" &&
      ! "${pkg_mngr_check_installed[@]}" "${pkg}" >/dev/null 2>&1
    then
      pkg_not_installed+=( "${pkg}" )
    fi
  done

  if [ "${#pkg_not_installed[@]}" != '0' ]; then
    if [ "${dry_run}" = "1" ]; then
      log info "Failed to locate package(s) and ignoring via '--dry-run' option: '${pkg_not_installed[*]}'"
    else
      log info "Failed to locate package(s): '${pkg_not_installed[*]}'"
      return 1
    fi
  fi
}


check_vm_running_general() {
  case "${guest}" in
    whonix)
        check_vm_running_virtualbox "${guest_full_vm_name_gateway}"
        check_vm_running_virtualbox "${guest_full_vm_name_workstation}"
      ;;
    kicksecure)
        check_vm_running_virtualbox "${guest_full_vm_name_kicksecure}"
      ;;
  esac
}


## Abort if user wants to reimport a VM that is running.
## This function is called before attempting to reimport an image.
check_vm_running_virtualbox() {
  local vm

  vm="${1}"
  ## Paused state should be considered as running. Instead of grepping
  ## possible states, grep VM from list of running VMs.
  # vboxmanage list runningvms | grep -- "^\"${vm}\" " >/dev/null
  if run_as_target_user "${vboxmanage_locale_english[@]}" showvminfo "${vm}" 2>&1 | \
    grep -E -- "^State:[[:space:]]+(running|paused)" >/dev/null
  then
    log error "Cannot proceed. You have the following VM running: ${vm}"
    die 1 "${underline}VM Running Check:${nounderline} Please turn it off before re-running this installer."
  fi
}


## Check if VM exists on VirtualBox
check_vm_registered_virtualbox() {
  local extra_message
  extra_message="$1"

  case "${guest}" in
    whonix)
      ## Test if machine exists.
      workstation_exists=0
      gateway_exists=0
      if run_as_target_user "${vboxmanage_locale_english[@]}" showvminfo "${guest_full_vm_name_gateway}" >/dev/null 2>&1 ; then
        gateway_exists=1
        log info "Existing VM Check Result $extra_message: guest '${guest_full_vm_name_gateway}' exists."
      fi
      if run_as_target_user "${vboxmanage_locale_english[@]}" showvminfo "${guest_full_vm_name_workstation}" >/dev/null 2>&1 ; then
        workstation_exists=1
        log info "Existing VM Check Result $extra_message: guest '${guest_full_vm_name_workstation}' exists."
      fi
      ## Find discrepancies.
      if [ "${workstation_exists}" = "0" ] && [ "${gateway_exists}" = "1" ]; then
        log info "Existing VM Check Result $extra_message: Gateway exists but Workstation doesn't."
      fi
      if [ "${workstation_exists}" = "1" ] && [ "${gateway_exists}" = "0" ]; then
        log info "Existing VM Check Result $extra_message: Workstation exists but Gateway doesn't."
      fi
      vm_or_vms_already_existing_test_result=false
      if [ "${workstation_exists}" = "1" ] || [ "${gateway_exists}" = "1" ]; then
        ## If either one of the guests exists, proceed.
        vm_or_vms_already_existing_test_result=true
      fi
      if [ "${vm_or_vms_already_existing_test_result}" = "false" ]; then
        ## Both guests are still non-existing. Therefore return from this function.
        log info "Existing VM Check Result $extra_message: None existing yet, ok."
        return 0
      fi
      log notice "Existing VM Check Result $extra_message: Virtual Machine(s) have been imported previously."
      ;;
    kicksecure)
      vm_or_vms_already_existing_test_result=false
      if ! run_as_target_user "${vboxmanage_locale_english[@]}" showvminfo "${guest_full_vm_name_kicksecure}" >/dev/null 2>&1 ; then
        log info "Existing VM Check Result $extra_message: None existing yet, ok."
        return 0
      fi
      log notice "Existing VM Check Result $extra_message: Virtual Machine(s) were imported previously."
      vm_or_vms_already_existing_test_result=true
      ;;
  esac
}


## Check if VM exists using hypervisor tools.
check_vm_exists_general() {
  if [ "${virtualbox_only}" = "1" ]; then
    return 0
  fi

  if [ "${hypervisor}" = "kvm" ]; then
    return 0
  fi

  check_vm_registered_virtualbox "$1"
}


check_vm_file_exists_virtualbox_general() {
  local file_name_list file_name_item

  ## '/home/user/VirtualBox VMs/Kicksecure-Xfce/Kicksecure-Xfce.vbox'
  ## '/home/user/VirtualBox VMs/Whonix-Gateway-Xfce/Whonix-Gateway-Xfce.vbox'
  ## '/home/user/VirtualBox VMs/Whonix-Workstation-Xfce/Whonix-Workstation-Xfce.vbox'

  if [ -z "${user_home_dir+x}" ]; then
    log warn "VM Import Check: Skip testing if there is an extraneous '.vbox' file because environment variable user_home_dir is unset."
    return 0
  fi

  file_name_list=()
  case "${guest}" in
    whonix)
      if [ "${import_only}" = "gateway" ]; then
        file_name_list+=("${user_home_dir}/VirtualBox VMs/${guest_full_vm_name_gateway}/${guest_full_vm_name_gateway}.vbox")
      elif [ "${import_only}" = "workstation" ]; then
        file_name_list+=("${user_home_dir}/VirtualBox VMs/${guest_full_vm_name_workstation}/${guest_full_vm_name_workstation}.vbox")
      else
        file_name_list+=("${user_home_dir}/VirtualBox VMs/${guest_full_vm_name_gateway}/${guest_full_vm_name_gateway}.vbox")
        file_name_list+=("${user_home_dir}/VirtualBox VMs/${guest_full_vm_name_workstation}/${guest_full_vm_name_workstation}.vbox")
      fi
    ;;
    kicksecure)
      file_name_list+=("${user_home_dir}/VirtualBox VMs/${guest_full_vm_name_kicksecure}/${guest_full_vm_name_kicksecure}.vbox")
    ;;
  esac

  for file_name_item in "${file_name_list[@]}" ; do
    if test_file -e "$file_name_item" ; then
      log warn "VM Import Check: Inconsistent state.
This might have happened by previously using VirtualBox GUI 'Remove...' with 'Remove only' instead of 'Delete all files'.
File '$file_name_item' exists but there is no associated VM registered in VirtualBox."
      if [ -z "${import_only}" ]; then
        die 1 "VM Import Check: Aborting because of inconsistent state and missing '--import-only' option."
      fi
      if [ "${destroy_existing_guest}" != "1" ]; then
        die 1 "VM Import Check: Aborting because of inconsistent state and missing '--destroy-existing-guest' option."
      fi
      log warn "VM superfluous '.vbox' File Deletion: Removing file '$file_name_item' via '--import-only=${import_only}' and '--destroy-existing-guest' option."
      log_run warn rm -f -- "${file_name_item}"
    fi
  done
}


vm_delete_kicksecure() {
  if [ "${vm_or_vms_already_existing_test_result}" = "true" ]; then
    log_run notice run_as_target_user vboxmanage unregistervm "${guest_full_vm_name_kicksecure}" --delete
  else
    log notice "VM Deletion: Kicksecure VM does not exist, no need to delete, ok."
  fi
}


vm_delete_gateway() {
  if [ "${gateway_exists}" = "1" ]; then
    log_run notice run_as_target_user vboxmanage unregistervm "${guest_full_vm_name_gateway}" --delete
  else
    log notice "VM Deletion: Gateway VM does not exist, no need to delete, ok."
  fi
}


vm_delete_workstation() {
  if [ "${workstation_exists}" = "1" ]; then
    log_run notice run_as_target_user vboxmanage unregistervm "${guest_full_vm_name_workstation}" --delete
  else
    log notice "VM Deletion: Workstation VM does not exist, no need to delete, ok."
  fi
}


vm_delete_maybe() {
  case "${guest}" in
    whonix)
      if [ "${destroy_existing_guest}" = "1" ]; then
        ## '--destroy-existing-guest' option is set.
        if [ "${import_only}" = "gateway" ]; then
          log warn "VM Deletion: 'yes' - Deleting previously imported gateway via '--import-only=gateway' option... (If it exists.)"
          vm_delete_gateway
        elif [ "${import_only}" = "workstation" ]; then
          log warn "VM Deletion: 'yes' - Deleting previously imported workstation via '--import-only=workstation' option... (If it exists.)"
          vm_delete_workstation
        elif [ "${import_only}" = "both" ]; then
          log warn "VM Deletion: 'yes' - Deleting previously imported gateway via '--import-only=both' option... (If it exists.)"
          vm_delete_gateway
          log warn "VM Deletion: 'yes' - Deleting previously imported workstation via '--import-only=both' option... (If it exists.)"
          vm_delete_workstation
        else
          die 1 "VM Deletion: 'no' - Not deleting any previously imported VMs because '--import-only' option is not set..."
        fi
      else
        ## '--destroy-existing-guest' option not yet.
        if [ -n "${import_only}" ]; then
          if [ "${gateway_exists}" = "1" ] && [ "${import_only}" = "gateway" ]; then
            die 1 "${underline}Existing VM Check Result:${nounderline} '--import-only' option was set to 'gateway', but it already exists and '--destroy-existing-guest' option was not set."
          elif [ "${workstation_exists}" = "1" ] && [ "${import_only}" = "workstation" ] ; then
            die 1 "${underline}Existing VM Check Result:${nounderline} '--import-only' option was set to 'workstation', but it already exists and '--destroy-existing-guest' option was not set."
          fi
        ## FIXME: Shouldn't import_only=both be handled here?
        else
          log info "Existing VM Check: Neither '--destroy-existing-guest' nor '--import-only' option was set, ok."
        fi
      fi
      ;;
    kicksecure)
      if [ "${destroy_existing_guest}" = "1" ]; then
        ## If VMs exists and '--destroy-existing-guest' is set, remove VMs as they are gonna
        ## be imported later by main.
        log warn "VM Deletion: 'yes' - Deleting previously imported Virtual Machine(s) via '--destroy-existing-guest' option. (If it exists.)"
        vm_delete_kicksecure
      else
        log notice "VM Deletion: 'no' - Not deleting previously any imported VMs because neither using '--destroy-existing-guest' option."
      fi
      ;;
  esac
}


## Check if guest should start or not
check_guest_boot() {
  if [ "${virtualbox_only}" = "1" ]; then
    return 0
  fi
  if [ "${hypervisor}" = "kvm" ]; then
    return 0
  fi

  if [ "${can_boot_virtualbox_guest_vms}" = 'false' ]; then
    log warn "A reboot may be required before the installed virtual machine(s) can be launched."
  fi

  ## Refresh variables vm_or_vms_already_existing_test_result, gateway_exists and workstation_exists.
  check_vm_exists_general "(check after import)"

  case "${guest}" in
    whonix)
      if [ "${gateway_exists}" = "1" ]; then
        log notice "Available guest: '${guest_full_vm_name_gateway}'"
      fi
      if [ "${workstation_exists}" = "1" ]; then
        log notice "Available guest: '${guest_full_vm_name_workstation}'"
      fi
      ;;
    kicksecure)
      log notice "Available guest: '${guest_full_vm_name_kicksecure}'"
      ;;
  esac

  if [ "$kernel_module_has_been_loaded" = "false" ]; then
    ## Kernel modules have not been loaded.
    user_warned_potential_startup_issue=true
    log warn "Debugging information:
Secure Boot Status: '$secure_boot_status_pretty'
Kernel module has been loaded: '$kernel_module_has_been_loaded'
Kernel modules can be loaded: '$kernel_modules_can_be_loaded' (sysctl kernel.modules_disabled=0 is set.)
Only signed kernel modules can be loaded: '$modules_signed_only'
Kernel module vboxdrv signer: '$kernel_module_signer'
kernel_module_modprobe_output ('$sucmd modprobe vboxdrv'): '$kernel_module_modprobe_output'"

    if [ "$kernel_modules_can_be_loaded" = "true" ]; then
      log error "${underline}VirtualBox Installation Result:${nounderline} ${red}${bold}'FAIL'${nobold}${nocolor} - because kernel modules have not been loaded yet.
  - Kernel modules can be loaded in theory, should be loaded by now, but are not.
  - This is a VirtualBox installation issue."
      if [ "${ci}" != "1" ]; then
        virtualbox_start_failed
      fi
    else
      log error "${underline}VirtualBox Installation Result:${nounderline} ${red}${bold}'REBOOT REQUIRED'${nobold}${nocolor} - because kernel module loading is disabled on your system:
  - The system administrator might have disabled module loading.
  - Module loading might have been disabled by Kicksecure package security-misc-shared.
  - You can re-run this installer again after reboot."
      if [ "${ci}" != "1" ]; then
        virtualbox_start_failed
      fi
    fi
  fi

  log notice "${underline}VirtualBox Installation Result:${nounderline} 'success'"

  log info "VM Start: Checking if user wants to start Virtual Machine(s) now..."

  if [ "${qubes_template_detected}" = "true" ]; then
    log notice "${underline}VirtualBox Startup Check:${nounderline} Not starting VirtualBox because running inside a Qubes Template."
    ## Avoid needlessly starting VirtualBox inside a Qubes Template.
    ## That would not be useful, would create unnecessary files inside the Template's home folder and might be confusing.
    ## What instead might be useful for development purposes is to install VirtualBox inside a Qubes Template for the purpose
    ## of using a Qubes App Qube for building VirtualBox VM images.
    ## Run end_installer here, because no VMs were downloaded anyhow if a Qubes Template was detected.
    end_installer
  elif [ -n "${target_user}" ]; then
    log notice "${underline}VirtualBox Startup Check:${nounderline} Not starting VirtualBox because user is installing VMs as another user."
    ## Starting VMs installed under a different user account is difficult and could cause serious problems.
    end_installer
  elif [ "${no_boot}" = "1" ]; then
    log notice "${underline}VirtualBox Startup Check:${nounderline} User declined to start VirtualBox via '--no-boot' option."
    log notice "VirtualBox can be started manually."
    ## Not running end_installer here, because that happens below and we want the following output messages.
    #end_installer
  elif [ "${non_interactive}" = "1" ]; then
    log notice "${underline}VirtualBox Startup Check:${nounderline} Starting VirtualBox automatically via '--non-interactive' option."
    if [ "${virtualbox_only}" = "1" ]; then
      start_virtualbox_gui
      end_installer
    fi
  else
    if [ "${virtualbox_only}" = "1" ]; then
      log notice "${bold}Question:${nobold} Do you want to start VirtualBox now? [y/n] (default: yes): "
    else
      log notice "${bold}Question:${nobold} Do you want to start VirtualBox and the ${guest_pretty} Virtual Machine(s) now? [y/n] (default: yes): "
    fi
    printf '%s' "Your answer: "
    local response
    read -r response
    log notice "User replied: '${response}'"
    case ${response} in
      ""|[Yy]|[Yy][Ee][Ss])
        log notice "${underline}VirtualBox Startup Check:${nounderline} User accepted to start VirtualBox."
        if [ "${virtualbox_only}" = "1" ]; then
          start_virtualbox_gui
          end_installer
        fi
        ;;
      *)
        log notice "${underline}VirtualBox Startup Check:${nounderline} User declined to start VirtualBox."
        end_installer
        ;;
    esac
  fi

  log info "Virtual Machine(s) already exist."
  if [ "${redownload}" != "1" ]; then
    log notice "Hint: If you would like to redownload the image, read about '--redownload' option (safe)."
  fi
  if [ "${destroy_existing_guest}" != "1" ]; then
    log notice "Hint: If you would like to delete a VM and re-import, read about '--destroy-existing-guest' option (danger)."
  fi

  if [ "${no_boot}" = "1" ]; then
    ## Skip guest boot
    log notice "${underline}VM Startup Check:${nounderline} User declined to start Virtual Machine(s) via '--no-boot' option."
    log notice "Virtual Machine(s) can be started manually."
    end_installer
  fi

  if [ "${non_interactive}" = "1" ]; then
    ## start guest without interaction
    log notice "${underline}VM Startup Check:${nounderline} VM start agreed by the user via '--non-interactive' setting."
    ## Try to start VMs before trying to start VirtualBox Manager,
    ## because if it fails, it is less confusing to avoid starting VirtualBox Manager.
    start_guest
    start_virtualbox_gui
    end_installer
  fi

  case ${response} in
    ""|[Yy]|[Yy][Ee][Ss])
      log notice "${underline}VM Startup Check:${nounderline} User accepted to start Virtual Machine(s)."
      start_guest
      start_virtualbox_gui
      ;;
    *)
      log notice "${underline}VM Startup Check:${nounderline} User declined to start Virtual Machine(s)."
      log notice "The following Virtual Machine(s) can be started manually: '${guest_pretty}'"
      ;;
  esac
  end_installer
}


import_guest() {
  if [ "${virtualbox_only}" = "1" ]; then
    return 0
  fi

  if [ "${no_import}" = "1" ]; then
    log notice "VM Import: Not importing guest via '--import-only' option."
    end_installer
  fi

  case "${hypervisor}" in
    virtualbox)
      import_virtualbox
      ;;
    kvm)
      import_kvm
      ;;
  esac
}


extract_vm_name_from_virtualbox_ova() {
  local output system_number a b c d
  has awk || die 1 "${underline}extract_vm_name_from_virtualbox_ova:${nounderline} Package 'gawk' is missing. Please install."
  output="$1"
  system_number="$2"
  a=$(printf '%s' "$output" | grep --max-count=1 --after-context=4 -- "Virtual system $system_number:")
  b=$(printf '%s' "$a" | grep -- "Suggested VM name")
  c=$(printf '%s' "$b" | grep -oP -- '"\K[^"]+')
  ## Take first word only because VirtualBox would suggest name "Whonix-Gateway-Xfce 1" in case,
  ## a VM "Whonix-Gateway-Xfce" is already registered in VirtualBox.
  d=$(printf '%s' "$c" | awk '{print $1}')
  printf '%s' "$d"
}


## Import VirtualBox images
import_virtualbox() {
  local vbox_arg_normal vbox_arg_general vm_purge vbox_arg_importonly \
    vsys_0_actual vsys_1_actual do_continue output

  ## Check how many systems to import.
  ## vsys 0: gateway or Kicksecure
  ## vsys 1: workstation

  case "${guest}" in
    whonix)
      ## if importing whonix, import 2 virtual systems
      vbox_arg_normal=( '--vsys' '0' '--eula' 'accept' '--vsys' '1' '--eula' 'accept' )
    ;;
    kicksecure)
      vbox_arg_normal=( '--vsys' '0' '--eula' 'accept' )
    ;;
  esac

  vbox_arg_general=( "${vbox_arg_normal[@]}" )
  vm_purge="purgeme"

  case "${import_only}" in
    workstation)
      vbox_arg_importonly=( '--vsys' '0' '--eula' 'accept' '--vmname' "${vm_purge}" '--vsys' '1' '--eula' 'accept' )
      vbox_arg_general=( "${vbox_arg_importonly[@]}" )
      ;;
    gateway)
      vbox_arg_importonly=( '--vsys' '0' '--eula' 'accept' '--vsys' '1' '--eula' 'accept' '--vmname' "${vm_purge}" )
      vbox_arg_general=( "${vbox_arg_importonly[@]}" )
      ;;
    "")
      ;;
  esac

  if [ "${vm_or_vms_already_existing_test_result}" = "true" ]; then
    if [ -n "${import_only}" ]; then
      do_continue=yes
      log info "VM Import Check Result: Continue via '--import-only' option."
    else
      do_continue=no
      log info "VM Import Check Result: Skipping import because at least 1 VM already exists but '--import-only' option is not set."
    fi
  else
    do_continue=yes
    log info "VM Import Check Result: Continue because no VM(s) exist yet."
  fi
  if [ "$do_continue" = "no" ]; then
    log notice "VM Import Check Result: Skipping to import, because Virtual Machine(s) have been imported previously and not using '--import-only' option."
    return 0
  fi

  check_vm_file_exists_virtualbox_general

  output=$(log_run notice run_as_target_user "${vboxmanage_locale_english[@]}" import "${directory_prefix}/${guest_file}.${guest_file_ext}" "${vbox_arg_normal[@]}" --dry-run) || \
    die 105 "${underline}VM Import:${nounderline} Failed to import virtual machines."

  if [ "${dry_run}" = "1" ]; then
    log info "VM import: Skip vsys test via '--dry-run'."
  else
    case "${guest}" in
      whonix)
        vsys_0_actual=$(extract_vm_name_from_virtualbox_ova "$output" 0) || true
        vsys_1_actual=$(extract_vm_name_from_virtualbox_ova "$output" 1) || true
        if [ ! "${guest_full_vm_name_gateway}" = "${vsys_0_actual}" ]; then
          die 105 "${underline}VM Import:${nounderline} BUG: vsys0 is actually '${vsys_0_actual}', not '${guest_full_vm_name_gateway}'."
        fi
        if [ ! "${guest_full_vm_name_workstation}" = "$vsys_1_actual" ]; then
          die 105 "${underline}VM Import:${nounderline} BUG: vsys1 is actually '${vsys_1_actual}', not '${guest_full_vm_name_workstation}'."
        fi
        log info "vsys_0_actual: ${vsys_0_actual}"
        log info "vsys_1_actual: ${vsys_1_actual}"
      ;;
      kicksecure)
        vsys_0_actual=$(extract_vm_name_from_virtualbox_ova "$output" 0) || true
        if [ ! "${guest_full_vm_name_kicksecure}" = "${vsys_0_actual}" ]; then
          die 105 "${underline}VM Import:${nounderline} BUG: vsys0 is actually '${vsys_0_actual}', not '${guest_full_vm_name_kicksecure}'."
        fi
        log info "vsys_0_actual: ${vsys_0_actual}"
      ;;
    esac
  fi

  log notice "VM Import: Importing Virtual Machine(s)..."

  ## import VirtualBox image
  log_run notice run_as_target_user "${vboxmanage_locale_english[@]}" import "${directory_prefix}/${guest_file}.${guest_file_ext}" "${vbox_arg_general[@]}" || \
    die 105 "${underline}VM Import:${nounderline} Failed to import virtual machines."

  ## VirtualBox does not accept any command to import a single virtual system
  ## out from an ova with multiple ones.
  ## https://forums.virtualbox.org/viewtopic.php?f=1&t=107965
  if [ -n "${import_only}" ] && [ "${import_only}" != "both" ]; then
    log_run notice run_as_target_user "${vboxmanage_locale_english[@]}" unregistervm "${vm_purge}" --delete ||
      die 1 "${underline}VM Import:${nounderline} Failed to remove extraneous VM '${vm_purge}'."
  fi

  log notice "VM Import: 'success'"
  log notice "You can now open the VirtualBox application and start using: '${guest_pretty}'"
}


## Import KVM images
import_kvm() {
  ## placeholder
  log notice "KVM import feature does not exist. Ending run."
  exit 0
}


start_guest() {
  if [ "${virtualbox_only}" = "1" ]; then
    return 0
  fi
  case "${hypervisor}" in
    virtualbox)
      start_virtualbox_vm
      ;;
    kvm)
      start_kvm_vm
      ;;
  esac
}


## Start the hypervisor with the desired guest
start_virtualbox_vm() {
  log notice "Virtual Machine Startup: Starting Virtual Machine(s)..."
  ## 'virtualbox' is called after 'vboxmanage' because it hangs till the app
  ## is closed, while 'vboxmanage startvm' exits after starting the VMs.
  case "${guest}" in
    whonix)
      if [ "${gateway_exists}" = "1" ]; then
        log_run notice vboxmanage startvm "${guest_full_vm_name_gateway}" || virtualbox_start_failed
      fi
      if [ "${workstation_exists}" = "1" ]; then
        log_run notice vboxmanage startvm "${guest_full_vm_name_workstation}" || virtualbox_start_failed
      fi
      ;;
    kicksecure)
      log_run notice vboxmanage startvm "${guest_full_vm_name_kicksecure}" || virtualbox_start_failed
      ;;
  esac
  log notice "${underline}Virtual Machine Startup Result:${nounderline} 'success'"
}


start_virtualbox_gui() {
  if pgrep -- 'VirtualBox$' >/dev/null ; then
    log notice "VirtualBox Manager Startup Result: Not starting duplicate VirtualBox GUI because already running."
    return 0
  fi

  run_background=1
  log_run notice virtualbox
  if [ "${dry_run}" != "1" ]; then
    log notice "VirtualBox Manager Startup Result: Launched VirtualBox GUI into the background. (pid: '$background_pid')"
  fi
  run_background=""
}


virtualbox_start_failed() {
  local likely_cause
  likely_cause=""
  if [ "$user_warned_potential_startup_issue" = "true" ]; then
    likely_cause="- This is likely happening due to the warnings that have been reported above."
  fi
  die 106 "${underline}Virtual Machine Startup Result:${nounderline} ${red}${bold}FAIL${nobold}${nocolor}
  $likely_cause
  - The installer succeeded with download and import, but
  - failed to start the virtual machines (VMs).
  - The root cause for this issue is likely not the installer.
  - This issue would likely also happen if the user tried to manually start the VMs.
  - Resolving these issues (as per documentation hyperlinks above) would likely resolve this issue.
  https://www.kicksecure.com/wiki/VirtualBox/Troubleshooting"
}


## Detect if repository is configured in the one-line format sources list.
get_pattern_sources_debian() {
  local file pattern

  file="${1}"
  pattern="${2}"
  grep -v "^\s*#" -- "${file}" | grep -E -- "${pattern}" >/dev/null || return 1
}

## Detect if repository is configured in the deb822 format sources.
get_pattern_sources_deb822_debian() {
  local file types_pattern uris_pattern suites_pattern components_pattern \
    current_types current_uris current_suites current_components match_hit \
    line

  file="${1:-}"
  types_pattern="${2:-}"
  uris_pattern="${3:-}"
  suites_pattern="${4:-}"
  components_pattern="${5:-}"

  current_types=""
  current_uris=""
  current_suites=""
  current_components=""

  while read -r line; do
    if [[ "${line}" =~ ^\s*# ]]; then
      continue
    fi

    line="$(sed 's/^\s*//; s/\s*$//;' <<< "${line}")"
    if [ -z "${line}" ]; then
      match_hit='yes'
      if [ -n "${types_pattern}" ] \
        && ! [[ "${current_types}" =~ ${types_pattern} ]]; then
        match_hit='no'
      elif [ -n "${uris_pattern}" ] \
        && ! [[ "${current_uris}" =~ ${uris_pattern} ]]; then
        match_hit='no'
      elif [ -n "${suites_pattern}" ] \
        && ! [[ "${current_suites}" =~ ${suites_pattern} ]]; then
        match_hit='no'
      elif [ -n "${current_components}" ] \
        && ! [[ "${current_components}" =~ ${components_pattern} ]]; then
        match_hit='no'
      fi

      if [ "${match_hit}" = 'yes' ]; then
        return 0
      fi

      current_types=""
      current_uris=""
      current_suites=""
      current_components=""
    fi

    if [[ "${line}" =~ ^Types: ]]; then
      current_types="$(cut -d' ' -f2-)"
    elif [[ "${line}" =~ ^URIs: ]]; then
      current_uris="$(cut -d' ' -f2-)"
    elif [[ "${line}" =~ ^Suites: ]]; then
      current_suites="$(cut -d' ' -f2-)"
    elif [[ "${line}" =~ ^Components: ]]; then
      current_components="$(cut -d' ' -f2-)"
    fi
  done < "${file}"

  return 1
}

write_sources_debian() {
  local url file signed_by_key

  url="${1}"
  file="${2}"
  signed_by_key="${3}"

  test -e "${signed_by_key}" || die 1 "${underline}Sources List Writer:${nounderline} signed-by key missing: '${signed_by_key}'"

  printf '%s\n' "${url}" | root_cmd tee -- "${file}" ||
    die 1 "${underline}Sources List Writer:${nounderline} Failed to write to file: '${file}'"

  root_cmd chmod o+r -- "${file}"
}


## https://stackoverflow.com/a/54239534
## "apt list --installed $pkg" does not fail if package is not installed.
check_installed_debian() {
  local status

  status="$(dpkg-query --show --showformat='${db:Status-Status}' "$1" 2>&1)"
  # shellcheck disable=SC2181
  if [ "$?" != 0 ] || [ "$status" != "installed" ]; then
    return 1
  fi
  return 0
}


disable_kvm_virt_at_load() {
  local module_list_str kvm_variant kvm_variant_unload_failed \
    kvm_core_unload_failed kvm_reload_failed

  module_list_str=''
  kvm_variant=''
  kvm_variant_unload_failed='false'
  kvm_core_unload_failed='false'
  kvm_reload_failed='false'

  ## KVM's 'enable_virt_at_load' feature renders VirtualBox unusable on systems
  ## using Linux 6.12 or higher.
  ##
  ## VirtualBox can be fixed by setting 'enable_virt_at_load=0' in the KVM
  ## driver. This does not break KVM, it simply prevents it from conflicting
  ## with VirtualBox (at the expense of making the first KVM VM launch and the
  ## last KVM VM shutdown take slightly longer). This is safe to set even on
  ## kernels older than 6.12, it will act as a no-op there.
  ##
  ## Using modprobe.d for setting this parameter as recommended by
  ## https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1082157#29
  ##
  ## Using same method documented at
  ## https://www.kicksecure.com/wiki/Template:VirtualBox_Host_Software_Installation#Ubuntu
  log notice "Disabling KVM virt-at-load feature to enable VirtualBox to function. This will not break the ability to use KVM virtual machines..."
  printf '%s\n' "\
## Prevent VirtualBox and KVM from conflicting. This does not break KVM.
options kvm enable_virt_at_load=0" \
    | root_cmd tee /etc/modprobe.d/disable-kvm-virt-at-load.conf >/dev/null
  log notice "KVM virt-at-load feature disabled."

  ## Hot-reloading KVM kernel module if possible. This is necessary for VM
  ## autostart to work later.
  log notice "Attempting hot reload of KVM kernel modules to allow VirtualBox VM auto-start later..."
  module_list_str="$(lsmod | cut -d' ' -f1)"

  if grep '^kvm_intel$' <<< "${module_list_str}" ; then
    kvm_variant='intel'
  elif grep '^kvm_amd$' <<< "${module_list_str}" ; then
    kvm_variant='amd'
  elif grep '^kvm$' <<< "${module_list_str}" ; then
    ## KVM hot-reload will almost certainly fail if we can't detect the KVM
    ## variant, as the core KVM module requires a helper module for the
    ## specific CPU architecture. Warn, but try to reload anyway.
    log warn "Could not detect KVM variant. KVM hot reload will probably fail."
    kvm_variant='unknown'
  fi

  if [ "${kvm_variant}" != 'unknown' ]; then
    root_cmd modprobe -r "kvm_${kvm_variant}" || kvm_variant_unload_failed='true'
  fi
  root_cmd modprobe -r 'kvm' || kvm_core_unload_failed='true'

  if [ "${kvm_variant_unload_failed}" = 'true' ]; then
    log warn "Failed to unload KVM kernel modules. A reboot will possibly be required before VirtualBox VMs will function properly."
    can_boot_virtualbox_guest_vms='false'
  elif [ "${kvm_core_unload_failed}" = 'true' ]; then
    ## Try to put the variant kernel module back. It is not fatal if this
    ## fails.
    root_cmd modprobe "kvm_${kvm_variant}" || true
    log warn "Failed to unload KVM kernel modules. A reboot will possibly be required before VirtualBox VMs will function properly."
    can_boot_virtualbox_guest_vms='false'
  fi

  if [ "${kvm_variant}" != 'unknown' ]; then
    root_cmd modprobe "kvm_${kvm_variant}" || kvm_reload_failed='true'
  else
    root_cmd modprobe 'kvm' || kvm_reload_failed='true'
  fi

  if [ "${kvm_reload_failed}" = 'true' ]; then
    log warn "Failed to reload KVM kernel modules. A reboot will possibly be required before either VirtualBox or KVM VMs will function properly."
    can_boot_virtualbox_guest_vms='false'
    return 0
  fi

  log notice "Successfully reloaded KVM kernel modules. VirtualBox VMs should be functional."
}


install_package_fedora_common() {
  pkg_mngr="dnf"
  pkg_mngr_install=( "${pkg_mngr}" 'install' '--assumeyes' '--setopt=install_weak_deps=False' )
  ## Fedora lacks an equivalent to Debian's '--error-on=any'.
  #pkg_mngr_update=( "${pkg_mngr}" 'update' '--assumeyes' )
  ## 'dnf makecache' does exit non-zero if a repository is unavailable.
  ## --assumeyes good or bad idea? For now not needed. So not using.
  #pkg_mngr_update( "${pkg_mngr}" 'makecache' )
  pkg_mngr_update=( "${pkg_mngr}" '--assumeyes' 'update' )
  pkg_mngr_check_installed=( "${pkg_mngr}" 'list' '--installed' )
  pkg_mngr_upgrade_check=( '/bin/true' 'dnf does not seem to support an alternative to apt-get full-upgrade --simulate, skipping.' )
  pkg_mngr_upgrade_install=( "${pkg_mngr}" 'upgrade' )

  pkg_mngr_upgradable_check() {
    /bin/true 'dnf does not seem to support an alternative to apt-get full-upgrade --simulate, skipping.'
    return 0
  }

  update_sources
  #check_upgrades_simulation

  if [ "${virtualbox_only}" = "1" ]; then
    return 0
  fi

  install_pkg torsocks dnf-plugins-core procps-ng

  ## Conflicting packages requires '--allowerasing'.
  if "${pkg_mngr_check_installed[@]}" lsb_release >/dev/null 2>&1 ||
    "${pkg_mngr_check_installed[@]}" redhat-lsb-core >/dev/null 2>&1
  then
    true
  else
    install_pkg lsb_release >/dev/null 2>&1
  fi

  ## Requires lsb_release / lsb-release.
  get_specifics

  install_signify signify
}


install_package_debian_common() {
  local dpkg_audit_output sources_list_d_file

  if [[ -v APTGETOPT_SERIALIZED ]]; then
    # shellcheck disable=SC2128
    mapfile -t APTGETOPT <<< "$APTGETOPT_SERIALIZED"
  else
    APTGETOPT_SERIALIZED=()
    if [[ -v APTGETOPT ]]; then
      APTGETOPT=()
    fi
  fi
  # shellcheck disable=SC2145
  log info "APTGETOPT_SERIALIZED: '${APTGETOPT_SERIALIZED[@]}'"
  # shellcheck disable=SC2145
  log info "APTGETOPT: '${APTGETOPT[@]}'"

  pkg_mngr="apt-get"
  pkg_mngr_install=( "${pkg_mngr}" '--yes' '--no-install-recommends' "${APTGETOPT[@]}" 'install' )
  pkg_mngr_update=( "${pkg_mngr}" '--yes' '--error-on=any' "${APTGETOPT[@]}" 'update' )
  pkg_mngr_check_installed=( "check_installed_debian" )
  pkg_mngr_upgrade_check=( "${pkg_mngr}" '--simulate' "${APTGETOPT[@]}" 'full-upgrade' )
  pkg_mngr_upgrade_install=( "${pkg_mngr}" "${APTGETOPT[@]}" 'full-upgrade' )

  # shellcheck disable=SC2145
  log info "pkg_mngr_update: '${pkg_mngr_update[@]}'"

  pkg_mngr_upgradable_check() {
    local upgrade_check_output upgrade_check_grep_output number_upgradable
    ## Not using 'apt list --upgradable', because it is not the same as
    ## 'sudo apt-get dist-upgrade --target-release=trixie-fasttrack'.
    ## It misses out on packages from "trixie-fasttrack".
    upgrade_check_output="$(root_cmd "${pkg_mngr_upgrade_check[@]}" "$@" 2>&1)" || true
    ## Counting both 'Inst' and 'Conf' is not precise and will double count, but it
    ## still indicates that APT has work to do.
    upgrade_check_grep_output="$(printf '%s' "${upgrade_check_output}" | grep -e '^Inst' -e '^Conf')" || true
    number_upgradable=0
    if [ -n "$upgrade_check_grep_output" ]; then
      number_upgradable="$(printf '%s' "${upgrade_check_grep_output}" | wc -l)"
    fi
    if [ "$number_upgradable" -gt "0" ]; then
      return 1
    fi
    return 0
  }

  dpkg_audit_output="$(dpkg --audit 2>&1)" || true
  if [ -n "${dpkg_audit_output}" ]; then
    die 1 "${underline}DPKG Audit Test Result:${nounderline} ${red}${bold}'FAIL'${nobold}${nocolor}
- What happened in simple terms? This installer has performed a check to ensure it's sane to proceed, which has failed.
- What exactly happened? The command 'dpkg --audit' generated output, signaling a system issue.
- What was the expected behavior? The 'dpkg --audit' command should execute without producing any output.
- Is this installer to blame for the issue? It's unlikely that this installer is the root cause of the issue.
- So, what might have caused it? The issue likely originates from pre-existing DPKG / APT package management system issues on your machine.
- What steps should you take now? Start by running the following command yourself:

dpkg --audit

- What should you do after that? Analyze the output of the command to identify and address the underlying problem. Once resolved, you can re-run this installer.

- Do you have any suggestions? The following link might offer helpful insights:

https://www.kicksecure.com/wiki/Operating_System_Software_and_Updates#Broken_APT

If that doesn't resolve the issue, consider reaching out to your operating system's support team for assistance."
  fi

  ## Dumping all sources.list files could have privacy implications.
  ## Hence only doing it for '--dev'.
  if [ "${dev}" = "1" ]; then
    for sources_list_d_file in /etc/apt/sources.list /etc/apt/sources.sources /etc/apt/sources.list.d/*; do
      if [ -f "$sources_list_d_file" ]; then
        cat -- "$sources_list_d_file"
      fi
    done
  fi

  update_sources
  check_upgrades_simulation

  if [ "$debian_derivative_detected" = "1" ]; then
    install_pkg lsb-release procps
  fi

  ## Requires lsb_release / lsb-release.
  get_specifics

  if [ "${virtualbox_only}" = "1" ]; then
    return 0
  fi

  true "debian_testing_or_unstable_detected: $debian_testing_or_unstable_detected"
  true "kali_derivative_detected: $kali_derivative_detected"

  ## https://bugs.debian.org/1066313
  #if [ "${debian_testing_or_unstable_detected}" != "1" ] && [ "${kali_derivative_detected}" != "1" ]; then
    #true "INFO: either debian_testing_or_unstable_detected or kali_derivative_detected = 1"
    #install_pkg torsocks
  #fi

  install_signify signify-openbsd torsocks
}


add_user_to_vbox_group() {
  local id_of_user

  id_of_user="$(id --name --user)" || die 1 "${underline}Linux user ID check:${nounderline} Failed to run: 'id --name --user'"
  if id -nG "${id_of_user}" | grep -w -- "${virtualbox_linux_user_group}\$" >/dev/null; then
    log info "Linux Group Configuration: Account '${id_of_user}' is already a member of the Linux group 'vboxusers'."
    return 0
  fi

  if [ "${debian_derivative_detected}" = "1" ]; then
    root_cmd adduser "${id_of_user}" "${virtualbox_linux_user_group}" || \
      die 1 "${underline}Linux Group Configuration: adduser to Linux user group virtualbox:${nounderline} Failed to add user '$id_of_user' to group '${virtualbox_linux_user_group}'."
  else
    root_cmd usermod --append --groups "${virtualbox_linux_user_group}" "${id_of_user}" || \
      die 1 "${underline}Linux Group Configuration: adduser to Linux user group virtualbox:${nounderline} Failed to add user '$id_of_user' to group '${virtualbox_linux_user_group}'."
  fi
}


## End installation of VirtualBox on Fedora and derived systems.
install_virtualbox_fedora_common_end() {
  if ! has vboxmanage ; then
    if [ "${dry_run}" = "1" ]; then
      log error "Failed to locate 'vboxmanage' program. Ignoring via '--dry-run' option."
    else
      die 1 "${underline}vboxmanage test:${nounderline} Failed to locate 'vboxmanage' program."
    fi
  fi

  add_user_to_vbox_group
  disable_kvm_virt_at_load
}


## End installation of VirtualBox on Debian and derived systems.
install_virtualbox_debian_common_end() {
  if ! has vboxmanage ; then
    if [ "${dry_run}" = "1" ]; then
      log error "Failed to locate 'vboxmanage' program. - Ignoring via '--dry-run' option."
    else
      die 1 "${underline}vboxmanage test:${nounderline} Failed to locate 'vboxmanage' program."
    fi
  fi

  add_user_to_vbox_group
  disable_kvm_virt_at_load
}


kernel_modules_check() {
  if ! [ "${hypervisor}" = "virtualbox" ]; then
    return 0
  fi

  if [ ! -e /dev/vboxdrv ]; then
    kernel_module_has_been_loaded=false
    user_warned_potential_startup_issue=true
    log warn "${underline}VirtualBox Kernel Module Loaded Test Result${nounderline}: 'no' - file '/dev/vboxdrv' does not exist. This probably means that the 'vboxdrv' kernel module has not been loaded yet."
  else
    kernel_module_has_been_loaded=true
    log info "VirtualBox Kernel Module Loaded Test Result: 'yes' - file '/dev/vboxdrv' exists."
  fi

  ## Debugging.
  log info "Command executing: $ $sucmd -- modinfo --field signer vboxdrv"
  if kernel_module_signer=$(root_cmd_loglevel="echo" root_cmd modinfo --field signer vboxdrv 2>&1) ; then
    log info "VirtualBox Kernel Module Signer: '$kernel_module_signer'"
  else
    user_warned_potential_startup_issue=true
    log warn "${underline}VirtualBox Kernel Module Signer${nounderline}: '$kernel_module_signer'"
  fi

  ## Debugging.
  ## modprobe should be automatic but if it fails, figure out why.
  log notice "Command executing: $ $sucmd -- modprobe vboxdrv"
  if kernel_module_modprobe_output=$(root_cmd_loglevel="echo" root_cmd modprobe vboxdrv 2>&1) ; then
    log info "VirtualBox Kernel Module modprobe output: '$kernel_module_modprobe_output'"
  else
    user_warned_potential_startup_issue=true
    log warn "${underline}VirtualBox Kernel modprobe output${nounderline}: '$kernel_module_modprobe_output'"
  fi
}


install_repositories_for_virtualbox_on_debian() {
  local \
    oracle_clearnet oracle_clearnet_uri_base \
    oracle_types_debsource \
    oracle_components_debsource oracle_suites_debsource unstable_clearnet \
    unstable_onion unstable_clearnet_uri_base unstable_onion_uri_base \
    unstable_types_debsource unstable_signedby_debsource \
    unstable_components_debsource unstable_suites_debsource \
    backports_clearnet backports_onion backports_clearnet_uri_base \
    backports_onion_uri_base backports_types_debsource \
    backports_components_debsource \
    backports_suites_debsource fasttrack_clearnet fasttrack_onion \
    fasttrack_clearnet_uri_base fasttrack_onion_uri_base \
    fasttrack_types_debsource \
    fasttrack_components_debsource fasttrack_components_debsource \
    fasttrack_suites_debsource fasttrack_components_debsource_regex \
    fasttrack_dir_regex fasttrack_suffix_debsource_regex \
    fasttrack_backports_staging_suites_debsource \
    fasttrack_backports_staging_suffix_debsource_regex kicksecure_clearnet \
    kicksecure_onion kicksecure_clearnet_uri_base kicksecure_onion_uri_base \
    kicksecure_types_debsource \
    kicksecure_components_debsource kicksecure_suites_debsource \
    kali_clearnet kali_clearnet_uri_base kali_types_debsource \
    kali_components_debsource kali_suites_debsource \
    apt_torified apt_onion oracle_uri_debsource kicksecure_uri_debsource \
    unstable_uri_debsource fasttrack_uri_debsource backports_uri_debsource \
    kali_uri_debsource sources_list_file sources_deb822_file

  if [ "${dev}" = "1" ]; then
    distro_codename_kicksecure_use="${distro_codename_common_use}-developers"
  else
    distro_codename_kicksecure_use="${distro_codename_common_use}"
  fi

  oracle_found=""
  oracle_clearnet="download.virtualbox.org"
  #oracle_onion="" ## non-existing
  oracle_clearnet_uri_base="${oracle_clearnet}/virtualbox/debian/"
  #oracle_onion_uri_base="" ## non-existing
  oracle_file_debsource="/etc/apt/sources.list.d/oracle.list"
  oracle_file_deb822source="/etc/apt/sources.list.d/oracle.sources"
  oracle_types_debsource="deb"
  oracle_signedby_debsource="/usr/share/keyrings/oracle-virtualbox-2016.asc"
  oracle_components_debsource="contrib"
  ## Oracle does not have a Kali dist section, use Debian stable.
  if [ "${ubuntu_derivative_detected}" = "1" ]; then
    oracle_suites_debsource="${distro_codename_ubuntu}"
  elif [ "${debian_derivative_detected}" = "1" ]; then
    oracle_suites_debsource="${distro_codename_debian}"
  else
    oracle_suites_debsource="${distro_codename_common_use}"
  fi

  unstable_found=""
  unstable_clearnet="deb.debian.org"
  unstable_onion="2s4yqjx5ul6okpp3f2gaunr2syex5jgbfpfvhxxbbjwnrsvbk5v3qbid.onion"
  unstable_clearnet_uri_base="${unstable_clearnet}/debian/"
  unstable_onion_uri_base="${unstable_onion}/debian/"
  unstable_file_debsource="/etc/apt/sources.list.d/unstable.list"
  unstable_file_deb822source="/etc/apt/sources.list.d/unstable.sources"
  unstable_types_debsource="deb"
  ## CI /etc/apt/sources.list.d/debian.sources uses:
  ## Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
  ## So we have to use signed-by here too, otherwise we get an apt-get error:
  ## E: Conflicting values set for option Signed-By regarding source
  ## Also needed to avoid an apt notice when using deb822 format sources.
  if test -e "/usr/share/keyrings/debian-archive-keyring.pgp"; then
    ## new
    unstable_signedby_debsource="/usr/share/keyrings/debian-archive-keyring.pgp"
  elif test -e "/usr/share/keyrings/debian-archive-keyring.gpg"; then
    ## old
    unstable_signedby_debsource="/usr/share/keyrings/debian-archive-keyring.gpg"
  else
    unstable_signedby_debsource="/usr/share/keyrings/unknown-please-report-this-bug"
  fi
  unstable_components_debsource="main contrib non-free"
  unstable_suites_debsource="unstable"

  backports_found=""
  backports_clearnet="deb.debian.org"
  backports_onion="2s4yqjx5ul6okpp3f2gaunr2syex5jgbfpfvhxxbbjwnrsvbk5v3qbid.onion"
  backports_clearnet_uri_base="${backports_clearnet}/debian/"
  backports_onion_uri_base="${backports_onion}/debian/"
  backports_file_debsource="/etc/apt/sources.list.d/backports.list"
  backports_file_deb822source="/etc/apt/sources.list.d/backports.sources"
  backports_types_debsource="deb"
  backports_signedby_debsource="/usr/share/keyrings/debian-archive-keyring.gpg"
  backports_components_debsource="main contrib non-free"
  backports_suites_debsource="${distro_codename_common_use}-backports"

  fasttrack_found=""
  fasttrack_clearnet="fasttrack.debian.net"
  fasttrack_onion="5phjdr2nmprmhdhw4fdqfxvpvt363jyoeppewju2oqllec7ymnolieyd.onion"
  fasttrack_clearnet_uri_base="${fasttrack_clearnet}/debian-fasttrack/"
  fasttrack_onion_uri_base="${fasttrack_onion}/debian-fasttrack/"
  fasttrack_file_debsource="/etc/apt/sources.list.d/fasttrack.list"
  fasttrack_file_deb822source="/etc/apt/sources.list.d/fasttrack.sources"
  fasttrack_types_debsource="deb"
  case "${distro_codename_real}" in
    bullseye|bookworm)
      fasttrack_signedby_debsource="/etc/apt/trusted.gpg.d/fasttrack-archive-keyring.gpg"
      ;;
    trixie|forky|sid)
      fasttrack_signedby_debsource="/usr/share/keyrings/fasttrack-archive-keyring.gpg"
      ;;
    *)
      fasttrack_signedby_debsource="/usr/share/keyrings/unknown-please-report-this-bug"
      ;;
  esac
  fasttrack_components_debsource="main contrib non-free"
  fasttrack_suites_debsource="${distro_codename_common_use}-fasttrack"
  fasttrack_components_debsource_regex="main\s+contrib\s+non-free"
  fasttrack_dir_regex="/debian(-fasttrack)?/?\s"
  fasttrack_suffix_debsource_regex="${fasttrack_dir_regex}\s*${fasttrack_suites_debsource}\s*${fasttrack_components_debsource_regex}"

  fasttrack_backports_staging_found=""
  ## same clearnet as fasttrack
  ## same onion as fasttrack
  ## same clearnet_uri_base as fasttrack
  ## same onion_uri_base as fasttrack
  fasttrack_backports_staging_file_debsource="/etc/apt/sources.list.d/fasttrack-backports-staging.list"
  fasttrack_backports_staging_file_deb822source="/etc/apt/sources.list.d/fasttrack-backports-staging.sources"
  ## same types as fasttrack
  ## same signedby as fasttrack
  ## same components as fasttrack
  fasttrack_backports_staging_suites_debsource="${distro_codename_common_use}-backports-staging"
  ## same components_debsource_regex as fasttrack
  ## same dir_regex as fasttrack
  fasttrack_backports_staging_suffix_debsource_regex="${fasttrack_dir_regex}\s*${fasttrack_backports_staging_suites_debsource}\s+${fasttrack_components_debsource_regex}"

  kicksecure_found=""
  kicksecure_clearnet="deb.kicksecure.com"
  kicksecure_onion="deb.w5j6stm77zs6652pgsij4awcjeel3eco7kvipheu6mtr623eyyehj4yd.onion"
  kicksecure_clearnet_uri_base="${kicksecure_clearnet}/"
  kicksecure_onion_uri_base="${kicksecure_onion}/"
  kicksecure_file_debsource="/etc/apt/sources.list.d/derivative.list"
  kicksecure_file_deb822source="/etc/apt/sources.list.d/derivative.sources"
  kicksecure_types_debsource="deb"
  kicksecure_signedby_debsource="/usr/share/keyrings/derivative.asc"
  kicksecure_components_debsource="main contrib non-free"
  kicksecure_suites_debsource="${distro_codename_kicksecure_use}"

  kali_found=""
  kali_clearnet="http.kali.org"
  #kali_onion="" ## non-existing
  kali_clearnet_uri_base="${kali_clearnet}/"
  #kali_onion_uri_base="" ## non-existing
  kali_file_debsource="/etc/apt/sources.list.d/kali.list"
  kali_file_deb822source="/etc/apt/sources.list.d/kali.sources"
  kali_types_debsource="deb"
  ## TODO: Test.
  kali_signedby_debsource="/usr/share/keyrings/kali-archive-keyring.gpg"
  kali_components_debsource="main contrib non-free non-free-firmware"
  kali_suites_debsource="${distro_codename_common_use}"

  apt_torified=""
  apt_onion=""

  ## TODO:
  ## - handle case if using Debian mirrors
  ## - unit tests

  for sources_deb822_file in /etc/apt/sources.list.d/*.sources; do
    [ -f "${sources_deb822_file}" ] || continue

    if get_pattern_sources_deb822_debian "${sources_deb822_file}" "" "${oracle_clearnet}" "" ""
    then
      oracle_found=1
    fi

    if get_pattern_sources_deb822_debian "${sources_deb822_file}" "" "${kicksecure_clearnet}|${kicksecure_onion}" "" ""
    then
      kicksecure_found=1
    fi

    if get_pattern_sources_deb822_debian "${sources_deb822_file}" "" "(${unstable_clearnet}|${unstable_onion})/debian.*" "(unstable|sid)" ""
    then
      unstable_found=1
    fi

    if get_pattern_sources_deb822_debian "${sources_deb822_file}" "" "(${unstable_clearnet}|${unstable_onion})/debian.*" "(unstable|sid)" "contrib"
    then
      unstable_found_with_contrib=1
    fi

    if get_pattern_sources_deb822_debian "${sources_deb822_file}" "" "(${backports_clearnet}|${backports_onion})/debian/?" "${distro_codename_common_use}-backports" ""
    then
      backports_found=1
    fi

    if get_pattern_sources_deb822_debian "${sources_deb822_file}" "" "(${fasttrack_clearnet}|${fasttrack_onion})${fasttrack_dir_regex}" "${fasttrack_suites_debsource}" "${fasttrack_components_debsource_regex}"
    then
      fasttrack_found=1
    fi

    if get_pattern_sources_deb822_debian "${sources_deb822_file}" "" "(${fasttrack_clearnet}|${fasttrack_onion})${fasttrack_dir_regex}" "${fasttrack_backports_staging_suites_debsource}" "${fasttrack_components_debsource_regex}"
    then
      fasttrack_backports_staging_found=1
    fi

    if get_pattern_sources_deb822_debian "${sources_deb822_file}" "" "${kali_clearnet}" "" ""
    then
      kali_found=1
    fi

    ## Kali VirtualBox is in 'contrib'.
    if get_pattern_sources_deb822_debian "${sources_deb822_file}" "" "${kali_clearnet}/kali" "" "contrib"
    then
      kali_found_with_contrib=1
    fi

    if get_pattern_sources_deb822_debian "${sources_deb822_file}" "" "tor://|tor\+" "" ""; then
      apt_torified=1
    fi

    if get_pattern_sources_deb822_debian "${sources_deb822_file}" "" "\.onion" "" ""; then
      apt_onion=1
    fi
  done

  for sources_list_file in /etc/apt/sources.list /etc/apt/sources.list.d/*.list; do
    [ -f "${sources_list_file}" ] || continue

    if get_pattern_sources_debian "${sources_list_file}" "${oracle_clearnet}"
    then
      oracle_found=1
    fi

    if get_pattern_sources_debian "${sources_list_file}" "${kicksecure_clearnet}|${kicksecure_onion}"
    then
      kicksecure_found=1
    fi

    if get_pattern_sources_debian "${sources_list_file}" "(${unstable_clearnet}|${unstable_onion})/debian.* (unstable|sid)"
    then
      unstable_found=1
    fi

    ## Need to check if not only unstable repository or sid codename is enabled but also check if the 'contrib' suite is enabled,
    ## because VirtualBox is only available from 'contrib'.
    if get_pattern_sources_debian "${sources_list_file}" "(${unstable_clearnet}|${unstable_onion})/debian.*\s(unstable|sid).*\scontrib"
    then
      unstable_found_with_contrib=1
    fi

    if get_pattern_sources_debian "${sources_list_file}" "(${backports_clearnet}|${backports_onion})/debian/?\s*${distro_codename_common_use}-backports"
    then
      backports_found=1
    fi

    if get_pattern_sources_debian "${sources_list_file}" "(${fasttrack_clearnet}|${fasttrack_onion})${fasttrack_suffix_debsource_regex}"
    then
      fasttrack_found=1
    fi

    if get_pattern_sources_debian "${sources_list_file}" "(${fasttrack_clearnet}|${fasttrack_onion})${fasttrack_backports_staging_suffix_debsource_regex}"
    then
      fasttrack_backports_staging_found=1
    fi

    if get_pattern_sources_debian "${sources_list_file}" "${kali_clearnet}"
    then
      kali_found=1
    fi

    ## Kali VirtualBox is in 'contrib'.
    if get_pattern_sources_debian "${sources_list_file}" "${kali_clearnet}/kali.*\scontrib"
    then
      kali_found_with_contrib=1
    fi

    if get_pattern_sources_debian "${sources_list_file}" "tor://|tor\+"; then
      apt_torified=1
    fi

    if get_pattern_sources_debian "${sources_list_file}" "\.onion"; then
      apt_onion=1
    fi
  done

  if [ "${apt_torified}" = "1" ]; then
    ## If apt-transport-tor is not installed, we shouldn't, because we got
    ## a false positive that updates should be torified. This can happen if
    ## user configured the sources list to be torified but hasn't installed
    ## apt-transport-tor. It is better to abort, the user should configure
    ## tor by other means to ensure a working connection (bridges, proxy etc).
    log info "APT is supposed to be torified, checking if package apt-transport-tor is installed."
    test_pkg apt-transport-tor ||
      die 1 "${underline}Torified APT Test Result:${nounderline} APT is supposed to be torified, but package apt-transport-tor is not installed. Please install."
  fi

  ## If user has onion repositories configured, prefer it.
  if [ "${apt_onion}" = '1' ]; then
    connection_type_debsource="onion"
    if [ "${oracle_repo}" = "1" ] || [ "${kali_derivative_detected}" = "1" ]; then
      if [ "${oracle_repo}" = "1" ]; then
        log warn "Oracle doesn't provide onion repositories."
      fi
      if [ "${kali_derivative_detected}" = "1" ]; then
        log warn "Kali doesn't provide onion repositories."
      fi
      if [ "${apt_torified}" = '1' ]; then
        if [ "${oracle_repo}" = "1" ]; then
          log warn "Fallback Oracle repository to torified clearnet."
        fi
        if [ "${kali_derivative_detected}" = "1" ]; then
          log warn "Fallback Kali repository to torified clearnet."
        fi
        kali_uri_debsource="tor+https://${kali_clearnet_uri_base}"
        oracle_uri_debsource="tor+https://${oracle_clearnet_uri_base}"
      else
        if [ "${oracle_repo}" = "1" ]; then
          log warn "Fallback Oracle repository to clearnet."
        fi
        if [ "${kali_derivative_detected}" = "1" ]; then
          log warn "Fallback Kali repository to clearnet."
        fi
        kali_uri_debsource="https://${kali_clearnet_uri_base}"
        oracle_uri_debsource="https://${oracle_clearnet_uri_base}"
      fi
    else
      if [ "${apt_torified}" = '1' ]; then
        kali_uri_debsource="tor+https://${kali_clearnet_uri_base}"
        oracle_uri_debsource="tor+https://${oracle_clearnet_uri_base}"
      else
        kali_uri_debsource="https://${kali_clearnet_uri_base}"
        oracle_uri_debsource="https://${oracle_clearnet_uri_base}"
      fi
    fi
    fasttrack_uri_debsource="http://${fasttrack_onion_uri_base}"
    kicksecure_uri_debsource="http://${kicksecure_onion_uri_base}"
    unstable_uri_debsource="http://${unstable_onion_uri_base}"
    backports_uri_debsource="http://${backports_onion_uri_base}"
  ## If user has torified repositories configured, prefer it.
  elif [ "${apt_torified}" = '1' ]; then
    connection_type_debsource="torified clearnet"
    oracle_uri_debsource="tor+https://${oracle_clearnet_uri_base}"
    kicksecure_uri_debsource="tor+https://${kicksecure_clearnet_uri_base}"
    unstable_uri_debsource="tor+https://${unstable_clearnet_uri_base}"
    fasttrack_uri_debsource="tor+https://${fasttrack_clearnet_uri_base}"
    backports_uri_debsource="tor+https://${backports_clearnet_uri_base}"
    kali_uri_debsource="tor+https://${kali_clearnet_uri_base}"
  ## If user doesn't have torified repositories, use clearnet one.
  else
    connection_type_debsource="clearnet"
    oracle_uri_debsource="https://${oracle_clearnet_uri_base}"
    kicksecure_uri_debsource="https://${kicksecure_clearnet_uri_base}"
    unstable_uri_debsource="https://${unstable_clearnet_uri_base}"
    fasttrack_uri_debsource="https://${fasttrack_clearnet_uri_base}"
    backports_uri_debsource="https://${backports_clearnet_uri_base}"
    kali_uri_debsource="https://${kali_clearnet_uri_base}"
  fi

  kicksecure_oneline_source="${kicksecure_types_debsource} [signed-by=${kicksecure_signedby_debsource}] ${kicksecure_uri_debsource} ${kicksecure_suites_debsource} ${kicksecure_components_debsource}"
  unstable_oneline_source="${unstable_types_debsource} [signed-by=${unstable_signedby_debsource}] ${unstable_uri_debsource} ${unstable_suites_debsource} ${unstable_components_debsource}"
  fasttrack_oneline_source="${fasttrack_types_debsource} [signed-by=${fasttrack_signedby_debsource}] ${fasttrack_uri_debsource} ${fasttrack_suites_debsource} ${fasttrack_components_debsource}"
  fasttrack_backports_staging_oneline_source="${fasttrack_types_debsource} [signed-by=${fasttrack_signedby_debsource}] ${fasttrack_uri_debsource} ${fasttrack_backports_staging_suites_debsource} ${fasttrack_components_debsource}"
  backports_oneline_source="${backports_types_debsource} [signed-by=${backports_signedby_debsource}] ${backports_uri_debsource} ${backports_suites_debsource} ${backports_components_debsource}"
  kali_oneline_source="${kali_types_debsource} [signed-by=${kali_signedby_debsource}] ${kali_uri_debsource} ${kali_suites_debsource} ${kali_components_debsource}"

  kicksecure_deb822_source="\
Types: ${kicksecure_types_debsource}
URIs: ${kicksecure_uri_debsource}
Suites: ${kicksecure_suites_debsource}
Components: ${kicksecure_components_debsource}
Signed-By: ${kicksecure_signedby_debsource}"
  unstable_deb822_source="\
Types: ${unstable_types_debsource}
URIs: ${unstable_uri_debsource}
Suites: ${unstable_suites_debsource}
Components: ${unstable_components_debsource}
Signed-By: ${unstable_signedby_debsource}"
  fasttrack_deb822_source="\
Types: ${fasttrack_types_debsource}
URIs: ${fasttrack_uri_debsource}
Suites: ${fasttrack_suites_debsource}
Components: ${fasttrack_components_debsource}
Signed-By: ${fasttrack_signedby_debsource}"
  fasttrack_backports_staging_deb822_source="\
Types: ${fasttrack_types_debsource}
URIs: ${fasttrack_uri_debsource}
Suites: ${fasttrack_backports_staging_suites_debsource}
Components: ${fasttrack_components_debsource}
Signed-By: ${fasttrack_signedby_debsource}"
  backports_deb822_source="\
Types: ${backports_types_debsource}
URIs: ${backports_uri_debsource}
Suites: ${backports_suites_debsource}
Components: ${backports_components_debsource}
Signed-By: ${backports_signedby_debsource}"
  kali_deb822_source="\
Types: ${kali_types_debsource}
URIs: ${kali_uri_debsource}
Suites: ${kali_suites_debsource}
Components: ${kali_components_debsource}
Signed-By: ${kali_signedby_debsource}"

  oracle_oneline_source="${oracle_types_debsource} [signed-by=${oracle_signedby_debsource}] ${oracle_uri_debsource} ${oracle_suites_debsource} ${oracle_components_debsource}"
  oracle_deb822_source="\
Types: ${oracle_types_debsource}
URIs: ${oracle_uri_debsource}
Suites: ${oracle_suites_debsource}
Components: ${oracle_components_debsource}
Signed-By: ${oracle_signedby_debsource}"

  if [ "${oracle_repo}" = "1" ]; then
    install_oracle_repository_debian
    return 0
  fi

  case "${distro_codename_real}" in
    bullseye|bookworm)
      install_backports_and_fasttrack_repository_debian
      return 0
      ;;
    trixie)
      ## Please upload VirtualBox to trixie-fasttrack
      ## https://salsa.debian.org/fasttrack-team/support/-/issues/74
      #install_backports_and_fasttrack_repository_debian
      ##
      ## 'trixie' is no longer compatible with 'unstable'.
      #install_unstable_repository_debian
      ## Therefore, fall back to Oracle repository.
      ## No need, because checking 'if [ "${oracle_repo}" = "1" ]; then' above.
      #install_oracle_repository_debian
      return 0
      ;;
    forky)
      install_unstable_repository_debian
      return 0
      ;;
    kali-rolling)
      install_kali_repository_debian
      return 0
      ;;
    *)
      return 0
      ;;
  esac

  ## Out-commented. Unreachable code.
  #if test "${ubuntu_derivative_detected}" = "1"; then
  #  ## Doesn't require extra repository, but this function is used to get
  #  ## Oracle repository information and protocol (onion, https, tor+http etc)
  #  return 0
  #fi
  #
  #die 1 "${underline}Repository Add:${nounderline} Unsupported distribution codename: '${distro_codename_real}'!"
}


install_oracle_repository_fedora() {
  oracle_found=""
  if dnf repolist --all virtualbox | grep -- "." >/dev/null; then
    oracle_found="1"
  fi
  if dnf repolist --disabled virtualbox | grep -- "." >/dev/null; then
    dnf config-manager --set-enabled virtualbox
  fi
  if [ "${oracle_found}" = "1" ]; then
    log info "Oracle Repository: Skipped adding Oracle repository because it was already found."
  else
    log notice "Oracle Repository: Adding Oracle's clearnet repository to /etc/yum.repos.d/oracle.repo"
    if [ -f /etc/pki/rpm-gpg/RPM-GPG-KEY-oracle ]; then
      log info "Oracle Repository: Key /etc/pki/rpm-gpg/RPM-GPG-KEY-oracle already exists."
    else
      printf '%s\n' "${oracle_pgp}" | \
        run_as_target_user tee -- "${log_dir_cur}/oracle-virtualbox-2016.asc" >/dev/null
      printf '%s\n' "${oracle_pgp}" | \
        root_cmd tee -- /etc/pki/rpm-gpg/RPM-GPG-KEY-oracle >/dev/null
      root_cmd rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-oracle
    fi

    ## Based on:
    ## https://download.virtualbox.org/virtualbox/rpm/fedora/virtualbox.repo
    # shellcheck disable=SC2016
    printf '%s\n' '[virtualbox]
name=Fedora $releasever - $basearch - VirtualBox
baseurl=https://download.virtualbox.org/virtualbox/rpm/fedora/$releasever/$basearch
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-oracle
' | root_cmd tee -- /etc/yum.repos.d/oracle.repo

    ## dnf does not have a command to add the key to its database.
    ## https://bugzilla.redhat.com/show_bug.cgi?id=1768206
    ## Workaround to accept the key with --assumeyes without accepting other things.
    ## Does not work.
    #root_cmd dnf --assumeyes --cacheonly search VirtualBox
    #root_cmd dnf --assumeyes module list
  fi
}


install_oracle_repository_debian() {
  if [ "${oracle_found}" = "1" ]; then
    log info "Oracle Repository: Skipped adding Oracle repository because it was already found."
  else
    if [ -f /usr/share/keyrings/oracle.asc ]; then
      log info "Oracle Repository: Key /usr/share/keyrings/oracle.asc already exists."
    else
      printf '%s\n' "${oracle_pgp}" | \
        run_as_target_user tee -- "${log_dir_cur}/oracle-virtualbox-2016.asc" >/dev/null
      printf '%s\n' "${oracle_pgp}" | \
        root_cmd tee -- /usr/share/keyrings/oracle-virtualbox-2016.asc >/dev/null
    fi
    if [ "${use_deb822_sources}" = 'yes' ]; then
      log notice "Oracle Repository: Adding Oracle's ${connection_type_debsource} repository to ${oracle_file_deb822source}"
      write_sources_debian "${oracle_deb822_source}" "${oracle_file_deb822source}" "${oracle_signedby_debsource}"
    else
      log notice "Oracle Repository: Adding Oracle's ${connection_type_debsource} repository to ${oracle_file_debsource}"
      write_sources_debian "${oracle_oneline_source}" "${oracle_file_debsource}" "${oracle_signedby_debsource}"
    fi
  fi
}


install_kicksecure_repository_debian() {
  ## Contains file:
  ## /usr/share/extrepo/offline-data/debian/bullseye/whonix.asc
  ## Same as: kicksecure.asc
  ## (Which is not yet in the extrepo-offline-data package as of Debian 12.0.)
  install_pkg extrepo-offline-data
  ## Not using extrepo directly because it does not support torified and/or onion repositories:
  ## https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1037254

  if [ -f /usr/share/keyrings/derivative.asc ]; then
    log info "Kicksecure Repository: Key /usr/share/keyrings/derivative.asc already exists."
    log info "Kicksecure Repository: Not copying /usr/share/extrepo/offline-data/debian/bullseye/whonix.asc to /usr/share/keyrings/derivative.asc."
  else
    root_cmd cp --verbose /usr/share/extrepo/offline-data/debian/bullseye/whonix.asc /usr/share/keyrings/derivative.asc
  fi

  if [ "${kicksecure_found}" = "1" ]; then
    log info "Kicksecure Repository: Skipped adding Kicksecure repository because it was already found."
  else
    if [ "${use_deb822_sources}" = 'yes' ]; then
      log notice "Kicksecure Repository: Adding Kicksecure's ${connection_type_debsource} repository to ${kicksecure_file_deb822source}"
      write_sources_debian "${kicksecure_deb822_source}" "${kicksecure_file_deb822source}" "${kicksecure_signedby_debsource}"
    else
      log notice "Kicksecure Repository: Adding Kicksecure's ${connection_type_debsource} repository to ${kicksecure_file_debsource}"
      write_sources_debian "${kicksecure_oneline_source}" "${kicksecure_file_debsource}" "${kicksecure_signedby_debsource}"
    fi
  fi
}


install_kali_repository_debian() {
  if [ "${kali_found}" = "1" ] && [ "${kali_found_with_contrib}" = "1" ]; then
    log info "APT Repository Configuration: Skipped adding additional APT repositories because 'kali' (with 'contrib') were already found."
    return 0
  fi

  if [ "${use_deb822_sources}" = 'yes' ]; then
    log notice "APT Repository Configuration: Adding 'kali' ${connection_type_debsource} repository to ${kali_file_deb822source}"
    write_sources_debian "${kali_deb822_source}" "${kali_file_deb822source}" "${kali_signedby_debsource}"
  else
    log notice "APT Repository Configuration: Adding 'kali' ${connection_type_debsource} repository to ${kali_file_debsource}"
    write_sources_debian "${kali_oneline_source}" "${kali_file_debsource}" "${kali_signedby_debsource}"
  fi
}


install_unstable_repository_debian() {
  if [ "${unstable_found}" = "1" ] && [ "${unstable_found_with_contrib}" = "1" ]; then
    log info "APT Repository Configuration: Skipped adding additional APT repositories because 'unstable' (with 'contrib') were already found."
    return 0
  fi

  log notice "APT Preferences Configuration Writer: Adding APT pinning configuration to prefer 'stable' ('trixie') over 'unstable'."
  file="/etc/apt/preferences.d/40-installer-dist-pinning"
  printf '%s\n' "\
## This file was created by: $0
## It can be safely deleted if you know what you are doing.

## Allow package installation from 'unstable' only if explicitly requested
## (never chosen automatically by APT).
Package: *
Pin: release a=unstable
Pin-Priority: 25

## In case using specific codename 'sid' instead of generic suite 'unstable'.
Package: *
Pin: release n=sid
Pin-Priority: 25
" | root_cmd tee -- "$file" ||
    die 1 "${underline}APT Preferences Configuration Writer:${nounderline} Failed to write to file: '$file'"

  if [ "${use_deb822_sources}" = 'yes' ]; then
    log notice "APT Repository Configuration: Adding 'unstable' ${connection_type_debsource} repository to ${unstable_file_deb822source}"
    write_sources_debian "${unstable_deb822_source}" "${unstable_file_deb822source}" "${unstable_signedby_debsource}"
  else
    log notice "APT Repository Configuration: Adding 'unstable' ${connection_type_debsource} repository to ${unstable_file_debsource}"
    write_sources_debian "${unstable_oneline_source}" "${unstable_file_debsource}" "${unstable_signedby_debsource}"
  fi
}


install_backports_and_fasttrack_repository_debian() {
  log info "Installing packages required for 'backports' and 'fasttrack' repository..."
  install_pkg ca-certificates fasttrack-archive-keyring

  if [ "${backports_found}" = "1" ]; then
    log info "APT Repository Configuration: Skipped adding 'backports' repository because it was already found."
  else
    if [ "${use_deb822_sources}" = 'yes' ]; then
      log notice "APT Repository Configuration: Adding 'backports' ${connection_type_debsource} repository to ${backports_file_deb822source}"
      write_sources_debian "${backports_deb822_source}" "${backports_file_deb822source}" "${backports_signedby_debsource}"
    else
      log notice "APT Repository Configuration: Adding 'backports' ${connection_type_debsource} repository to ${backports_file_debsource}"
      write_sources_debian "${backports_oneline_source}" "${backports_file_debsource}" "${backports_signedby_debsource}"
    fi
  fi

  if [ "${fasttrack_found}" = "1" ]; then
    log info "APT Repository Configuration: Skipped adding 'fasttrack' repository because it was already found."
  else
    if [ "${use_deb822_sources}" = 'yes' ]; then
      log notice "APT Repository Configuration: Adding 'fasttrack' ${connection_type_debsource} repository to ${fasttrack_file_deb822source}"
      write_sources_debian "${fasttrack_deb822_source}" "${fasttrack_file_deb822source}" "${fasttrack_signedby_debsource}"
    else
      log notice "APT Repository Configuration: Adding 'fasttrack' ${connection_type_debsource} repository to ${fasttrack_file_debsource}"
      write_sources_debian "${fasttrack_oneline_source}" "${fasttrack_file_debsource}" "${fasttrack_signedby_debsource}"
    fi
  fi

  if [ "${fasttrack_backports_staging_found}" = "1" ]; then
    log info "APT Repository Configuration: Skipped adding 'fasttrack' with suite '${fasttrack_backports_staging_suites_debsource}' repository because it was already found."
  else
    if [ "${use_deb822_sources}" = 'yes' ]; then
      log notice "APT Repository Configuration: Adding 'fasttrack' ${connection_type_debsource} repository with suite '${fasttrack_backports_staging_suites_debsource}' to ${fasttrack_backports_staging_file_deb822source}"
      write_sources_debian "${fasttrack_backports_staging_deb822_source}" "${fasttrack_backports_staging_file_deb822source}" "${fasttrack_signedby_debsource}"
    else
      log notice "APT Repository Configuration: Adding 'fasttrack' ${connection_type_debsource} repository with suite '${fasttrack_backports_staging_suites_debsource}' to ${fasttrack_backports_staging_file_debsource}"
      write_sources_debian "${fasttrack_backports_staging_oneline_source}" "${fasttrack_backports_staging_file_debsource}" "${fasttrack_signedby_debsource}"
    fi
  fi


  ## virtualbox-guest-additions-iso is available from Debian stable and fasttrack.
  ## The Debian stable version, however, is outdated.
  ## If not using APT with option '--target-release=trixie-fasttrack' then
  ## APT would download an outdated version from Debian stable instead of the
  ## newer version from Debian fasttrack.
  install_pkg_fasttrack_extra_args_maybe=( "--target-release=${distro_codename_common_use}-fasttrack" )
}


virtualbox_installation_failure_debug() {
  local virtualbox_dkms_main_folder latest_folder \
    virtualbox_dkms_latest_folder make_log

  virtualbox_dkms_main_folder=/var/lib/dkms/virtualbox

  if [ ! -d "$virtualbox_dkms_main_folder" ]; then
    log notice "VirtualBox Installation Debug: folder '$virtualbox_dkms_main_folder' does not exist."
    return 0
  fi

  ## TODO: review
  log_run notice ls --format=single-column --sort=time "$virtualbox_dkms_main_folder"
  # shellcheck disable=SC2012
  latest_folder=$(ls --format=single-column --sort=time "$virtualbox_dkms_main_folder" | head --lines=1)
  if [ "$latest_folder" = "" ]; then
    log notice "VirtualBox Installation Debug: latest_folder in '$virtualbox_dkms_main_folder' is empty, could not be determined."
    log_run notice ls -la "$virtualbox_dkms_main_folder" || true
    return 0
  fi

  log notice "VirtualBox Installation Debug: latest_folder: '$latest_folder'"

  virtualbox_dkms_latest_folder="${virtualbox_dkms_main_folder}/${latest_folder}"

  if [ ! -d "$virtualbox_dkms_latest_folder" ]; then
    log notice "VirtualBox Installation Debug: virtualbox_dkms_latest_folder '$virtualbox_dkms_latest_folder' is not a directory."
    log_run notice ls -la "$virtualbox_dkms_latest_folder" || true
    return 0
  fi

  make_log="${virtualbox_dkms_latest_folder}/build/make.log"

  if [ ! -f "$make_log" ]; then
    log notice "VirtualBox Installation Debug: make_log '$make_log' does not exist."
    log_run notice ls -la "$virtualbox_dkms_latest_folder" || true
    return 0
  fi

  if [ ! -r "$make_log" ]; then
    log notice "VirtualBox Installation Debug: make_log '$make_log' not readable."
    log_run notice ls -la "$virtualbox_dkms_latest_folder" || true
    log_run notice ls -la "$make_log" || true
    return 0
  fi

  log_run notice cat -- "$make_log" || true
}


## Install VirtualBox on Fedora
install_virtualbox_fedora() {
  local has_virtualbox_qt

  install_pkg kernel-headers kernel-devel dkms

  get_virtualbox_version_fedora() {
    ## Too many issues with auto detection. Therefore hardcoded.
    virtualbox_qt_package_name="VirtualBox-7.2"
    return 0

#     log info "VirtualBox Version Detection: Running 'dnf search VirtualBox-*'. This can take a while..."
#     ## --cacheonly does not work.
#     ## Maybe dnf would need to be run with root_cmd.
#     virtualbox_version=$(dnf search "VirtualBox-*")
#     virtualbox_version=$(printf '%s' "$virtualbox_version" | grep -- "^VirtualBox-[[:digit:]]\{1,2\}\.[[:digit:]]\{1,2\}\.")
#     virtualbox_version=$(printf '%s' "$virtualbox_version" | tail -1)
#     virtualbox_version=$(printf '%s' "$virtualbox_version" | cut -d " " -f1)
#     virtualbox_version=$(printf '%s' "$virtualbox_version" | cut -d "-" -f2-)
#     virtualbox_version=$(printf '%s' "$virtualbox_version" | cut -d "." -f1)
#     virtualbox_qt_package_name=VirtualBox-"${virtualbox_version}"
#     if test -z "$virtualbox_version"; then
#       ## return non-zero late to avoid unbound variable virtualbox_qt_package_name.
#       return 1
#     fi
  }

  has_virtualbox_qt=0
  if get_virtualbox_version_fedora ; then
    test_pkg "${virtualbox_qt_package_name}" 2>/dev/null && has_virtualbox_qt=1
  else
    true "INFO: get_virtualbox_version_fedora failed the first time which is normal as the repository has not been enabled yet."
  fi

  ## Guard against adding extraneous repositories.
  if [ "${has_virtualbox_qt}" != "1" ]; then
    log notice "VirtualBox Installation: Preparing to install VirtualBox..."
    install_oracle_repository_fedora
    ## Does not work.
    #root_cmd dnf makecache --assumeyes '--disablerepo=*' --enablerepo=virtualbox
    update_sources
    #check_upgrades_simulation
    get_virtualbox_version_fedora || die 2 "\
${underline}VirtualBox Package Version Detection:${nounderline} 'FAIL'"
  fi

  ## This is to make sure loglevel info message is shown:
  ## Package already installed: $virtualbox_qt_package_name
  install_pkg "$virtualbox_qt_package_name"

  install_virtualbox_fedora_common_end
}


## Discover the VirtualBox version to install in Debian and derivatives.
get_virtualbox_version_debian() {
  local virtualbox_version

  if [ "${oracle_repo}" = "1" ]; then
    virtualbox_version=$("${virtualbox_cache_version_cmd[@]}" | tail -1)
    virtualbox_version=${virtualbox_version#virtualbox-}
    virtualbox_version=${virtualbox_version%% *}
    virtualbox_qt_package_name=virtualbox-"${virtualbox_version}"
    ## Package virtualbox-guest-additions-iso is unavailable from Oracle repository.
    virtualbox_guest_additions_iso_package_name=""
    if [ -z "$virtualbox_version" ]; then
      ## return non-zero late to avoid unbound variable virtualbox_qt_package_name.
      return 1
    fi
  else
    virtualbox_qt_package_name=virtualbox-qt
    ## virtualbox-guest-additions-iso is Freedom Software:
    ## https://www.kicksecure.com/wiki/Dev/VirtualBox#VirtualBox_Guest_Additions_ISO_Freedom_vs_Non-Freedom
    ## Provides file: /usr/share/virtualbox/VBoxGuestAdditions.iso
    ## Useful to easily install VirtualBox guest additions in custom VirtualBox VMs.
    virtualbox_guest_additions_iso_package_name="virtualbox-guest-additions-iso"
  fi
}


## Begin installation of VirtualBox on Debian and derived systems.
install_virtualbox_debian_common_begin() {
  local pkg vbox_manage_full_path

  log notice "VirtualBox Installation: Preparing to install VirtualBox..."
  if [ "${oracle_repo}" = "1" ]; then
    ## Workaround for undeclared dependencies bug by virtualbox.org (Oracle) repository.
    ##
    ## udev:
    ## https://www.virtualbox.org/ticket/21804
    ##
    ## gcc:
    ## virtualbox-"${virtualbox_version}" by virtualbox.org (Oracle) `Recommends:` `gcc`
    ## Otherwise kernel modules will not be compiled during installation.
    install_pkg gcc udev
  fi

  virtualbox_cache_version_cmd=('apt-cache' 'search' '--names-only' '--quiet' '^virtualbox-[[:digit:]]{1,2}\.[[:digit:]]{1,2}$')
  ## Dealing with installation from tarball is not possible, we can identify
  ## if 'vboxmanage' is in the PATH while no related package was found, but
  ## then there is not a way to clearly explain to the user that they should
  ## remove TODO
  if [ "${oracle_repo}" != '1' ]; then
    for pkg in $("${virtualbox_cache_version_cmd[@]}" | cut -d " " -f1); do
      if "${pkg_mngr_check_installed[@]}" "${pkg}" >/dev/null 2>&1; then
        die 108 "Found Oracle VirtualBox package but option '--oracle-repo' was not set! Run with the option '--oracle-repo' or uninstall the package '$pkg' and then rerun this script."
      fi
    done
  fi
  if has vboxmanage; then
    vbox_manage_full_path="$(command -v vboxmanage)"
    if ! dpkg --search "${vbox_manage_full_path}" >/dev/null ; then
      die 109 "Found Oracle VirtualBox installed from tarball/source. Using a package manager can conflict with your already manually installed VirtualBox. Uninstall VirtualBox first and then rerun this script. Use the option '--oracle-repo' if you want to install from the Oracle repository."
    fi
  fi

  install_repositories_for_virtualbox_on_debian
  update_sources
  check_upgrades_simulation
  get_virtualbox_version_debian || die 2 "${underline}VirtualBox Package Version Detection:${nounderline} 'FAIL'"

}


## Install VirtualBox on Debian
## See also comments for install_virtualbox_fedora.
install_virtualbox_debian() {
  local linux_image linux_headers install_virtualbox_args

  linux_image="linux-image-$(dpkg --print-architecture)"
  linux_headers="linux-headers-$(dpkg --print-architecture)"
  ## Doing here and now below because we $install_pkg_fasttrack_extra_args_maybe should not be used here.
  install_pkg "${linux_image}" "${linux_headers}"

  install_virtualbox_debian_common_begin

  install_virtualbox_args=()
  [ "${#install_pkg_fasttrack_extra_args_maybe[@]}" != '0' ] \
    && install_virtualbox_args+=( "${install_pkg_fasttrack_extra_args_maybe[@]}" )
  install_virtualbox_args+=( "${virtualbox_qt_package_name}" )
  [ -n "${virtualbox_guest_additions_iso_package_name}" ] \
    && install_virtualbox_args+=( "${virtualbox_guest_additions_iso_package_name}" )

  install_pkg "${install_virtualbox_args[@]}"
  install_virtualbox_debian_common_end
}


## Install VirtualBox on Ubuntu
install_virtualbox_ubuntu() {
  local install_virtualbox_args

  install_virtualbox_debian_common_begin

  install_virtualbox_args=()
  [ "${#install_pkg_fasttrack_extra_args_maybe[@]}" != '0' ] \
    && install_virtualbox_args+=( "${install_pkg_fasttrack_extra_args_maybe[@]}" )
  install_virtualbox_args+=( "${virtualbox_qt_package_name}" )
  [ -n "${virtualbox_guest_additions_iso_package_name}" ] \
    && install_virtualbox_args+=( "${virtualbox_guest_additions_iso_package_name}" )
  install_pkg "${install_virtualbox_args[@]}" linux-image-generic linux-headers-generic

  install_virtualbox_debian_common_end
}


## Helper to install signify on different systems.
install_signify() {
  local pkg_name

  pkg_name="${1:-signify}"
  has "${pkg_name}" && return 0
  install_pkg "${pkg_name}"
}


## Test if user accepts the license, if not, abort.
check_license() {
  local license_agreement dialog_box

  if [ "${non_interactive}" = "1" ]; then
    log notice "License Check: 'success' - User agreement confirmed via '--non-interactive' option."
    return 0
  fi

  log notice "The license will be shown in 5 seconds."
  log notice "(Use '-n' or '--non-interactive' for non-interactive mode.)"

  [ "${dry_run}" != "1" ] && sleep 5

  dialog_box=""
  ## 'whiptail' is the Debian version of Dialog with much less features.
  ## Dialog types problems:
  ## - whiptail does not allow to set default option with scrolltext on.
  ## - dialog leaves empty lines on exit.
  while true; do
    has dialog && dialog_box="dialog" && break
    has whiptail && dialog_box="whiptail" && break
    break
  done

  case "${dialog_box}" in
    dialog)
      ## output-fd to stdout because currently 'main 2>&1 | tee file' if used
      ## and makes the dialog fail to recognize the characters as it is
      ## receiving from stderr and writing to stdout.
      dialog --erase-on-exit --no-shadow \
        --title "${dialog_title}" \
        --yes-label "Agree" \
        --no-label "Disagree" \
        --output-fd 1 \
        --yesno "${license}" 640 480 || return 1
      log notice "${underline}License Check:${nounderline} 'success' - User has accepted the license."
      ;;
    whiptail)
      ## When text is too long and scrolltext is needed, the yesno box
      ## does not display a default item. (note: --default-item is for items
      ## in the box to be selected as menu for example, not for buttons).
      whiptail \
        --scrolltext \
        --title "${dialog_title}" \
        --yes-button "Agree" \
        --no-button "Disagree" \
        --yesno "${license}" 24 80 || return 1
      log notice "${underline}License Check:${nounderline} 'success' - User has accepted the license."
      ;;
    *)
      printf '%s\n' "${license}"
      printf '%s' "Do you accept the license(s)? (yes/no): "
      read -r license_agreement
      case "${license_agreement}" in
        [yY][eE][sS])
          log notice "${underline}License Check:${nounderline} 'success' - User has accepted the license."
          ;;
        *)
          log warn "${underline}License Check:${nounderline} User replied: '${license_agreement}'"
          return 1
          ;;
      esac
      ;;
  esac
}


get_checkhash_cmd() {
  if [ "${virtualbox_only}" = "1" ]; then
    return 0
  fi
  while true; do
    has sha512sum && checkhash=( 'sha512sum' '--check' '--strict' '--warn' ) && break
    has shasum && checkhash=( 'shasum' '--algorithm' '512' '--check' '--strict' '--warn' ) && break
    ## TODO: Does it have an equivalent to --check? Compatible file format?
    #has sha512 && checkhash=( 'sha512' '-c' ) && break
    #has digest && checkhash=( 'digest' '-a' 'sha512' '-c' ) && break
    ## TODO: How to make openssl check the file without workarounds?
    #has openssl && checkhash=( 'openssl' 'dgst' '-sha512' '-r' ) && break
    [ -z "${checkhash[0]}" ] && {
      die 1 "${underline}get_checkhash_cmd:${nounderline} Failed to find program that checks SHA512 hash sum."
    }
  done
}

get_transfer_cmd() {
  local transfer_io_timeout transfer_connect_timeout

  ## curl|rsync

  ## rsync is always better to retry failed downloads, but some distributions
  ## might not have torsocks installed, which is necessary for rsync as it
  ## doesn't support SOCKS5.
  ## https://pkg.kali.org/news/583693/torsocks-240-1-removed-from-kali-rolling/
  #if [ "${debian_testing_or_unstable_detected}" = "1" ] || [ "${kali_derivative_detected}" = "1" ]; then
    #transfer_utility=curl
  #else
    #transfer_utility=rsync
  #fi
  transfer_utility=rsync

  case "${transfer_utility}" in
    rsync)
      use_rsync=1
      ;;
    curl)
      use_curl=1
      ;;
  esac
  ## 45m
  transfer_max_time_large_file="2700"
  ## 3m
  transfer_max_time_small_file="180"
  ## 10m
  transfer_io_timeout="600"
  ## 3m
  transfer_connect_timeout="180"
  transfer_size_test_connection="200K"
  transfer_size_small_file="2K"
  transfer_size_large_file="3G"
  case ${transfer_utility} in
    curl)
      ## Maximum time in seconds that we allow the whole operation to take.
      ## The option works, but because rsync doesn't support it, we are using the timeout
      ## utility from coreutils.
      #transfer_max_time_opt="--max-time"
      ## curl does not have an I/O timeout.
      transfer_io_timeout_opt=()
      ## Maximum time in seconds that we allow curl's connection to take.
      ## This only limits the connection phase, so if curl connects within the
      ## given period, it will continue.
      transfer_connect_timeout_opt=( '--connect-timeout' "${transfer_connect_timeout}" )
      ## curl max-filesize is not a definitive barrier:
      ## The file size is not always known prior to download, and for
      ## such files this option has no effect even if the file transfer ends
      ## up being larger than this given limit.
      transfer_size_opt="--max-filesize"
      transfer_dryrun_opt=""
      transfer_output_dir_opt="--output-dir"
      transfer_output_file_opt="--remote-name"
      transfer_verbosity_opt=()
      transfer_speed_optimization_opt=()
      ;;
    rsync*)
      ## Rsync does not have an option to set the maximum time for the operation.
      ## The option works, but because rsync doesn't support it, we are using the timeout
      ## utility from coreutils.
      #transfer_max_time_opt=""
      ## If no data is transferred in the specified time, rsync will exit.
      transfer_io_timeout_opt=( '--timeout' "${transfer_io_timeout}" )
      ## Amount of time the client will wait for its connection to a server
      ## to succeed.
      ## Error when using this option:
      ##   The --contimeout option may only be used when connecting to an
      ##   rsync daemon.
      #transfer_connect_timeout_opt=( '--contimeout' "${transfer_connect_timeout}" )
      transfer_connect_timeout_opt=()
      transfer_size_opt="--max-size"
      transfer_dryrun_opt="--dry-run"
      transfer_output_dir_opt=""
      transfer_output_file_opt=""
      transfer_verbosity_opt=( '--no-motd' '--progress' '--verbose' '--verbose' )
      transfer_speed_optimization_opt=( '--compress' '--partial' )
      ;;
  esac
}


## Set default traps
set_trap() {
  trap 'handle_exit $? ${LINENO:-}' ERR EXIT HUP INT QUIT ABRT ALRM TERM
}


## Check if system status is supported
get_system_stat() {
  local min_ram_mb total_mem_kB total_mem df_output free_space_available \
    free_space_required live_status_detected live_status_extra_message

  if [ "${arch}" != "x86_64" ]; then
    die 101 "${underline}Architecture Check:${nounderline} Only supported architecture is 'x86_64', yours is: '${arch}'."
  fi

  ## https://www.whonix.org/wiki/RAM#Whonix_RAM_and_VRAM_Defaults
  ## TODO
  ## min_ram_mb not used currently because less than total 4GB is too low
  ## already
  # shellcheck disable=SC2034
  case "${interface}" in
    xfce)
      min_ram_mb="3328"
      ;;
    cli)
      min_ram_mb="1024"
      ;;
  esac
  if [ "${virtualbox_only}" = "1" ]; then
    # shellcheck disable=SC2034
    min_ram_mb="1024"
  fi

  has awk || die 1 "${underline}Architecture Check:${nounderline} Package 'gawk' is missing. Please install."
  ## 4GB RAM machine reports 3844Mi and 4031MB
  ## /proc/meminfo replies in kB
  ## https://ux.stackexchange.com/a/13850
  total_mem_kB="$(awk '/MemTotal/{print $2}' /proc/meminfo)"
  ## convert kB to MB
  total_mem="$((total_mem_kB/1000))"
  ## capped to 4200MB to report that 4GB RAM on the host is too little
  if [ "${total_mem}" -lt "4200" ]; then
    user_warned_potential_startup_issue=true
    log warn "${underline}Minimum RAM Check:${nounderline} Your systems has a low amount of total RAM: '${total_mem} MB'"
    if [ "${virtualbox_only}" != "1" ]; then
      log warn "  - For more information, refer to:"
      log warn "  ${url_version_domain}/wiki/RAM"
    fi
  fi

  df_output="$(run_as_target_user df --output=avail -BG "${directory_prefix}")"
  free_space_available="$(printf '%s' "$df_output" |
                awk '/G$/{print substr($1, 1, length($1)-1)}')"

  ## TODO: do not hardcode 10 GB - Kicksecure vs Whonix
  free_space_required="10"
  ## Free space require set to approximately double the image size (2023-03-21)
  case "${guest}" in
    whonix)
      case "${interface}" in
        xfce)
          free_space_required="5"
          ;;
        cli)
          free_space_required="3"
          ;;
      esac
      ;;
    kicksecure)
      case "${interface}" in
        xfce)
          free_space_required="2"
          ;;
        cli)
          free_space_required="1"
          ;;
      esac
      ;;
  esac

  if [ "${virtualbox_only}" = "1" ]; then
    free_space_required="1"
  fi

  if [ "${dev}" = "1" ]; then
    free_space_required="1"
  fi

  ## live-status-detected is provided only if package helper-scripts is installed.
  if has live-status-detected; then
    live_status_detected="$(live-status-detected)" || true
    if [ "$live_status_detected" = "true" ]; then
      log warn "Live Mode Detected: Yes, live mode detected. Unusual. Most users use this installer in persistent mode. But can be OK."
      live_status_extra_message="  - ${underline}Live Mode detected:${nounderline} Yes. NOTE:

Running this installer in Live Mode will usually not work unless the system has a huge amount of RAM. Booting into persistent mode is recommended for using this installer.

- If this is an ISO: Consider installing the operating system to an internal or external hard drive first.
  For more information, refer to:
  ${url_version_domain}/wiki/USB_Installation

- If this is grub-live mode: Boot into persistent mode.
  For more information, refer to:
  ${url_version_domain}/wiki/Persistent_Mode"
    elif [ "$live_status_detected" = "false" ]; then
      live_status_extra_message=""
      log info "Live Mode Detected: No, live mode not detected, ok."
    else
      log info "Live Mode Detected: Detection failed."
      live_status_extra_message="  - ${underline}Live Mode detected:${nounderline} 'Detection failed.'"
    fi
  else
    log info "Live Mode Detected: Skipped, because live-status-detected (from helper-scripts) is unavailable, ok."
    live_status_extra_message=""
  fi

  if [ "${free_space_available}" -lt "$free_space_required" ]; then
    die 101 "\
${underline}Free Disk Space Check Result:${nounderline} 'FAIL.' - Insufficient free disk space!

  - ${underline}available:${nounderline} '${free_space_available}G'
  - ${underline}required :${nounderline} '${free_space_required}G'
${live_status_extra_message}

Debugging information:
- Command to test the available free space on ${directory_prefix}:
df --output=avail -BG \"${directory_prefix}\"
- Available free space result:
$df_output"
  fi

  log info "Free Disk Space Check Result: Success."
}


## Generate SOCKS credentials for stream isolation
get_proxy_cred() {
  local proxy_user proxy_pass

  [ "${transfer_utility}" != "curl" ] && return 0
  [ -z "${transfer_proxy_suffix:-}" ] && return 0
  proxy_user="anonym"
  proxy_pass="${1:?}"
  printf '%s' "--proxy-user ${proxy_user}:${proxy_pass}"
}


## Test if can connect to SOCKS proxy and expect the correct Tor reply.
check_tor_proxy() {
  local expected_response_header cmd_check_proxy actual_response_header \
    parsed_response_header

  log notice "Testing SOCKS proxy: '${proxy}'"
  expected_response_header="HTTP/1.0 501 Tor is not an HTTP Proxy"
  log info "Expected response header:"
  log info "'$expected_response_header'"
  cmd_check_proxy=( 'env' 'UWT_DEV_PASSTHROUGH=1' 'curl' '--silent' '--show-error' '--max-time' '3' '--head' "http://${proxy}" )
  log info "Command used to check if proxy is functional:"
  log info "${cmd_check_proxy[*]}"
  actual_response_header="$("${cmd_check_proxy[@]}" 2>&1)"
  parsed_response_header=$(printf '%s' "${actual_response_header}" | head -1)
  parsed_response_header=$(printf '%s' "${parsed_response_header}" | tr -d "\r")
  ## Globs are necessary to match patterns in the event the header has more
  ## characters them expected but still has the expected string.
  ## Rsync header response example:
  ##   bad response from proxy -- HTTP/1.0 501 Tor is not an HTTP Proxy
  case "${parsed_response_header}" in
    *"${expected_response_header}"*)
      log info "Received header:"
      log info "'${parsed_response_header}'"
      log notice "Connected to Tor SOCKS proxy successfully."
      return 0
      ;;
    *)
      log error "\
Unexpected proxy response, maybe not a Tor proxy?

Debugging information:
- Command used to check if proxy is functional:
'${cmd_check_proxy[*]}'

- Expected response header:
'${expected_response_header}'

- Actual received header:
'${actual_response_header}'

- Parsed received header:
'${parsed_response_header}'"
      return 1
  esac
}


## Set transference proxy depending on transfer utility.
## usage: set_transfer_proxy ${proxy}
set_transfer_proxy() {
  local proxy_port proxy_addr

  proxy_port="${1##*:}"
  proxy_addr="${1%%:*}"
  ## Used for transfers that only curl can do.
  curl_transfer_proxy=( '--proxy' "socks5h://${1}" )
  ## Set transfer proxy per utility.
  case "${transfer_utility}" in
    curl)
      transfer_proxy_suffix=( '--proxy' "socks5h://${1}" )
      transfer_proxy_prefix=()
      ;;
    rsync*)
      transfer_proxy_suffix=()
      transfer_proxy_prefix=( 'torsocks' '--isolate' '--address' "${proxy_addr}" '--port' "${proxy_port}" )
      ;;
  esac
}


## Useful to test if it is a SOCKS proxy before attempting to make requests.
## If connection to proxy fails, abort to avoid leaks.
torify_conn_maybe() {
  ## onion=1 -- Always torify onion connections.
  ## onion=* -- Only torify clearnet if SOCKS proxy is specified.
  if [ ! "${onion}" = "1" ]; then
    if [ -z "${socks_proxy:-}" ]; then
      ## TODO: lower log level
      log notice "Not torifying connection, because the 'socks_proxy' environment variable is unset nor using '--onion'."
      return 0
    fi
  fi

  if ! has tor; then
    log warn "System 'tor' binary ('little-t-tor') was not found on the system."
    log warn "Unless your SOCKS connection is made available by the Tor Browser"
    log warn "  or by your uplink network, the proxy check may fail."
    log warn "The installer with torified connections depends on a working SOCKS proxy,"
    log warn "  it won't configure the proxy, only establish the connection."
    log warn "If the proxy connection fails, try installation of the 'tor' package on your system."
  fi
  ## curl and many other viable applications do not support SOCKS proxy to
  ## connect with Unix Domain Socket:
  ##   https://curl.se/mail/archive-2021-03/0013.html
  if [ -n "${socks_proxy:-}" ]; then
    proxy="${socks_proxy}"
    set_transfer_proxy "${proxy}"
  elif [ -n "${TOR_SOCKS_PORT:-}" ]; then
    proxy="${TOR_SOCKS_HOST:-127.0.0.1}:${TOR_SOCKS_PORT}"
    set_transfer_proxy "${proxy}"
  else
    ## Stream Isolation will be enforced get_proxy_cred()
    log warn "Missing SOCKS proxy for torified connections."
    log warn "Trying Tor defaults: system Tor ('little-t-tor') (port: '9050') and TBB (Tor Browser Bundle) (port: '9150')."
    proxy="127.0.0.1:9050"
    set_transfer_proxy ${proxy}
    if ! check_tor_proxy; then
      proxy="127.0.0.1:9150"
      set_transfer_proxy ${proxy}
    else
      return 0
    fi
  fi
  check_tor_proxy || die 2 "\
${underline}Check Tor Proxy:${nounderline} 'FAIL'

Unable to connect to Tor SOCKS proxy.

  - This issue is unlikely caused by this installer.
  - It is more probable that the problem stems from absent software, improper configuration, or network issues.

Please note that in order to torify connections, an already functional Tor connection is needed:

  - A) A pre-installed and running system Tor ('little-t-tor'), or,
  - B) A pre-installed and running TBB (Tor Browser Bundle).

Additional details:

  - This installer does not support setting up a functional Tor connection. This task needs to be performed by the system administrator.
  - When running the above cmd_check_proxy manually, ensure it includes the expected_response_header."
}


## Set version by user input or by querying the API
get_version() {
  local cmd_raw_version raw_version cmd_raw_version_len cmd_raw_version_idx

  log notice "Version Detection: Detecting guest version..."
  if [ -n "${guest_version:-}" ]; then
    log notice "Version Detection: 'skipped' - Autodetection using API not required via '--guest-version', '--dry-run' or '--dev' option."
    return 0
  fi
  log info "Version Detection: Acquiring guest version using API..."
  log info "Version Detection: API host: ${1}"
  cmd_raw_version=(
    'curl' "${curl_transfer_proxy[@]}"
    "$(get_proxy_cred version)" "${curl_opt_ssl[@]}"
    '--max-time' "${transfer_max_time_small_file}"
    '--max-filesize' "${transfer_size_small_file}"
    '--url' "${1}"
  )
  cmd_raw_version_len="${#cmd_raw_version[@]}"
  for (( cmd_raw_version_idx=0; cmd_raw_version_idx < cmd_raw_version_len; cmd_raw_version_idx++ )); do
    if [ -z "${cmd_raw_version[cmd_raw_version_idx]}" ]; then
      unset "cmd_raw_version[${cmd_raw_version_idx}]"
    fi
  done
  ## this is necessary because we log will not be printed as the command is
  ## assigned to a variable at 'raw_version=$()'.
  if [ "${dry_run}" = "1" ]; then
    log_run notice "${cmd_raw_version[@]}"
    return 0
  fi
  raw_version="$("${cmd_raw_version[@]}")"
  ## First line only.
  guest_version="$(printf '%s' "$raw_version" | head -n 1)"
  guest_version="$(printf '%s' "${guest_version}" | sed "s/<.*//")"
  ## Distrust the API version
  ## Block anything that is not made purely out of numbers and dots
  ## Not printing queried version to avoid it showing a very long version
  ## that could inhibit the user from seeing the error message.
  ## The user would still see a failed exit code.
  [ "${guest_version%%*[^0-9.]*}" ] ||
    die 1 "${underline}Version Check Result:${nounderline} Invalid guest version: contains unexpected characters."
  ## block string containing more than 12 chars
  [ "${#guest_version}" -le 12 ] ||
    die 1 "${underline}Version Check Result:${nounderline} Invalid guest version: contains more than 12 characters."
}


## Helper for download_files() to make it less repetitive.
## usage: get_file small|large $url
get_file() {
  local size url download_opt_prefix download_opt download_opt_full round \
    download_opt_full_len download_opt_full_idx

  size="${1}"
  url="${2}"
  ## Round is only used to get a different password every time.
  [ -z "${round:-}" ] && round=10
  round=$((round+1))

  download_opt=()

  case "${size}" in
    small)
      download_opt_prefix=( 'timeout' '--foreground' "${transfer_max_time_small_file}" )
      download_opt+=( "${transfer_size_opt}" "${transfer_size_small_file}" )
      ;;
    large)
      download_opt_prefix=( 'timeout' '--foreground' "${transfer_max_time_large_file}" )
      [ "${#transfer_speed_optimization_opt[@]}" != '0' ] && download_opt+=( "${transfer_speed_optimization_opt[@]}" )
      download_opt+=( "${transfer_size_opt}" "${transfer_size_large_file}" )
      ;;
    *)
      log bug "Missing size option for get_file()."
      ;;
  esac
  download_opt_full=( "${download_opt_prefix[@]}" "${transfer_proxy_prefix[@]}" "${transfer_utility}" "${transfer_verbosity_opt[@]}" "${transfer_proxy_suffix[@]}" "$(get_proxy_cred ${round})" "${transfer_connect_timeout_opt[@]}" "${transfer_io_timeout_opt[@]}" "${download_opt[@]}" "${transfer_output_file_opt}" "${url}" "${transfer_output_dir_opt}" "${directory_prefix}" )
  download_opt_full_len="${#download_opt_full[@]}"
  for (( download_opt_full_idx=0; download_opt_full_idx < download_opt_full_len; download_opt_full_idx++ )); do
    if [ -z "${download_opt_full[download_opt_full_idx]}" ]; then
      unset "download_opt_full[${download_opt_full_idx}]"
    fi
  done
  download_opt_full=( "${download_opt_full[@]}" )

  log_run notice run_as_target_user "${download_opt_full[@]}" || return 1
}


test_file() {
  local mode file real_file
  mode="${1}"
  file="${2}"

  ## 'realpath' needs to be able to access the path it checks.
  ## Using '2>/dev/null' to suppress error output such as:
  ## > realpath: /home/sysmaint/dist-installer-cli-download/logs/1: No such file or directory
  if ! real_file="$(root_cmd_loglevel="echo" dry_run=0 root_cmd realpath -- "${file}" 2>/dev/null)"; then
    true "INFO: realpath failed to determine the real path of file: ${file}"
    return 1
  fi

  run_as_target_user /usr/bin/test "${mode}" "${real_file}"
}


files_already_downloaded_check() {
  if [ "${dry_run}" = "1" ]; then
    log notice "files_already_downloaded_check: Creating image file '${directory_prefix}/${guest_file}.${guest_file_ext}' (sha512sums.sig and sha512sums) via '--dry-run' option."
    touch -- "${directory_prefix}/${guest_file}.${guest_file_ext}"
    touch -- "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.sig"
    touch -- "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums"
  fi

  test_file -f "${directory_prefix}/${guest_file}.${guest_file_ext}"
  test_file -f "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.sig"
  test_file -f "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums"
}


## Check if files were already downloaded, if not, try to download everything
## and only if succeeds, set download flag.
download_files() {
  log_time

  log notice "Download: Files will be stored in the directory: '${directory_prefix}'"
  get_file large "${url_guest_file}.${guest_file_ext}" || return 1
  get_file small "${url_guest_file}.${guest_file_ext}.sha512sums.sig" || return 1
  get_file small "${url_guest_file}.${guest_file_ext}.sha512sums" || return 1

  log info "Download: Checking if files exists locally..."

  if ! files_already_downloaded_check ; then
    die 103 "${underline}Download:${nounderline} Failed to download files."
  fi

  log_time
}


## https://en.wikipedia.org/wiki/X86_virtualization
get_virtualization() {
  local virt_flag brand virt_detection_success virt msr virt_disabled virt_bit

  ## Check if virtualization is enabled.
  ## Check CPU flags for capability
  virt_flag="$(root_cmd grep -m1 -w -- '^flags[[:blank:]]*:' /proc/cpuinfo | grep -wo -E -- '(vmx|svm)' || true)"

  case "${virt_flag:=none}" in
    vmx) brand=intel;;
    svm) brand=amd;;
    *) brand=unknown;;
  esac

#  if compgen -G "/sys/kernel/iommu_groups/*/devices/*" > /dev/null; then
#    log notice "${brand}'s I/O Virtualization Technology is enabled in the BIOS/UEFI"
#  else
#    log warn "${brand}'s I/O Virtualization Technology is not enabled in the BIOS/UEFI"
#  fi

  case "${virt_flag:=}" in
    vmx|svm)
      log notice "Virtualization Support Test Result: 'success'"
      log info "cpu_brand: '${brand}' virt_flag: '${virt_flag}'"
      virt_detection_success=true
      return 0
      ;;
    none)
      user_warned_potential_startup_issue=true
      log warn "${underline}Virtualization Support Test:${nounderline} ${red}${bold}'FAIL'${nobold}${nocolor} - No virtualization flag found."
      virt_detection_success=false
      ;;
    *)
      user_warned_potential_startup_issue=true
      log warn "${underline}Virtualization Support Test:${nounderline} ${red}${bold}'FAIL'${nobold}${nocolor} - Unknown virtualization flag. (Use '--log-level=info' for potentially privacy sensitive details.)"
      log info "cpu_brand: '${brand}' virt_flag: '${virt_flag}'"
      virt_detection_success=false
      ;;
  esac

  if [ "$virt_detection_success" = "false" ]; then
    user_warned_potential_startup_issue=true
    log warn "  - The virtualization detection feature of this installer may not be flawless and could potentially fail to detect virtualization support (this is known as a 'false negative')."
    if [ "${hypervisor}" = "virtualbox" ]; then
      log warn "  - Refer to user documentation on how to enable virtualization:"
      log warn "  https://www.kicksecure.com/wiki/VirtualBox/Troubleshooting#Enable_VT-x_in_BIOS"
    fi
    return 0
    ## Let's not hard fail here, let the user do it later.
    #return 101
  fi

  ## msr is blocked by security-misc. If no other solution is found,
  ## remove the rest of this function.
  ## $ modprobe msr
  ## /bin/disabled-msr-by-security-misc: ERROR: This CPU MSR kernel module is disabled by package security-misc by default. See the configuration file /etc/modprobe.d/30_security-misc.conf | args:
  ## modprobe: ERROR: ../libkmod/libkmod-module.c:990 command_do() Error running install command '/bin/disabled-msr-by-security-misc' for module msr: retcode 1
  ## modprobe: ERROR: could not insert 'msr': Invalid argument

  install_pkg msr-tools
  # https://bazaar.launchpad.net/~cpu-checker-dev/cpu-checker/trunk/view/head:/kvm-ok
  # kvm-ok - check whether the CPU we're running on supports KVM acceleration
  # Copyright (C) 2008-2010 Canonical Ltd.
  #
  # Authors:
  #  Dustin Kirkland <kirkland@canonical.com>
  #  Kees Cook <kees.cook@canonical.com>
  #
  # This program is free software: you can redistribute it and/or modify
  # it under the terms of the GNU General Public License version 3,
  # as published by the Free Software Foundation.
  #
  # 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 <http://www.gnu.org/licenses/>.

  ## Print verdict
  verdict() {
    case "${1}" in
      0)
        log notice "Virtualization can be used."
        log warn "Virtualization availability can be a false negative."
        return 0
        ;;
      1)
        user_warned_potential_startup_issue=true
        log warn "Virtualization can NOT be used."
        log warn "Virtualization availability can be a false negative."
        return 0
        ## let's not hard fail here, let the user do it later.
        #return 1
        ;;
      2)
        user_warned_potential_startup_issue=true
        log warn "Virtualization can be used, but not enabled."
        log warn "Virtualization availability can be a false negative."
        return 0
        ## let's not hard fail here, let the user do it later.
        #return 1
        ;;
    esac
  }

  ## Check CPU flags for capability
  virt=$(root_cmd grep -m1 -w -- '^flags[[:blank:]]*:' /proc/cpuinfo |
         grep -wo -E -- '(vmx|svm)') || true
  if [ -z "${virt}" ]; then
    log error "Your CPU does not support Virtualization."
    verdict 1
  fi
  [ "${virt}" = "vmx" ] && brand="intel"
  [ "${virt}" = "svm" ] && brand="amd"

  ## Now, check that the device exists
  if [ -e /dev/kvm ]; then
    log notice "Device /dev/kvm exists"
    verdict 0
  else
    user_warned_potential_startup_issue=true
    log warn "Device /dev/kvm does not exist"
    log warn "hint: '${sucmd} modprobe kvm_$brand'"
  fi

  ## Prepare MSR access
  msr="/dev/cpu/0/msr"
  if root_cmd [ ! -r "${msr}" ]; then
    root_cmd modprobe msr ||
      die 1 "${underline}modprobe:${nounderline} Could not add module 'msr' to the kernel."
  fi
  if root_cmd [ ! -r "${msr}" ]; then
    log error "Cannot read: '${msr}'"
    return 1
  fi

  log notice "Your CPU supports Virtualization extensions."

  virt_disabled=0
  ## check brand-specific registers
  if [ "${virt}" = "vmx" ]; then
    virt_bit=$(root_cmd rdmsr --bitfield 0:0 0x3a 2>/dev/null || true)
    if [ "${virt_bit}" = "1" ]; then
      ## and FEATURE_CONTROL_VMXON_ENABLED_OUTSIDE_SMX clear (no tboot)
      virt_bit=$(root_cmd rdmsr --bitfield 2:2 0x3a 2>/dev/null || true)
      [ "${virt_bit}" = "0" ] && virt_disabled=1
    fi
  elif [ "${virt}" = "svm" ]; then
    virt_bit=$(root_cmd rdmsr --bitfield 4:4 0xc0010114 2>/dev/null || true)
    [ "${virt_bit}" = "1" ] && virt_disabled=1
  else
    log error "Unknown virtualization extension: '${virt}'"
    verdict 1
  fi

  if [ "${virt_disabled}" -eq 1 ]; then
    user_warned_potential_startup_issue=true
    log warn "'${virt}' is disabled by your BIOS"
    log warn "Enter your BIOS setup and enable Virtualization Technology (VT),"
    log warn "      and then reboot your system."
    verdict 2
  fi

  verdict 0
}

#########################
## END SCRIPT SPECIFIC ##
#########################

################
## BEGIN MAIN ##
################
get_download_links() {
  local site_clearnet_whonix site_onion_whonix site_clearnet_kicksecure \
    site_onion_kicksecure site_clearnet site_onion site_download_clearnet \
    site_download_onion protocol_prefix_clearnet protocol_prefix_onion \
    url_download_clearnet url_download_onion url_download url_version_template \
    url_version_prefix url_version_suffix

  ## Set upstream links as base, especially for API.
  ## clearnet project domain
  site_clearnet_whonix="whonix.org"
  ## onion project domain
  site_onion_whonix="dds6qkxpwdeubwucdiaord2xgbbeyds25rbsgr73tbfpqpt4a6vjwsyd.onion"
  ## clearnet project domain
  site_clearnet_kicksecure="kicksecure.com"
  ## onion project domain
  site_onion_kicksecure="w5j6stm77zs6652pgsij4awcjeel3eco7kvipheu6mtr623eyyehj4yd.onion"

  case "${guest}" in
    whonix)
      site_clearnet="${site_clearnet_whonix}"
      site_onion="${site_onion_whonix}"
      ;;
    kicksecure)
      site_clearnet="${site_clearnet_kicksecure}"
      site_onion="${site_onion_kicksecure}"
      ;;
    *)
      ## Variables need to be set for --virtualbox-only.
      site_clearnet="${site_onion_kicksecure}"
      site_onion="${site_clearnet_kicksecure}"
      ;;
  esac

  if [ -z "${mirror}" ]; then
    ## No mirror chosen, use default values.
    mirror=0
  fi

  ## ${variable+string} means use string as value if variable is not empty.
  ## get download links by mirror of choice.
  case "${mirror}" in
    0)
      site_download_clearnet="${use_curl+download.}${site_clearnet}${use_rsync+/${guest}}"
      site_download_onion="${use_curl+download.}${site_onion}${use_rsync+/${guest}}"
      ;;
    1)
      site_download_clearnet="mirrors.dotsrc.org/${guest}"
      site_download_onion="dotsrccccbidkzg7oc7oj4ugxrlfbt64qebyunxbrgqhxiwj3nl6vcad.onion/${guest}"
      ;;
    2)
      site_download_clearnet="quantum-mirror.hu/mirrors/pub/${guest}"
      site_download_onion=""
      ;;
    *)
      ## range_arg should have catch this error before, just safeguarding.
      log bug "Invalid mirror number: '${mirror}'"
      ;;
  esac

  case "${transfer_utility}" in
    curl)
      protocol_prefix_clearnet="https"
      protocol_prefix_onion="http"
      ;;
    rsync)
      protocol_prefix_clearnet="rsync"
      protocol_prefix_onion="rsync"
      ;;
  esac

  ## clearnet download url
  url_download_clearnet="${protocol_prefix_clearnet}://${site_download_clearnet}"
  ## onion download url
  url_download_onion="${protocol_prefix_onion}://${site_download_onion}"

  case "${onion}" in
    1)
      log info "Onion preferred."
      curl_opt_ssl=()
      ## Used to test internet connection.
      url_origin="${protocol_prefix_onion}://www.${site_onion}"
      ## URL to download files from.
      [ -n "${site_download_onion}" ] ||
        die 1 "${underline}Mirror Selection:${nounderline} Mirror ${mirror} doesn't provide an onion service."
      url_download="${url_download_onion}"
      ## Used to query version number.
      url_version_domain="http://www.${site_onion}"
      ;;

    *)
      log info "Clearnet preferred."
      [ "${transfer_utility}" = "rsync" ] && transfer_utility="rsync-ssl"
      curl_opt_ssl=( '--tlsv1.3' '--proto' '=https' )
      ## Used to test internet connection.
      url_origin="${protocol_prefix_clearnet}://www.${site_clearnet}"
      ## URL to download files from.
      url_download="${url_download_clearnet}"
      ## Used to query version number.
      url_version_domain="https://www.${site_clearnet}"
      ;;
  esac


  case "${hypervisor}" in
    virtualbox)
      if [ "${testers}" = "1" ]; then
        url_version_template="VersionTesters"
      else
        url_version_template="VersionNew"
      fi
      ## image signer
      signify_key="${adrelanos_signify}"
      signify_signer="adrelanos"
      ## url directory to find files of the selected hypervisor
      url_domain="${url_download}/ova"
      ## image file extension
      guest_file_ext="Intel_AMD64.ova"
      ## function to call when importing guest
      ;;

    kvm)
      if [ "${testers}" = "1" ]; then
        die 1 "${underline}Version Selection:${nounderline} KVM does not have testers version."
        #url_version_template=""
      else
        url_version_template="Version_KVM"
      fi
      ## image signer
      signify_key="${hulahoop_signify}"
      signify_signer="hulahoop"
      ## url directory to find files of the selected hypervisor
      url_domain="${url_download}/libvirt"
      ## image file extension
      guest_file_ext="Intel_AMD64.qcow2.libvirt.xz"
      ## function to call when importing guest
      ## TODO
      ;;
  esac

  url_version_prefix="w/index.php?title=Template:"
  url_version_suffix="&stable=0&action=raw"
  url_version="${url_version_domain}/${url_version_prefix}${url_version_template}${url_version_suffix}"
}


## Test if files should be downloaded
should_download() {
  local download_msg_done

  if [ "${virtualbox_only}" = "1" ]; then
    ## 'return 1' so the result of should_download is "no".
    return 1
  fi
  if [ "${dry_run}" = "1" ]; then
    log notice "Download: Creating download flag via '--dry-run' option."
    dry_run=0 log_run notice run_as_target_user touch -- "${download_flag}"
    return 0
  fi

  if [ "${redownload}" = "1" ]; then
    ## Do not print further messages as it was already printed before.
    ## Occurs if the should_download() function was called more than once.
    [ "${download_msg_done:-}" = "1" ] && return 0
    # shellcheck disable=SC2034
    download_msg_done=1
    ## Download if redownload option is set.
    log notice "Download: Re-downloading files via '--redownload' option."
    return 0
  elif test_file -f "${download_flag:-}"; then
    ## Do not download if flag exists.
    log notice "Download: Skipping download because download and integrity check previously succeeded."
    return 1
  fi
  ## Download as no obstacles prohibit it.
  return 0
}


## Check signature of signed checksum.
check_signature() {
  local signify_checksum_file signify_pub_file

  signify_checksum_file="${1}"
  log info "Signify key:\n${signify_key}"
  log info "Verifying file: '${signify_checksum_file}'"
  signify_pub_file="${log_dir_cur}/${signify_signer}.pub"
  ## Newline '\n' at the end needed. Otherwise error:
  #signify-openbsd: missing new line after base64 in /home/user/dist-installer-cli-download/logs/1/adrelanos.pub
  printf '%s\n' "${signify_key}" | run_as_target_user tee -- "${signify_pub_file}" >/dev/null

  log_run info run_signify -V -p "${signify_pub_file}" \
    -m "${signify_checksum_file}" || return 1

  log info "Signify Signature Verification: 'success'"
}


## Check hash sum.
check_hash() {
  local shafile dir

  shafile="${1}"
  dir="$(dirname -- "${shafile}")"
  log info "Checking SHA512 checksum: '${shafile}"
  ## $checkhash needs to be executed on the same folder as the compared file.
  log info "Changing to directory: '${dir}'"
  if [ -n "${target_user}" ]; then
    run_as_target_user_and_dir "${dir}" "${checkhash[@]}" "${shafile}" || return 1
  else
    cd "${dir}"
    log_run info run_as_target_user "${checkhash[@]}" "${shafile}" || return 1
  fi
  log info "SHA512 Hash Verification: 'success'"
}


check_signature_test() {
  log info "Unit testing signature, expecting non-zero exit code."
  run_as_target_user rm -f -- "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test"
  run_as_target_user cp -- "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums" "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test"
  printf '\n' | run_as_target_user tee -- "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test" >/dev/null
  if ! check_signature "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test" 2>/dev/null; then
    run_as_target_user rm -f -- "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test"
    log info "Received expected non-zero exit code from unit test."
  else
    run_as_target_user rm -f -- "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test"
    die 104 "${underline}SHA512 Hash Verification (unit test):${nounderline} 'FAIL' - received a zero as exit code, expected non-zero."
  fi
}


check_hash_test() {
  log info "Unit testing checksum, expecting non-zero exit code."
  run_as_target_user rm -f -- "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test"
  run_as_target_user cp -- "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums" "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test"
  printf '\n' | run_as_target_user tee -- "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test" >/dev/null
  if ! check_hash "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test" 2>/dev/null; then
    run_as_target_user rm -f -- "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test"
    log info "Received expected non-zero exit code from unit test."
  else
    run_as_target_user rm -f -- "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test"
    die 104 "${underline}SHA512 Hash Verification (unit test):${nounderline} 'FAIL' - received a zero as exit code, expected non-zero."
  fi
}

## Check integrity of files
check_integrity() {
  if [ "${virtualbox_only}" = "1" ]; then
    return 0
  fi

  if [ "${dry_run}" = "1" ]; then
    log notice "Integrity Check: Skipping integrity checks via '--dry-run' option."
    return 0
  fi
  log notice "Integrity Check: Performing integrity checks..."
  check_signature "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums" || die 104 "${underline}Signify Signature Verification:${nounderline} 'FAIL'"
  check_hash "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums" \
    || die 104 "${underline}SHA512 Hash Verification:${nounderline} 'FAIL'"

  check_signature_test
  check_hash_test

  log_run info run_as_target_user touch -- "${download_flag}"

  log notice "Integrity Check Result: 'success'"
}


## Self explanatory name, make everything after option parsing.
main() {
  local item cmd_check_internet cmd_check_internet_idx cmd_check_internet_len

  ###############
  ## BEGIN PRE ##
  ###############
  log info "Starting main function."

  case "${guest}" in
    whonix)
      guest_full_vm_name_gateway="${guest_pretty}-Gateway-${interface_name}"
      guest_full_vm_name_workstation="${guest_pretty}-Workstation-${interface_name}"
      ;;
    kicksecure)
      guest_full_vm_name_kicksecure="${guest_pretty}-${interface_name}"
      ;;
  esac

  log info "Parsed options:"
  for item in $(print_getopt); do
    log info "  ${item}"
  done

  if [ "${non_interactive}" != "1" ]; then
    log notice "If you wish to cancel installation, press Ctrl+C."
  fi
  ## The license function sleeps for some seconds to give time to abort
  check_license || die 100 "${underline}License Check:${nounderline} User declined the license."

  log_time

  #############
  ## END PRE ##
  #############

  ####################
  ## BEGIN DOWNLOAD ##
  ####################
  ## Skip making internet requests if flag already exists and user
  ## specified the desired version.
  ## If version is set, use it now to set the download flag path.
  if [ -n "${guest_version}" ]; then
    guest_file="${guest_pretty}-${interface_name}-${guest_version}"
    download_flag="${directory_prefix}/${guest_file}.${guest_file_ext}.flag"
  fi

  if should_download; then
    if [ "${dry_run}" != "1" ]; then
      log notice "Connectivity Test: Testing internet connection to '${url_origin}'..."
      cmd_check_internet=( 'timeout' '--foreground' "${transfer_max_time_small_file}" "${transfer_proxy_prefix[@]}" "${transfer_utility}" "${transfer_proxy_suffix[@]}" "${transfer_dryrun_opt}" "${transfer_size_opt}" "${transfer_size_test_connection}" "${url_origin}" )
      cmd_check_internet_len="${#cmd_check_internet[@]}"
      for (( cmd_check_internet_idx=0; cmd_check_internet_idx < cmd_check_internet_len; cmd_check_internet_idx++ )); do
        if [ -z "${cmd_check_internet[cmd_check_internet_idx]}" ]; then
          unset "cmd_check_internet[${cmd_check_internet_idx}]"
        fi
      done
      log info "Executing: $ ${cmd_check_internet[*]}"
      "${cmd_check_internet[@]}" >/dev/null ||
        die $? "${underline}Connectivity Test Result:${nounderline} 'FAIL' - Cannot connect, perhaps no internet?"
      log notice "Connectivity Test Result: 'success'"
    fi
    get_version "${url_version}"
    log notice "Version Detection Result: '${guest_version}'"
    guest_file="${guest_pretty}-${interface_name}-${guest_version}"
    download_flag="${directory_prefix}/${guest_file}.${guest_file_ext}.flag"
    url_domain="${url_domain}/${guest_version:?}"
    url_guest_file="${url_domain}/${guest_file}"

    ## Check again for download flag after version was queried.
    if should_download; then
      download_files || die 103 "${underline}Download:${nounderline} Failed to download files."
    fi
  else
    if [ "${virtualbox_only}" = "1" ]; then
      true "INFO: Skip showing version via '--virtualbox-only' option."
    else
      log notice "Version Detection Result: '${guest_version}'"
    fi
  fi
  ##################
  ## END DOWNLOAD ##
  ##################

  ##########################################
  ## BEGIN VERIFICATION, IMPORT AND START ##
  ##########################################
  check_integrity
  check_vm_running_general
  check_vm_exists_general "(check before maybe deletion)"
  vm_delete_maybe
  check_vm_exists_general "(check before import)"
  import_guest
  check_guest_boot
  ########################################
  ## END VERIFICATION, IMPORT AND START ##
  ########################################
}


## Print usage message and exit with set exit code, depending if usage was
## called by [-h|--help] or because user tried and invalid option.
usage() {
  printf '%s' "Usage: ${me} [options...]

User Options:
 -g, --guest=<guest> Specify the guest. Options: kicksecure, whonix (default)
 -u, --guest-version=<version>
                     Specify guest version, or query from API if not provided.
 -i, --interface=<interface>
                     Choose the interface. Options: cli, xfce (default)
 -m, --hypervisor=<platform>
                     Select the virtualization platform. Options: kvm, virtualbox (default)
 --oracle-repo       Use Oracle's repository for VirtualBox
 -o, --onion         Enable downloading files via onion.
 -s, --socks-proxy=<proxy>
                     Set TCP SOCKS proxy for onion client connections.
                       (Defaults to TOR_SOCKS_HOST:TOR_SOCKS_PORT; if unset,
                       attempts to use TBB (Tor Browser Bundle) proxy at port '9150',
                       or system Tor ('little-t-tor') proxy at port '9050'.)
 -l, --log-level=<level>
                     Choose log verbosity. Options: debug, info,
                       notice (default), warn, error.
 -V, --version       Display version information and exit.
 -h, --help          Show this help message and exit.

Developer options:
 --no-show-errors    Suppress error messages.
 --allow-errors      Continue execution despite errors. Dirty mode. Use with caution.
 --mirror=<number>   Choose a download mirror by index. Defaults to mirror 0 for
                       clearnet and mirror 0 for onion if unspecified.
                       Mirror indices:
                         0 [DE] download.whonix.org (onion available)
                         1 [DK] mirrors.dotsrc.org (onion available)
                         2 [HU] quantum-mirror.hu
 --redownload        Re-download the guest image, even if previously successful.
 --import-only=<vm>  Select specific VM to import. Only if guest is Whonix.
                       Options: workstation, gateway.
 --no-import         Skip guest import. Default behavior is to import.
 --destroy-existing-guest Deletes any existing virtual machine(s) and re-imports them.
                       Warning: This action poses a risk of data loss as it involves
                       a complete reinstallation of the VM(s). Proceed with caution.
 -k, --no-boot       Do not boot the guest after setup. Default is to start.
 -P, --directory-prefix=<dir>
                    Specify the absolute path for the directory where
                      files will be saved. Ensure that the directory already exists
                      and is readable and writable by the user. If this directory
                      is changed and previously downloaded files are not moved
                      to the new directory, the download will restart. Default:
                      \$HOME/dist-installer-cli-download.
 -n, --non-interactive
                     Enable non-interactive mode; license will be accepted.
 -D, --dev           Activate development mode. Downloads an empty image.
 --noupdate          Skip package manager list update. For development only.
 --noupgrade         Ignore pending upgrades. For development only.
 --ci                Enable Continuous Integration (CI) mode.
 --testers           Download the tester's version.
 -d, --dry-run       Simulate execution; log commands without executing.
 --virtualbox-only   Restrict actions to downloading and installing VirtualBox.
 -t, --getopt        Display parsed options and exit.
 --user=username     Specify the user to install the distribution VM under.
File name:
  The default file name is dist-installer-cli. Basic options can be set by using
  file name following the format 'guest-installer-interface'.
  Names not adhering to this format or the default are rejected.
  Command-line options take precedence over file name settings.
"
  exit "${1:-0}"
}


## Set default values for variables.
set_default() {
  ## Options
  user_home_dir="${HOME}"
  directory_prefix="${HOME}/dist-installer-cli-download"
  guest=whonix
  hypervisor=virtualbox
  interface=xfce
  oracle_repo=""
  log_level=notice
  guest_version=""
  socks_proxy=""
  onion=""
  non_interactive=""
  dev=""
  noupdate=""
  noupgrade=""
  dry_run=""
  getversion=""
  getopt=""
  ci=""
  no_import=""
  no_boot=""
  redownload=""
  import_only=""
  destroy_existing_guest=""
  testers=""
  allow_errors=""
  mirror=""
  virtualbox_only=""
  target_user=""

  ## Runtime variables.
  last_exit=0
  guest_pretty=""
  xtrace=""
  url_version_domain=""
  run_background=""
  background_pid=""
  debian_derivative_detected=""
  ubuntu_derivative_detected=""
  kali_derivative_detected=""
  fedora_derivative_detected=""
  virtualbox_linux_user_group="vboxusers"
  # shellcheck disable=SC2034
  qubes_template_detected=""
  install_pkg_fasttrack_extra_args_maybe=()
  user_warned_potential_startup_issue=""
  can_boot_virtualbox_guest_vms="true"
  ## 'vboxmanage' locale needs to be "C" (default, English) so output can be grepped.
  vboxmanage_locale_english=( 'env' 'LC_ALL=C.UTF-8' 'LANG=C.UTF-8' 'LANGUAGE=C' 'vboxmanage' )
}


set_target_user_account() {
  target_user="${1:-}"

  if ! id --user -- "${target_user}" >/dev/null; then
    die 1 "target_user '${target_user}' does not exist!"
  fi

  if [ "${sucmd}" != 'sudo' ]; then
    log warn "Privilege escalation utilities other than 'sudo' for installing to an alternate user account or running under account 'sysmaint' is untested."
  fi

  ## 'getent' may fail if the account doesn't exist, however we ignore it for now.
  ## The reason is because if this is run as a user account `sysmaint` on a
  ## system with no account 'user', this will fail, but the account may have passed
  ## their own '--user' and '--directory-prefix' flags which will fix it.
  user_home_dir="$(getent passwd "${target_user}" | cut -d':' -f6)" || true
}


adjust_default_for_sysmaint_maybe() {
  if getent passwd sysmaint >/dev/null; then
    log info "Account 'sysmaint' exists: 'Yes'"

    if [ "$(id -un)" = 'sysmaint' ]; then
      log info "Running under account 'sysmaint': 'Yes', adjusting behavior."

      set_target_user_account 'user'
      directory_prefix="${user_home_dir}/dist-installer-cli-download"
    else
      log info "Running under account 'sysmaint': 'No'"
    fi
  else
    log info "Account 'sysmaint' exists: 'No'"
  fi
}


## Print parsed options
print_getopt() {
  printf '%s\n' "directory_prefix=${directory_prefix}
  guest=${guest}
  hypervisor=${hypervisor}
  interface=${interface}
  oracle_repo=${oracle_repo}
  log_level=${log_level}
  guest_version=${guest_version}
  socks_proxy=${socks_proxy}
  onion=${onion}
  non_interactive=${non_interactive}
  dev=${dev}
  noupdate=${noupdate}
  noupgrade=${noupgrade}
  dry_run=${dry_run}
  getversion=${getversion}
  getopt=${getopt}
  ci=${ci}
  no_import=${no_import}
  no_boot=${no_boot}
  redownload=${redownload}
  import_only=${import_only}
  destroy_existing_guest=${destroy_existing_guest}
  testers=${testers}
  allow_errors=${allow_errors}
  mirror=${mirror}
  virtualbox_only=${virtualbox_only}"
}

## Parse script name.
parse_name() {
  ## if using default file name, ignore the rest
  [ "${me}" = "dist-installer-cli" ] && return 0
  [ "${me}" = "dist-installer-cli-standalone" ] && return 0
  ## check if file name is valid
  case "${me}" in
    whonix-xfce-installer-cli | whonix-cli-installer-cli | \
      kicksecure-xfce-installer-cli | kicksecure-cli-installer-cli )
      log info "Valid script name to set options: '${me}'"
      ;;
    virtualbox-installer-cli )
      log info "Valid script name to set options: '${me}'"
      virtualbox_only=1
      hypervisor=virtualbox
      return 0
      ;;
    *)
      log error "Invalid script name: '${me}'"
      log error "If you don't know why this happened, rename this script to"
      log error "  dist-installer-cli and use command-line options instead."
      return 2
  esac
  ## assign values according to script name
  guest="$(printf '%s' "${me}" | cut -d "-" -f1)"
  interface="$(printf '%s' "${me}" | cut -d "-" -f2)"
  log info "Assigned guest and interface according to script name: '${me}'"
  return 0
}


copy_thru_barrier() {
  local source dest source_basename dest_realpath dest_file

  source="${1}"
  dest="${2}"
  source_basename="$(basename -- "${source}")"
  dest_realpath="$(root_cmd_loglevel="echo" dry_run=0 root_cmd realpath -- "${dest}")"
  if test_file -d "${dest_realpath}"; then
    dest_file="${dest_realpath}/${source_basename}"
  else
    dest_file="${dest_realpath}"
  fi

  run_as_target_user tee -- "${dest_file}" >/dev/null < "${source}"
}


## Parse command-line options.
parse_opt() {
  local directory_prefix_parent last_run_integer cur_run_integer log_dir_main

  #[ -z "${1:-}" ] && usage 2
  while true; do
    begin_optparse "${1:-}" "${2:-}" || break
    ## SC2154: Variable ${opt} gets set in file: /usr/libexec/helper-scripts/parse_opt.sh
    case "${opt}" in
      P|directory-prefix)
        get_arg
        directory_prefix="${arg}"
        ;;
      o|onion)
        onion=1
        ;;
      s|socks-proxy)
        get_arg
        socks_proxy="${arg}"
        ;;
      l|log-level)
        get_arg
        log_level="${arg}"
        ;;
      g|guest)
        get_arg
        guest="${arg}"
        ;;
      u|guest-version)
        get_arg
        guest_version="${arg}"
        ;;
      i|interface)
        get_arg
        interface="${arg}"
        ;;
      m|hypervisor)
        get_arg
        hypervisor="${arg}"
        ;;
      oracle-repo)
        oracle_repo=1
        ;;
      mirror)
        get_arg
        mirror="${arg}"
        ;;
      import-only)
        get_arg
        import_only="${arg}"
        ;;
      allow-errors)
        allow_errors=1
        ;;
      redownload)
        redownload=1
        ;;
      destroy-existing-guest)
        destroy_existing_guest=1
        ;;
      n|non-interactive)
        non_interactive=1
        ;;
      k|no-boot)
        no_boot=1
        ;;
      no-import)
        no_import=1
        ;;
      D|dev)
        dev=1
        ;;
      noupdate)
        noupdate=1
        ;;
      noupgrade)
        noupgrade=1
        ;;
      testers)
        testers=1
        ;;
      t|getopt)
        getopt=1
        ;;
      ci)
        ci=1
        ;;
      d|dry-run)
        dry_run=1
        ;;
      virtualbox-only)
        virtualbox_only=1
        hypervisor=virtualbox
        guest=none
        ;;
      user)
        get_arg
        set_target_user_account "${arg}"
        ;;
      V|version)
        getversion=1
        ;;
      h|help)
        usage 0
        ;;
      "")
        break
        ;;
      *)
        die 2 "Invalid option: '${opt_orig}'"
        ;;
    esac
    shift "${shift_n:-1}"
  done

  ## Test if options are valid
  range_arg log_level "${log_level}" error warn notice info debug
  if [ "${log_level}" = "debug" ]; then
    xtrace=1
    set -o xtrace
  fi
  range_arg guest "${guest}" none whonix kicksecure
  guest_pretty="$(capitalize_first_char "${guest}")"
  range_arg interface "${interface}" cli xfce
  range_arg hypervisor "${hypervisor}" kvm virtualbox
  range_arg import_only "${import_only}" workstation gateway both

  case "${interface}" in
    xfce)
      ## Whonix 17 and above uses Xfce instead of XFCE.
      interface_name="Xfce"
      ;;
    cli)
      interface_name="CLI"
      ;;
  esac

  if [ "${guest}" != "whonix" ]; then
    ## Guest is not 'whonix'. I.e. is 'kicksecure'
    if [ -n "${import_only}" ]; then
      die 1 "The option '--import-only' option can only be set when the guest is 'whonix'."
    fi
  fi

  [ -n "${mirror}" ] && range_arg mirror "${mirror}" 0 1 2

  [ -n "${socks_proxy}" ] && is_addr_port "${socks_proxy}"

  if [ -n "${directory_prefix}" ]; then
    ## Remove trailing slash from directory.
    directory_prefix="${directory_prefix%*/}"
    ## Only accept an absolute path.
    if [ "${directory_prefix}" = "${directory_prefix#/}" ]; then
      log error "Invalid directory prefix: '${directory_prefix}'"
      die 1 "Directory prefix cannot be a relative path, must be an absolute path."
    fi

    ## Test if parent directory exists.
    directory_prefix_parent="$(dirname -- "${directory_prefix}")"
    if ! test_file -d "${directory_prefix_parent}"; then
      die 1 "Directory doesn't exist: '${directory_prefix_parent}'"
    fi
    ## Not possible to check if parent dir is writable because if the prefix
    ## is set to '~/', the parent '/home' is not writable.

    log info "Creating directory: '${directory_prefix}'"
    run_as_target_user mkdir -p -- "${directory_prefix}" ||
      die 1 "Failed to created directory: '${directory_prefix}'"
    test_file -w "${directory_prefix}" ||
      die 1 "Directory isn't writable: '${directory_prefix}'"
    test_file -r "${directory_prefix}" ||
      die 1 "Directory isn't readable: '${directory_prefix}'"

    log_dir_main="${directory_prefix}/logs"

    ## Log to incrementing integer to avoid leaking other information such
    ## as PID or date (even if UTC).
    if ! test_file -d "${log_dir_main}/1"; then
      log_dir_cur="${log_dir_main}/1"
    else
      has awk || die 1 "${underline}Parse options:${nounderline} Package 'gawk' is missing. Please install."
      last_run_integer="$(printf '%s ' "${log_dir_main}"/* | awk '{print NF}')"
      cur_run_integer=$((last_run_integer+1))
      log_dir_cur="${log_dir_main}/${cur_run_integer}"
    fi
    log_file_user="${log_dir_cur}/user.log"
    log_file_debug="${log_dir_cur}/debug.log"

    ## If the commands below fail, it should have failed earlier for the
    ## parent directory permissions, not below.
    run_as_target_user mkdir -p -- "${log_dir_cur}"
    copy_thru_barrier "${0}" "${log_dir_cur}";
    run_as_target_user touch -- "${log_file_user}"
  fi

  if [ "${getopt}" = "1" ]; then
    print_getopt
    exit 0
  fi
  if [ "${getversion}" = "1" ]; then
    printf '%s\n' "${me} ${version}"
    exit 0
  fi
  if [ "${dev}" = "1" ]; then
    if [ -z "${guest_version}" ]; then
      log notice "Version Detection: Setting development testing empty software version via '--dev' option."
      guest_version="17.0.3.4"
    fi
  fi
  if [ "${dry_run}" = "1" ]; then
    if [ -z "${guest_version}" ]; then
      log notice "Simulation: commands will be printed but not executed via '--dry-run' option."
      log notice "Version Detection: Using simulated software version via '--dry-run' option."
      guest_version="17.0.3.4"
    fi
  fi
  if [ "${allow_errors}" = "1" ]; then
    set +o errexit
    set +o errtrace
  fi

  case "${hypervisor}" in
    virtualbox)
      hypervisor_pretty="VirtualBox"
      ;;
    kvm)
      hypervisor_pretty="KVM"
      ;;
    *)
      hypervisor_pretty="${hypervisor}"
      ;;
  esac

  log info "Option Parsing: 'success'"
}


## Logging mechanism.
## Bash supports process substitution and saving xtrace to a file, which is a
## simpler way to log to file and console.
log_term_and_file() {
  local temp_folder xtrace_fifo

  ## Discover if terminal is attached to stdout
  if [ ! -t 1 ]; then
    log warn "Terminal: Output is not being sent to the terminal because terminal is not connected to stdout."
    return 0
  fi

  run_as_target_user touch -- "${log_file_user}"
  run_as_target_user touch -- "${log_file_debug}"

  ## By appending '&' at the end, log_file_user would remain empty.
  true "\
  exec > >(run_as_target_user tee -a -- \"${log_file_user}\") \
      2> >(run_as_target_user tee -a \"${log_file_debug}\" >&2)"
  exec > >(run_as_target_user tee -a -- "${log_file_user}") \
      2> >(run_as_target_user tee -a -- "${log_file_debug}" >&2)

  temp_folder="$(mktemp --directory)"
  xtrace_fifo="/${temp_folder}/xtrace_fifo"
  mkfifo -- "$xtrace_fifo"

  if [ "${log_level}" = "debug" ] || test -o xtrace; then
    ## log_level is debug or xtrace is already enabled.
    ## Therefore keep send xtrace to both console and debug file.
    run_as_target_user tee -a -- "${log_file_debug}" < "$xtrace_fifo" >&2 &
  else
    run_as_target_user tee -a -- "${log_file_debug}" < "$xtrace_fifo" > /dev/null &
  fi

  ## Automatically exists at the end of this script.
  #xtrace_pid="$!"

  exec 9> "$xtrace_fifo"
  export BASH_XTRACEFD=9
  set -o xtrace
  xtrace=1
}

welcome() {
  if [ "${virtualbox_only}" = "1" ]; then
    log notice "${underline}Installer:${nounderline} ${bold}'VirtualBox Installer'${nobold}"
  else
    log notice "${underline}Installer:${nounderline} ${bold}'${guest_pretty} ${interface_name} for ${hypervisor_pretty} Installer'${nobold}"
  fi

  log notice "Saving user log to: '${log_file_user}'"

  if test_file -f "${log_file_debug}"; then
    log notice "Saving debug log to: '${log_file_debug}'"
  fi
}

end_installer() {
  log notice "Installer Result: ${green}${bold}'SUCCESS'${nobold}${nocolor}"
  end_exit
}


## Wrapper to call all necessary functions in one.
run_installer() {
  set_globals "${@}"
  ## Set default values.
  set_default
  get_su_cmd
  not_as_root
  ## Set trap for common signals.
  set_trap
  ## Parse script name for wanted values.
  parse_name
  ## If account 'sysmaint' is in use and the user hasn't configured their own directory
  ## prefix and target user, default to target account 'user'.
  ## Runs function 'set_target_user_account'.
  adjust_default_for_sysmaint_maybe
  ## Parse command-line options.
  ## Uses function 'test_file' which in turn uses 'root_cmd'.
  parse_opt "${@}"
  ## Logging mechanism.
  log_term_and_file
  welcome
  test_run_as_target_user

  get_os
  get_distro
  get_installer_version
  check_not_qubes_template
  get_system_stat
  need_reboot_check_first
  get_host_virtualizer_pkgs
  need_reboot_check_second
  get_independent_host_pkgs
  kernel_modules_check
  nested_virtualization_test
  secure_boot_test
  dkms_signing_key_enrollment
  kernel_modules_load
  kernel_modules_signed_only
  get_transfer_cmd
  torify_conn_maybe
  get_virtualization
  get_checkhash_cmd
  get_download_links

  main
}


if [ "$dist_installer_cli_was_sourced" = "true" ]; then
  true "INFO: Not running the install script because it was sourced by another script."
else
  true "INFO: Running the install script because it was executed."
  run_installer "${@}"
fi
