#!/bin/bash
# install-redir  -- redirected file copying
# Copyright (c) 2009 Institute for Defense Analyses
# 
# "MIT" license
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

# This program redirects the destination to inside another directory
# REDIR_DESTDIR, if writing to the original destination's directory is
# privileged.  Source files are already in REDIR_DESTDIR will take precedence
# when used as sources.
# This automatically creates redirected target directories if needed.
# Note: this is implemented by parameter-twiddling and calling the "real"
# program, and depends on the helper program redir_name.
# Example:
#  REDIR_DESTDIR=/tmp/mydir ./cp myprogram /usr/bin
# will copy "myprogram" to "/tmp/mydir/usr/bin/myprogram".

# LIMITATIONS/BUGS:
# Options with optional arguments cannot accept arguments that begin with "-",
#    due to a limitation of getopt(1).
# Shell I/O redirection (e.g., < and >) aren't redirected

IFS=`printf '\n\t'`

progname="install-redir"
err="${progname}: error: "

if `getopt -T >/dev/null 2>&1` ; [ $? -ne 4 ] ; then
  echo "$err Need an enhanced getopt(1) that can handle quoted text." >&2
  exit 1
fi

declare -a newargs
declare -a files
declare -i minus1=-1

# These are the options of GNU "install", from its code;
# the code below uses this order:
# FIXME: SELINUX_CONTEXT needs doing.
#   {"backup", optional_argument, NULL, 'b'},
#   {GETOPT_SELINUX_CONTEXT_OPTION_DECL},
#   {"directory", no_argument, NULL, 'd'},
#   {"group", required_argument, NULL, 'g'},
#   {"mode", required_argument, NULL, 'm'},
#   {"no-target-directory", no_argument, NULL, 'T'},
#   {"owner", required_argument, NULL, 'o'},
#   {"preserve-timestamps", no_argument, NULL, 'p'},
#   {"preserve-context", no_argument, NULL, PRESERVE_CONTEXT_OPTION},
#      /* --preserve_context is obsolete, will be removed...  */
#   {"preserve_context", no_argument, NULL, PRESERVE_CONTEXT_OPTION},
#   {"strip", no_argument, NULL, 's'},
#   {"strip-program", required_argument, NULL, STRIP_PROGRAM_OPTION},
#   {"suffix", required_argument, NULL, 'S'},
#   {"target-directory", required_argument, NULL, 't'},
#   {"verbose", no_argument, NULL, 'v'},
#   {GETOPT_HELP_OPTION_DECL},
#   {GETOPT_VERSION_OPTION_DECL},
#   {NULL, 0, NULL, 0}

# NOTE: Many "install" programs support a -C (--compare) option, which
# doesn't modify the destination if source+dest and identical content+metadata.


TEMP=`getopt -o "b::dg:m:To:psS:t:vC" \
      --long backup:: --long directory \
      --long group: --long mode: --long no-target-directory \
      --long owner: --long preserve-timestamps --long preserve-context \
      --long preserve_context --long strip \
      --long strip-program: --long suffix: --long target-directory \
      --long verbose --long help --long version \
      --long compare \
      -n "$progname"  -- "$@"`

# Do this to handle whitespace; see getopt(1).
eval set -- "$TEMP"

no_target_directory=""
target_directory=""
skip_last=""
directory_mode=""

