#!/bin/bash
#---------------------------------------------------------------------
## @Synopsis Functions for dealing with tracking of files, and other installwatch related things.
## @Copyright Copyright (C) 2004 The Source Mage Team <http://www.sourcemage.org>
## Functions for dealing with tracking of files, and other installwatch related things.
#---------------------------------------------------------------------

#---------------------------------------------------------------------
##
##  Initialize libtrack, currently this is just the modified files list.
##  This is used by cast to exclude files it's modified from the md5 list.
##
#---------------------------------------------------------------------
libtrack_init() {
  [[ $__MODIFIED_FILES ]] || export __MODIFIED_FILES="$TMP_DIR/modified_files"
}


#---------------------------------------------------------------------
## @Stdin list of files
## @Stdout list of files
## Reads a list of files from standard in, and returns a list of the
## files that exist.
##
#---------------------------------------------------------------------
exists()  {
  local item
  while read item; do
    [[ -e $item ]] && echo $item
  done
}


#---------------------------------------------------------------------
##
## Given a list of files it will notify installwatch of them.
## Useful for spells whose components are not dynamically linked
## to glibc. Uses simple hack of touching files while
## installwatch is running.
##
#---------------------------------------------------------------------
real_track_manual()  {
  if  [[ -z "$STAGED_INSTALL" || $STAGED_INSTALL == off ]] &&
      [[ -z "$INSTALLWATCHFILE" && -z "$INSTW_LOGFILE" ]] ;  then
    echo "Can't tell installwatch to manually track... installwatch isn't running."
    return 1
  fi
  touch -c "$@"
  return 0
}

#--------------------------------------------------------------------
## Some simple castfs sanity checking
## make a file, check to see if it exists, check to see if the
## contents of the file is what we made it
#--------------------------------------------------------------------
run_castfs_sanity()
{
  export CASTFS_LOGFILE=${CASTFS_DBGLOG}
  export CASTFS_DBGLVL=${CASTFS_DEBUG_LEVEL}
  debug  "libtrack" "$FUNCNAME on $SPELL"

  # first check if /dev/null is really a character device #13315
  if [[ ! -c /dev/null ]]; then
    debug "libtrack" "$FUNCNAME failure: damaged /dev/null"
    return 1
  fi

  mkdir -p "${TMP_DIR}/test-mount" &&
  mkdir -p "${TMP_DIR}/test-stage" &&
  castfs "${TMP_DIR}/test-mount" -o "stage=${TMP_DIR}/test-stage" -o "ignore=/tmp" &&
  pushd "${TMP_DIR}/test-mount" > /dev/null &&
  (
    (echo stuff > foo) &&
    (ls foo > /dev/null 2>&1) &&
    (test "stuff" = "$(cat foo)") &&
    rm -f foo
  )
  local rc=$?
  popd > /dev/null
  if [[ $rc == 0 ]]
  then
    debug "libtrack" "$FUNCNAME success"
  else
    debug "libtrack" "$FUNCNAME failure"
  fi
  umount -l "${TMP_DIR}/test-mount"
  return $rc
}

#---------------------------------------------------------------------
##
## Starts Translation Stage Root
##
#---------------------------------------------------------------------
real_invoke_stage_root()
{
  export CASTFS_LOGFILE=${CASTFS_DBGLOG}
  export CASTFS_DBGLVL=${CASTFS_DEBUG_LEVEL}
  local CASTFS_IGNORE_LIST=""
  local LOCAL_IGNORE_DIRS="${CCACHE_DIR} ${DISTCC_DIR}"
  # this forks and daemonizes so it will exit you simply unmount the dir to stop the process
  for dir in ${CASTFS_UNSTAGED_PATHS} ${LOCAL_IGNORE_DIRS}
  do
    if [[ -d $dir ]]; then
      CASTFS_IGNORE_LIST="${CASTFS_IGNORE_LIST} -o ignore=$(readlink -f $dir)"
    else
      debug "libtrack" "User wanted $FUNCNAME to treat $dir as a directory!"
    fi
  done &&
  castfs "${STAGE_DIRECTORY}/MOUNT" -o "stage=${STAGE_DIRECTORY}/TRANSL" $CASTFS_IGNORE_LIST &&
  for dir in /dev /dev/pts /proc /sys
  do
    mount -o bind "$dir" "${STAGE_DIRECTORY}/MOUNT$dir"
  done
}

