#!/bin/bash
#---------------------------------------------------------------------
##
## @Libgrimoire
##
## @Synopsis Set of functions containing the spell writing API.
##
##
## These functions can be used in the PRE_BUILD, BUILD, POST_BUILD
## and POST_INSTALL sections of spells.
##
## @Copyright
## Original version Copyright 2001 by Kyle Sallee
## Additions/Corrections Copyright 2002 by the Source Mage Team
## New World libunpack Additions/Corrections by Seth Woolley (2005)
##
#---------------------------------------------------------------------

#===================== libunpack common ==============================

#---------------------------------------------------------------------
## @Type API
## @param SOURCE suffix
##
## unpack_file takes the SOURCE suffix and figures out if it is supposed
## to hash or gpg check it -- then it does its dirty work and runs unpack_hash
## or unpack_gpg depending upon the circumstances.  That's the only argument it
## takes and needs: '' '2' '3', etc.  It is run in default_pre_build for the
## null argument only.  Custom unpacking still requires a custom PRE_BUILD.
##
## valid formats: vendor-provided gpg, guru-provided gpg, any
## hash-algorithm provided by gpg (currently md5, sha1, sha256, sha384,
## sha512, ripemd160
##
##           SOURCE=blah
##          SOURCE2=blah.asc
##       SOURCE_URL=http://blah.com/$SOURCE
##      SOURCE2_URL=http://blah.com/$SOURCE2
##       SOURCE_GPG=blah.gpg:$SOURCE2:UPSTREAM_KEY
##   SOURCE2_IGNORE=signature # for auditing purposes
##
##           SOURCE=blah
##       SOURCE_URL=http://blah.com/$SOURCE
##       SOURCE_GPG=swoolley.gpg:$SOURCE.asc:WORKS_FOR_ME
##
##           SOURCE=blah
##       SOURCE_URL=http://blah.com/$SOURCE
##           MD5[0]=d41d8cd98f00b204e9800998ecf8427e
##
##           SOURCE=blah
##       SOURCE_URL=http://blah.com/$SOURCE
##       SOURCE_HASH=md5:d41d8cd98f00b204e9800998ecf8427e:WORKS_FOR_ME
##
## In GPG mode:
##   Validates the verification level (the third parameter) and the
##   hash algorithm against user defined lists.
##   It finds the public key and signature using locate_spell_file,
##   Then it validates it at the beginning.
##   see unpack_gpg()
##
## In HASH mode:
##   Validates the verification level (the third parameter) and the
##   hash algorithm against user defined lists.
##   It uses gpg to calculate the hash value except for md5 and sha1, which
##   coreutils provides.
##   see unpack_hash()
##
## In IGNORE mode:
##   It checks for the following text:
##     volatile (for cvs/svn/any-other-scm)
##     unversioned (the source file changes frequently, but not a direct scm)
##     signature (for gnupg signatures)
##   as reasons for ignoring the source code validation.  Signatures
##   are silently ignored.  Everything else respects MD5SUM_DL.
##   see unpack_ignore
##
## Otherwise, it falls back to MISSING mode, see unpack_missing
##
## The default verification level is "WORKS_FOR_ME"
##
## Verification levels are, these indicate how much effort was put into
## validating the integrity of the source from the upstream vendor.
##   WORKS_FOR_ME No verification was done.
##   UPSTREAM_HASH Checked the upstream hash file
##   UPSTREAM_KEY Checked upstream (gpg) key, signature matched, but the
##                key was not validated
##   ESTABLISHED_UPSTREAM_KEY Upstream key was not validated against
##                            multiple independent sources, but has been
##                            in use for several years
##   VERIFIED_UPSTREAM_KEY Upstream key id was verified against multiple
##                         independent sources.
##   ID_CHECK_UPSTREAM_KEY Key was verified in person with a photo id check.
##
## Also if you want to include more than one signature, hash, etc, just put
## a 2, 3, 4, etc on the end of the variable like so:
##   SOURCE2_HASH2=...
##
## For cascading, currently it will still ask abort questions: a no abort
## will make it fail over all cascades; a yes abort will have it skip to
## the next cascades.  Missing binaries or other failures like that (error 200
## below) will silently fail over to the next check.  The cascade order is:
##  GPG, HASH, IGNORE, MISSING
##
## The cascade setup allows you to place a higher bit checksum earlier
## in the cascade and even if the binary doesn't work it will just print
## out an abort query which can be said no to and it will continue to
## fail over to the lower bit checksum that should be available in
## coreutils (like sha1/md5).  That's if you're not using gpg, which is
## preferred.  If multiple hashes are included of different ciphers, the
## user can abort on either that go bad, so it can be considered a
## security increase to have more than one, but only if the harder cipher
## is first in the cascade order, as the first successful hash match will
## go ahead and prompt an untarball.  I may change it later, but for now I
## think first successful match skipping the rest is least intrusive, and
## I'd need to add an interface element to let the user choose to run all
## checks on a single source.
##
#---------------------------------------------------------------------
function real_unpack_file() {
  debug "libgrimoire" "real_unpack_file - $*"

  local FILENUM="$1"
  local SVAR="SOURCE${FILENUM}"

  real_verify_file "$@"
  rc=$?
  case "$rc" in
    200) debug "libunpack" "unable to verify $SVAR ${!SVAR}" ;;
      1) return 1 ;; # verification failed
      0) uncompress_unpack ${!SVAR}; return $? ;;
  esac

  debug "libgrimoire" "falling back to missing verification"
  unpack_missing "${!SVAR}"
  rc="$?"
  case "$rc" in
      0) uncompress_unpack "${!SVAR}"; return "$?"             ;;
      *) return "$rc"                                          ;;
  esac
}


