#!/bin/bash
# Authors: Sabayon Team 2019

set -e

# Accepted values: x86_64, armv7l
export SAB_ARCH="${SAB_ARCH:-}"
export DRY_RUN=${DRY_RUN:-0}
export DEBUG="${DEBUG:-}"
export FORCE="${FORCE:-0}"
export USE_EQUO="${USE_EQUO:-1}"

declare -a MANUALLY_REMOVED_LINKS
declare -a BROKEN_LINKS2REMOVE

log() {
  local with_dashes=$1
  shift || true

  [ "$with_dashes" != 1 ] || \
    echo "==============================================================="
  echo "$@"
  [ "$with_dashes" != 1 ] || \
    echo "==============================================================="
}

summary() {

  local msg="SAB_ARCH = ${SAB_ARCH}
EXAMINED_LIBDIRS = ${EXAMINED_LIBDIRS}
DRY_RUN = ${DRY_RUN}
USE_EQUO = ${USE_EQUO}
FORCE = ${FORCE}"

  log 1 "$msg"
}

check_arch() {
  if [ -z "${SAB_ARCH}" ] ; then
    SAB_ARCH=$(uname -m)
  fi

  [[ "${SAB_ARCH}" != "x86_64" && "${SAB_ARCH}" != "armv7l" ]] && {
    log 1 "Unsupported arch ${SAB_ARCH}."
    exit 1
  }

  export SAB_ARCH
}

search_pkg() {
  local basename=$1
  # passed by reference
  local -n p="$2"
  local -n libdir="$3"
  local lib="$4"
  local l=""
  local ll=""

  # Try to search for package
  if [ "${USE_EQUO}" = "0" ] ; then
    p=$(equery -N -q -C belongs ${lib}/$basename --early-out || true)
  else
    p=$(equo q belongs ${lib}/${basename} -q || true)
  fi

  if [ -z "$p" ] ; then

    for l in "/usr/lib64" "/usr/lib32" "/usr/lib" ; do
      if [ "$lib" = "$l" ] ; then
        ll="${l/\/usr\///}"
        break
      fi
    done

    if [ -n "${ll}" ] ; then
      if [ "${USE_EQUO}" = "0" ] ; then
        p=$(equery -N -q -C belongs ${ll}/$basename --early-out || true)
      else
        p=$(equo q belongs ${ll}/${basename} -q || true)
      fi
    fi

  fi

  if [ -z "$p" ] ; then
    libdir=""
  fi
}

detect_libs_dir() {
  local libs=""

  if [ -n "$SCANDIR" ] ; then
    libs=$SCANDIR
  else
    if [ "${SAB_ARCH}" = "x86_64" ] ; then
      if [ -d "/usr/lib32" ] ; then
        libs="/usr/lib32 /lib32"
      fi
      if [ -d "/usr/lib64" ] ; then
        libs="$libs /usr/lib64 /lib64"
      fi
    else
      if [ -d "/usr/lib" ] ; then
        libs="/usr/lib /lib"
      fi
    fi
    if [ -z "$libs" ] ; then
      log 0 "Error on detect libraries directories."
      exit 1
    fi
  fi

  EXAMINED_LIBDIRS=$libs
  export EXAMINED_LIBDIRS
}

examine_libs() {
  local lib=""
  local file=""
  local broken_files=""

  log 1 "The exam could be long sorry for the wait."

  for lib in ${EXAMINED_LIBDIRS} ; do
    broken_files=""

    log 1 "Checking directory $lib..."
    broken_files=$(find $lib -type l -and -xtype l -and -name *.so* -or -name *.a -and -type l -and -xtype l 2>/dev/null || true )

    [ -z "$DEBUG" ] || {
      find $lib -type l -and -xtype l -and -name *.so* -or -name *.a -and -type l -and -xtype l || true
    }

    for file in ${broken_files} ; do
      examine_file "${file}" "${lib}"
    done
  done
}