#---------------------------------------------------------------------
##
## Stops Translation Stage Root
##
#---------------------------------------------------------------------
real_devoke_stage_root()
{
  unset CASTFS_LOGFILE
  unset CASTFS_DBGLVL
  for dir in /dev/pts /dev /proc /sys
  do
    umount -l "${STAGE_DIRECTORY}/MOUNT$dir"
  done &&
  umount -l "${STAGE_DIRECTORY}/MOUNT"
}

#---------------------------------------------------------------------
##
## Prepare Stage Root
##
#---------------------------------------------------------------------
prepare_stage_root()
{
  mk_source_dir "${STAGE_DIRECTORY}" &&
  mk_source_dir "${STAGE_DIRECTORY}/TRANSL" &&
  mk_source_dir "${STAGE_DIRECTORY}/MOUNT"
}

#---------------------------------------------------------------------
##
## Destroy Stage Root
##
#---------------------------------------------------------------------
destroy_stage_root()
{
  rm_source_dir $STAGE_DIRECTORY
}

#---------------------------------------------------------------------
##
## Sets up installwatch.
##
#---------------------------------------------------------------------
real_invoke_installwatch()  {
  if [[ -e $INSTALLWATCH_SO ]]; then
    export  INSTALLWATCHFILE=$IW_LOG
    export  INSTW_LOGFILE=$IW_LOG
    export  LD_PRELOAD=$INSTALLWATCH_SO
  fi
}

#---------------------------------------------------------------------
##
## Stops using installwatch
##
#---------------------------------------------------------------------
real_devoke_installwatch()  {
  unset  LD_PRELOAD
  unset  INSTALLWATCHFILE INSTW_LOGFILE
}

is_castfs_installed()
{
  if type castfs >/dev/null 2>&1 &&
     (
       modprobe fuse > /dev/null 2>&1 ||
       grep -q '^nodev[\t\v\f\r ]*fuse$' /proc/filesystems
     ) &&
     [ -c /dev/fuse ]
  then
    message "${MESSAGE_COLOR}Staging enabled${DEFAULT_COLOR}"
    return 0
  else
    message "${MESSAGE_COLOR}Staging disabled${DEFAULT_COLOR}"
    return 1
  fi
}

#---------------------------------------------------------------------
##
## Parses the installwatch log for files installed by a spell.
##
#---------------------------------------------------------------------
parse_iw()  {
  local INPUT=$1

  # it is EXTREMELY IMPORTANT that this variable contains an actual
  # tab character and not some number of spaces. Otherwise BAD THINGS
  # will happen.
  local TAB=$'\t'
  OMIT_IN="${TAB}rename\|${TAB}symlink\|${TAB}unlink\|${TAB}access\|${TAB}utimes"

  grep -v "$OMIT_IN" $INPUT | cut -f3 | grep "^/" | sed 's#^//#/#g'
  cat                $INPUT | cut -f4 | grep "^/" | sed 's#^//#/#g'
}


#---------------------------------------------------------------------
##
## Creates the install log containing all files installed by the spell.
##
#---------------------------------------------------------------------
create_install_log()  {
  debug  "libtrack" "$FUNCNAME on $SPELL"
  local INPUT=$1
  local OUTPUT=$2

  rm -f $OUTPUT
  if [[ $STAGED_INSTALL != off ]]
  then
    get_all_package_files               |
    filter_excluded                     > $OUTPUT
  else
    parse_iw $INPUT                     |
    sed "s#/\(\./\)\+#/#g"              |
    sort -u                             |
    install_log_filter $INSTALL_ROOT "" |
    grep -v -x ""                       |
    filter_excluded                     |
    install_log_filter "" $INSTALL_ROOT |
    exists                              >  $OUTPUT
  fi

  echo "$C_LOG_COMP"                  >> $OUTPUT
  echo "$MD5_LOG"                     >> $OUTPUT
  echo "$INST_LOG"                    >> $OUTPUT

}

#---------------------------------------------------------------------
##
## Creates the install log containing all staged files. It reuses
## the existing install log and just prepends $STAGE_DIRECTORY/TRANSL
## to every line
##
## @param input log file
## @param output log file
##
#---------------------------------------------------------------------
create_stage_install_log()  {
  debug  "libtrack" "$FUNCNAME on $SPELL"
  local input="$1"
  local output="$2"

  # treat the logs specially, as they weren't staged
  grep -v "$C_LOG_COMP\|$MD5_LOG\|$INST_LOG" "$input" > "$output"
  sed -i "s#^#$STAGE_DIRECTORY/TRANSL#" "$output"
  echo "$C_LOG_COMP"                 >> "$output"
  echo "$MD5_LOG"                    >> "$output"
  echo "$INST_LOG"                   >> "$output"

}

