#!/bin/sh
# This script attempts to automatically install 32-bit compatibility libraries
# on 64-bit Linux systems. This addresses the issue where the 32-bit
# compatibility packages for some distributions (e.g. ia32-libs) don't provide
# all the 32-bit libraries needed for some applications (e.g. 64-bit Ubuntu
# Feisty doesn't provide a 32-bit libssl package).
#
# Copyright 2007 Google Inc. All Rights Reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
#   notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
#   copyright notice, this list of conditions and the following disclaimer
#   in the documentation and/or other materials provided with the
#   distribution.
# * Neither the name of Google Inc. nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# Include common functions.
export SCRIPTDIR=$(readlink -f $(dirname "$0"))
. "$SCRIPTDIR/common.sh"

CFGFILE="$HOME/.repackage32"
FAILURES=0

usage() {
  SEP="-------------------------------------------------------"
  prompt_info "
$SEP
This program attempts to install 32-bit compatibility libraries on
64-bit Linux systems. For instance, to install the 32-bit OpenSSL
libraries, you would run:

  $(basename $0) openssl

This would allow your 64-bit system to run 32-bit programs which
require OpenSSL. See below for a complete list of program options
and supported compatibility packages.
$SEP
Usage: $(basename $0) [-t|-n|-f] [-p message] [-c] [-v] <package ...>
  -t  Test if <package> is already installed.
  -n  Don't actually install, just show what would happen.
  -f  Force <package> install, even if already installed.
  -p  Prompt to display before installing <package>.
  -c  Create the <package>, but don't install.
  -v  Be verbose, even when no actions taken or errors encountered.
  -h  Show this help message.
$SEP
Packages:
  openssl   OpenSSL libraries
$SEP"
}

trap cleanup HUP INT QUIT ABRT TERM EXIT
cleanup() {
  set +ex
  close_progress
}

if [ "`uname -m`" != "x86_64" ] ; then
  prompt_err "$(basename $0) is intended only for x86_64 systems."
  exit 1
fi

fetch_deb() {
  DISTROPKG=$1
  SAVEFILE="$2"

  DPKGDIR="$PWD/dpkg.dummy"
  mkdir -p "$DPKGDIR/partial"

  # Figure out where to fetch the package from.
  # Create a modified dpkg status file to make it look like libssl is not
  # installed yet, otherwise --print-uris won't output anything.
  DPKGDUMMYSTAT="$DPKGDIR/status"
  DPKGSTATUS=$(apt-config dump | sed -e '/Dir::State::status/!d' \
    -e "s/Dir::State::status \"\(.*\)\".*/\1/")
  sed -e "s/Package: $DISTROPKG/Package: $DISTROPKG.orig/" "$DPKGSTATUS" \
    > "$DPKGDUMMYSTAT"
  DEBURL=$(apt-get -o Dir::Cache::Archives="$DPKGDIR" \
    -o Dir::State::status="$DPKGDUMMYSTAT" --print-uris install $DISTROPKG | \
    grep $DISTROPKG | tail -n 1 | sed -e "s/'\(.*\.deb\)'.*/\1/" -e "s/_amd64/_i386/")

  rm -rf "$DPKGDIR"

  # Fetch and unpack the deb to reformat it as a 64-bit compatibility package.
  do_fetch "$DEBURL" "$SAVEFILE"
  if [ $? -ne 0 ]; then
    return 1
  fi
}

guess_latest_openssl_deb() {
  apt-cache search --names-only "libssl[^-]+$" | \
    sed -e '/libssl[[:digit:]\.]\+.*/!d' \
        -e "s/libssl\([[:digit:]\.]\+\).*/\1/" | \
    sort | tail -n 1
}

guess_fedora_openssl() {
  # HACK!
  # 'ldconfig -p' on Fedora doesn't report libssl/crypto based on the OpenSSL
  # version, but rather uses a Fedora-specific(?) version number. This
  # determines that version number by finding the package's libssl.so file
  # without any '.' in the version portion of the name.
  # (i.e. libssl.so.6 instead of libssl.so.0.9.8b).
  rpm -ql openssl |
    sed -e '/libssl\.so\.[^.]*$/!d;s#.*/libssl\.so\.\(.*\)#\1#' | head -n 1
}

guess_latest_openssl() {
  # Try to determine the latest libssl version available for this distro.
  case $DISTRO in
    Debian )
      guess_latest_openssl_deb
      ;;
    Ubuntu )
      guess_latest_openssl_deb
      ;;
    Fedora )
      guess_fedora_openssl
      ;;
    * )
      echo "$BASEVER"
      ;;
  esac
}