#---------------------------------------------------------------------
## @Type API
## @param SOURCE suffix
##
## Does the work of verifying a file with the new-world verification
## system.
#---------------------------------------------------------------------
function real_verify_file() {
  debug "libunpack" "real_verify_file - $*"

  local FILENUM="$1"
  local SVAR="SOURCE${FILENUM}"

  local crypto_func
  for crypto_func in GPG HASH IGNORE; do
    debug "libgrimoire" "checking $crypto_func verification"

    local AVAR="SOURCE${FILENUM}_${crypto_func}"
    [[ -n ${!AVAR} ]] || continue

    local rc=""
    local lcase_crypto_func="$(echo $crypto_func | tr 'A-Z' 'a-z')"
    unpack_$lcase_crypto_func "${!SVAR}" "${!AVAR}"
    rc="$?"

    case "$rc" in
      200) debug "libgrimoire" "unable to verify $AVAR with $crypto_func" ;;
        *) return $rc ;;
    esac

  done
  return 200
}


#---------------------------------------------------------------------
## @param filename
## @param compressor
## @Stdout uncompressed
##
## Just uncompresses the file, but does not expand it. i.e. bunzip
## it, but don't untar it. It dumps the expanded file to stdout.
## Note: zip is a special case because it doesn't work with streams.
##
#---------------------------------------------------------------------
function uncompress_core() {
  debug "libgrimoire" "uncompress_core - $*"

  case  "$2"  in
          bzip2)  bzip2  -cdf   "$1"  ;;
           gzip)  gzip   -cdf   "$1"  ;;
      compress*)  gzip   -cdf   "$1"  ;;
            Zip)  cat           "$1"  ;;
            RPM)  rpmunpack  <  "$1" | gzip  -cd    ;;
            tar)  cat           "$1"  ;;
          xz|XZ)  xz -cdf "$1" ;;
           LZMA)  xz -cdf "$1" ;;
          7-zip)  cat "$1" ;; # 7z supports stdout, but it unpacks at the same time
              *)  cat           "$1"  ;;
  esac

}


#---------------------------------------------------------------------
## @param filename
## @param compressor
## @Stdout uncompressed
##
## unpack_core takes the uncompressed stream and turns it into the
## fully unarchived form.
## Note: zip is a special case because it doesn't work with streams.
##
#---------------------------------------------------------------------
function unpack_core() {
  debug "libgrimoire" "unpack_core - $*"

  case  "$2"  in
            bzip2|gzip|compress*|tar|XZ|xz|LZMA)
                    if real_list_find "$3" same-permissions; then
                      tar -xf /dev/stdin 2> /dev/null
                    else
                      tar --no-same-owner --no-same-permissions -xf \
                      /dev/stdin 2> /dev/null
                    fi || cat > /dev/null ;;
              Zip)  cat /dev/stdin >/dev/null   #get rid of unused output
                    unzip  -q  "$1"                                    ;;
            7-zip)  cat /dev/stdin >/dev/null   #get rid of unused output
                    7z e "$1"                                          ;;
              RPM)  cpio  -idm < /dev/stdin                            ;;
                *)  cat > /dev/null                                    ;;
  esac

}