examine_file() {
  local f=$1
  local res=""
  local libdir=$2
  local is_present_pkg=1
  local pkg=""

  [ -z "$DEBUG" ] || log 1 "Analyze file $f"

  local base=$(basename $1)

  local targetlink=$(readlink $f)
  local link2remove=false
  local link2fix=false
  local newlibdir=""
  local tmp=""
  local current_link=""

  log 1 "Broken link $f => $targetlink"

  # Check if file is without revision (es. libudev.so)
  res=$(perl -e "if ('$base' =~ /(.so|.a$)/) { print '1' }")
  if [ -n "$res" ] ; then
    # 
    #echo "BASE = $base"

    search_pkg "$base" pkg newlibdir "$lib"
    if [ -z "$pkg" ] ; then
      MANUALLY_REMOVED_LINKS+=( "$f" )
      return
    else

      if [ -n "$newlibdir" ] ; then
        log 0 "Link $base available under $newlibdir. I remove it."
        link2remove=true
      fi
    fi

    if [ "${USE_EQUO}" = "0" ] ; then
      res=$(qlist -IC $pkg 2>/dev/null | grep $f | wc -l)
    else
      res=$(equo q files  $pkg -q | grep $f | wc -l)
    fi
    if [ "$res" = 1 ] ; then
      link2fix=true
      echo "Link $f related to existing package. Try to reinstall $pkg."
    else
      MANUALLY_REMOVED_LINKS+=( "$f" )
    fi

    log 0 "Link $f of the package $pkg"

  else

    # Check if file has only one revision number (es. libudev.so.0)
    res=$(perl -e "if ('$base' =~ /(.so[.][\d]+$)/) { print '1' }")
    if [ -n "$res" ] ; then

      search_pkg "$base" pkg newlibdir "$lib"
      if [ -z "$pkg" ] ; then
        # Try to search package with .so
        base=$(echo "$base" | awk 'match($0, /.so/) { print substr($0, 0, RSTART+2) }')
        search_pkg "$base" pkg newlibdir "$lib"
        if [ -z "$pkg" ] ; then
          log 0 "No package found for link $f."
          MANUALLY_REMOVED_LINKS+=( "$f" )
          return
        fi

        if [ -n "$newlibdir" ] ; then
          log 0 "Link $base available under $newlibdir. I remove it."
          link2remove=true
        else
          # POST: we have a broken link or old link

          current_link=$(readlink $lib/$base)
          log 0 "Link $f related to package $pkg (with an updated version $current_link). I remove it."
          link2remove=true

        fi

      else

        log 0 "Link $f of the package $pkg"

        if [ -n "$newlibdir" ] ; then
          log 0 "Link $base available under $newlibdir. I remove it."
          link2remove=true
        else

          # Check new 
          if [ "${USE_EQUO}" = "0" ] ; then
            res=$(qlist -IC $pkg 2>/dev/null | grep $f | wc -l)
          else
            res=$(equo q files  $pkg -q | grep $f | wc -l)
          fi
          if [ "$res" = 1 ] ; then
            link2fix=true
            echo "Link $f related to existing package. Try to reinstall $pkg."
          else
            MANUALLY_REMOVED_LINKS+=( "$f" )
          fi

        fi

      fi

    else
      # Check if file has only one revision number (es. libudev.so.0.63)
      res=$(perl -e "if ('$base' =~ /(.so[.][\d]+[.][\d]+$)/) { print '1' }")
      if [ -n "$res" ] ; then

        search_pkg "$base" pkg newlibdir "$lib"
        if [ -z "$pkg" ] ; then

          base=$(echo "$base" | awk 'match($0, /.so[.][0-9]+/) { print substr($0, 0, RSTART+RLENGTH-1) }')
          search_pkg "$base" pkg newlibdir "$lib"
          if [ -z "$pkg" ] ; then
            log 0 "No package found for link $f."
            MANUALLY_REMOVED_LINKS+=( "$f" )
            return
          fi

          if [ -n "$newlibdir" ] ; then
            log 0 "Link $base available under $newlibdir. I remove it."
            link2remove=true
          else

            current_link=$(readlink $lib/$base)
            log 0 "Link $f related to package $pkg (with an updated version $current_link). I remove it."
            link2remove=true

          fi

        else

          log 0 "Link $f of the package $pkg"

          if [ -n "$newlibdir" ] ; then
            log 0 "Link $base available under $newlibdir. I remove it."
            link2remove=true
          else

            # Check new 
            if [ "${USE_EQUO}" = "0" ] ; then
              res=$(qlist -IC $pkg 2>/dev/null | grep $f | wc -l)
            else
              res=$(equo q files  $pkg -q | grep $f | wc -l)
            fi
            if [ "$res" = 1 ] ; then
              link2fix=true
              echo "Link $f related to existing package. Try to reinstall $pkg."
            else
              MANUALLY_REMOVED_LINKS+=( "$f" )
            fi

          fi

        fi

      else
        echo "Link $f not supported".
        MANUALLY_REMOVED_LINKS+=( "$f" )

      fi

    fi

  fi

  if [ $link2remove = true ] ; then
    BROKEN_LINKS2REMOVE+=( "$f" )
  fi

}

