#!/bin/bash

## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

## The idea of this bash fragment is:
## Say nothing (when output capture is enabled), if everything goes well,
## but dump everything on error.
##
## It allows to easily look inside the xtrace of a (Debian maintainer) script,
## when the DEBDEBUG environment variable is set to 1.
##
## To use it in other scripts, use something like this:
# if [ -f /usr/libexec/helper-scripts/pre.bsh ]; then
#    source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/pre.bsh
# fi
#
## Assumptions / supported usage:
## - Intended to be sourced by Debian maintainer scripts (non-interactive).
## - Caller is expected to already enable 'set -o errexit' ('set -e') as desired.
##   This is useful in case 'pre.bsh' is unavailable.
## - Output capture ("silent by default") is OPT-IN by the caller:
##     Call 'disable_echo' to redirect stdout/stderr (including xtrace output) into
##     environment variable 'TEMP_FILE_PRE_BSH'.
##     Call 'enable_echo' to restore stdout/stderr.

## Practical use cases:
## - The ability to easily tell users
##   > use `sudo DEBDEBUG=1 apt dist-upgrade`
##   and seeing the xtrace. Otherwise debugging Debian maintainer scripts
##   (enabling xtrace) is complicated.
## - Skipping broken Debian maintainer scripts.
## - Reusable configuration code for Debian maintainer scripts.

## Error log / features:
## - enables 'errtrace' ('set -o errtrace')
## - installs an ERR trap 'error_handler_pre' only if no ERR trap exists yet
## - output capture ("silent by default") is OPT-IN by the caller ('disable_echo'/'enable_echo')
## - when output capture is enabled, write stdout/stderr (incl xtrace) to a temporary log
## - when output capture is enabled, dump the full captured log on unexpected non-zero exit code
## - always shows exit code on unexpected non-zero exit code
## - cleanup temporary log on normal exit (without clobbering an existing EXIT trap)
## - run syntax check "bash -n" on this script
## - run syntax check "bash -n" on the script that sourced this script (if readable)

## DEBDEBUG:
##
## enable xtrace (-x) for maintainer script when DEBDEBUG environment
## variable is set to 1.
## For example:
##    sudo DEBDEBUG=1 dpkg -i /path/to/package.deb
##
## Note: xtrace output is only collected into TEMP_FILE_PRE_BSH if the caller
## enabled output capture using 'disable_echo'.

## SKIP_SCRIPTS
##
## The SKIP_SCRIPTS environment variable to skip scripts by name
## For example:
##    sudo DEBDEBUG=1 SKIP_SCRIPTS=" security-misc-shared.postinst " dpkg -i /path/to/package.deb
##
## another example:
##
##    export DEBDEBUG=1
##     export SKIP_SCRIPTS+=" security-misc-shared.postinst "
##     sudo --preserve-env dpkg -i /path/to/package.deb

## Provides colors by 'source'ing 'get_colors.sh'.

## Shell options: enables errtrace

## Configuration Folders
##
## For example if the name of the package is 'security-misc-shared':
## - /etc/security-misc-shared_maint.d/*.conf
## - /usr/local/etc/security-misc-shared_maint.d/*.conf
##
## For example if the name of the script is 'panic-on-oops':
## - /etc/panic-on-oops_pre.d/*.conf
## - /usr/local/etc/panic-on-oops_pre.d/*.conf

## {{{ pre.bsh 1.1

## bash script fragment

# shellcheck source=./check_runtime.bsh
source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/check_runtime.bsh

if was_executed "${BASH_SOURCE[0]}"; then
  pre_bsh_sourced='false'
else
  pre_bsh_sourced='true'
fi

if [ "${pre_bsh_sourced}" = 'false' ]; then
  true "$0: START"
  set -o errexit
  set -o nounset
  set -o errtrace
  set -o pipefail
fi

if test -r "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/get_colors.sh; then
  # shellcheck source=./get_colors.sh
  if ! source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/get_colors.sh; then
    true "$0: ERROR: Failed to source '${HELPER_SCRIPTS_PATH:-}/usr/libexec/helper-scripts/get_colors.sh'"
  fi