#---------------------------------------------------------------------
## @Type API
## @param filename
## @Stdout compressor
##
## Guesses what program was used to compress a file
## Return value is always success due to `file' workings
##
#---------------------------------------------------------------------
function real_guess_compressor()  {
  # NOTE: if the file doesn't exist, `file' still completes successfully
  #       the COMPRESSOR value in this case will be "can't"

  local OUTPUT="$($FILEPROG -L -b "$1")"
  local COMPRESSOR="$(echo "$OUTPUT" | cut -d ' ' -f1)"
  [ "$COMPRESSOR" = "GNU" -o "$COMPRESSOR" = "POSIX" ] &&
    COMPRESSOR="$(echo "$OUTPUT" | cut -d ' ' -f2)"
  debug "libgrimoire" "guess_compressor() - guessed $1 compressor <$COMPRESSOR>"
  echo "$COMPRESSOR"
}


#---------------------------------------------------------------------
## @Type API
##
## Used to be uncompress_md5(), now it is uncompress_core()
##
#---------------------------------------------------------------------
function real_uncompress() { uncompress_core "$@"; }


#---------------------------------------------------------------------
## @param required spell
##
## Returns 200 if the user says not to Abort in the face, otherwise
##
#---------------------------------------------------------------------
function unpack_spell_required() {
  debug "libgrimoire" "Running unpack_spell_required -- $1"

  local x
  if ! spell_ok "$1" && ! smgl_which $2 x &> /dev/null; then
    query "This spell has an option to check its integrity via spell "\
"${SPELL_COLOR}${1}${DEFAULT_COLOR}${QUERY_COLOR} with the $2 command for $3, you might consider installing it. "\
"Abort?" n &&
      return 1 ||
        return 200
  else
    return 0
  fi

}


#===================== libunpack newworld ============================

#--------------------------------------------------------------------
## @param the verification level
##
## returns 0 if the specified verification level is in the user's
## list of allowed verification levels, or if they allow unknown
## verification levels, 1 otherwise
##
#--------------------------------------------------------------------
function is_allowed_verf_level() {
  local rc=0
  local VRFLEVEL=$1
  message "${MESSAGE_COLOR}Checking spell level ${VRFLEVEL}${DEFAULT_COLOR}"
  if list_find "${VRF_ALLOWED_LEVELS}" "${VRFLEVEL}:on"
  then
    message "${MESSAGE_COLOR}Spell level is an allowed level${DEFAULT_COLOR}"
  elif list_find "${VRF_ALLOWED_LEVELS}" "${VRFLEVEL}:off"
  then
      message "${PROBLEM_COLOR}Spell level is not an allowed level${DEFAULT_COLOR}"
      rc=1
  else
    if [[ "${VRF_ALLOW_NEW_LEVELS}" == "on" ]]
    then
      message "${MESSAGE_COLOR}Spell level is a new allowed level${DEFAULT_COLOR}"
    else
      message "${PROBLEM_COLOR}Spell level is not an allowed level${DEFAULT_COLOR}"
      rc=1
    fi
  fi
  return $rc
}

