#!/bin/bash

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

## x

[ -o xtrace ]
## returns:
## - 0, if -x is set
## - 1, if -x is not set
MINUS_X_SET="$?"

set -x
set -e

true "INFO: Currently running script: $BASH_SOURCE $@"

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

cd "$MYDIR"
cd ..

dist_build_internal_run="true"

cd help-steps
source pre
source colors
source variables

script_help() {
   echo "\
Required options:
--report reportfile
--tempfolder folder

And either one of the following:
--ova ovafile
--vdi vdifile
--qcow qcowfile
--qcow2 qcow2file
--raw rawfile
--root /path/to/folder

Optional options:
--topcomment comment
--endcomment comment
--errorcomment comment
--nodeltempfolder

Usage examples:
From Whonix Source Code Folder...

sudo ./help-steps/analyze_image --report ~/report1 --tempfolder ~/derivative-binary/report_temp1 --ova ~/Whonix-Gateway-7.4.3.ova

sudo ./help-steps/analyze_image --report ~/report2 --tempfolder ~/derivative-binary/report_temp2 --ova ~/derivative-binary/Whonix-Gateway-7.4.3.ova

sudo ./help-steps/analyze_image --report ~/report3 --tempfolder ~/derivative-binary/report_temp3 --root /

sudo ./help-steps/analyze_image --report ~/report4 --tempfolder ~/derivative-binary/report_temp4 --raw ~/derivative-binary/Whonix-Gateway.raw

sudo ./help-steps/analyze_image --report ~/report5 --tempfolder ~/derivative-binary/report_temp5 --qcow2 ~/Whonix-Gateway.qcow2

From ~/derivative-binary folder...
vbindiff ./report_temp1/manual_analysis_folder/Whonix-Gateway-7.4.3.raw.dd.1000000 ./report_temp2/manual_analysis_folder/Whonix-Gateway-7.4.3.raw.dd.1000000

See https://www.whonix.org/wiki/Verifiable_Builds
and https://www.whonix.org/wiki/Trust"
}

parse_cmd() {
   ## Thanks to:
   ## http://mywiki.wooledge.org/BashFAQ/035

   if [ "$*" = "" ]; then
      error "no option chosen. Use --help."
   fi

   ## defaults
   ova="0"
   vmdk="0"
   vdi="0"
   qcow="0"
   qcow2="0"
   raw="0"

   while :
   do
       case $1 in
           -h | --help | -\?)
               script_help
               exit 0
               ;;
           -o | --ova)
               ova="1"
               vmdk="1"
               vdi="1"
               ova_file="$2"
               to_analyze_file="$2"
               shift 2
               ;;
           -vmdk | --vmdk)
               vmdk="1"
               vdi="1"
               vmdk_file="$2"
               to_analyze_file="$2"
               shift 2
               ;;
           -q | --vdi)
               vdi="1"
               vdi_file="$2"
               to_analyze_file="$2"
               shift 2
               ;;
           -q | --qcow)
               qcow="1"
               qcow_file="$2"
               to_analyze_file="$2"
               shift 2
               ;;
           -q2 | --qcow2)
               qcow2="1"
               qcow2_file="$2"
               to_analyze_file="$2"
               shift 2
               ;;
           -raw | --raw | -img | --img)
               raw="1"
               raw_file="$2"
               to_analyze_file="$2"
               shift 2
               ;;
           -bm | --bare-metal | --install-to-root | -itr | --root)
               dist_build_install_to_root="true"
               mount_folder="$2"
               if [ "$mount_folder" = "" ]; then
                  echo 'Mount folder must not be empty. Use for example --root "/".'
                  exit 1
               fi
               shift 2
               ;;
           -r | --report)
               report_file="$2"
               shift 2
               ;;
           -t | --tempfolder)
               tempfolder="$2"
               shift 2
               ;;
           -n | --nodeltempfolder)
               nodeltempfolder="1"
               shift
               ;;
           -c | --topcomment)
               topcomment="$2"
               shift 2
               ;;
           -e | --endcomment)
               endcomment="$2"
               shift 2
               ;;
           -f | --errorcomment)
              errorcomment="$2"
              shift 2
              ;;
           --minimal)
              minimal_report="true"
              shift
              ;;
           --)
               shift
               break
               ;;
           -*)
               error "unknown option: $1"
               ;;
           *)
               break
               ;;
       esac
   done

   if [ "$report_file" = "" ]; then
      echo "${red}${bold} ERROR: no report_file chosen! ${reset}"
      exit 1
   fi
   if [ "$tempfolder" = "" ]; then
      echo "${red}${bold} ERROR: no tempfolder chosen! ${reset}"
      exit 1
   fi
}

error_handler() {
   local exit_code="$?"
   local bash_command="$BASH_COMMAND"

   ## Re-enable xtrace, in case it was deactivated.
   set -x

   unmount_image

   true "
${red}${bold}bash_command${reset}: $bash_command
${red}${bold}exit_code${reset}: $exit_code
"

   if [ "$errorcomment" = "" ]; then
      errorcomment="ERROR: Unfinished report! Error detected!"
   fi

   errorcomment="\
################################################################################
$errorcomment
bash_command: $bash_command
exit_code: $exit_code"

   echo "$errorcomment" | tee -a "$report_file" >/dev/null

   exit 1
}

trap "error_handler" ERR INT TERM

