#!/bin/bash

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

## This file is 'source'd by update-torbrowser. Variables like
## tbb_version_stripped, tbb_locally_installed_version_stripped and
## tbb_recommended_versions_error are set here and consumed by the
## sourcer. Conversely RecommendedTBBVersions, tbb_folder and
## tb_local_version_file are set by the sourcer before calling our
## functions. Both directions confuse shellcheck, which only sees one
## file at a time.
# shellcheck disable=SC2034,SC2154

## style-ok: no-strict
## This file is a library, 'source'd by update-torbrowser.

# shellcheck source=../../../../helper-scripts/usr/libexec/helper-scripts/strings.bsh
## The source= paths document where these live in the derivative-maker tree.
## CI checks this repo out ALONE and installs no helper-scripts, so shellcheck
## can follow neither the relative nor the runtime absolute path there.
##
## TODO: Let's just not run shellcheck unless we have a full derivative-maker
## source tree.
# shellcheck disable=SC1091
source /usr/libexec/helper-scripts/strings.bsh
# shellcheck source=../../../../helper-scripts/usr/libexec/helper-scripts/has.sh
##
## TODO: See above TODO.
# shellcheck disable=SC1091
source /usr/libexec/helper-scripts/has.sh

## Resolve version-parser.py next to this file rather than by its installed
## absolute path, so the test suite can exercise the code from a checkout.
tb_updater_libexec_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
version_parser="${tb_updater_libexec_dir}/version-parser.py"

set_stripped_tbb_version() {
   ## Join all positional arguments into a single raw string. Using "$*"
   ## (not "$@") so shellcheck can see this is a scalar assignment.
   tbb_version_raw="$*"
   tbb_version_stripped="$(sanitize-string -- 20 "${tbb_version_raw}")"
}

hexdump_file() {
   local hex_dumped
   printf '%s\n' "hexdump:"
   printf '%s\n' "----------"
   hex_dumped="$(timeout --kill-after=2 2 hexdump "$1")" || true
   stecho "${hex_dumped}"
   printf '%s\n' "----------"
}

