#!/bin/bash

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

#set -x
set -o errexit
set -o nounset
set -o pipefail
set -o errtrace
shopt -s inherit_errexit
shopt -s shift_verbose
export LC_ALL=C

true "$0: START."

MYDIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" && pwd )"

if [ "${MYDIR}" = "/usr/bin" ]; then
   true "INFO: Run from: /usr/bin"
   ## XXX: hardcoded path
   derivative_maker_source_code_dir="${HOME}/derivative-maker"
else
   true "INFO: Run from: source code folder"
   derivative_maker_source_code_dir="$(cd -- "${MYDIR}" && cd -- "../../../../../" && pwd)"
fi

## helper-scripts location: prefer HELPER_SCRIPTS_PATH (exported by the
## derivative-maker build's help-steps during bootstrap, before
## helper-scripts is installed); fall back to the in-tree copy. Export it so
## run-via-helper-scripts (below) inherits this same tree instead of
## re-deriving its own.
[ -v HELPER_SCRIPTS_PATH ] || HELPER_SCRIPTS_PATH="${derivative_maker_source_code_dir}/packages/kicksecure/helper-scripts"
export HELPER_SCRIPTS_PATH
grep_find_unicode_wrapper="${HELPER_SCRIPTS_PATH}/usr/bin/grep-find-unicode-wrapper"
## run-via-helper-scripts sets HELPER_SCRIPTS_PATH + PYTHONPATH so the in-tree
## Python helpers (here the security-hardened 'stcat') run during the
## build bootstrap, before helper-scripts is installed.
run_via_helper_scripts="${HELPER_SCRIPTS_PATH}/usr/bin/run-via-helper-scripts"

## Sanity tests.
test -d "${derivative_maker_source_code_dir}"
test -x "${grep_find_unicode_wrapper}"
test -x "${run_via_helper_scripts}"

## Need to 'cd' for relative paths.
cd -- "${derivative_maker_source_code_dir}"

declare -A symlink_whitelist

symlink_whitelist["./qubes/qubes-template-whonix/whonix-workstation"]="whonix-gateway"

## Detect symlinks from Git metadata. This works even when 'core.symlinks' is
## set to 'false', like Kicksecure sets by default.
declare -A git_symlink_paths
git_ls_files_stage="$(git ls-files --recurse-submodules --stage)"
while IFS=$'\t' read -r git_index_meta git_symlink_path; do
   [ "${git_index_meta%% *}" = "120000" ] || continue
   git_symlink_paths["${git_symlink_path}"]=1
done <<< "${git_ls_files_stage}"

read_link_target() {
   local path
   path="$1"
   if [ -L "${path}" ]; then
      readlink -- "${path}"
   else
      ## When Git's 'core.symlinks' setting is set to 'false', symlinks are
      ## text files containing the path. `read_link_target` will only ever be
      ## called with a path that is guaranteed to be a symlink from Git's
      ## perspective, so we can dump the contents of those files to get the
      ## symlink target.
      "${run_via_helper_scripts}" stcat "${path}"
   fi
}

check_symlink() {
   local symlink
   symlink="$1"
   local target
   target="$(read_link_target "${symlink}")"

   if [ -n "${symlink_whitelist[${symlink}]+_}" ]; then
      if [ "${symlink_whitelist[${symlink}]}" == "${target}" ]; then
         printf '%s\n' "$0: INFO: Whitelisted symlink: '${symlink}' -> '${target}'" >/dev/null
         return 0
      fi
      printf '%s\n' "$0: ERROR: Whitelisted symlink: '${symlink}' but wrong target '${target}'" >&2
      return 1
   fi

   printf '%s\n' "$0: ERROR: NOT whitelisted symlink: '${symlink}' -> '${target}'" >&2
   return 1
}

find_symlinks() {
   local symlink found_symlink
   local -A seen_symlink
   local -a all_symlinks=()
   found_symlink=""

   ## Find all symlinks Git knows about.
   for symlink in "${!git_symlink_paths[@]}"; do
      ## Normalize symlink names; the untracked symlink routine below will
      ## print names that start with "./".
      symlink="./${symlink}"
      [ -v "seen_symlink[${symlink}]" ] && continue
      seen_symlink["${symlink}"]=1
      all_symlinks+=("${symlink}")
   done

   ## Find untracked symlinks. Ignore .git in all subdirs.
   while IFS= read -r -d '' symlink; do
      [ -v "seen_symlink[${symlink}]" ] && continue
      seen_symlink["${symlink}"]=1
      all_symlinks+=("${symlink}")
   done < <(find "." -path '*/.git' -prune -o -type l -print0)

   for symlink in "${all_symlinks[@]}"; do
      if check_symlink "${symlink}" ; then
         continue
      fi
      found_symlink=true
      printf '%s\n' "----------" >&2
   done

   if [ "${found_symlink}" = "true" ]; then
      exit 1
   fi
}

find_symlinks

## overwrite with '|| true' because `grep` exits non-zero if no match was found.
##
## Using because a real name contains a special character.
## XXX: This is clearly a non-ideal solution but fixing this is an issue for
##      whole Free and Open Source community. See also:
##      https://www.kicksecure.com/wiki/Unicode
##      https://forums.whonix.org/t/detecting-malicious-unicode-in-source-code-and-pull-requests/13754
## --exclude=LICENSE
## --exclude=lkrg-openrc.sh
grep_find_unicode_wrapper_output="$(\
   "${grep_find_unicode_wrapper}" \
      --recursive \
      --binary-files=without-match \
      --exclude=control.authcookie \
      --exclude=LICENSE \
      --exclude=lkrg-openrc.sh \
      --exclude=changelog \
      --exclude=changelog.upstream \
      --exclude-dir=.git \
      -- \
      "./" \
   )" \
   || true