unmount_image() {
   if [ "$dist_build_install_to_root" = "true" ]; then
      true "${bold}${cyan}INFO: Skipping $FUNCNAME, because dist_build_install_to_root is set to true. ${reset}"
      return 0
   fi

   true "${bold}${cyan}INFO: Unmounting raw image... ${reset}"

   ## dist_build_mount_raw_file us read by help-steps/mount-raw
   export dist_build_mount_raw_file="$raw_file_short_link"
   "$dist_source_help_steps_folder"/unmount-raw "$@"

   #sync
   #sleep 1 &
   #wait "$!"
   #sync

   #local command_v_exit_code="0"
   #command -v guestunmount >/dev/null || { command_v_exit_code="$?" ; true; };

   #if [ "$command_v_exit_code" = "0" ]; then
      #true "${bold}${cyan}INFO: guestunmount available, using it, ok... ${reset}"
      #guestunmount "$mount_folder"
   #else
      ## guestunmount is not available in Debian Wheezy. Only since Debian Jessie.
      #true "${bold}${cyan}INFO: guestunmount not available, using \"fusermount -u\" instead, ok... ${reset}"
      #fusermount -u "$mount_folder"
   #fi

   unlink "$raw_file_short_link" || true

   sync

   true "${bold}${cyan}INFO: Unmounted raw image. ${reset}"
}

preparation() {
   if [ "$minimal_report" = "true" ]; then
      true "${cyan}INFO: Only creating minimal report file because minimal_report is true.${reset}"
      rm --force "$report_file"
      echo "$topcomment" | tee -a "$report_file" >/dev/null
      echo "Only creating minimal report file because minimal_report is true." | tee -a "$report_file" >/dev/null
      echo "$endcomment" | tee -a "$report_file" >/dev/null
      exit 0
   fi

   true "${cyan}INFO: user_name is set to $user_name ${reset}"
   true "${cyan}INFO: Benchmarking '$SUDO_TO_ROOT time echo \"This is a test echo.\"' using \"time\"... ${reset}"
   $SUDO_TO_ROOT time echo "This is a test echo."

   ## {{ Sanity Tests.

   local tool
   local tools

   tools="
      readlink
      sha512sum
      dirname
      basename
      pwd
      sudo
      echo
      mkdir
      touch
      command
      rm
      cp
      chown
      dd
      sync
      find
      sort
      stat
      unlink
      ln
   "
   for tool in $tools; do
      command -v "$tool" >/dev/null
   done
   unset tool
   unset tools

   if [ "$dist_build_install_to_root" = "true" ]; then
      ## Tools for VM mouting not installed (not required) in this case.
      true
   else
      tools="
         guestmount
         guestfish
         virt-filesystems
      "
      for tool in $tools; do
         command -v "$tool" >/dev/null
      done
      unset tool
      unset tools
   fi

   ## }}

   rm --force "$report_file"
   sync

   ## The binary_build_folder_dist folder should already be created. Just make sure it
   ## really is to allow running this script in non-Whonix environments as
   ## well.
   mkdir --parents "$binary_build_folder_dist"

   touch "$report_file"
   sync

   if [ ! "$nodeltempfolder" = "1" ]; then
      true "${cyan} INFO: --nodeltempfolder was not used. Deleting $tempfolder... ${reset}"
      rm --recursive --force "$tempfolder"
   fi

   ## Using export, because mount_folder gets read by help-steps/mount-raw and
   ## help-steps/unmount-raw.
   if [ "$dist_build_install_to_root" = "true" ]; then
      ## mount_folder has been set in parse_cmd
      ## Remove trailing slash so for example "/" becomes "".
      mount_folder="${mount_folder%/}"
      export mount_folder
   else
      export mount_folder="$tempfolder/mount_folder"
   fi

   extracted_ova_folder="$tempfolder/extracted_ova_folder"
   vdi_folder="$tempfolder/vdi_folder"
   qcow_folder="$tempfolder/qcow_folder"
   qcow2_folder="$tempfolder/qcow2_folder"
   raw_folder="$tempfolder/raw_folder"
   auto_hash_folder="$tempfolder/auto_hash_folder"
   initrd_folder="$tempfolder/initrd_folder"
   extracted_initrd_folder="$tempfolder/extracted_initrd_folder"
   debug_folder="$tempfolder/debug_folder"
   manual_analysis_folder="$tempfolder/manual_analysis_folder"

   to_analyze_file_basename="$(basename "$to_analyze_file")"
   file_type_filename_without_extension="${to_analyze_file_basename%.*}"

   if [ "$vmdk_file" = "" ]; then
      vmdk_file="$extracted_ova_folder/$file_type_filename_without_extension-disk1.vmdk"
   fi
   if [ "$vdi_file" = "" ]; then
      vdi_file="$vdi_folder/$file_type_filename_without_extension.vdi"
   fi
   if [ "$qcow_file" = "" ]; then
      qcow_file="$qcow_folder/$file_type_filename_without_extension.qcow"
   fi
   if [ "$qcow2_file" = "" ]; then
      qcow2_file="$qcow2_folder/$file_type_filename_without_extension.qcow2"
   fi
   if [ "$raw_file" = "" ]; then
      raw_file="$raw_folder/$file_type_filename_without_extension.raw"
   fi
   raw_file_short_link="$binary_build_folder_dist/temp_short_link.raw"

   vmdk_file_basename="$(basename "$vmdk_file")"
   vdi_file_basename="$(basename "$vdi_file")"
   qcow_file_basename="$(basename "$qcow_file")"
   qcow2_file_basename="$(basename "$qcow2_file")"
   raw_file_basename="$(basename "$raw_file")"

   mkdir --parents "$extracted_ova_folder"
   mkdir --parents "$mount_folder" || true
   mkdir --parents "$vdi_folder"
   mkdir --parents "$qcow_folder"
   mkdir --parents "$qcow2_folder"
   mkdir --parents "$raw_folder"
   mkdir --parents "$auto_hash_folder"
   mkdir --parents "$initrd_folder"
   mkdir --parents "$extracted_initrd_folder"
   mkdir --parents "$debug_folder"
   mkdir --parents "$manual_analysis_folder"

   file_list_file="$auto_hash_folder/file_list"
   rm --force "$file_list_file"
   touch "$file_list_file"

   sync
}