tbbversion() {
   local tbb_version_remote_file tbb_version_output_file \
      tbb_parser_exit_code tbb_version_max_length tbb_version_length parsed_version

   ## 'has' can handle both command names (resolved via $PATH) and absolute
   ## paths.
   has sanitize-string unicode-show append grep-find-unicode-wrapper \
      "${version_parser}"

   ## fallback
   set_stripped_tbb_version "UNKNOWN"
   tbb_recommended_versions_error=""

   ## Example see /usr/share/tb-updater/unit-test/RecommendedTBBVersions
   if [ ! -f "${RecommendedTBBVersions}" ]; then
      local MSG="${SCRIPTNAME} tbbversion: RecommendedTBBVersions: '${RecommendedTBBVersions}' does not exist."
      log error "${MSG}"
      return 1
   fi

   ## AppArmor policy requires that both input and output files be under /tmp.
   tbb_version_remote_file="$(mktemp)"
   tbb_version_output_file="$(mktemp)"
   tbb_parser_exit_code='0'
   ## AUDIT: string limit?
   tbb_version_max_length='20'
   cp -- "${RecommendedTBBVersions}" "${tbb_version_remote_file}"

   ## Sanity test.
   test -f "${tbb_version_remote_file}"
   test -r "${tbb_version_remote_file}"

   ## Global variable, expected to be set by the caller
   ## stdout and stderr redirected to /dev/null in case the version-parser gets exploited to prevent it from echoing malicious advice.
   ## $RecommendedTBBVersions file should have a maximum size of 2 MB. This is enforced at update-torbrowser level.
   2>/dev/null >/dev/null "${version_parser}" --quiet "${tbb_version_remote_file}" "${tbb_version_output_file}" || tbb_parser_exit_code="$?"

   case "${tbb_parser_exit_code}" in
      0)
         true "INFO: version-parser exit code ok."

         ## Check file size to mitigate a potential DoS attack
         tbb_version_length="$(du -b -- "${tbb_version_output_file}" | cut -f1)"
         if [ "${tbb_version_length}" -gt "${tbb_version_max_length}" ]; then
            set_stripped_tbb_version 'UNKNOWN'
            tbb_recommended_versions_error="Rejected invalid RecommendedTBBVersions versions file. \
(Excessive version string length of '${tbb_version_length}' bytes.)"
            hexdump_file "${tbb_version_output_file}"
            safe-rm -- "${tbb_version_remote_file}"
            safe-rm -- "${tbb_version_output_file}"
            return 0
         fi

         ## Check for non-ASCII weirdness to mitigate potential attacks against
         ## bash or our scripts

         ## Check with python (unicode-show) first.
         ## Handle expected missing newline...
         ## /tmp/user/1000/tmp.pwSsfFTTML:1: [missing newline at end]
         ## ...by making sure there is a newline at the end.
         append "${tbb_version_output_file}" "" >/dev/null

         ## https://www.kicksecure.com/wiki/Unicode#unicode-show
         ## Use '>/dev/null' to avoid outputting potentially malicious message.
         if unicode-show "${tbb_version_output_file}" >/dev/null; then
            true "INFO: No unicode found using unicode-show, ok."
         else
            set_stripped_tbb_version 'UNKNOWN'
            tbb_recommended_versions_error="Rejected invalid RecommendedTBBVersions versions file. \
(Non-ASCII characters found in version using unicode-show.)"
            hexdump_file "${tbb_version_output_file}"
            safe-rm -- "${tbb_version_remote_file}"
            safe-rm -- "${tbb_version_output_file}"
            return 0
         fi

         ## Additional check using C (grep-find-unicode-wrapper / grep).
         if grep-find-unicode-wrapper -- "${tbb_version_output_file}"; then
            set_stripped_tbb_version 'UNKNOWN'
            tbb_recommended_versions_error="Rejected invalid RecommendedTBBVersions versions file. \
(Non-ASCII characters found in version using grep-find-unicode-wrapper.)"
            hexdump_file "${tbb_version_output_file}"
            safe-rm -- "${tbb_version_remote_file}"
            safe-rm -- "${tbb_version_output_file}"
            return 0
         fi

         ## At this point we trust the version string enough to load it - if
         ## it's not ridiculously long, and it's entirely 7-bit ASCII, the
         ## worst it could be is an invalid version number.
         parsed_version="$(stcat "${tbb_version_output_file}")"
         ## Thanks to:
         ## https://stackoverflow.com/questions/369758/how-to-trim-whitespace-from-bash-variable/3352015#3352015
         ## for the leading/trailing whitespace removal code
         ## Keep the limit local even though the file-size check above already
         ## caps at $tbb_version_max_length and update-torbrowser re-limits
         ## on the consumer side. Passing the explicit cap here makes the
         ## bound self-contained so a future refactor of either side cannot
         ## silently drop it.
         parsed_version="$(sanitize-string -- "${tbb_version_max_length}" "${parsed_version}")"
         parsed_version="${parsed_version#"${parsed_version%%[![:space:]]*}"}" ## remove leading whitespace characters
         parsed_version="${parsed_version%"${parsed_version##*[![:space:]]}"}" ## remove trailing whitespace characters

         if [ -z "${parsed_version}" ]; then
            set_stripped_tbb_version "UNKNOWN"
            tbb_recommended_versions_error="Rejected invalid RecommendedTBBVersions versions file. \
(version-parser output empty.)"
            hexdump_file "${tbb_version_output_file}"
            safe-rm -- "${tbb_version_remote_file}"
            safe-rm -- "${tbb_version_output_file}"
            return 0
         fi

         true "INFO: version-parser output not empty, ok."

         ## Use '&>/dev/null' to avoid output.
         if ! check_is_not_empty_and_only_one_line parsed_version &>/dev/null; then
            set_stripped_tbb_version "UNKNOWN"
            tbb_recommended_versions_error="Rejected invalid RecommendedTBBVersions versions file. \
(version-parser output failed check_is_not_empty_and_only_one_line test.)"
            hexdump_file "${tbb_version_output_file}"
            safe-rm -- "${tbb_version_remote_file}"
            safe-rm -- "${tbb_version_output_file}"
            return 0
         fi

         ## Regex-validate version to be absolutely sure it's valid.
         ## Should match versions like 14.0.2, 14.0a9, and 13.0.9-arm64, but
         ## reject malformed versions like 14.5:8 or 1.2..3.
         if ! [[ "${parsed_version}" =~ ^([0-9a-z]+\.)+[0-9a-z]+(-arm64|-armhf)?$ ]]; then
            set_stripped_tbb_version "UNKNOWN"
            tbb_recommended_versions_error="Rejected invalid RecommendedTBBVersions versions file. \
(version-parser output does not look like a valid version number.)"
            hexdump_file "${tbb_version_output_file}"
            safe-rm -- "${tbb_version_remote_file}"
            safe-rm -- "${tbb_version_output_file}"
            return 0
         fi

         set_stripped_tbb_version "${parsed_version}"

         safe-rm -- "${tbb_version_remote_file}"
         safe-rm -- "${tbb_version_output_file}"

         true "INFO: version-parser output is ok."

         ## If we get here, tbb_version is set to the sanitized version
         ## number, and trusted code can now use it freely.
         ;;
      1)
         set_stripped_tbb_version 'UNKNOWN'
         tbb_recommended_versions_error="Rejected invalid RecommendedTBBVersions versions file. \
(version-parser failed - missing arguments)"
         hexdump_file "${tbb_version_output_file}  "
         safe-rm -- "${tbb_version_remote_file}"
         safe-rm -- "${tbb_version_output_file}"
         return 0
         ;;
      2)
         set_stripped_tbb_version 'UNKNOWN'
         tbb_recommended_versions_error="Rejected invalid RecommendedTBBVersions versions file. \
(version-parser failed - file missing or inaccessible)"
         hexdump_file "${tbb_version_output_file}  "
         safe-rm -- "${tbb_version_remote_file}"
         safe-rm -- "${tbb_version_output_file}"
         return 0
         ;;
      3)
         set_stripped_tbb_version 'UNKNOWN'
         tbb_recommended_versions_error="Rejected invalid RecommendedTBBVersions versions file. \
(version-parser failed - file invalid or malicious)"
         hexdump_file "${tbb_version_output_file}  "
         safe-rm -- "${tbb_version_remote_file}"
         safe-rm -- "${tbb_version_output_file}"
         return 0
         ;;
      *)
         set_stripped_tbb_version 'UNKNOWN'
         tbb_recommended_versions_error="Rejected invalid RecommendedTBBVersions versions file. \
(version-parser failed - unknown error)"
         hexdump_file "${tbb_version_output_file}  "
         safe-rm -- "${tbb_version_remote_file}"
         safe-rm -- "${tbb_version_output_file}"
         return 0
         ;;
   esac
}