fi

source_config_folder() {
  local pre_bsh_settings_folder i etc_config_dir usr_local_etc_config_dir \
    bash_n_output

  ## dpkg sets environment variables
  ## example:
  ## DPKG_MAINTSCRIPT_PACKAGE=security-misc-shared

  [[ -v DPKG_MAINTSCRIPT_PACKAGE ]] || DPKG_MAINTSCRIPT_PACKAGE=""
  if [ "$DPKG_MAINTSCRIPT_PACKAGE" = "" ]; then
    pre_bsh_settings_folder="${own_filename}_pre.d"
  else
    pre_bsh_settings_folder="${DPKG_MAINTSCRIPT_PACKAGE}_maint.d"
  fi

  ## example:
  ## pre_bsh_settings_folder=security-misc-shared_maint.d

  shopt -s nullglob

  ## example:
  ## /etc/panic-on-oops_pre.d/*.conf
  ## /usr/local/etc/panic-on-oops_pre.d/*.conf
  etc_config_dir="/etc/${pre_bsh_settings_folder}"
  usr_local_etc_config_dir="/usr/local/etc/${pre_bsh_settings_folder}"

  true "folder 1: ${etc_config_dir}"
  true "folder 2: ${usr_local_etc_config_dir}"

  for i in "${etc_config_dir}/"*.conf "${usr_local_etc_config_dir}/"*.conf; do
    if ! [ -f "${i}" ]; then
      continue
    fi
    if ! bash_n_output="$(bash -n "$i" 2>&1)"; then
      force_output "Invalid config file: '$i'
   bash_n_output:
   $bash_n_output" >&2
      exit 1
    fi
    # shellcheck disable=SC1090
    source "$i"
  done

  shopt -u nullglob
}

check_scripts_to_skip() {
  [[ -v SKIP_SCRIPTS ]] || SKIP_SCRIPTS=""
  local skip_script
  for skip_script in $SKIP_SCRIPTS; do
    if [ "$skip_script" = "$own_filename" ]; then
      force_output "INFO: Skipping $own_filename, because SKIP_SCRIPTS includes it."
      exit 0
    fi
  done
}

disable_echo() {
  ## Internal state variables (do NOT set from env/config):
  ## - PRE_BSH_ECHO_DISABLED: "true" iff stdout/stderr are redirected and fds saved
  ## - PRE_BSH_STDOUT_SAVE_FD / PRE_BSH_STDERR_SAVE_FD: saved fd numbers for restore
  [[ -v PRE_BSH_ECHO_DISABLED ]] || PRE_BSH_ECHO_DISABLED=""
  if [ "$PRE_BSH_ECHO_DISABLED" = "true" ]; then
    return 0
  fi

  [[ -v TEMP_FILE_PRE_BSH ]] || TEMP_FILE_PRE_BSH="$(mktemp)"

  true "INFO: exec {PRE_BSH_STDOUT_SAVE_FD}>&1"
  exec {PRE_BSH_STDOUT_SAVE_FD}>&1
  true "INFO: exec {PRE_BSH_STDERR_SAVE_FD}>&2"
  exec {PRE_BSH_STDERR_SAVE_FD}>&2
  true "INFO: exec 1>>${TEMP_FILE_PRE_BSH} 2>>${TEMP_FILE_PRE_BSH}"
  exec 1>>"${TEMP_FILE_PRE_BSH}" 2>>"${TEMP_FILE_PRE_BSH}"

  PRE_BSH_ECHO_DISABLED=true
}

enable_echo() {
  [[ -v PRE_BSH_ECHO_DISABLED ]] || PRE_BSH_ECHO_DISABLED=""
  if [ "$PRE_BSH_ECHO_DISABLED" = "true" ]; then
    ## Be defensive if internal variables were mis-set.
    if [[ -n "${PRE_BSH_STDOUT_SAVE_FD-}" && -n "${PRE_BSH_STDERR_SAVE_FD-}" ]]; then
      true "INFO: 1>&'${PRE_BSH_STDOUT_SAVE_FD}' 2>&'${PRE_BSH_STDERR_SAVE_FD}'"
      exec 1>&"${PRE_BSH_STDOUT_SAVE_FD}" 2>&"${PRE_BSH_STDERR_SAVE_FD}"
      true "INFO: exec {PRE_BSH_STDOUT_SAVE_FD}>&- {PRE_BSH_STDERR_SAVE_FD}>&-"
      exec {PRE_BSH_STDOUT_SAVE_FD}>&- {PRE_BSH_STDERR_SAVE_FD}>&-
    fi
    PRE_BSH_ECHO_DISABLED=false
  fi
}