parse_topcomment() {
   if [ "$topcomment" = "" ]; then
      topcomment="No topcomment."
   fi
   topcomment="\
$topcomment
################################################################################"
   echo "$topcomment" | tee -a "$report_file" >/dev/null
}

extract_ova() {
   if [ "$ova" = "0" ]; then
      true "${bold}${cyan}INFO: Skipping $FUNCNAME, because no ova chosen. ${reset}"
      return 0
   fi

   if [ "$dist_build_install_to_root" = "true" ]; then
      true "${bold}${cyan}INFO: Skipping $FUNCNAME, because dist_build_install_to_root is set to true. ${reset}"
      return 0
   fi

   cd "$extracted_ova_folder"

   if [ -f "$vdi_file" ]; then
      true "${bold}${cyan}INFO: Unpacking .ova not required, .vdi already exists, skipping. ${reset}"
   else
      if [ ! -f "$ova_file" ]; then
         error "${red}${bold}ERROR: $ova_file does not exist. ${reset}"
      else
         true "${bold}${cyan}INFO: Unpacking ova: $ova_file... (This can take a while.) ${reset}"
         tar -xvf "$ova_file"
         true "${bold}${cyan}INFO: Unpacked ova_file. ${reset}"
      fi
   fi
}

convert_vmdk_to_vdi() {
   if [ "$vmdk" = "0" ]; then
      true "${bold}${cyan}INFO: Skipping $FUNCNAME, because no vmdk chosen. ${reset}"
      return 0
   fi

   if [ "$dist_build_install_to_root" = "true" ]; then
      true "${bold}${cyan}INFO: Skipping $FUNCNAME, because dist_build_install_to_root is set to true. ${reset}"
      return 0
   fi

   if [ ! -f "$vmdk_file" ]; then
      error "${red}${bold}ERROR: vmdk_file: $vmdk_file does not exist. ${reset}"
   fi

   if [ -f "$vdi_file" ]; then
      true "${bold}${cyan}INFO: Converting vmdk to vdi not required, already done, skipping. ${reset}"
   else
      ## Convert .vmdk to .vdi, since there is no Free Software for mounting .vmdk using command line.
      true "${bold}${cyan}INFO: Converting vmdk to vdi... (This can take a while.) ${reset}"

      ## qemu-img version 1.6.1 fails with:
      ## qemu-img: 'image' uses a vmdk feature which is not supported by this qemu version: VMDK version 3
      ## https://bugs.launchpad.net/qemu/+bug/1253465
      #qemu-img convert "$vmdk_file" -O raw "$vdi_file"

      ## Debugging.
      qemu-img info "$vmdk_file"

      ## TODO: $SUDO_TO_VBOX_TEMP
      VBoxManage clonehd --format VDI "$vmdk_file" "$vdi_file"

      ## Debugging.
      qemu-img info "$vdi_file"

      true "${bold}${cyan}INFO: Converted vmdk to vdi. ${reset}"
   fi
}

convert_x_to_raw() {
   if [ "$dist_build_install_to_root" = "true" ]; then
      true "${bold}${cyan}INFO: Skipping $FUNCNAME, because dist_build_install_to_root is set to true. ${reset}"
      return 0
   fi

   if [ -f "$raw_file" ]; then
      true "${bold}${cyan}INFO: Converting x to raw not required, already done, skipping. ${reset}"
   else

      if [ "$vdi" = "1" ]; then
         image_type="vdi"
         image_file="$vdi_file"
      fi
      if [ "$qcow" = "1" ]; then
         image_type="qcow"
         image_file="$qcow_file"
      fi
      if [ "$qcow2" = "1" ]; then
         image_type="qcow2"
         image_file="$qcow2_file"
      fi

      true "${bold}${cyan}INFO: Converting $image_type to raw... (This can take a while.) ${reset}"

      ## Debugging.
      qemu-img info "$image_file"

      qemu-img convert -p -O raw "$image_file" "$raw_file"

      ## Debugging.
      qemu-img info "$raw_file"

      true "${bold}${cyan}INFO: Converted $image_type to raw. ${reset}"
   fi
}

mount_image() {
   if [ "$dist_build_install_to_root" = "true" ]; then
      mount_raw_exit_code="0"
      true "${bold}${cyan}INFO: Skipping $FUNCNAME, because dist_build_install_to_root is set to true. ${reset}"
      return 0
   fi

   true "${bold}${cyan}INFO: Mounting raw image... ${reset}"

   ## Workaround for a bug in kpartx, which fails to delete the loop device
   ## when using very long file names:
   ## https://www.redhat.com/archives/dm-devel/2014-July/msg00053.html
   unlink "$raw_file_short_link" || true
   ln -s "$raw_file" "$raw_file_short_link"

   ## dist_build_mount_raw_file us read by help-steps/mount-raw
   export dist_build_mount_raw_file="$raw_file_short_link"

   mount_raw_exit_code="0"
   "$dist_source_help_steps_folder"/mount-raw "$@" || { mount_raw_exit_code="$?" ; true; };

   #sync
   ## Mounting read-only so the user or script can not accidentally delete
   ## files within the image.
   #guestmount -o allow_other -a "$raw_file_short_link" -m /dev/sda1 --ro "$mount_folder"
   #sync

   if [ "$mount_raw_exit_code" = "0" ]; then
      true "${bold}${cyan}INFO: Mounted raw image. ${reset}"
   else
      msg="WARNING: Mounting of raw image failed. This is expected for Whonix-Custom-Workstation."
      true "${bold}${red}${msg}${reset}"
      echo "${msg}" | tee -a "$report_file" >/dev/null
   fi
}

