#!/bin/bash
# Apply Gibraltar patch packages to a running system by mount and symlink-farms.
# Copyright Rene Mayrhofer 2003

# This script is idempotent for all valid patch files.

# this is the key ID to use for verifying GPG signatures
GPG_KEYID=1b6eff58f18625f0

. /usr/lib/gibraltar-bootsupport/common-definitions.sh

if [ $# -ne 1 -a $# -ne 2 ] || [ $# -eq 2 -a "$2" != "skip-hooks" ]; then
  echo "Usage: $0 <patch file name> [skip-hooks]"
  exit 1
fi

if [ ! -e "$1" ]; then
  echo "Error: $1 does not exist or is not readable"
  exit 2
fi

if [ "$2" = "skip-hooks" ]; then
  skip_hooks=1
else
  skip_hooks=0
fi

patchfile="$1"
tmpdir="/tmp/patch-package-$$"

cleanup() {
  if [ -d $tmpdir ]; then
    rm -r $tmpdir
  fi
}

umask 077
set -e
trap 'cleanup' HUP INT QUIT TERM EXIT

realmount() {
  dirsize=`ls -ld $1 | awk '{print $5;}'`
  size=`expr $dirsize \* 1024 \+ $maxsize`
  mount -t tmpfs -o size=$size none "/$1"
  if [ -n "`ls /system/root/$1/`" ]; then
    if [ $2 -eq 1 ]; then
      echo -n "link ... "
    fi
    ln -s "/system/root/$1"/* "/$1"
  fi
}

# first and only parameter is the directory to create - it will be done
# recursively
mount_dir() {
  # maybe it is already mounted ?
  if grep -q "$1 " /proc/mounts; then
    # the directory is already mounted, check that it is a RAM disk
    if ! grep "$1 " /proc/mounts | grep -q "tmpfs"; then
      echo "Error: directory is already mounted somewhere, but not on a RAM disk !"
      return 1
    else
      # ok, already mounted as tmpfs - TODO: resize ??
      return 0
    fi
  else
    # first check if we need to create the base dir to be able to create this
    # here we traverse the tree upwards
    if [ ! -d "$1" ]; then
      if [ -e "$1" ]; then
        echo "Error: $1 exists, but is not a directory - can not create"
        echo "new files inside it !"
        return 1
      else
        # the directory to mount to does not exist - mount the base dir and
        # create it
        basedir=`dirname $newdir`
        if ! mount_dir "$basedir"; then
          return 1
        fi
        mkdir "$1"
      fi
    fi

    # not mounted, but the directory should exist now
    # here we traverse the tree downwards
    for subdir in `grep "$1/" /proc/mounts | awk '{print $2;}'`; do
      # oops, there is already a subdirectory mounted under the one we want to
      # mount-replace, do a little double bind mount trick here to get the old
      # subdirectory again mounted under the new one
      echo -n "Saving '$subdir' ... "
      mkdir -p "$tmpdir/save-dirs/$subdir"
      mount --bind "$subdir" "$tmpdir/save-dirs/$subdir"
      umount "$subdir"
      echo "done"
    done

    echo -n "'$1': "
    echo -n "mount ... "
    realmount $1 1
    echo "done"

    for subdir2 in `grep "patch-package-.*/save-dirs/" /proc/mounts | \
                   awk '{print $2;}'`; do
      subdir=`expr "$subdir2" : "/.*/patch-package-.*/save-dirs\(/.*\)"`
      echo -n "Restoring '$subdir' ... "
      
      # OK, this is bloody - before restoring, we may need to create the
      # _whole_ directory hierarchy between the currently mounted (new) 
      # directory and the one to restore, else it will be mounted over some
      # /system/root/... directory (we just linked them above.....)
      subdir_tmp=`echo "$1" | sed 's/\\//\\\\\\//g'`
      diffdirs=`echo "$subdir" | sed "s/$subdir_tmp//"`
      curdir="$1"
      if [ -n "$diffdirs" ]; then
        echo -n "mount between ... "
        for dir in `echo $diffdirs | sed 's/\\// /g'`; do
          curdir="$curdir/$dir"
          if [ -L $curdir ]; then
            rm $curdir
            mkdir $curdir
            if [ "$curdir" != "$subdir" ]; then
              realmount $curdir 0
            fi
          else
            echo "Error: script failure at between_mount for $curdir !"
          fi
        done
      fi
      
      mount --bind "$subdir2" "$subdir"
      umount "$subdir2"
      echo "done"
    done
    
    return 0
  fi
}

mkdir "$tmpdir"
cd "$tmpdir"
remount_tmpfs 32m /var/tmp

# before doing anything else, check the signature
if ! gpg --options /dev/null --batch --no-default-keyring \
            --keyring /usr/share/keyrings/gibraltar-keyring.gpg \
	    --secret-keyring /dev/null --trusted-key $GPG_KEYID \
	    --out "$tmpdir/files.tgz" "$patchfile" 2>/dev/null; then
  echo "Error: Invalid signature on file $patchfile. This may be a forged patch, exiting now !"
  exit 100
fi

tar -xzf "$tmpdir/files.tgz"
# now we have the following files in $tmpdir:
# - patch.tgz (might be missing)
# - config.tgz (might be missing)
# - optionally pre-patch
# - optionally post-patch
# - version
# - package
# - created
echo "Patch file was created at '`cat $tmpdir/created`' for Gibraltar version '`cat $tmpdir/version`'"
#echo "Patch file is for Debian package:"
#cat "$tmpdir/package"

# a sanity check
if [ `cat $tmpdir/version` != `cat /system/etc-static/gibraltar/version` ]; then
  echo "Error: patch is not for current Gibraltar version !"
  exit 10
fi

# before mounting over some dirs, check that we can access the whole filesystem
if [ ! -d /system/root ]; then
  echo "Critical error: /system/root does not exist !"
  rm -r "$tmpdir"
  exit 3
fi
if [ ! -e /system/root/system/etc-static/gibraltar/version ]; then
  echo "Mounting root file system for symlinks"
  mount --bind / /system/root
fi

# before doing anything else, execute the patch's pre-hook
if [ "$skip_hooks" != "1" ] && [ -x "$tmpdir/pre-patch" ]; then
  echo "Executing pre-patch script ... "
  "$tmpdir/pre-patch" || true
  echo "done"
fi

# each RAM disk will be mounted with the max size of this patch, just to be
# sure

cd /
# check which directories are affected by the patch
if [ -e "$tmpdir/patch.tgz" ]; then
  maxsize=`zcat "$tmpdir/patch.tgz" | wc -c`
  
  echo "Stage 1: creating directories for updated files"
  # stage 1: create all necessary directories in RAM disk
  tar -tvzf "$tmpdir/patch.tgz" | sort -k 6 | \
  while read perms uidgid size date time path; do
    if [ `expr "$perms" : "d.*"` -eq 0 ]; then
      # do to sorting, we already get the directories in increasing depth
      dir=`dirname "$path"`
      mount_dir "/$dir"
    fi
    # no files are processed in this stage
  done

  echo "Stage 2: replacing files"
  tar -tvzf "$tmpdir/patch.tgz" | sort -k 6 | \
  while read perms uidgid size date time path; do
    # ignore directories, we only work on files now
    if [ `expr "$perms" : "d.*"` -eq 0 ]; then
      dir=`dirname "$path"`
      file=`basename "$path"`
      echo -n "'$file' in '$dir': "
      if [ -e "/$path" ]; then
        echo -n "remove ... "
        rm "/$path"
      fi 
      # at this point, the location should be writable, simply extract it from the patch
      echo -n "extract ... "
      tar --preserve --same-owner --atime-preserve -xzf "$tmpdir/patch.tgz" "$path"
      echo "done"
    fi
  done
fi

# now the config file patches, but we need to unpack them anyway
if [ -e "$tmpdir/config.tgz" ]; then
  mkdir "$tmpdir/config-patches"
  cd "$tmpdir/config-patches"
  tar -xzf "$tmpdir/config.tgz"
  for diff in `find . -name "*.diff"`; do
    file=`expr "$diff" : "\(.*\).diff"`
    echo -n "Patching config file "$file" ... "
    if patch --force --dry-run --silent "/etc/$file" < "$diff" >/dev/null; then
      patch --force --silent "/etc/$file" < "$diff"
      echo "done"
    else
      echo "unable to patch, probably changed by user"
    fi
  done
fi

# finally, execute the patch's post-hook
if [ "$skip_hooks" != "1" ] && [ -x "$tmpdir/post-patch" ]; then
  echo "Executing post-patch script ... "
  "$tmpdir/post-patch" || true
  echo "done"
fi

rm -r "$tmpdir"
remount_tmpfs $TMPDISK_SIZE /var/tmp || true

exit 0