while true ; do
  case "$1" in
    -p|--preserve-timestamps|--preserve-context|--preserve_context|-s|--strip|-v|--verbose)
      newargs[${#newargs[*]}]="$1"
      shift ;;

    -b|--backup) # Optional argument
      case "$2" in
        -*) # No argument.
          newargs[${#newargs[*]}]="$1"
          shift ;;
        *) # Option has an argument.
          newargs[${#newargs[*]}]="$1=$2"
          shift ; shift ;;
      esac ;;
    -g|--group|-m|--mode|--owner|-o|--strip-program|-S|--suffix)
      # Pass through the option's required argument.
      newargs[${#newargs[*]}]="$1=$2"
      shift ; shift ;;

    -d|--directory)
      directory_mode=true
      shift ;;

    -T|--no-target-directory) # FIXME: Should we do anything about this?
      no_target_directory=true
      newargs[${#newargs[*]}]="$1"
      shift ;;

    -t|--target-directory)
      if [ -n "$target_directory" ] ; then
        echo "$err Multiple target directoriess: '$target_directory' and '$2'" >&2
        exit 1
      fi
      target_directory="$2"
      shift ; shift ;;

    # FIXME:
    # --help
    # --version
    --) shift; break ;;
    -*) # Shouldn't normally get here (getopt should screen these),
        # but if we get here, handle it gracefully.
      echo "$progname: Warning - unrecognized option: $1" >&2
      newargs[${#newargs[*]}]="$1"
      shift ;;
    *) echo "$err Internal error with argument $1!" >&2 ; exit 1 ;;
  esac
done


# If given -d (directory) option, create the directories specially.
if [ -n "$directory_mode" ] ; then
  for directory ; do
    redir_directory=`redir_name "$directory"`
    /usr/bin/install -d "${newargs[@]}" "$redir_directory" || exit
  done
  exit 0
fi

# Determine list of files and how many, but don't consume last one yet.
while [ $# -gt 1 ] ; do
  files[${#files[*]}]="$1"
  shift
done
file2name=""
number_of_files=${#files[*]}
if [ $# -gt 0 ] ; then
  number_of_files=$(( $number_of_files + 1 ))
fi

# Determine what to do, based on the number of files passed.
if [ $number_of_files -eq 0 ] ; then
  echo "$err No files to process." >&2
  exit 1
elif [ $number_of_files -eq 1 ] ; then
  if [ ! -n "$target_directory" ] ; then
    echo "$err No destination provided." >&2
    exit 1
  fi
  files[${#files[*]}]="$1"
  shift
elif [ $number_of_files -eq 2 ] ; then
  if [ -n "$target_directory" ] ; then
    files[${#files[*]}]="$1"
    shift
  else
    # We must determine if the 2nd argument (now $1) is a directory or file -
    # it's a dir if it ends in "/", is a dir, or is a dir when redirected.
    redirected_name=`redir_name "$1"`
    if [ ${1:$minus1:1} = "/" -o -d "$1" -o -d "$redirected_name" ] ; then
      target_directory="$1"
      shift
    else
      # Special case: "install file1 file2".
      # FIXME: Should we just ALWAYS redirect?
      file2name=`redir_name --write "$1"`
      shift
    fi
  fi
elif [ $number_of_files -gt 2 ] ; then
  if [ -n "$target_directory" ] ; then
    echo "$err Target directory set, but too many files" >&2
    exit 1
  fi
  target_directory="$1"
  shift
fi
if [ $# -gt 0 ] ; then  # This should never happen.
  echo "$err Unprocessed end file." >&2
  exit 1
fi

# Now we have the complete list of files to read from, and the
# target_directory or file2name to write to.

# Redirect where to read files FROM, if there are updated files/dirs there.
# FIXME: If "from" dir exists in redirected dir, do something special?
for i in "${!files[@]}" ; do
  files[$i]=`redir_name --read "${files[$i]}"`
done

if [ -n "$target_directory" ] ; then
  target_directory=`redir_name "$target_directory"`
  /bin/mkdir -p "$target_directory" || exit
  if [ ! -d "$target_directory" ] ; then
    echo "$err Failed to create $target_directory" >&2
    exit 1
  fi
  newargs[${#newargs[*]}]="-t"
  newargs[${#newargs[*]}]="$target_directory"
elif [ -n "$file2name" ] ; then
  # We're copying to a specific named destination file; add it to file list:
  # Slip the new name into the "files" list
  files[${#files[*]}]="$file2name"
fi

for file in "${files[@]}" ; do
  newargs[${#newargs[*]}]="$file"
done

exec /usr/bin/install "${newargs[@]}" || exit