force_output() {
  local echo_setting_undo=""
  [[ -v PRE_BSH_ECHO_DISABLED ]] || PRE_BSH_ECHO_DISABLED=""
  if [ "$PRE_BSH_ECHO_DISABLED" = "true" ]; then
    echo_setting_undo="true"
    enable_echo
  fi

  ## If xtrace is set and output was NOT captured, force_output would be redundant.
  ## But if output was captured (echo_setting_undo=true), we must print regardless.
  if ! test -o xtrace || [ "$echo_setting_undo" = "true" ]; then
    printf '%s\n' "$*"
  fi
  if [ "$echo_setting_undo" = "true" ]; then
    disable_echo
  fi
}

error_handler_pre() {
  local exit_code="$?"

  ## Prevent recursion if anything fails inside the handler under caller 'set -e'.
  trap - ERR
  set +e

  local error_message="## See above."

  if [[ -r "${TEMP_FILE_PRE_BSH-}" ]]; then
    error_message="$(cat -- "$TEMP_FILE_PRE_BSH" 2>/dev/null || true)"
  fi

  if [ -z "${error_message}" ]; then
    error_message="## Empty."
  fi

  force_output "
####################################################################
## ${red-}${bold-}BEGIN ERROR in '$0' detected!${reset-}
##
## ${under-}ERROR LOG${reset-}:
$error_message
##
## ${under-}BASH_COMMAND${reset-}: '${BASH_COMMAND-}'
## ${under-}EXIT_CODE${reset-}: '${exit_code-}'
##
## ${red-}${bold-}END ERROR in '$0' detected!${reset-}
## ${red-}${bold-}Please report this bug!${reset-}
####################################################################
" 1>&2

  exit 1
}

exit_handler_pre() {
  if [ -n "${TEMP_FILE_PRE_BSH-}" ] \
    && [ -f "${TEMP_FILE_PRE_BSH-}" ]; then
    rm -f -- "${TEMP_FILE_PRE_BSH-}" || true
  fi
  true "$0: END."
}

## config-package-dev doesn't like 'set -o pipefail'
## http://mailman.mit.edu/pipermail/config-package-dev/2015-May/000041.html
#set -o pipefail

set -o errtrace

## Cleanup temp file on normal exit, but do not clobber an existing EXIT trap.
if [ -z "$(trap -p EXIT)" ]; then
  true "INFO: exit trap exists yet: no - Enabling a simple exit handler trap."
  trap exit_handler_pre EXIT
fi

if test -o xtrace; then
  true "INFO: Setting DEBDEBUG to 1, because xtrace (-x) is set."
  DEBDEBUG="1"
fi

[[ -v DEBDEBUG ]] || DEBDEBUG=""
if [ "$DEBDEBUG" = "1" ]; then
  set -x
fi

## Check if any trap is already declared.
## 'trap -p' does not exit non-zero if there is no trap.
if [ -z "$(trap -p ERR)" ]; then
  true "INFO: error trap exists yet: no - Enabling a simple error handler trap."
  trap "error_handler_pre" ERR
fi

## syntax check this script
# shellcheck disable=SC1090
bash -n "${BASH_SOURCE[0]}"

## syntax check script that sourced this script
## Non-interactive usage expected; if $0 is not a readable script file, skip this check.
# shellcheck disable=SC1090
if [[ -f "$0" && -r "$0" ]]; then
  bash -n "$0"
fi

own_filename="${0##*/}"

source_config_folder

check_scripts_to_skip

if [ "${pre_bsh_sourced}" = 'false' ]; then
  true "$0: END"
fi

## }}}