init_openssl() {
  BASEVER=0.9.8
  if [ ! "$CURRVER" ]; then
    CURRVER=$(guess_latest_openssl)
  fi
}

test_openssl() {
  init_openssl
  # TODO - Do we want to test for other versions? Whatever we test for should
  # match what we attempt to install with fetch_deb.
  SSLCHECK=$(/sbin/ldconfig -p | grep -v "," | \
    grep "libssl.so.\($BASEVER\|$CURRVER\)")
  SSLRES=$?
  CRYPTOCHECK=$(/sbin/ldconfig -p | grep -v "," | \
    grep "libcrypto.so.\($BASEVER\|$CURRVER\)")
  CRYPTORES=$?
  if [ $SSLRES -eq 0 ] && [ $CRYPTORES -eq 0 ]; then
    if [ $VERBOSE ]; then
      prompt_info "The openssl libs are already installed.
$SSLCHECK
$CRYPTOCHECK"
    fi
    return 0
  else
    return 1
  fi
}

repackage_openssl_deb() {
  SAVEFILE="$1"

  if [ $DRYRUN ]; then
    return 0
  fi

  # Sanity check to make sure the deb conatins the files we ultimately want, so
  # it will pass the test_openssl tests.
  dpkg -c "$SAVEFILE" | grep -qs "libssl.so.\($BASEVER\|$CURRVER\)"
  SSLRES=$?
  dpkg -c "$SAVEFILE" | grep -qs "libcrypto.so.\($BASEVER\|$CURRVER\)"
  CRYPTORES=$?
  if [ $SSLRES -ne 0 ] || [ $CRYPTORES -ne 0 ]; then
    prompt_err "Don't know how to repackage $SAVEFILE."
    return 1
  fi

  ar xo "$SAVEFILE"
  tar -zxf data.tar.gz
  rm data.tar.gz
  tar -zxf control.tar.gz
  rm control.tar.gz

  # Put the libs in the 32-bit hierarchy
  mv usr/lib usr/lib32
  sed -i -e "s/usr\/lib\//usr\/lib32\//" md5sums

  # Remove unused files
  rm -rf usr/share
  sed -i -e "/usr\/share\//d" md5sums
  rm shlibs templates

  # Rewrite package files
  sed -i -e "s/\(Architecture:\).*/\1 amd64/" control
  sed -i -e "s/\(Package:\).*/\1 lib32ssl0.9.8/" control
  # Ignore dependencies and conflicts since the compatibility packages don't
  # necessarily conincide with the regular i386 packages.
  # TODO Maybe we can rewrite these rather than just ignoring them.
  sed -i -e "/^Depends:/d" control
  sed -i -e "/^Conflicts:/d" control

  # Rebuild the deb
  tar -czf data.tar.gz ./usr
  tar -czf control.tar.gz --exclude data.tar.gz --exclude "$SAVEFILE" \
    --exclude control.tar.gz --exclude usr .
  ar r "$SAVEFILE" control.tar.gz data.tar.gz
}

install_openssl_deb() {
  prompt_progress "Installing openssl libs."
  DISTROPKG=libssl0.9.8
  COMPATPKG=lib32ssl0.9.8
  TOPDIR="$PWD"
  WORKDIR=$(mktemp -d)
  if [ $? -gt 0 ]; then
    prompt_err "Couldn't create working directory."
    return 1
  fi
  cd "$WORKDIR"

  SAVEFILE="$WORKDIR/$COMPATPKG.deb"
  fetch_deb "$DISTROPKG" "$SAVEFILE"
  if [ $? -ne 0 ]; then
    return 1
  fi

  prompt_progress "Creating $COMPATPKG package."
  mkdir -p "$WORKDIR/$DISTROPKG"
  cd "$WORKDIR/$DISTROPKG"
  repackage_openssl_deb "$SAVEFILE"
  if [ $? -ne 0 ]; then
    return 1
  fi
  cd "$TOPDIR"
  rm -rf "$WORKDIR/$DISTROPKG"

  if [ $CREATEONLY ]; then
    prompt_info "openssl package saved as:
'$SAVEFILE'"
  else
    prompt_progress "Installing $COMPATPKG package.
Enter your administrative password if prompted."
    if [ ! $DRYRUN ]; then
      run_as_root "dpkg -i $SAVEFILE"
      if [ $? -ne 0 ]; then
        return 1
      fi
    fi
    # cleanup
    rm -rf "$WORKDIR"
  fi
}

install_openssl() {
  init_openssl
  case $DISTRO in
    Debian )
      install_openssl_deb
      ;;
    Ubuntu )
      install_openssl_deb
      ;;
    * )
      prompt_err "Don't know how to install openssl on $DISTRO."
      return 1
      ;;
  esac
  return $?
}