parse_file_system() {
   if [ "$dist_build_install_to_root" = "true" ]; then
      true "${bold}${cyan}INFO: Skipping $FUNCNAME, because dist_build_install_to_root is set to true. ${reset}"
      return 0
   fi

   true "${bold}${cyan}INFO: Parsing file systems... ${reset}"

   if [ "$vdi" = "1" ]; then
      $SUDO_TO_ROOT virt-filesystems -a "$vdi_file" 2>&1 | tee -a "$auto_hash_folder/$vdi_file_basename.virt-filesystems-a" >/dev/null
   fi
   if [ "$qcow" = "1" ]; then
      $SUDO_TO_ROOT virt-filesystems -a "$qcow_file" 2>&1 | tee -a "$auto_hash_folder/$qcow_file_basename.virt-filesystems-a" >/dev/null
   fi
   if [ "$qcow2" = "1" ]; then
      $SUDO_TO_ROOT virt-filesystems -a "$qcow2_file" 2>&1 | tee -a "$auto_hash_folder/$qcow2_file_basename.virt-filesystems-a" >/dev/null
   fi
   $SUDO_TO_ROOT virt-filesystems -a "$raw_file" 2>&1 | tee -a "$auto_hash_folder/$raw_file_basename.virt-filesystems-a" >/dev/null

   true "${bold}${cyan}INFO: Parsed file systems. ${reset}"
}

parse_mbr() {
   if [ "$dist_build_install_to_root" = "true" ]; then
      true "${bold}${cyan}INFO: Skipping $FUNCNAME, because dist_build_install_to_root is set to true. ${reset}"
      return 0
   fi

   ## For information about disk signatures, see:
   ## https://en.wikipedia.org/wiki/Master_boot_record

   true "${bold}${cyan}INFO: Parsing MBR... ${reset}"

   ## Read ~1 MB at once.
   dd if="$raw_file" of="$auto_hash_folder/$raw_file_basename.dd.0-1000000" bs=1000000 count=1

   ## Read byte by byte from 0 to 440.
   ## There should be no differences here.
   dd if="$raw_file" of="$auto_hash_folder/$raw_file_basename.dd.0-440" bs=1 count=440

   ## Read byte by byte from 441 to 444.
   ## Disk signature may differ. (Fixed in Whonix 8.5.0.1 and above.)
   dd if="$raw_file" of="$auto_hash_folder/$raw_file_basename.dd.441-444" bs=1 skip=440 count=3

   ## Read byte by byte from 445 to 1000000.
   ## There should be no differences here.
   dd if="$raw_file" of="$auto_hash_folder/$raw_file_basename.dd.445-1000000" bs=1 skip=444 count=1000000

   true "${bold}${cyan}INFO: Parsed MBR. ${reset}"
}

parse_vbr() {
   if [ "$dist_build_install_to_root" = "true" ]; then
      true "${bold}${cyan}INFO: Skipping $FUNCNAME, because dist_build_install_to_root is set to true. ${reset}"
      return 0
   fi

   true "${bold}${cyan}INFO: Parsing VBR... ${reset}"

   if [ "$vdi" = "1" ]; then
      $SUDO_TO_ROOT guestfish --ro -a "$vdi_file" run : pread-device /dev/sda1 512 0 2>&1 | tee -a "$auto_hash_folder/$vdi_file_basename.sda1_pread-device.512" >/dev/null || true
   fi
   if [ "$qcow" = "1" ]; then
      $SUDO_TO_ROOT guestfish --ro -a "$qcow_file" run : pread-device /dev/sda1 512 0 2>&1 | tee -a "$auto_hash_folder/$qcow_file_basename.sda1_pread-device.512" >/dev/null || true
   fi
   if [ "$qcow2" = "1" ]; then
      $SUDO_TO_ROOT guestfish --ro -a "$qcow2_file" run : pread-device /dev/sda1 512 0 2>&1 | tee -a "$auto_hash_folder/$qcow2_file_basename.sda1_pread-device.512" >/dev/null || true
   fi
   $SUDO_TO_ROOT guestfish --ro -a "$raw_file" run : pread-device /dev/sda1 512 0 2>&1 | tee -a "$auto_hash_folder/$raw_file_basename.sda1_pread-device.512" >/dev/null || true

   true "${bold}${cyan}INFO: Parsed VBR. ${reset}"
}

cp_to_user() {
   $SUDO_TO_ROOT cp "$1" "$2"
   $SUDO_TO_ROOT chown --recursive "$user_name:$user_name" "$2"
}

