#!/bin/bash

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

## VirusForget is inspired by Christopher Laprise.
## tasket@protonmail.com
## https://github.com/tasket
## https://www.patreon.com/tasket/creators
## Because of his work on Qubes-VM-Hardening.
## https://github.com/tasket/Qubes-VM-hardening

#set -x
set -e

error_handler() {
   ## TODO
   exit 1
}

trap error_handler ERR

root_check() {
   if [ "$(id -u)" != "0" ]; then
      echo "ERROR: must be run as root! sudo $0"
      exit 1
   fi
}

parse_cmd_options() {
   ## Thanks to:
   ## https://mywiki.wooledge.org/BashFAQ/035

   while :
   do
       case $1 in
           --user)
               user_name="$2"
               if [ "$user_name" = "" ]; then
                  echo "ERROR: --user needs username as argument!" >&2
                  shift
                  exit 1
               else
                  shift 2
               fi
               ;;
           --simulate)
               test_mode="true"
               shift
               ;;
           --unittest)
               unit_test="true"
               shift
               ;;
           --commit)
               commit="true"
               shift
               ;;
           --clean)
               clean="true"
               shift
               ;;
           --check)
               check="true"
               shift
               ;;
           --)
               shift
               break
               ;;
           -*)
               echo "ERROR: unknown option: $1" >&2
               exit 1
               ;;
           *)
               break
               ;;
       esac
   done

   ## If there are input files (for example) that follow the options, they
   ## will remain in the "$@" positional parameters.

   if [ "$user_name" = "" ]; then
      echo "ERROR: must set --user username" >&2
      exit 1
   fi
}

variables() {
   chfiles+=" .bashrc "
   chfiles+=" .bash_profile "
   chfiles+=" .bash_login "
   chfiles+=" .bash_logout "
   chfiles+=" .profile "
   chfiles+=" .pam_environment "
   chfiles+=" .xprofile "
   chfiles+=" .xinitrc "
   chfiles+=" .xserverrc "
   chfiles+=" .Xsession "
   chfiles+=" .xsession "
   chfiles+=" .xsessionrc "
   chfiles+=" .virusforgetunitestone "
   chfiles+=" .virusforgetunitesttwo "

   chdirs+=" bin "
   chdirs+=" .local/bin "
   chdirs+=" .config/autostart "
   chdirs+=" .config/plasma-workspace/env "
   chdirs+=" .config/plasma-workspace/shutdown "
   chdirs+=" .config/autostart-scripts "
   chdirs+=" .config/systemd "

   ## TODO
   privdirs+=" /rw/config "
   privdirs+=" /rw/usrlocal "
   privdirs+=" /rw/bind-dirs "

   backup_folder="/home/virusforget/backup"
   dangerous_folder="/home/virusforget/dangerous"
}

init() {
   adduser --home /home/virusforget --quiet --system --group virusforget
   home_folder="/home/$user_name"
}

process_file_system_objects() {
   if [ "$store" = "true" ]; then
      if [ "$test_mode" = "true" ]; then
         echo Simulate: rm -r -f "$backup_folder"
      else
         rm -r -f "$backup_folder"
      fi
   fi

   if [ "$test_mode" = "true" ]; then
      true
   else
      mkdir -p "$backup_folder"
   fi

   process_files
   process_folders
}

process_files() {
   for file_name in $chfiles ; do
      full_path_original="$home_folder/$file_name"
      full_path_original_dirname="${full_path_original%/*}"
      full_path_backup="$backup_folder/$file_name"
      full_path_dangerous="$dangerous_folder/$file_name"
      full_path_dangerous_dirname="${full_path_dangerous%/*}"
      if [ "$store" = "true" ]; then
         if [ -e "$full_path_original" ]; then
            if [ "$test_mode" = "true" ]; then
               echo Simulate: cp --no-dereference --archive "$full_path_original" "$backup_folder/"
            else
               cp --no-dereference --archive "$full_path_original" "$backup_folder/"
            fi
         fi
      else
         check_file_walker
      fi
   done
}

process_folders() {
   for folder_name in $chdirs ; do
      full_folder_name="$home_folder/$folder_name"

      if [ -e "$full_folder_name" ]; then
         find "$full_folder_name" -print0 | \
            while IFS= read -r -d '' line; do
               true "line: $line"
               if [ "$full_folder_name" = "$line" ]; then
                  continue
               fi
               full_path_original="$line"
               full_path_original_dirname="${full_path_original%/*}"
               ## remove prepeneded $home_folder from $full_path_original
               temp_one="$home_folder/"
               temp="${full_path_original/#$temp_one/""}"
               full_path_backup="$backup_folder/$temp"
               full_path_backup_dirname="${full_path_backup%/*}"
               full_path_dangerous="$dangerous_folder/$temp"
               full_path_dangerous_dirname="${full_path_dangerous%/*}"

               if [ "$store" = "true" ]; then
                  if [ -d "$full_path_original" ]; then
                     true "ok"
                  else
                     ## Not needed since starting with new backup folder anyhow.
                     #if [ -e "$full_path_backup" ]; then
                     #   echo chattr -i "$full_path_backup"
                     #   echo rm "$full_path_backup"
                     #fi
                     if [ "$test_mode" = "true" ]; then
                        echo Simulate: cp --no-dereference --archive "$full_path_original" "$full_path_backup_dirname/"
                     else
                        mkdir -p "$full_path_backup_dirname"
                        cp --no-dereference --archive "$full_path_original" "$full_path_backup_dirname/"
                     fi
                  fi
               else
                  check_file_walker
               fi
            done
      fi
   done
}

