#!/bin/bash

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

## Scripts that source this file are expected to set at minimum:
##   set -o errexit
## Modern scripts should use:
##   set -o errexit
##   set -o errtrace
##   set -o pipefail
##   set -o nounset

## check_runtime.bsh cannot be sourced here (this will be run when
## derivative-maker is used, and the build host may not have helper-scripts
## installed).
## Literal code duplication is bad.
## Confuses shellcheck for dist-installer-cli-standalone.
if test -f "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/check_runtime.bsh; then
  ## Use 'source_encoded' to avoid
  ## /usr/share/usability-misc/build-dist-installer-cli
  ## from in-lining the source code.
  source_encoded=source
  "$source_encoded" "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/check_runtime.bsh
fi

## NOTE: br_add and br_add_to_file are not HTML sanitization functions.
## They only insert '<br />' tags at newlines.
## For HTML sanitization, use strip-markup or sanitize-string from this
## repository instead.
br_add() {
  local in out

  if (($# > 1)); then
    printf '%s\n' "br_add: too many arguments" >&2
    return 1
  fi

  if (($# == 1)); then
    in="$1"
  else
    in="$(cat)"
  fi

  out="${in//$'\n'/'<br />'$'\n'}"
  printf '%s\n' "$out"
}

br_add_to_file() {
  local file_name="$1"

  test -f "${file_name}"
  safe-rm -f -- "${file_name}_br"
  touch -- "${file_name}_br"

  local line
  while IFS= read -r line; do
    printf '%s\n' "$line<br />" | tee -a -- "${file_name}_br" >/dev/null
  done <"${file_name}"
}

read_integer_file() {
  local target_file lower_bound upper_bound file_contents

  target_file="${1:-}"
  lower_bound="${2:-}"
  upper_bound="${3:-}"
  if [ -z "$target_file" ]; then
    printf '%s\n' "$0: ERROR: No target file specified!" >&2
    return 1
  fi
  if [ -n "$lower_bound" ] && ! [[ "$lower_bound" =~ ^[0-9]+$ ]]; then
    printf '%s\n' "$0: ERROR: Non-numeric lower bound specified!" >&2
    return 1
  fi
  if [ -n "$upper_bound" ] && ! [[ "$upper_bound" =~ ^[0-9]+$ ]]; then
    printf '%s\n' "$0: ERROR: Non-numeric upper bound specified!" >&2
    return 1
  fi

  if ! test -f "$target_file"; then
    printf '%s\n' "$0: ERROR: Target file '$target_file' does not exist!" >&2
    return 1
  fi
  if ! test -r "$target_file"; then
    printf '%s\n' "$0: ERROR: Target file '$target_file' not readable!" >&2
    return 1
  fi
  if ! file_contents="$(stcat "$target_file")"; then
    printf '%s\n' "$0: ERROR: Cannot stcat target file '$target_file'!" >&2
    return 1
  fi
  if [ -z "$file_contents" ]; then
    printf '%s\n' "$0: ERROR: Empty target file '$target_file'!" >&2
    return 1
  fi
  if ! [[ "$file_contents" =~ ^[0-9]+$ ]]; then
    printf '%s\n' "$0: ERROR: Non-numeric value in target file '$target_file'!" >&2
    return 1
  fi
  if [ -n "$lower_bound" ] && ((file_contents < lower_bound)); then
    printf '%s\n' "$0: ERROR: Target file's '$target_file' value '$file_contents' is below the lower bound '$lower_bound'!" >&2
    return 1
  fi
  if [ -n "$upper_bound" ] && ((file_contents > upper_bound)); then
    printf '%s\n' "$0: ERROR: Target file's '$target_file' value '$file_contents' is above the upper bound '$upper_bound'!" >&2
    return 1
  fi
  printf '%s\n' "$0: INFO: Target file '$target_file' file_contents: '$file_contents'" >/dev/null
  printf '%s\n' "$file_contents"
}

check_is_alpha_numeric() {
  local varname value

  if [ -z "$1" ]; then
    printf '%s\n' "$0: ERROR: No variable name provided to check_is_alpha_numeric." >&2
    return 1
  fi

  varname="$1"
  check_variable_name "${varname}" || return 1
  value="${!varname}"

  if ! [[ "$value" =~ ^[0-9a-zA-Z_-]+$ ]]; then
    stecho "$0:
\$1 (variable name): '$varname'
\$2 (variable content): '$value'
Invalid character!" >&2
    return 1
  fi
  if [ "$value" = "" ]; then
    stecho "$0:
\$1 (variable name): '$varname'
\$2 (variable content): empty!" >&2
    return 1
  fi

  return 0
}

validate_safe_filename() {
  local varname value reason re

  varname="$1"
  check_variable_name "${varname}" || return 1
  value="${!varname}"

  if [[ -z "$value" ]]; then
    reason="Empty value!"
  elif [[ "$value" == *"/"* || "$value" == *\\* ]]; then
    reason="Path separator not allowed!"
  elif [[ "$value" == "." ]]; then
    reason="Dot path not allowed!"
  elif [[ "$value" == ".." ]]; then
    reason="Double dot path not allowed!"
  elif [[ "$value" == *".."* ]]; then
    reason="Contains '..'!"
  elif [[ "$value" =~ [[:cntrl:]] ]]; then
    reason="Control character not allowed!"
  elif [[ "$value" == -* ]]; then
    reason="Leading '-' not allowed!"
  elif ((${#value} > 255)); then
    reason="Too long (>255 chars)!"
  else
    ## Allow: letters/digits (locale-aware), underscore, dash, dot, space,
    ## plus, comma, equals, @, parentheses, percent, braces, square brackets
    re='^[][:alnum:]_.:@+=,(){}%\[ -]+$'
    if [[ "$value" =~ $re ]]; then
      return 0
    fi
    reason="Invalid character!"
  fi

  printf 'RAW: <%q>\n' "$value"
  LC_ALL=C od -An -tx1 <<<"$value"

  stecho "$0: ${FUNCNAME[0]}
\$1 (variable name): '$varname'
\$2 (variable content): '$value'
$reason" >&2

  return 1
}

check_valid_linux_user_account_name() {
  ## Validates per /etc/adduser.conf SYS_NAME_REGEX
  ## Return: 0 valid, 1 invalid. Prints nothing.
  [[ "${1:-}" =~ ^[a-zA-Z_][a-zA-Z0-9_-]*\$?$ ]]
}

check_is_not_empty_and_only_one_line() {
  local varname value number_of_lines trimmed_value lines

  if [ -z "$1" ]; then
    printf '%s\n' "$0: ERROR: No variable name provided to check_is_not_empty_and_only_one_line." >&2
    return 1
  fi

  varname="$1"
  check_variable_name "${varname}" || return 1
  value="${!varname}"

  ## Trim leading/trailing spaces while preserving newlines.
  IFS= read -r trimmed_value <<<"$value"

  ## Check if value is empty or only contains spaces/newlines.
  if [[ -z "$trimmed_value" || -z "${value//[[:space:]]/}" ]]; then
    printf '%s\n' "$0: ERROR: Variable '$varname' is empty or contains only whitespace/newlines." >&2
    return 1
  fi

  mapfile -t lines <<<"$value"
  number_of_lines="${#lines[@]}"

  if [ "$number_of_lines" -ne 1 ]; then
    printf '%s\n' "$0: ERROR: Variable '$varname' contains more than one line. Content:
'$value'" >&2
    return 1
  fi

  return 0
}

## Converts $1 to a newline-separated, properly escaped list.
## Strips leading and trailing space, crunches multiple
## consecutive spaces into single spaces, then converts spaces to newlines
## and adds spaces and backslashes where necessary.
trim_collapse_split() {
  local input_list="$1"
  local trimmed_list cleaned_list formatted_list

  ## Step 1: Trim leading and trailing spaces.
  trimmed_list="$(printf '%s\n' "$input_list" | sed 's/^\s\+//g; s/\s\+$//g')"

  ## Step 2: Replace multiple spaces with a single space.
  cleaned_list="$(printf '%s\n' "$trimmed_list" | sed 's/\s\+/ /g')"

  ## Step 3: Replace spaces with ' \n' (newline prefixed by space).
  formatted_list="$(printf '%s\n' "$cleaned_list" | sed 's/\s/ \\\n/g')"

  printf '%s\n' "$formatted_list"
}

## Print a string quoted to prevent special characters in the string from being
## interpreted by the shell. The output is guaranteed pure-ASCII printable
## regardless of caller locale:
##
##   - Bytes > 0x7E (incl. valid UTF-8 multibyte sequences such as U+202E
##     RIGHT-TO-LEFT OVERRIDE) are octal-escaped (\NNN).
##   - Control bytes (NUL, ESC, BEL, DEL, ...) are escaped via bash $'...'
##     literals.
##   - Newlines are rendered as literal '\n', not real LF.
##
## LC_ALL=C is REQUIRED. Without it, printf %q under a UTF-8 locale (the
## default on Debian / Ubuntu / GitHub Actions runners) passes valid UTF-8
## multibyte sequences through unchanged because it considers them "printable"
## - which would let trojan codepoints like U+202E survive the quote and
## obfuscate text printed in proximity to attacker-controlled text or attempt
## to exploit Unicode parsers.
##
## Use this whenever logging an untrusted-origin string (filename, archive
## entry, network input, etc.).
##
## Example:
##   log error "target '$(string_quote_safe "$path")' does not exist"
##
## Tested by ./unicode-testscript on the trojan-source corpus.
string_quote_safe() {
  LC_ALL=C printf '%q' "$1"
}

## success: whole numbers such as 1 2 3 456 etc. (purely numeric only) (isnum)
## fail:
## 5.5
## 5,500
## trailing spaces
## leading spaces
## empty input
## space only
## scientific notation such as 1.23e10
## negative numbers such as -1
is_whole_number() {
  [[ "$1" =~ ^[0-9]+$ ]]
}

## same results as is_whole_number, but also allows negative numbers
is_integer(){
  printf '%d' "${1}" >/dev/null 2>&1 || return 1
}

## NOTE: Duplicated in package 'uwt' file 'curl.anondist'.
## Input is trusted. The caller is expected to pass a valid whole number.
## /dev/random is used intentionally instead of /dev/urandom.
## reason: https://www.kicksecure.com/wiki/Dev/Entropy
random_alpha_numeric() {
  local n="${1:-43}"
  if ! is_whole_number "$n"; then
    printf '%s\n' "$0: ERROR: random_alpha_numeric: non-numeric byte count: '$n'" >&2
    return 1
  fi
  LC_ALL=C tr -dc 'A-Za-z0-9' </dev/random | head --bytes="$n" || true
  printf '%s\n' ""
}

gpg_bash_lib_function_displaytime() {
  ## Thanks to Stephane Gimenez!
  ## http://unix.stackexchange.com/a/27014/49297
  ## Modified by Patrick Schleizer.
  local T Y D H M S sep
  T="${1-0}"

  if ! is_whole_number "${T}"; then
    T=0
  fi

  Y=$((T/60/60/24/365))
  D=$((T/60/60/24%365))
  H=$((T/60/60%24))
  M=$((T/60%60))
  S=$((T%60))

  sep=''

  if (( Y > 0 )); then
    printf '%s%d years' "$sep" "$Y"
    sep=' '
  fi

  if (( D > 0 )); then
    printf '%s%d days' "$sep" "$D"
    sep=' '
  fi

  if (( H > 0 )); then
    printf '%s%d hours' "$sep" "$H"
    sep=' '
  fi

  if (( M > 0 )); then
    printf '%s%d minutes' "$sep" "$M"
    sep=' '
  fi

  if (( S > 0 )); then
    printf '%s%d seconds' "$sep" "$S"
    sep=' '
  fi

  if (( Y == 0 && D == 0 && H == 0 && M == 0 && S == 0 )); then
    printf '0 seconds'
  fi

  printf '%s\n' ''
}

check_variable_name() {
  ## Ensures a string is a valid Bash variable name.
  [[ "$1" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]
}

run_test() {
  local test_id function_name expected_exit_code var_name
  local -a input_parts
  local input_value actual_exit_code=0

  test_id="$1"
  shift
  function_name="$1"
  shift
  expected_exit_code="$1"
  shift
  var_name="$1"
  shift

  # Preserve whitespace/newlines in the value by keeping the remainder as an array
  input_parts=("$@")
  input_value="${input_parts[*]}"

  # Assign without eval (works for simple variable names like testvar/a/etc.)
  check_variable_name "$var_name" || return 1
  printf -v "$var_name" '%s' "$input_value"

  printf '%s\n' "$0: Running test $test_id: $function_name ($var_name)"

  "$function_name" "$var_name" || actual_exit_code=$?

  if [[ "$actual_exit_code" -eq "$expected_exit_code" ]]; then
    printf '%s\n' "$0: Test Passed"
  else
    printf '%s\n' "$0: Test Failed (Expected: $expected_exit_code, Got: $actual_exit_code)" >&2
    return 1
  fi

  printf '%s\n' "---------------------------------"
}

string_bsh_tests() {
  local input_string expected_result actual_result

  ## Should be inside a function to avoid doing this if this script gets 'source'ed.
  set -o errexit
  set -o errtrace
  set -o pipefail
  set -o nounset
  trap strings_bsh_error_trap ERR

  shellcheck "$0"

  ## run_test <test_name> <function> <expected_exit_code> <var_name> <value...>

  run_test "alpha_numeric: rejects empty" check_is_alpha_numeric 1 test_value ""
  run_test "alpha_numeric: rejects dot" check_is_alpha_numeric 1 test_value "."
  run_test "alpha_numeric: rejects space" check_is_alpha_numeric 1 test_value "plain text"
  run_test "alpha_numeric: accepts dash/underscore" check_is_alpha_numeric 0 test_value "test_123-OK"
  ## TODO: unclear if this is possible without unicode-show
  #run_test "alpha_numeric: rejects unicode"                 check_is_alpha_numeric 1 test_value $'caf\xC3\xA9'

  run_test "safe_filename: accepts simple name" validate_safe_filename 0 test_value "test"
  run_test "safe_filename: accepts spaces and punctuation" validate_safe_filename 0 test_value "plain text (v1) [ok].txt"
  run_test "safe_filename: rejects dot" validate_safe_filename 1 test_value "."
  run_test "safe_filename: rejects double dot" validate_safe_filename 1 test_value ".."
  run_test "safe_filename: rejects embedded double dot" validate_safe_filename 1 test_value "test..test"
  run_test "safe_filename: rejects slash" validate_safe_filename 1 test_value "dir/file"
  run_test "safe_filename: rejects backslash" validate_safe_filename 1 test_value $'dir\\file'
  run_test "safe_filename: rejects control char newline" validate_safe_filename 1 test_value $'hello\nworld'
  run_test "safe_filename: rejects leading dash" validate_safe_filename 1 test_value "-option"
  run_test "safe_filename: accepts colon" validate_safe_filename 0 test_value "test:test"
  run_test "safe_filename: rejects overlong name" validate_safe_filename 1 test_value "$(printf 'a%.0s' {1..256})"
  ## TODO: unclear if this is possible without unicode-show
  #run_test "safe_filename: rejects unicode"                 validate_safe_filename 1 test_value $'caf\xC3\xA9'

  run_test "one_line: rejects empty" check_is_not_empty_and_only_one_line 1 test_value ""
  run_test "one_line: rejects only whitespace/newlines" check_is_not_empty_and_only_one_line 1 test_value $'\n\n   '
  run_test "one_line: rejects multi-line string" check_is_not_empty_and_only_one_line 1 test_value $'Hello\nWorld'
  run_test "one_line: accepts normal text" check_is_not_empty_and_only_one_line 0 test_value "Hello, World!"
  run_test "one_line: accepts text with surrounding spaces" check_is_not_empty_and_only_one_line 0 test_value "  Hello  "

  ## check_valid_linux_user_account_name tests
  ## Regex: ^[a-zA-Z_][a-zA-Z0-9_-]*\$?$ (SYS_NAME_REGEX)
  check_valid_linux_user_account_name "user" || error "linux_user_account_name: 'user' should be valid"
  check_valid_linux_user_account_name "_user" || error "linux_user_account_name: '_user' should be valid"
  check_valid_linux_user_account_name "user123" || error "linux_user_account_name: 'user123' should be valid"
  check_valid_linux_user_account_name "user-name" || error "linux_user_account_name: 'user-name' should be valid"
  check_valid_linux_user_account_name "_sysuser\$" || error "linux_user_account_name: '_sysuser\$' should be valid"

  check_valid_linux_user_account_name "" && error "linux_user_account_name: empty string should be invalid"
  check_valid_linux_user_account_name "1user" && error "linux_user_account_name: '1user' should be invalid"
  check_valid_linux_user_account_name "-user" && error "linux_user_account_name: '-user' should be invalid"
  check_valid_linux_user_account_name "user name" && error "linux_user_account_name: 'user name' should be invalid"
  check_valid_linux_user_account_name "user@host" && error "linux_user_account_name: 'user@host' should be invalid"
  check_valid_linux_user_account_name "user\$bad" && error "linux_user_account_name: 'user\$bad' should be invalid"

  run_test "time: 0" gpg_bash_lib_function_displaytime 0 test_value 0
  run_test "time: 500" gpg_bash_lib_function_displaytime 0 test_value 500
  run_test "time: 10000" gpg_bash_lib_function_displaytime 0 test_value 10000
  run_test "time: 100000" gpg_bash_lib_function_displaytime 0 test_value 100000
  run_test "time: 500000" gpg_bash_lib_function_displaytime 0 test_value 500000
  run_test "time: 50000000" gpg_bash_lib_function_displaytime 0 test_value 50000000

  input_string=$'test1\ntest2'
  expected_result=$'test1<br />\ntest2'

  actual_result="$(br_add "$input_string")"
  if ! [ "$expected_result" = "$actual_result" ]; then
    error "br_add command failed!"
  fi

  actual_result="$(printf '%s\n' "$input_string" | br_add)"
  if ! [ "$expected_result" = "$actual_result" ]; then
    error "br_add pipe failed!"
  fi

  printf '%s\n' "$0: All Tests Passed"
}

strings_bsh_error_trap() {
  local last_exit_code="$?"
  echo "$0: ERROR: failed: '$BASH_COMMAND' | exit code: '$last_exit_code'"
  exit 1
}

## Useful in case this script is copied verbatim into another script such as
## in package 'usability-misc' file 'dist-installer-cli-standalone'.
## Another option would be to move the unit testing out of this script.
if declare -F should_run_unit_tests >/dev/null; then
  if should_run_unit_tests "${BASH_SOURCE[0]}"; then
    string_bsh_tests
  fi
fi

#trim_collapse_split " aaa  bbb ccc ddd "
#flavor_meta_packages_to_install='kicksecure-baremetal-gui-lxqt kicksecure-baremetal-server'
#trim_collapse_split "$flavor_meta_packages_to_install"