parse_special_files() {
   true "${bold}${cyan}INFO: Parsing special files... ${reset}"

   ###########################
   ## manual_analysis_folder #
   ###########################

   cp_to_user "$mount_folder/etc/shadow" "$manual_analysis_folder/etc_shadow"
   cp_to_user "$mount_folder/etc/shadow-" "$manual_analysis_folder/etc_shadow-"

   ## "|| true", because CI (Ubuntu) and custom builders may not use sysvinit.
   cp_to_user "$mount_folder/etc/init.d/.depend.boot" "$manual_analysis_folder/etc_init.d_.depend.boot" || true
   cp_to_user "$mount_folder/etc/init.d/.depend.start" "$manual_analysis_folder/etc_init.d_.depend.start" || true
   cp_to_user "$mount_folder/etc/init.d/.depend.stop" "$manual_analysis_folder/etc_init.d_.depend.stop" || true

   #################
   ## debug_folder #
   #################

   cp_to_user "$mount_folder/etc/fstab" "$debug_folder/etc_fstab"

   ## Legacy, old paths to build version file.
   ## Allow running this script in non-Whonix environments as well.
   if [ -e "$mount_folder/usr/share/whonix/build_version" ]; then
      cp_to_user "$mount_folder/usr/share/whonix/build_version" "$debug_folder/usr_share_anon-dist_build_version"
   fi
   if [ -e "$mount_folder/var/lib/anon-dist/build_version" ]; then
      cp_to_user "$mount_folder/var/lib/anon-dist/build_version" "$debug_folder/usr_share_anon-dist_build_version"
   fi

   ## Allow running this script in non-Whonix environments as well.
   if [ -e "$mount_folder/var/lib/dist-base-files/build_version" ]; then
      cp_to_user "$mount_folder/var/lib/dist-base-files/build_version" "$debug_folder/usr_lib_dist-base-files_build_version"
      echo "$debug_folder/usr_lib_dist-base-files_build_version BEGIN..."
      cat "$debug_folder/usr_lib_dist-base-files_build_version"
      echo "END $debug_folder/usr_lib_dist-base-files_build_version"
   else
      msg="Strange, $mount_folder/var/lib/dist-base-files/build_version does not exist. Perhaps not running in Kicksecure or a derivative such as Whonix."
      echo "$msg" > "$debug_folder/usr_lib_dist-base-files_build_version"
   fi

   cp_to_user "$mount_folder/etc/debootstrap/config" "$debug_folder/etc_debootstrap_config" || true
   cp_to_user "$mount_folder/etc/debootstrap/stages/default_locales" "$debug_folder/etc_debootstrap_stages_default_locales" || true

   ## {{{ /usr/share/doc/whonix-*/changelog.Debian.gz

   mkdir --parents "$debug_folder/usr_share_doc_whonix-x"

   rm --force "$debug_folder/usr_share_doc_whonix-x/file_list"
   touch "$debug_folder/usr_share_doc_whonix-x/file_list"
   local i
   i="0"
   local file_name
   shopt -s nullglob dotglob

   for file_name in "$mount_folder/usr/share/doc/whonix-"*"/changelog.Debian.gz"; do
      i="$(( $i + 1 ))"
      echo "file_name number: $i | file_name: $file_name" | tee -a "$debug_folder/usr_share_doc_whonix-x/file_list" >/dev/null
      cp_to_user "$file_name" "$debug_folder/usr_share_doc_whonix-x/changelog.Debian.gz.$i"
   done

   unset file_name
   shopt -u nullglob dotglob

   ## }}}

   mkdir --parents "$debug_folder/var_lib_dpkg_info_whonix-x"

   ## {{{ /var/lib/dpkg/info/whonix-*.md5sums

   local file_name
   shopt -s nullglob dotglob

   for file_name in "$mount_folder/var/lib/dpkg/info/whonix-"*".md5sums"; do
      cp_to_user "$file_name" "$debug_folder/var_lib_dpkg_info_whonix-x/"
   done

   unset file_name
   shopt -u nullglob dotglob

   ## }}}

   ## {{{ /var/cache/debconf/*

   mkdir --parents "$debug_folder/var_cache_debconf"

   local file_name
   shopt -s nullglob dotglob

   for file_name in "$mount_folder/var/cache/debconf/"*; do
      cp_to_user "$file_name" "$debug_folder/var_cache_debconf/"
   done

   unset file_name
   shopt -u nullglob dotglob

   ## }}}

   ## {{{ /var/lib/anon-dist/grub-backup/*

   mkdir --parents "$debug_folder/var_lib_anon-dist_grub-backup"

   local file_name
   shopt -s nullglob dotglob

   for file_name in "$mount_folder/var/lib/anon-dist/grub-backup/"*; do
      cp_to_user "$file_name" "$debug_folder/var_lib_anon-dist_grub-backup/"
   done

   unset file_name
   shopt -u nullglob dotglob

   ## }}}

   ## "|| true", because these files are expected to be deleted.
   ## Copying them out of the image for easier analysis just in case.
   cp_to_user "$mount_folder/var/cache/apt/pkgcache.bin" "$debug_folder/var_cache_apt_pkgcache.bin" || true
   cp_to_user "$mount_folder/var/lib/dpkg/available" "$debug_folder/var_lib_dpkg_available" || true
   cp_to_user "$mount_folder/var/lib/dpkg/available-old" "$debug_folder/var_lib_dpkg_available-old" || true

   true "${bold}${cyan}INFO: Parsed special files. ${reset}"
}

parse_initrd() {
   true "${bold}${cyan}INFO: Parsing initrd... ${reset}"

   ## {{{ /boot/*

   ## Debugging.
   ls -la "$mount_folder/boot/" || true

   local file_name
   shopt -s nullglob dotglob

   for file_name in "$mount_folder/boot/initrd.img"*; do
      cp_to_user "$file_name" "$initrd_folder"
   done

   unset file_name
   shopt -u nullglob dotglob

   ## }}}

   ## {{{ $initrd_folder/*

   shopt -s nullglob dotglob
   local file_name

   ## TODO: requires root? $SUDO_TO_ROOT
   for file_name in "$initrd_folder/"*; do
      cd "$extracted_initrd_folder"
      true "file_name: $file_name"
      local basename_file
      basename_file="$(basename "$file_name")"
      mkdir --parents "$basename_file"
      cd "$basename_file"
      gzip -dc < "$file_name" | cpio -i
   done

   unset file_name
   shopt -u nullglob dotglob

   ## }}}

   true "${bold}${cyan}INFO: Parsed initrd. ${reset}"
}