#---------------------------------------------------------------------
## Makes a list of files with the md5sum
#---------------------------------------------------------------------
create_md5list() {
  local INPUT=$1
  local OUTPUT=$2
  debug  "libtrack" "$FUNCNAME on $SPELL"

  [[ $__MODIFIED_FILES ]] || export __MODIFIED_FILES="$TMP_DIR/modified_files"
  touch $__MODIFIED_FILES
  filter "$__MODIFIED_FILES" < $INPUT | while read LINE ; do
    debug "libtrack" "Checking file $LINE"
    if  [ -f "$LINE" ] ; then
      debug "libtrack" "Running md5 on $LINE"
      md5sum "$LINE"
    fi
  done 2>/dev/null > $OUTPUT
}

#---------------------------------------------------------------------
## External api to note config files
#---------------------------------------------------------------------
real_note_config_file() {
  if check_if_modified "$1"; then
    mark_file_modified "$1"
  fi
}


#---------------------------------------------------------------------
## Notes that a file was previously modified so that its md5 is
## deliberatly munged
#---------------------------------------------------------------------
mark_file_modified() {
  [[ "$1" ]] || return 1
  [[ $__MODIFIED_FILES ]] || export __MODIFIED_FILES="$TMP_DIR/modified_files"
  echo "^$1\$" >> $__MODIFIED_FILES
}


#---------------------------------------------------------------------
## @param file to check
## @param md5 file (optional)
#---------------------------------------------------------------------
check_if_modified() {
  local to=$1
  local md5_log=$2
  if ! [[ $2 ]] ; then
    md5_log="$TMP_DIR/$SPELL.md5"
    if [[ $OLD_SPELL_VERSION ]] ; then
      old_md5_log="$MD5SUM_LOGS/$SPELL-$OLD_SPELL_VERSION"
      # log must be in filterable form
      log_adjuster "$old_md5_log" "$md5_log" filterable root
    else
      old_md5_log=/dev/null
    fi
  fi
  local my_md5=$(md5sum "$to")
  if test -f "$md5_log" && grep -qx "$my_md5" "$md5_log"; then
    false
  else
    true
  fi
}

#---------------------------------------------------------------------
## Constructs the to-be cache name depending on the ARCHIVEBIN
## @param archive name
## @param upvar
#---------------------------------------------------------------------
construct_cache_name() {
  local name=$1
  if [[ -n $ARCHIVEBIN ]]; then
    # just use the bin, currently there is no need for another extension
    name="$name.$ARCHIVEBIN"
  fi
  upvar $2 "$name"
}

#---------------------------------------------------------------------
## Given a filename, will return the actual filename if a similar
## filename with a different extension exists. A more thorough version
## of guess_filename used for finding caches
## @param archive name (can be right trimmed, globbing will be done)
## @param upvar (optional)
#---------------------------------------------------------------------
find_cache() {
  debug "libtrack" "$FUNCNAME $@"
  local filename=$1
  local real_name

  if [[ -f  $filename ]]; then
    real_name=$filename
  else
    # use the first if more were found
    local basename
    smgl_basename "$filename" basename
    read real_name < <(find $INSTALL_CACHE/ -type f -name "$basename*")
  fi
  [[ -z $real_name ]] && return 1
  debug "libtrack" "$FUNCNAME found $real_name"

  if [[ -z $2 ]]; then
    echo $real_name
  else
    upvar $2 "$real_name"
  fi
  return 0
}