test_packages() {
  # When in Test-only mode, we return from here with these codes:
  # 0 - package already installed
  # 1 - package not found, but can be installed
  # 2 - package not found, and don't know how to install.
  # Note: Test-only mode only makes sense for one package at a time.
  # When not in test-only mode, returns the number of packages not found.
  NOTFOUND=0
  if [ $RUN_OPENSSL -gt 0 ]; then
    test_openssl
    TESTRES=$?
    if [ $TESTRES -eq 0 ]; then
      RUN_OPENSSL=0
    else
      NOTFOUND=$(($NOTFOUND+ 1))
    fi
    if [ $TESTMODE ]; then
      if [ $TESTRES -eq 0 ]; then
        exit 0
      fi
      "$0" -n openssl
      if [ $? -eq 0 ]; then
        exit 1
      else
        exit 2
      fi
    fi
  fi
  return $NOTFOUND
}

run_packages() {
  if [ $RUN_OPENSSL -gt 0 ]; then
    is_supressed openssl
    if [ $? -ne 0 ]; then
      install_openssl
      if [ $? -ne 0 ]; then
        fail openssl
      fi
    fi
  fi
}

parse_packages() {
  RUN_OPENSSL=0
  BADPKGS=""

  if [ $# -eq 0 ]; then
    prompt_err "Please specify at least one package to install."
    return 1
  fi

  while [ $# -gt 0 ]; do
    case $1 in
      openssl )
        RUN_OPENSSL=1
        ;;
      * )
        BADPKGS="$BADPKGS $1"
        ;;
    esac
    shift
  done

  if [ "$BADPKGS" ]; then
    for pkg in $BADPKGS; do
      prompt_err "invalid package name '$pkg'"
    done
    return 1
  fi
}

is_supressed() {
  PACKAGE=$1
  grep -qs "$PACKAGE 1 $(hostname -f)" "$CFGFILE"
  return $?
}

fail() {
  FAILURES=$(($FAILURES + 1))
  PACKAGE=$1
  # Call without an exit code to continue processing other packages.
  EXITCODE=$2
  prompt_err "Installation of $PACKAGE failed."
  if [ "$EXITCODE" ]; then
    exit $EXITCODE
  fi
}

#=========
# MAIN
#=========
while getopts ":tnfcp:vh" OPTNAME
do
  case $OPTNAME in
    t )
      TESTMODE=1
      ;;
    n )
      enable_dry_run
      ;;
    p )
      PROMPTMSG="$OPTARG"
      ;;
    f )
      FORCE=1
      ;;
    c )
      CREATEONLY=1
      ;;
    v )
      VERBOSE=1
      ;;
    h )
      usage
      exit 0
      ;;
    \: )
      prompt_err "'-$OPTARG' needs an argument."
      usage
      exit 1
      ;;
    * )
      prompt_err "invalid command-line option: $OPTARG"
      usage
      exit 1
      ;;
  esac
done
shift $(($OPTIND - 1))

# Scan remaining args for packages to run.
parse_packages "$@"
if [ $? -gt 0 ]; then
  usage
  exit 1
fi

# Now that we're past general usage errors, we'll allow GUI output.
# But not in test or dry run mode.
if [ ! $TESTMODE ] && [ ! $DRYRUN ]; then
  enable_gui_output
fi

guess_distro

# No point going farther if we don't have a way to download the files we need.
if [ ! $TESTMODE ]; then
  prep_fetch
  if [ $? -gt 0 ]; then
    exit 1
  fi
fi

# Test if packages are already installed before we try (unless forced).
if [ ! $FORCE ]; then
  test_packages
  # If no packages remain to install, don't bother with the prompt.
  if [ $? -eq 0 ]; then
    exit 0
  fi
fi

if [ "$PROMPTMSG" ]; then
  PACKAGE=$(echo "$PROMPTMSG" | md5sum | cut -d ' ' -f 1)
  is_supressed "$PACKAGE"
  if [ $? -eq 0 ]; then
    exit 0
  fi
  prompt_yesno_never "$PROMPTMSG"
  RES=$?
  if [ $RES -gt 0 ]; then
    if [ $RES -eq 2 ]; then
      # Similar to fail(), but will avoid retrying this specific, entire
      # request again, rather than individual packages.
      echo "$PACKAGE 1 $(hostname -f)" >> "$CFGFILE"
    fi
    exit 1
  fi
fi

# Now actually install any packages which weren't ruled out by the tests.
run_packages

exit $FAILURES