tbbversion_installed() {
   ## Getting currently installed version number

   has xargs jq

   ## Fallbacks.
   tbb_locally_installed_version="UNKNOWN. Please report this tb-updater Bug!"
   tbb_locally_installed_version_detect_success="false"

   tbbversion_checks_installed

   if [ "${tbb_locally_installed_version_detect_success}" = "true" ]; then
      ## Success. Real version number. Limit to 20 characters.
      tbb_locally_installed_version_stripped="$(sanitize-string -- 20 "${tbb_locally_installed_version}")"
      return 0
   fi

   ## Failure. Longer error message.
   tbb_locally_installed_version_stripped="$(sanitize-string -- 200 "${tbb_locally_installed_version}")"
}

tbbversion_checks_installed() {
   if [ ! -d "${tbb_folder}" ]; then
      tbb_locally_installed_version="None installed. (Folder <code>${tbb_folder}</code> does not exist.)"
      return 0
   fi

   if [ ! -f "${tb_local_version_file}" ]; then
      tbb_locally_installed_version="Version file not found. (File <code>${tb_local_version_file}</code> does not exist.) Please report this tb-updater Bug!"
      return 0
   fi

   local tb_local_version_contents parsed_version
   if ! tb_local_version_contents="$(stcat "${tb_local_version_file}")" ; then
      tbb_locally_installed_version="Reading version file <code>${tb_local_version_file}</code> failed. Please report this tb-updater Bug!"
      return 0
   fi

   if ! parsed_version="$(jq -er '.version? | select(type == "string" and length > 0)' <<< "${tb_local_version_contents}")" ; then
      tbb_locally_installed_version="Parsing version file (part 1) <code>${tb_local_version_file}</code> failed. Please report this tb-updater Bug!"
      return 0
   fi

   if ! parsed_version="$(stecho "${parsed_version}" | xargs -- printf '%s\n')" ; then
      tbb_locally_installed_version="Parsing version file (part 2) <code>${tb_local_version_file}</code> failed. Please report this tb-updater Bug!"
      return 0
   fi

   tbb_locally_installed_version="${parsed_version}"
   tbb_locally_installed_version_detect_success="true"
}