whitelist_list=(
   './packages/kicksecure/kicksecure-base-files/debian/copyright'
   './packages/kicksecure/helper-scripts/usr/lib/python3/dist-packages/stdisplay/tests/stdisplay.py'
   './windows/Whonix-Starter/COPYING.txt'
)
whitelist_pattern="($(IFS=$'\n'; printf '%s' "${whitelist_list[*]}" | sed -z 's/\n/|/g'))";

filtered_output=$(printf '%s\n' "${grep_find_unicode_wrapper_output}" | grep --invert-match --extended-regexp -- "${whitelist_pattern}" || true)

if [ -z "${filtered_output}" ]; then
   true "INFO: No Unicode issues found, everything is OK."
else
   printf '%s\n' "${filtered_output}" >&2

   printf '%s\n' "\
$0: ERROR: Unicode found!

See also:
- https://www.kicksecure.com/wiki/Unicode
- https://forums.whonix.org/t/detecting-malicious-unicode-in-source-code-and-pull-requests/13754
"
   exit 1
fi

## Check for trailing spaces.
if grep \
      --line-number \
      --recursive \
      --binary-files=without-match \
      --exclude=control.authcookie \
      --exclude=LICENSE \
      --exclude=lkrg-openrc.sh \
      --exclude=changelog \
      --exclude=changelog.upstream \
      --exclude-dir=.git \
      --exclude-dir=live-build \
      -- \
      '[[:blank:]]$' ; then
   true "\
$0: ERROR: Trailing whitespaces found!"
   exit 1
else
   true "INFO: No trailing whitespace issues found, everything is OK."
fi

## Check for missing newline at the end of the file (EOF).
##
## Thanks to:
## Julien Palard
## https://stackoverflow.com/a/25686825/2605155
##
## grep -Pzlvr '\x0a$'
## --perl-regexp --null --files-with-matches --invert-match
##
## '--null-data' unfortunately breaks '--binary-files'. Hence,
## binary file types need to be manually excluded.
grep_missing_newline_result_raw="$( \
   grep \
      --recursive \
      --binary-files=without-match \
      --exclude='*.png' --exclude='*.jpg' --exclude='*.jpeg' \
      --exclude='*.ico' --exclude='*.svg' --exclude='*.ai' \
      --exclude='*.gpg' --exclude='*.kbx' --exclude='*.pf2' \
      --exclude='*.iso' \
      --perl-regexp --null-data --files-with-matches --invert-match \
      --exclude="control.authcookie" \
      --exclude="RecommendedTBBVersions" \
      --exclude="qubes/qubes-template-whonix/whonix-workstation" \
      --exclude-dir="live-build" \
      --exclude-dir=".git" \
      -- \
      '\x0a$'
   )" \
   || true

true "grep_missing_newline_result_raw:
${grep_missing_newline_result_raw}"

grep_missing_newline_result_filtered=()

## grep --files-with-matches prints one filename per line; iterate
## line by line via while-read so paths containing spaces are not
## split into multiple fake "files" (the prior `for ... in $var`
## form word-split on every whitespace character).
while IFS= read -r file_name ; do
   [ -z "${file_name}" ] && continue

   ## git style symlinks actually not allowed.
#    if [ "$file_name" = "packages/kicksecure/helper-scripts/usr/bin/append-once" ]; then
#       size="$(stat -c '%s' -- "$file_name")"
#       if [ "$size" = "6" ]; then
#          continue
#       fi
#    fi
#    if [ "$file_name" = "packages/kicksecure/helper-scripts/usr/bin/overwrite" ]; then
#       size="$(stat -c '%s' -- "$file_name")"
#       ## Coincidentally also 6 bytes size.
#       if [ "$size" = "6" ]; then
#          continue
#       fi
#    fi

   ## Skip git-tracked symlinks. 'git ls-files' paths have no './' prefix,
   ## matching grep's output here.
   if [ -n "${git_symlink_paths[${file_name}]+_}" ]; then
      continue
   fi

   grep_missing_newline_result_filtered+=("${file_name}")
done <<< "${grep_missing_newline_result_raw}"

if [ "${#grep_missing_newline_result_filtered[@]}" -eq "0" ]; then
   true "INFO: No missing newlines at the end of file issues found, everything is OK."
else
   printf '%s\n' "$0: ERROR: Missing newlines at the end of file found! grep_missing_newline_result_filtered:
${grep_missing_newline_result_filtered[*]}"
   exit 1
fi

## Code duplication. Copied from helper-scripts/usr/bin/modeline-show and adapted.
modeline_regex='\(\(vi\|[Vv]im\([<=>]\?[0-9]\+\)\?\|[[:space:]]ex\):\|\([[:space:]]\|^\)-\*-\|Local'
modeline_regex+=' Variables:\)'
if grep \
      --line-number \
      --recursive \
      --binary-files=without-match \
      --exclude=dm-check-unicode \
      --exclude-dir=.git \
      --exclude-dir=grml-debootstrap \
      -- \
      "${modeline_regex}"; then
  true "\
$0: ERROR: Vim or Emacs modelines found!"
  exit 1
else
  true "INFO: No Vim or Emacs modelines found, everything is OK."
fi

true "$0: END."