check_file_walker() {
   if [ -e "$full_path_backup" ]; then
      if [ -e "$full_path_original" ]; then
         if [ -d "$full_path_original" ]; then
            ## REVIEW: ok to skip directory?
            true
            return 0
         fi
         if diff "$full_path_original" "$full_path_backup" &>/dev/null ; then
            true "OK"
         else
            echo "Difference detected! changed file: $full_path_original backup: $full_path_backup" >&2
            unexpected_file "$full_path_original"
         fi
      else
         echo "Missing file detected! missing: $full_path_original" >&2
         restore_file
      fi
   else
      if [ -e "$full_path_original" ]; then
         if [ -d "$full_path_original" ]; then
            ## REVIEW: ignore ok?
            true
            return 0
         fi
         echo "Extraneous file! $full_path_original" >&2
         unexpected_file "$full_path_original"
      else
         true "OK"
      fi
   fi
}

unexpected_file() {
   if [ -d "$full_path_original" ]; then
      ## REVIEW: ignore ok?
      true
      return 0
   fi

   mkdir -p "$full_path_dangerous_dirname"

   if [ "$test_mode" = "true" ]; then
      echo "Simulate backup of current version... $full_path_original" >&2
      echo cp "$full_path_original" --no-dereference --archive --backup=existing "$full_path_dangerous"
   elif [ "$clean" = "true" ]; then
      echo "Creating backup of current version... $full_path_original" >&2
      echo cp "$full_path_original" --no-dereference --archive --backup=existing "$full_path_dangerous"
      cp "$full_path_original" --no-dereference --archive --backup=existing "$full_path_dangerous"
      echo "Created backup." >&2
   fi

   if test -h "$full_path_original" ; then
      echo "is a symlink: $full_path_original" >&2
      if [ "$test_mode" = "true" ]; then
         echo "Simulate only. unexpected symlink. Removing... unlink '$full_path_original'" >&2
         echo unlink "$full_path_original"
      elif [ "$clean" = "true" ]; then
         echo "unexpected symlink. Removing... unlink '$full_path_original'" >&2
         unlink "$full_path_original"
         echo "Removed unexpected symlink." >&2
      fi
   else
      if [ "$test_mode" = "true" ]; then
         echo "Simulate deleting modified version '$full_path_original'." >&2
         echo rm "$full_path_original" >&2
      elif [ "$clean" = "true" ]; then
         ## chattr fails on symlinks such as symlink to /dev/random.
         chattr -i "$full_path_original"
         echo "Deleting modified version '$full_path_original'." >&2
         rm "$full_path_original" >&2
         echo "Deleted '$full_path_original'." >&2
      fi

      echo "View the diff:" >&2
      echo "diff $full_path_original $full_path_dangerous" >&2
   fi

   echo "" >&2

   restore_file
}

restore_file() {
   if [ "$test_mode" = "true" ]; then
      echo "Simulate restoring file... $full_path_original" >&2
      echo cp --no-dereference --archive "$full_path_backup" "$full_path_original"
      echo "" >&2
   elif [ "$clean" = "true" ]; then
      echo "Restoring file... $full_path_original" >&2
      echo mkdir --parents "$full_path_original_dirname" >&2
      mkdir --parents "$full_path_original_dirname"
      if [ ! "$home_folder" = "$full_path_original_dirname" ]; then
         chown --recursive "$user_name:$user_name" "$full_path_original_dirname"
      fi
      echo cp --no-dereference --archive "$full_path_backup" "$full_path_original"
      cp --no-dereference --archive "$full_path_backup" "$full_path_original" >&2
      echo "Restored." >&2
      echo "" >&2
   fi
}

unit_test_one() {
   if [ ! "$unit_test" = "true" ]; then
      return 0
   fi
   echo "x" >> /home/user/.virusforgetunitestone
   test -f /home/user/.virusforgetunitestone
}

unit_test_two() {
   if [ ! "$unit_test" = "true" ]; then
      return 0
   fi
   rm /home/user/.virusforgetunitestone
   echo "x" >> /home/user/.virusforgetunitesttwo
   test -f /home/user/.virusforgetunitesttwo
   echo "x" >> /home/user/.config/systemd/user/virusforgetunittest
   test -f /home/user/.config/systemd/user/virusforgetunittest
   if test -h /home/user/.config/systemd/user/virusforgetunittestsymlink ; then
      unlink /home/user/.config/systemd/user/virusforgetunittestsymlink
   fi
   ln -s /dev/random /home/user/.config/systemd/user/virusforgetunittestsymlink
}

root_check
parse_cmd_options "$@"
init
variables
unit_test_one

if [ "$commit" = "true" ]; then
   store=true
   process_file_system_objects
fi

unit_test_two

if [ "$check" = "true" ]; then
   store=false
   process_file_system_objects
fi
