#!/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

source /usr/libexec/helper-scripts/strings.bsh

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

   command -v printf >/dev/null
   command -v /usr/libexec/tb-updater/version-parser >/dev/null
   command -v sanitize-string >/dev/null
   command -v unicode-show >/dev/null
   command -v append >/dev/null
   command -v grep-find-unicode-wrapper >/dev/null

   ## 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."
      stecho "$MSG"
      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 /usr/libexec/tb-updater/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

   command -v xargs >/dev/null
   command -v printf >/dev/null
   command -v jq >/dev/null

   ## 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
   tb_local_version_contents="$(stcat "$tb_local_version_file")" || \
      { tbb_locally_installed_version="Reading version file <code>$tb_local_version_file</code> failed. Please report this tb-updater Bug!" ; return 0; };

   parsed_version="$(jq -r ".version" <<< "$tb_local_version_contents")" || \
      { tbb_locally_installed_version="Parsing version file (part 1) <code>$tb_local_version_file</code> failed. Please report this tb-updater Bug!" ; return 0; };

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

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