#--------------------------------------------------------------------
## @param hash used
## @param spells verification level
##
## first checks if the hash is in the user specified list in an on state then
## checks if the hash is there in an off state, if it can't find either then
## it checks the state of VRF_ALLOW_NEW_HASHES to see if we should succeed or
## not
## Returns 0 if the hash is allowed or (VRF_ALLOW_NEW_HASHES is on and the hash
## is not present in the hash list)
##
#--------------------------------------------------------------------
function is_allowed_hash() {
  local rc=0
  local hash=$1
  local HASHLEVEL=$2
  message "${MESSAGE_COLOR}Algorithm used: ${hash}${DEFAULT_COLOR}" &&
  if list_find "$VRF_ALLOWED_HASHES" "${hash}:on"
  then
    message "${MESSAGE_COLOR}Algorithm checks out${DEFAULT_COLOR}"
    if is_allowed_verf_level $HASHLEVEL ; then rc=0 ; else rc=1 ; fi
  elif list_find "$VRF_ALLOWED_HASHES" "${hash}:off"
  then
    message "${PROBLEM_COLOR}Algorithm is not in user selected list${DEFAULT_COLOR}"
    rc=1
  elif [[ "$VRF_ALLOW_NEW_HASHES" == "on" ]]
  then
    message "${MESSAGE_COLOR}Allowing new hash ${hash}${DEFAULT_COLOR}"
    if is_allowed_verf_level $HASHLEVEL ; then rc=0 ; else rc=1; fi
  else
    message "${PROBLEM_COLOR}Disallowing new hash ${hash}${DEFAULT_COLOR}"
    rc=1
  fi
  return $rc
}

#---------------------------------------------------------------------
## @param file to unpack
## @param gpg public key file (.gpg) ":" gpg signature file  (.asc)
##
## Given a file, unpack checks the gpg signature for that file, and, if
## appropriate, runs the decompression program for that file, as well as
## untar'ing the file. Note: zip is a special case because it doesn't
## work with streams.
##
#---------------------------------------------------------------------
function unpack_gpg() {
  debug "libgrimoire" "Running unpack_gpg -- $*"

  local FILENAME="$( guess_filename   "$SOURCE_CACHE/$1" )"
  local PFNAME="$( echo "$2" | cut -d: -f1  )"
  local SFNAME="$( echo "$2" | cut -d: -f2  )"
  local GPGLEVEL="$( echo "$2" | cut -d: -f3 )"
  if [[ -z $GPGLEVEL ]]
  then
    GPGLEVEL=$DEFAULT_SPELL_VRF_LEVEL
  elif ! list_find "${VERIFY_SPELL_LEVELS}" "${GPGLEVEL}"
  then
    message "${PROBLEM_COLOR}This is probably a spell bug ${GPGLEVEL} is not in ${VERIFY_SPELL_LEVELS}${DEFAULT_COLOR}"
    return 1
  fi
  local GPGALGO_USED=""
  local message_file=""

  message "${MESSAGE_COLOR}GPG checking source file $1...${DEFAULT_COLOR}"

  unpack_spell_required gnupg gpg || return "$?"

  gpg_verify_signature "$( locate_spell_file "$SFNAME" )" \
                       "$FILENAME" \
                       "$( locate_spell_file "$PFNAME" securely)" GPGALGO_USED
  rc="$?"
  case "$rc" in
    0)
      local algo
      rc=1
      for algo in $GPGALGO_USED; do
        if is_allowed_hash "$algo" "$GPGLEVEL"; then
          rc=0
          break
        fi
      done
      ;;
    3) message_file="Signature" ;;
    4) message_file="Source" ;;
    5) message_file="Keyring" ;;
  esac
  if [[ $message_file ]]
  then
      message "${PROBLEM_COLOR}AHHH!!! ${message_file} file not found${DEFAULT_COLOR}"
  fi
  if [ "$rc" -eq 200 ]; then
    return 200
  fi

  gpg_user_query $rc $SPELL spell || return 1
  return 0

}