#---------------------------------------------------------------------
##
## Creates a bzip/gzip'ed tar file containing an archived backup of
## file specified on standard input into the target dir.
##
## Input files are relative to install root for regular files and
## state root for state files
## @param files, one per line, to put into the archive
## @param archive name
## @param compressed archive name
##
#---------------------------------------------------------------------
create_cache_archive()  {

  debug  "libtrack" "$FUNCNAME on $SPELL"
  if    [  "$ARCHIVE"  ==  "off"  ]; then
    debug "libtrack" "$FUNCNAME - ARCHIVE=$ARCHIVE, aborting archival."
    return
  fi
  debug "libtrack" "$FUNCNAME - ARCHIVE=$ARCHIVE, archiving."
  local input=$1
  local CACHE=$2
  local CACHE_COMP=$3

  message  "${MESSAGE_COLOR}Creating cache file" \
           "${FILE_COLOR}${CACHE_COMP}${DEFAULT_COLOR}"
  # gather the queuing factors, so we can include them in the label, saving us the
  # need to compute the spell name and a few untars when dealing with just a cache
  local label
  label="$SPELL ${VERSION:-0} ${PATCHLEVEL:-0} ${SECURITY_PATCH:-0} ${UPDATED:-0}"

  local TMP_DATA=$TMP_DIR/foo.data
  local TMP_MDATA=$TMP_DIR/foo.mdata
  seperate_state_files $input $TMP_DATA $TMP_MDATA

  case "$ARCHIVEBIN" in
    tar)
      pushd $STATE_ROOT/ &>/dev/null
      install_log_filter $STATE_ROOT "." < $TMP_MDATA |
      tar --no-recursion -cPf "$CACHE" -T - -V "$label"
      popd &>/dev/null

      pushd $INSTALL_ROOT/ &>/dev/null
      install_log_filter $INSTALL_ROOT "." < $TMP_DATA |
      tar --no-recursion -rPf "$CACHE" -T -
      popd &>/dev/null
      ;;
  esac
  rm $TMP_DATA $TMP_MDATA

  case "$COMPRESSBIN" in
    gzip|bzip2|pbzip2|xz)
      if "$COMPRESSBIN" -c </dev/null &>/dev/null; then
        "$COMPRESSBIN" -c "$CACHE" > "$CACHE_COMP" &&
        rm "$CACHE"
      fi
      ;;
  esac
}

#---------------------------------------------------------------------
## this is to filter the install log from one form to another
## for install_root/track_root conversions
#---------------------------------------------------------------------
install_log_filter() {
  sed "s:^$1:$2:"
}
md5_log_filter() {
  sed "s:  $1:  $2:"
}