parse_folder() {
   local i
   i="0"

   local sub_folder
   sub_folder="$(basename "$folder/")"

   local file_name

   true "${bold}${cyan}INFO: Parsing $sub_folder... ${reset}"
   true "${cyan}${under}This will take a while.${eunder} This is a security feature. To learn more, read:
https://www.whonix.org/wiki/Verifiable_Builds${reset}"

   if [ "$disable_xtrace" = "1" ]; then
      true "\
${cyan}INFO: \"set +x\", because output would be too verbose. Feel \
free to comment this out.${reset}"
      true "\
${cyan}INFO: To view the progress of this or to check if it hangs, \
you could run:${reset}
ps aux | grep sha512sum
${cyan}(several times in a row) in another terminal window or watch the log using:${reset}
tail -f $report_file
"
      set +x
   fi

   ## read: using "-d ''" for NUL-delimited output.
   while read -r -d '' file_name; do

      i="$(( $i + 1 ))"

      local absolute_file_name
      absolute_file_name="${file_name#"$folder"}"

      ## Too verbose.
      #echo "$absolute_file_name: $file_name"

      echo "$absolute_file_name" | tee -a "$file_list_file"

      local skip_hash="0"
      local dir_name
      if [ -d "$file_name" ]; then
         ## $file_name is a directory.
         dir_name="$file_name"
         skip_hash="directory"
      else
         ## $file_name is not a directory.
         dir_name="${file_name%/*}"
      fi

      local dir_name_absolute
      dir_name_absolute="${dir_name#"$folder"}"

      ## Check if $file_name exists. Somehow on Debian Wheezy it does not exit 0 for
      ## symlinks.
      if [ ! -e "$file_name" ]; then
         ## Check if $file_name is a symlink.
         if [ -h "$file_name" ]; then
            ## Symlinks are parsed below.
            true
         else
            msg="($sub_folder) $absolute_file_name | $file_name does_not_exist"
            echo "${cyan} $msg ${reset}"
            echo "$msg" | tee -a "$report_file" >/dev/null
            if [ "$dist_build_install_to_root" = "true" ]; then
               ## File might no longer exist on bare metal.
               ## For example "sudo stat /proc/13165" could fail.
               continue
            else
               ## Should not happen inside virtual machine images, because /proc
               ## is not mounted there.
               set -x
               error "$msg"
            fi
         fi
      fi

      ## Get exit code and output of "stat".
      local stat_exit_code
      stat_exit_code="0"
      local stat_output
      stat_output="$(stat \
         --format "\
Access_rights_in_octal: %a \
Number_of_blocks_allocated: %b \
The_size_in_bytes_of_each_block_reported_by_b: %B \
Raw_mode_in_hex: %f \
Group_ID_of_owner: %g \
Number_of_hard_links: %h \
IO_block_size: %o \
Total_size_in_bytes: %s \
Major_device_type_in_hex: %t \
Minor_device_type_in_hex: %T \
User_ID_of_owner: %u \
Type_in_hex: %t" "$file_name" 2>&1)" \
         || { stat_exit_code="${PIPESTATUS[0]}" ; true; };

      ## Initialize local variables.
      local \
         temp1="" Access_rights_in_octal="" \
         temp2="" Number_of_blocks_allocated="" \
         temp3="" The_size_in_bytes_of_each_block_reported_by_b="" \
         temp4="" Raw_mode_in_hex="" \
         temp5="" Group_ID_of_owner="" \
         temp6="" Number_of_hard_links="" \
         temp7="" IO_block_size="" \
         temp8="" Total_size_in_bytes="" \
         temp9="" Major_device_type_in_hex="" \
         temp10="" Minor_device_type_in_hex="" \
         temp11="" User_ID_of_owner="" \
         temp12="" Type_in_hex="" \

      ## Read local variables from stat_output.
      read -r \
         temp1 Access_rights_in_octal \
         temp2 Number_of_blocks_allocated \
         temp3 The_size_in_bytes_of_each_block_reported_by_b \
         temp4 Raw_mode_in_hex \
         temp5 Group_ID_of_owner \
         temp6 Number_of_hard_links \
         temp7 IO_block_size \
         temp8 Total_size_in_bytes \
         temp9 Major_device_type_in_hex \
         temp10 Minor_device_type_in_hex \
         temp11 User_ID_of_owner \
         temp12 Type_in_hex \
         _ \
         <<< "$stat_output"

      ## Debugging.
      #true "
         #$temp1 Access_rights_in_octal
         #$temp2 Number_of_blocks_allocated
         #$temp3 The_size_in_bytes_of_each_block_reported_by_b
         #$temp4 Raw_mode_in_hex
         #$temp5 Group_ID_of_owner
         #$temp6 Number_of_hard_links
         #$temp7 IO_block_size
         #$temp8 Total_size_in_bytes
         #$temp9 Major_device_type_in_hex
         #$temp10 Minor_device_type_in_hex
         #$temp11 User_ID_of_owner
         #$temp12 Type_in_hex
      #"

      ## Debugging.
      #true "
         #$temp1 $Access_rights_in_octal
         #$temp2 $Number_of_blocks_allocated
         #$temp3 $The_size_in_bytes_of_each_block_reported_by_b
         #$temp4 $Raw_mode_in_hex
         #$temp5 $Group_ID_of_owner
         #$temp6 $Number_of_hard_links
         #$temp7 $IO_block_size
         #$temp8 $Total_size_in_bytes
         #$temp9 $Major_device_type_in_hex
         #$temp10 $Minor_device_type_in_hex
         #$temp11 $User_ID_of_owner
         #$temp12 $Type_in_hex
      #"

      ## $Number_of_blocks_allocated and $Total_size_in_bytes are not
      ## deterministic for directories. Leaving them out.
      stat_output_log_directory="\
$temp1 $Access_rights_in_octal | \
$temp3 $The_size_in_bytes_of_each_block_reported_by_b | \
$temp4 $Raw_mode_in_hex | \
$temp5 $Group_ID_of_owner | \
$temp6 $Number_of_hard_links | \
$temp7 $IO_block_size | \
$temp9 $Major_device_type_in_hex | \
$temp10 $Minor_device_type_in_hex | \
$temp11 $User_ID_of_owner | \
$temp12 $Type_in_hex\
"

      ## $Number_of_blocks_allocated is non deterministic for some files such
      ## as /usr/lib/python2.7/dist-packages/twisted/web/test/test_http.pyc.
      ## Therefore leaving it out. Alternatively, we could remove these files
      ## using the cleanup chroot-post.d script and re-create them later using
      ## Whonix First Run Initializer.
      stat_output_log_file="\
$temp1 $Access_rights_in_octal | \
$temp3 $The_size_in_bytes_of_each_block_reported_by_b | \
$temp4 $Raw_mode_in_hex | \
$temp5 $Group_ID_of_owner | \
$temp6 $Number_of_hard_links | \
$temp7 $IO_block_size | \
$temp8 $Total_size_in_bytes | \
$temp9 $Major_device_type_in_hex | \
$temp10 $Minor_device_type_in_hex | \
$temp11 $User_ID_of_owner | \
$temp12 $Type_in_hex\
"

      ## Check if $file_name is a symlink.
      if [ -h "$file_name" ]; then
         ## Symlink found...

         ## Not using readlink with -f, so we can compare the relative links.
         local file_link
         local read_link_exit_code
         read_link_exit_code="0"
         file_link="$(readlink "$file_name" 2>&1)" || { read_link_exit_code="${PIPESTATUS[0]}" ; true; };

         echo "($sub_folder) $absolute_file_name | read_link_exit_code: $read_link_exit_code | stat_output_log_file: $stat_output_log_file | stat_exit_code: $stat_exit_code | symlinks_to $file_link" | tee -a "$report_file" >/dev/null

         continue
      fi

      if [ "$skip" = "1" ]; then
         local file_extension
         file_extension="${file_name##*.}"
         if \
            [ "$file_extension" = "vmdk"  ] || \
            [ "$file_extension" = "mf"    ] || \
            [ "$file_extension" = "ovf"   ] || \
            [ "$file_extension" = "qcow"  ] || \
            [ "$file_extension" = "qcow2" ] || \
            [ "$file_extension" = "vdi"   ] || \
            [ "$file_extension" = "raw"   ] || \
            [ "$file_extension" = "img"   ] \
            ; then
               ## Skipping creating a sha512sum of the vmdk, because that wastes
               ## a lot time and we know in advance, there there will be
               ## differences. (Because there are no deterministically built
               ## operating systems yet.) We mount and analyze that image later,
               ## which is the whole point of this script.
               ##
               ## Skipping creation of sha512sum for mf and ovf file because those
               ## contain checksums and disk uuids which we expect to differ.
               ## Auditors are advised to manually diff those files.
               ##
               ## Still useful to have the file name and "skipped" in the report
               ## file, because this is a reminder to diff the mf and the ofv file
               ## and because the ova could also contain more than the three
               ## expected files.
               skip_hash="skip_extension_on_demand"
         fi
      fi

      if [ "$Number_of_blocks_allocated" = "0" ]; then
         ## Check if Number_of_blocks_allocated and Total_size_in_bytes are 0,
         ## to avoid any kind of trickery.
         if [ "$Total_size_in_bytes" = "0" ]; then
            ## Do the probably most expensive (from performance view)
            ## conditions check ("/dev/"**) at the end.
            if [[ "$absolute_file_name" == "/dev/"** ]]; then
               ## File in /dev with size of 0. No need to build a checksum of
               ## these. Also it would be difficult, because for example:
               ## "sudo sha512sum /dev/console",
               ## "sudo sha512sum /dev/xconsole",
               ## "sudo sha512sum /dev/initctl" or
               ## "sudo sha512sum /dev/full" would run forever.
               echo "($sub_folder) $absolute_file_name | skipped_0 | stat_output_log_file: $stat_output_log_file | stat_exit_code: $stat_exit_code" | tee -a "$report_file" >/dev/null
               continue
            fi
         fi
      fi

      local maybe_timeout
      if [ "$dist_build_install_to_root" = "true" ]; then
         if [[ "$absolute_file_name" == "/dev/"** ]]; then
            ## For example "sudo sha512sum /dev/tty1" would run forever.
            maybe_timeout="timeout --kill-after=1 1"
         elif [[ "$absolute_file_name" == "/proc/"** ]]; then
            ## For example "sudo sha512sum /proc/kmsg" would run forever.
            maybe_timeout="timeout --kill-after=1 1"
         elif [[ "$absolute_file_name" == "/run/"** ]]; then
            ## For example "sha512sum /run/acpi_fakekey" would run forever.
            maybe_timeout="timeout --kill-after=1 1"
         elif [[ "$absolute_file_name" == "/tmp/"** ]]; then
            ## For example "sudo sha512sum /tmp/20140115122244.19545.trace" would run forever.
            maybe_timeout="timeout --kill-after=1 1"
         else
            maybe_timeout=""
         fi
      else
         maybe_timeout=""
      fi

      local checksum_output checksum_exit_code
      if [ ! "$skip_hash" = "0" ]; then
         checksum_exit_code="$skip_hash"
         checksum_output="$skip_hash"
      else
         checksum_exit_code="0"
         checksum_output="$($maybe_timeout sha512sum "$file_name" 2>&1)" || { checksum_exit_code="${PIPESTATUS[0]}" ; true; };
      fi
      if [ ! "$checksum_exit_code" = "0" ]; then
         ## Handling cases such as:
         ## - sha512sum /home/user/derivative-binary/$tempfolder/mount_folder/dev/ida/c2d10p14
         ##   sha512sum: /home/user/derivative-binary/$tempfolder/mount_folder/dev/ida/c2d10p14: Permission denied
         ## - [ ! "$skip_hash" = "0" ]
         if [ "$checksum_output" = "" ]; then
            checksum_output="sha512sum_echoed_nothing"
         fi
         if [ "$skip_hash" = "directory" ]; then
            echo "($sub_folder) $absolute_file_name | checksum_exit_code: $checksum_exit_code | stat_output_log_directory: $stat_output_log_directory | stat_exit_code: $stat_exit_code | checksum_output: $checksum_output" | tee -a "$report_file" >/dev/null
         else
            echo "($sub_folder) $absolute_file_name | checksum_exit_code: $checksum_exit_code | stat_output_log_file: $stat_output_log_file | stat_exit_code: $stat_exit_code | checksum_output: $checksum_output" | tee -a "$report_file" >/dev/null
         fi
         continue
      else
         local checksum
         read -r checksum _ <<< "$checksum_output"
         echo "($sub_folder) $absolute_file_name | checksum_exit_code: $checksum_exit_code | stat_output_log_file: $stat_output_log_file | stat_exit_code: $stat_exit_code | checksum: $checksum" | tee -a "$report_file" >/dev/null
         continue
      fi

   ## The output of "find" when run on different images is non-deterministic,
   ## therefore piping it through "sort".
   ## find: using "-print0" for NUL-delimited output.
   ## sort: using "-z" for NUL-delimited output.
   ## TODO: Requires root? $SUDO_TO_ROOT
   done < <(find "$folder/" -print0 | sort -z)

   set -x

   true "${bold}${cyan}INFO: Parsed $sub_folder. ${reset}"
}