#---------------------------------------------------------------------
## @param file to unpack
## @param algorithm ":" hashsum
##
## Given a file, unpack checks the hash for that file, and, if
## appropriate, runs the decompression program for that file, as well as
## untar'ing the file. Note: zip is a special case because it doesn't
## work with streams.
##
#---------------------------------------------------------------------
function unpack_hash() {
  debug "libgrimoire" "Running unpack_hash() on $1"

  local FILENAME="$( guess_filename   "$SOURCE_CACHE/$1" )"
  local ALGORITHM="$( echo "$2" | cut -d: -f1  )"
  local HASHSUM="$(   echo "$2" | cut -d: -f2  )"
  local HLEVEL="$(    echo "$2" | cut -d: -f3  )"
  local rc=0
  if [[ -z "$HLEVEL" ]]
  then
    HLEVEL=$DEFAULT_SPELL_VRF_LEVEL
  fi

  message "${MESSAGE_COLOR}Hash checking source file $1...${DEFAULT_COLOR}"
  local HASH
  if [ "$MD5SUM_DL" != "off" ]; then

    if [[ "$ALGORITHM" == md5 ]] || [[ "$ALGORITHM" == sha1 ]] ; then
      unpack_spell_required coreutils "${ALGORITHM}sum" "$ALGORITHM" ||
        return "$?"
      HASH="$(${ALGORITHM}sum "$FILENAME" | cut -d' ' -f1)"
    else
      unpack_spell_required gnupg gpg "$ALGORITHM" || return "$?"
      if list_find "$(gpg_get_hashes)" $ALGORITHM; then
        HASH="$(gpg_hashsum "${ALGORITHM}" "$FILENAME" | cut -d' ' -f1)"
      else
        message "${PROBLEM_COLOR}Algorithm $ALGORITHM is not"\
                "known!${DEFAULT_COLOR}"
        return 200
      fi
    fi
    local rc=$?

    if [[ "$HASH" != "$HASHSUM" ]] || [[ $rc != 0 ]]
    then
      error_message "${PROBLEM_COLOR}$ALGORITHM check failed!" &&
      error_message "$HASH (computed) != $HASHSUM (expected)!$DEFAULT_COLOR" &&
      hash_user_query 1 "$SPELL" spell || return 1
    else
      is_allowed_hash "$ALGORITHM" "$HLEVEL"
      rc=$?
      hash_user_query $rc "$SPELL" spell || return 1
    fi
  else
    message "${PROBLEM_COLOR}Continuing!${DEFAULT_COLOR}"
  fi
  return 0
}

#--------------------------------------------------------------------
## @param return code from unpack_hash
## @param spell name
##
## Does some basic output to tell the user what failed and how then calls
## unpack_file_user_query
## Returns 0 if hash succeeded otherwise returns 1 if unpack_file_user_query
## fails
##
#--------------------------------------------------------------------
function hash_user_query() {
  local rc=$1
  local spell=$2
  case "$rc" in
    0)
      message "${MESSAGE_COLOR}Hash verification succeeded${DEFAULT_COLOR}"
      ;;
    *)
      message "${PROBLEM_COLOR}Hash verification failure${DEFAULT_COLOR}"
      unpack_file_user_query $rc || return 1
      ;;
  esac
  return 0
}

#--------------------------------------------------------------------
## @param return code from the unpack_gpg or unpack_hash
##
## checks MD5SUM_DL to abort or not
## Returns what query returns if it's called
##
#--------------------------------------------------------------------
function unpack_file_user_query() {
  local rc=$1
  case "$rc" in
    0)
      ;;
    *)
      case "$MD5SUM_DL" in
        ask_ignore)  query "Abort?" "n" && return 1  ;;
        ask_risky|ask_abort)  query "Abort?" "y" && return 1 ;;
        on|abort_all|*) message "${RED}Aborting.${DEFAULT_COLOR}" ; return 1 ;;
      esac
      ;;
  esac
  return 0
}

#---------------------------------------------------------------------
## @param file to unpack
## @param reason to ignore it, one of: volatile unversioned signature
##
## Given a file, unpack checks the ignore rules for that file, and, if
## appropriate, runs the decompression program for that file, as well as
## untar'ing the file. Note: zip is a special case because it doesn't
## work with streams.
##
#---------------------------------------------------------------------
function unpack_ignore() {
  debug "libgrimoire" "Running unpack_ignore() on $1"

  REASON="$2"

  message "${MESSAGE_COLOR}Not checking ${2} source file $1...${DEFAULT_COLOR}"

  if [ "$MD5SUM_DL" != "off" ]; then

    [ "$REASON" == "signature" ]  ||
      case "$MD5SUM_DL" in
ask_risky|ask_ignore)  query "Abort?" "n"         && return 1   ||  return 0 ;;
           abort_all)  message "${RED}Aborting.${DEFAULT_COLOR}"  ; return 1 ;;
      ask_abort|on|*)  query "Abort?" "y"         && return 1   ||  return 0 ;;
      esac

  else
    message "${RED}Continuing!${DEFAULT_COLOR}"
    return 0
  fi

}