#---------------------------------------------------------------------
## @param input file (can be /dev/stdin)
## @param output file (can be /dev/stdout)
## @param input format (root/log/filterable)
## @param output format (root/log/filterable)
## @param filter callback (optional install_log_filter, could be md5_log_filter)
##
## This filters an install log from a given format into another format
## <pre>
## root: relative to / all paths are relative to / file existence tests
## should work, INSTALL_ROOT and STATE_ROOT are prepended to data and
## state files respectively
##
## log: relative to track_root etc, format used in the logs (see note on
## special behavior below)
##
## filterable: track_root/install_root/state_root stripped out files can
## have filters applied to them
##
## "Special" handling applies depending on whether STATE_ROOT is inside
## or outside INSTALL_ROOT.
## For converting into log format:
## If STATE_ROOT is within INSTALL_ROOT
## eg: STATE_ROOT=/opt/stuff INSTALL_ROOT=/opt/stuff
## or  STATE_ROOT=/opt/stuff/state INSTALL_ROOT=/opt/stuff
## the portion of INSTALL_ROOT within STATE_ROOT is replaced with TRACK_ROOT
## if STATE_ROOT is outside of INSTALL_ROOT (eg /opt/stuff and /opt/state)
## then STATE_ROOT is left as is
##
## Converting from log to root format is the inverse, and of course going
## to filterable format just requires removing whatever the expected prefix is.
## </pre>
#---------------------------------------------------------------------
log_adjuster() {
  local input=$1
  local output=$2
  local informat=$3
  local outformat=$4
  local callback=${5:-install_log_filter}

  local data_in data_out metadata_in metadata_out cat_metadata


  if [[ "$informat" == root ]] ; then
    data_in=$INSTALL_ROOT
    if [[ "$outformat" == log ]] ; then
      # root to log
      data_out=$TRACK_ROOT

      # if the STATE_ROOT is within the install root, then the state files are
      # adjusted relative to track_root, otherwise they are left as is
      if ! [[ ${STATE_ROOT##$INSTALL_ROOT*} ]] ; then
        metadata_in=$STATE_ROOT
        metadata_out=$TRACK_ROOT${STATE_ROOT##$INSTALL_ROOT}
      else
        cat_metadata=yes
      fi
    elif [[ $outformat == filterable ]] ; then
      # root to filterable
      data_out=""
      metadata_in=$STATE_ROOT
      metadata_out=""
    fi
  elif [[ "$informat" == log ]] ; then
    data_in=$TRACK_ROOT
    if [[ "$outformat" == root ]] ; then
      # log to root
      data_out=$INSTALL_ROOT
      if ! [[ ${STATE_ROOT##$INSTALL_ROOT*} ]] ; then
        # we actually could do this another way by stripping off
        # $TRACK_ROOT${STATE_ROOT##$INSTALL_ROOT}, and replacing
        # it with $STATE_ROOT, but i think below is simpler and equivalent
        metadata_in=$TRACK_ROOT
        metadata_out=$INSTALL_ROOT
      else
        cat_metadata=yes
      fi
    elif [[ "$outformat" == filterable ]] ; then
      # log to filterable
      data_out=""
      metadata_out=""
      if ! [[ ${STATE_ROOT##$INSTALL_ROOT*} ]] ; then
        metadata_in=$TRACK_ROOT${STATE_ROOT##$INSTALL_ROOT}
      else
        metadata_in=$STATE_ROOT
      fi
    fi
  elif [[ "$informat" == filterable ]] ; then
    data_in=""
    metadata_in=""
    if [[ "$outformat" == root ]] ; then
      # filterable to root
      data_out=$INSTALL_ROOT
      metadata_out=$STATE_ROOT
    elif [[ "$outformat" == log ]] ; then
      # filterable to log
      data_out=$TRACK_ROOT
      if ! [[ ${STATE_ROOT##$INSTALL_ROOT*} ]] ; then
        metadata_out=$TRACK_ROOT${STATE_ROOT##$INSTALL_ROOT}
      else
        metadata_out=$STATE_ROOT
      fi
    fi
  fi

  local TMP_SSF=$(make_safe_dir)
  local TMP_DATA=$TMP_SSF/foo.data
  local TMP_MDATA=$TMP_SSF/foo.mdata

  seperate_state_files $input $TMP_DATA $TMP_MDATA
  {
    if [[ $cat_metadata ]] ; then
      cat $TMP_MDATA
    else
      eval "$callback \"$metadata_in\" \"$metadata_out\" < $TMP_MDATA"
    fi
    eval "$callback \"$data_in\" \"$data_out\" < $TMP_DATA"
  } > $output
  rm $TMP_DATA $TMP_MDATA
  rmdir $TMP_SSF
  return 0
}

#---------------------------------------------------------------------
## Split a log file into data that should be TRACK_ROOT'd versus
## STATE_ROOT'd.
##
## @param filename or - (or /dev/stdin) for a pipe. This routine will read the input only once in-order to work with pipes.
## @param filename for non-state files, possibly /dev/stdout or /dev/stderr
## @param filename for state files, possibly /dev/stdout or /dev/stderr don't use the same stream for both types.
##
#---------------------------------------------------------------------
seperate_state_files() {
  local REAL_LOG_DIR=${LOG_DIRECTORY#$STATE_ROOT}
  local REAL_STATE_DIR=${STATE_DIRECTORY#$STATE_ROOT}

  # the input file is almost certainly a pipe, and things get weird
  # since we have to grep twice, so just dump the data into a unique file

  local TMP_SSF=$(make_safe_dir)
  local FILE=$TMP_SSF/ssf
  cat $1 > $FILE
  grep -v "$REAL_LOG_DIR\|$REAL_STATE_DIR" $FILE | grep -xv '' > $2
  grep    "$REAL_LOG_DIR\|$REAL_STATE_DIR" $FILE | grep -xv '' > $3
  rm $FILE
  rmdir $TMP_SSF
  return 0
}

#---------------------------------------------------------------------
## Try to make a unique directory using $RANDOM, leverage the fact that
## two simultaneous mkdir's will have one succeed and the other fail.
##
## This is run from log_adjuster primarily which may be invoked several
## times in a pipe, when this happens, bash does a fork, but does not
## seem to reseed the random number generator, causing a high rate of
## collisions. This is not easily reproducable outside of sorcery at
## the time of writing, but inside it happens nearly everytime with bash
## 3.1. This may actually be a bash 3.1 bug.
##
## The collisions aren't bad necessarily, but they result in un-necesary
## delay.
##
## Setting RANDOM re-seeds the random number generator.
##
## Despite the fact that the subshell and invocation of date are slow
## the consequence for not doing them are worse. The nano-seconds
## are usually going to be different between forks so the liklihood
## of a collision is greatly reduced.
#---------------------------------------------------------------------
make_safe_dir() {
  mktemp -d -p "$TMP_DIR"
}

#---------------------------------------------------------------------
## @License
##
## This software is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This software is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this software; if not, write to the Free Software
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
##
#---------------------------------------------------------------------