parse_file_list_file() {
   sha512sum "$file_list_file" | tee -a "$report_file" >/dev/null
}

parse_endcomment() {
   if [ "$endcomment" = "" ]; then
      endcomment="INFO: No endcomment. | $dist_build_error_counter error(s) detected."
   fi

   endcomment="\
################################################################################
$endcomment"

   echo "$endcomment" | tee -a "$report_file" >/dev/null
}

end() {
   true "${bold}${cyan}INFO: End. | $dist_build_error_counter error(s) detected. ${reset}"
}

main() {
   parse_cmd "$@"

   trap "error_handler" ERR INT TERM

   preparation
   parse_topcomment

   extract_ova
   convert_vmdk_to_vdi
   convert_x_to_raw

   parse_file_system
   parse_vbr
   parse_mbr

   ## sets: mount_raw_exit_code
   mount_image "$@"

   if [ "$mount_raw_exit_code" = "0" ]; then
      parse_special_files
      parse_initrd
   fi

   ## skip, because we expect these files to be non-deterministic.
   ## Manual analysis required.
   skip="1"
   folder="$manual_analysis_folder"
   parse_folder
   skip="0"

   if [ "$mount_raw_exit_code" = "0" ]; then
      ## skip, because we expect the image to be non-deterministic.
      skip="1"
   fi
   folder="$extracted_ova_folder"
   parse_folder
   skip="0"

   if [ "$mount_raw_exit_code" = "0" ]; then
      ## skip, because we expect the image to be non-deterministic.
      skip="1"
   fi
   folder="$vdi_folder"
   parse_folder
   skip="0"

   if [ "$mount_raw_exit_code" = "0" ]; then
      ## skip, because we expect the image to be non-deterministic.
      skip="1"
   fi
   folder="$qcow_folder"
   parse_folder
   skip="0"

   if [ "$mount_raw_exit_code" = "0" ]; then
      ## skip, because we expect the image to be non-deterministic.
      skip="1"
   fi
   folder="$qcow2_folder"
   parse_folder
   skip="0"

   if [ "$mount_raw_exit_code" = "0" ]; then
      ## skip, because we expect the image to be non-deterministic.
      skip="1"
   fi
   folder="$raw_folder"
   parse_folder
   skip="0"

   skip="0"
   folder="$auto_hash_folder"
   parse_folder
   skip="0"

   skip="0"
   folder="$debug_folder"
   parse_folder
   skip="0"

   skip="0"
   folder="$initrd_folder"
   parse_folder
   skip="0"

   skip="0"
   folder="$extracted_initrd_folder"
   parse_folder
   skip="0"

   if [ "$mount_raw_exit_code" = "0" ]; then
      disable_xtrace="1"
      skip="0"
      folder="$mount_folder"
      parse_folder
      skip="0"
      disable_xtrace="0"
   fi

   if [ "$mount_raw_exit_code" = "0" ]; then
      unmount_image "$@"
   fi

   parse_file_list_file

   parse_endcomment
   end
}

main "$@"