#---------------------------------------------------------------------
## @param file to unpack
## @param reason to ignore it, one of: volatile unversioned signature
##
## Given a file, unpack checks the ignore rules for that file, and, if
## appropriate, runs the decompression program for that file, as well as
## untar'ing the file. Note: zip is a special case because it doesn't
## work with streams.
##
#---------------------------------------------------------------------
function unpack_missing() {
  debug "libgrimoire" "Running unpack_missing() on $1"

  message "${PROBLEM_COLOR}Missing check for source file $1!${DEFAULT_COLOR}"

  if [ "$MD5SUM_DL" != "off" ]; then

    case "$MD5SUM_DL" in
          ask_ignore)  query "Abort?" "n"         && return 1   ||  return 0 ;;
 ask_risky|ask_abort)  query "Abort?" "y"         && return 1   ||  return 0 ;;
      on|abort_all|*)  message "${RED}Aborting.${DEFAULT_COLOR}"  ; return 1 ;;
    esac

  else
    message "${RED}Continuing!${DEFAULT_COLOR}"
    return 0
  fi

}

#---------------------------------------------------------------------
## @param file to unpack
##
## Given a cache file, runs the decompression and the unarchival
## program on it. A wrapper around uncompress_unpack
##
#---------------------------------------------------------------------
function uncompress_unpack_cache() {
  uncompress_unpack "$1" root cache same-permissions
}

#---------------------------------------------------------------------
## @param file to unpack
## @param (optional) dirname of the file (default: $SOURCE_CACHE)
##                   'root' if you already have an absolute path
## @param (optional) type (default: source)
## @param (optional) hints - used for passing hints to unpack/uncompress
##
## Given a file, runs the decompression program for that file, as well as
## untar'ing the file.
##
#---------------------------------------------------------------------
function uncompress_unpack() {
  debug "libgrimoire" "Running uncompress_unpack() on $@"

  local basedir
  [[ $2 == root ]] || basedir=$SOURCE_CACHE
  local filename=$(guess_filename "$basedir/$1") &&
  local compressor=$(guess_compressor "$filename")
  local type=${3:-source}
  local hints="$4"

  if [[ $type != cache ]]; then
    message "${MESSAGE_COLOR}Unpacking $type file ${SPELL_COLOR}${1}${DEFAULT_COLOR}"  \
            "${MESSAGE_COLOR}for spell"  \
            "${SPELL_COLOR}${SPELL}${DEFAULT_COLOR}${MESSAGE_COLOR}.${DEFAULT_COLOR}"
  fi

  if [[ ! -f $filename ]]; then
    error_message "$PROBLEM_COLOR$type file not found!$DEFAULT_COLOR"
    return 1
  fi

  uncompress_core "$filename" "$compressor" "$hints" |
      unpack_core "$filename" "$compressor" "$hints"
}

#---------------------------------------------------------------------
## @param file to unpack
##
## Interface to unpack a file without any verification.
##
#---------------------------------------------------------------------
function real_unpack_file_simple() { uncompress_unpack "$@"; }


#---------------------------------------------------------------------
## @param absolute or relative file path
## @param empty or 'securely', which would skip SOURCE_CACHE
## @Stdout the real path of the file (sometimes relative to CWD)
##
## Given a file, locate_spell_file finds out where it really is within
## the spell hierarchy down to the grimoire root, and then tries cwd and
## then the source cache.
##
#---------------------------------------------------------------------
function locate_spell_file() {
  debug "libgrimoire" "Running locate_spell_file() $2 on $1"

  # checks in any case
  [ -f    "$SPELL_DIRECTORY/$1" ] && echo    "$SPELL_DIRECTORY/$1" && return 0
  [ -f  "$SECTION_DIRECTORY/$1" ] && echo  "$SECTION_DIRECTORY/$1" && return 0
  [ -f           "$GRIMOIRE/$1" ] && echo           "$GRIMOIRE/$1" && return 0
  [ -f                     "$1" ] && echo                     "$1" && return 0

  [ "$2" != "securely" ] &&  # checks in "secure" mode
  [ -f       "$SOURCE_CACHE/$1" ] && echo       "$SOURCE_CACHE/$1" && return 0

  message "${MESSAGE_COLOR}"                                 \
          "Problem: $1: file not found in spell hierarchy.${DEFAULT_COLOR}"  \
          > /dev/stderr
  echo  "$1"
  return 1

}

#---------------------------------------------------------------------
## @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
##
#---------------------------------------------------------------------