rm_broken_links() {
  for l in ${BROKEN_LINKS2REMOVE[@]} ; do
    if [ "$DRY_RUN" = 0 ]; then
      rm -vf ${l}
    else
      echo "rm -vf ${l}"
    fi
  done
}

print_links2remove() {
  for l in ${MANUALLY_REMOVED_LINKS[@]} ; do
    if [ "$FORCE" = 1 ] ; then
      if [ "$DRY_RUN" = 0 ] ; then
        rm -vf ${l}
      else
        echo "force: rm -vf ${l}"
      fi
    else
      echo "rm -vf ${l}"
    fi
  done
}

parse_args() {

  help_message() {

    echo "Sabayon Broken Links analyzer.
Sabayon Team (2019)

$0 [opts]

Available options:

-h|--help         This message.
--dry-run         Dry run execution.
--force           Force remove of the links to remove manually.
--scandir         Define a specific directory to analyze
--gentoo          Use gentoo tool instead of equo.
-d|--debug        Enable debug stuff.

Environment variables:
DRY_RUN           To use instead of --dry-run option. Values: 0 or 1.
DEBUG             Enable debug output.
USE_EQUO          To use instead of --gentoo option.
FORCE             To use instead of --force.

"

  }

  local short_opts="hd"
  local long_opts="help dry-run force debug scandir: gentoo"

  $(set -- $(getopt -u -q -a -o "$short_opts" -l "$long_opts" -- "$@"))

  while [ $# -gt 0 ] ; do
    case "$1" in

      -h|--help)
        help_message
        exit 1
        ;;
      -d|--debug)
        DEBUG=1
        ;;
      --force)
        FORCE=1
        ;;
      --dry-run)
        DRY_RUN=1
        ;;
      --scandir)
        SCANDIR="$2"
        shift
        ;;
      --gentoo)
        USE_EQUO=0
        ;;
      --)
        ;;
      *)
        echo "Invalid parameter $1."
        exit 1
        ;;

    esac

    shift

  done

  unset -f help_message
}

main() {

  parse_args "$@"

  log 1 "sabayon-brokenlinks: Examine system broken links"
  check_arch
  detect_libs_dir
  summary

  if [[ "$(id -u)" != "0" && "${DRY_RUN}" != "1" ]]; then
     echo "This script must be run as root" 1>&2
     exit 1
  fi

  parse_args
  unset -f parse_args

  if [ "${USE_EQUO}" = "0" ] ; then
    which equery 2>&1 >/dev/null || {
      echo "equery tool is needed. Please install app-portage/gentoolkit."
      exit 1
    }

    which qlist 2>&1 >/dev/null || {
      echo "qlist tool is needed. Please install app-portage/portage-utils."
      exit 1
    }
  fi

  examine_libs

  if [ ${#BROKEN_LINKS2REMOVE[@]} -gt 0 ] ; then
    log 1 "Remove Broken Links!"
    rm_broken_links
  fi

  if [ ${#MANUALLY_REMOVED_LINKS[@]} -gt 0 ] ; then
    if [ "${FORCE}" = 1 ] ; then
      log 1 "Hereinafter, links removed with force:"
    else
      log 1 "Hereinafter, links to manually remove:"
    fi
    print_links2remove
  fi

  echo "All Done."
}

main $@
