#!/bin/sh
#
#  scratchpkg
#
#  Copyright (c) 2018 by Emmett1  (emmett1.2miligrams@gmail.com)
#
#  This program 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 3 of the License, or
#  (at your option) any later version.
#
#  This program 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 program.  If not, see <https://www.gnu.org/licenses/>.
#

trap "interrupted" 1 2 3 15

export LC_ALL=C

interrupted() {
	echo
	ret 1
}

msg() {
	echo "==> $1"
}

msg2() {
	echo " -> $1"
}

msgerr() {
	echo "==> ERROR: $1" >&2
}

msgwarn() {
	echo "==> WARNING: $1" >&2
}

help() {
	cat << EOF
Usage:
  $(basename $0) [ <options> <package.spkg.txz> ]

Options:
  -u, --upgrade              upgrade package
  -r, --reinstall            reinstall package
  -c, --ignore-conflict      ignore conflict when installing package
  -v, --verbose              print files installed
  -h, --help                 show this help message
      --no-preinstall        skip preinstall script before build/install package
      --no-postinstall       skip postinstall script after install package
      --no-preupgrade        skip preupgrade script before upgrade package
      --no-postupgrade       skip postupgrade script after upgrade package
      --no-backup            skip backup when upgrading package
      --print-dbdir          print package database path
      --root=<path>          install to custom root directory
      
EOF
}

extract_opts() {
	while [ "$1" ]; do
		case $1 in
			--*) opts="$opts $1";;
			-*) char=${#1}; count=1
			    while [ "$count" != "$char" ]; do
			        count=$((count+1))
			        opts="$opts -$(echo $1 | cut -c $count)"
			   done;;
			*) opts="$opts $1"
		esac
		shift
	done
	echo $opts
}

parse_opts() {
	if [ -z "$1" ]; then
		SHOWHELP=yes
	else
		while [ "$1" ]; do
			case $1 in
			-u |           --upgrade) UPGRADE_PKG=yes ;;
			-r |         --reinstall) REINSTALL_PKG=yes ;;
			-c |   --ignore-conflict) IGNORE_CONFLICT=yes ;;
			-v |           --verbose) VERBOSE_INSTALL="-v" ;;
			-h |              --help) SHOWHELP=yes ;;
			         --no-preinstall) NO_PREINSTALL=yes ;;
			        --no-postinstall) NO_POSTINSTALL=yes ;;
			         --no-preupgrade) NO_PREUPGRADE=yes ;;
			        --no-postupgrade) NO_POSTUPGRADE=yes ;;
			             --no-backup) NO_BACKUP=yes ;;
			           --print-dbdir) PRINTDBDIR=yes ;;
			                --root=*) ROOT_DIR="${1#*=}" ;;
			            *.spkg.tar.*) PKGNAME="$(realpath $1)" ;;
			                       *) msg "Invalid option! ($1)"; exit 1 ;;
			esac
			shift
		done
	fi
}

ret() {
	# remove lock and all tmp files on exit
	rm -f "$ROOT_DIR/$LOCK_FILE" "$TMP_PKGADD" "$TMP_PKGINSTALL" "$TMP_CONFLICT"
	exit $1
}

isinstalled() {
	if [ -s "$ROOT_DIR/$PKGDB_DIR/$1/.pkginfo" ] && grep -q "$1" "$ROOT_DIR/$PKGDB_DIR/$1/.pkginfo"; then
		return 0
	else
		return 1
	fi
}

run_scripts() {
	if [ "$ROOT_DIR" ]; then
		xchroot "$ROOT_DIR" sh $@
	else
		sh $@
	fi
}

parse_opts $(extract_opts "$@")

SCRATCHPKG_DIR="var/lib/scratchpkg"
PKGDB_DIR="$SCRATCHPKG_DIR/index"
LOCK_FILE="$SCRATCHPKG_DIR/spkg.lock"

ROOT_DIR="${ROOT_DIR%/}" # remove trailing slash

[ "$PRINTDBDIR" ] && {
	echo "$ROOT_DIR/$PKGDB_DIR"
	ret 0
}

# show help page
[ "$SHOWHELP" ] || [ -z "$PKGNAME" ] && {
	help
	ret 0
}

[ -d "$ROOT_DIR/$PKGDB_DIR" ] || {
	msgerr "Package's database directory not exist: $ROOT_DIR/$PKGDB_DIR"
	ret 1
}

# check for root access
[ "$(id -u)" = "0" ] || {
	msgerr "Installing package need root access!"
	ret 1
}

# check for lock file
[ -f "$ROOT_DIR/$LOCK_FILE" ] && {
	msgerr "Cant install/remove package simultaneously."
	msgerr "remove '$ROOT_DIR/$LOCK_FILE' if no install/remove package process running."
	exit 1
}

touch "$ROOT_DIR/$LOCK_FILE" 2>/dev/null || {
	msgerr "Cant create lock file in '$ROOT_DIR/$LOCK_FILE'."
	exit 1
}

BASEPKGNAME=$(basename $PKGNAME)

# check existence of package file
[ -f "$PKGNAME" ] || {
	msgerr "Package '$1' not exist!"
	ret 1
}

noextname=${BASEPKGNAME%*.spkg.tar.*}
release=${noextname##*-}
noextname=${noextname%-*}
version=${noextname##*-}
name=${noextname%-*}

# get package information if installed
if isinstalled $name; then
	iversion=$(grep ^version $ROOT_DIR/$PKGDB_DIR/$name/.pkginfo | cut -d " " -f3-)
	irelease=$(grep ^release $ROOT_DIR/$PKGDB_DIR/$name/.pkginfo | cut -d " " -f3-)
	ALREADYINSTALLED=yes
fi

if [ "$ALREADYINSTALLED" = "yes" ] && [ ! "$UPGRADE_PKG" ] && [ ! "$REINSTALL_PKG" ]; then
	echo "Package '$name' already installed. ($iversion-$irelease)"
	ret 0
fi

# cant reinstall if not installed and cant upgrade if up-to-date
if [ "$UPGRADE_PKG" ] || [ "$REINSTALL_PKG" ]; then	
	if [ "$ALREADYINSTALLED" != "yes" ]; then
		msgerr "Package '$name' not installed."
		ret 1
	fi
	if [ "$UPGRADE_PKG" ]; then	
		if [ "$version-$release" = "$iversion-$irelease" ]; then
			echo "Package '$name' is up-to-date. ($iversion-$irelease)"
			ret 0
		fi
	fi
fi

TMP_PKGADD="$ROOT_DIR/$SCRATCHPKG_DIR/.tmp_pkgadd"
TMP_PKGINSTALL="$ROOT_DIR/$SCRATCHPKG_DIR/.tmp_pkginstall"
TMP_CONFLICT="$ROOT_DIR/$SCRATCHPKG_DIR/.tmp_conflict"

# check integrity of package and save list file/dirs to install in the meantime
tar -tf $PKGNAME > $TMP_PKGADD 2>/dev/null || {
	msgerr "Package '$1' is corrupted!"
	ret 1
}

# set operation whether install/upgrade/reinstall
# install is default
opr=install
[ "$UPGRADE_PKG" ] && opr=upgrade
[ "$REINSTALL_PKG" ] && opr=reinstall
echo "$opr: $name-$version-$release..."

# check for file conflict
if [ ! "$IGNORE_CONFLICT" ]; then
	grep -Ev "^.pkg*" "$TMP_PKGADD" | grep -v '/$' | while read -r line; do
		if [ "$line" = "${line%.*}.spkgnew" ]; then
			line=${line%.*}
		fi
		if [ -e "$ROOT_DIR/$line" ] || [ -L "$ROOT_DIR/$line" ]; then
			if [ "$UPGRADE_PKG" ] || [ "$REINSTALL_PKG" ]; then
				if ! grep -Fqx "$line" "$ROOT_DIR/$PKGDB_DIR/$name/.files"; then
					echo "$line"
					touch "$TMP_CONFLICT"
				fi
			else
				echo "$line"
				touch "$TMP_CONFLICT"
			fi
		fi
	done
	
	if [ -e "$TMP_CONFLICT" ]; then
		msgerr "File conflict found!"
		ret 1
	fi
fi

# pre-install and pre-upgrade script
if grep -qx .pkginstall $TMP_PKGADD; then
	TMP_PKGINSTALL_SCRIPT="$SCRATCHPKG_DIR/pkgadd_installscript"
	tar -xf "$PKGNAME" .pkginstall -O > "$ROOT_DIR/$TMP_PKGINSTALL_SCRIPT"
	if [ ! "$NO_PREINSTALL" ] && [ ! "$UPGRADE_PKG" ]; then
		(cd "$ROOT_DIR"/
			run_scripts "$TMP_PKGINSTALL_SCRIPT" pre-install "$version"
		)
	fi
	if [ "$UPGRADE_PKG" ] && [ ! "$NO_PREUPGRADE" ]; then
		(cd "$ROOT_DIR"/
			run_scripts "$TMP_PKGINSTALL_SCRIPT" pre-upgrade "$version" "$iversion"
		)
	fi
	rm -f "$ROOT_DIR/$TMP_PKGINSTALL_SCRIPT"
fi

# exclude .pkg* files when extract into system
for i in $(grep "^.pkg*" $TMP_PKGADD); do
	excludefile="$excludefile --exclude=$i"
done

rm -f $TMP_PKGINSTALL

# extract package into ROOT_DIR
tar --keep-directory-symlink -p -x -v -f "$PKGNAME" -C "$ROOT_DIR"/ $excludefile | while read -r line; do
	if [ "$line" = "${line%.*}.spkgnew" ]; then
		line=${line%.*}
		if [ "$UPGRADE_PKG" ] || [ "$REINSTALL_PKG" ]; then
			if [ ! -e "$ROOT_DIR/$line" ] || [ "$NO_BACKUP" = yes ]; then
				mv "$ROOT_DIR/$line".spkgnew "$ROOT_DIR/$line"
			fi
		else
			mv "$ROOT_DIR/$line".spkgnew "$ROOT_DIR/$line"
		fi
	fi
	[ "$VERBOSE_INSTALL" ] && echo "extracted '$line'"
	echo "$line" >> $TMP_PKGINSTALL
done

# remove old files from old package that not exist in new package
if [ "$UPGRADE_PKG" ] || [ "$REINSTALL_PKG" ]; then
	rmlist_file="$ROOT_DIR/$SCRATCHPKG_DIR/.rmlist_file"
	rmlist_dir="$ROOT_DIR/$SCRATCHPKG_DIR/.rmlist_dir"
	reserve_dir="$ROOT_DIR/$SCRATCHPKG_DIR/.reserve_dir"
	rmlist_all="$ROOT_DIR/$SCRATCHPKG_DIR/.rmlist_all"
	grep '/$' $ROOT_DIR/$PKGDB_DIR/*/.files \
		| grep -v $ROOT_DIR/$PKGDB_DIR/$name/.files \
		| awk -F : '{print $2}' \
		| sort \
		| uniq > $reserve_dir                                                        # get list reserved dirs
	grep -Fxv -f "$TMP_PKGINSTALL" $ROOT_DIR/$PKGDB_DIR/$name/.files > $rmlist_all   # get list files and dirs to remove
	grep -v '/$' "$rmlist_all" | tac > "$rmlist_file"                                # get files only to remove
	grep -Fxv -f "$reserve_dir" "$rmlist_all" | grep '/$' | tac > "$rmlist_dir"      # get dirs only (safe) to remove
	(cd "$ROOT_DIR"/
		[ -s $rmlist_file ] && xargs -a $rmlist_file -d'\n' rm $VERBOSE_INSTALL
		[ -s $rmlist_dir ]  && xargs -a $rmlist_dir -d'\n' rmdir $VERBOSE_INSTALL
	)
	rm -f "$rmlist_file" "$rmlist_dir" "$reserve_dir" "$rmlist_all"
fi

# register package into database
rm -fr "$ROOT_DIR/$PKGDB_DIR/$name"
mkdir "$ROOT_DIR/$PKGDB_DIR/$name"
echo "name = $name" > "$ROOT_DIR/$PKGDB_DIR/$name/.pkginfo"
echo "version = $version" >> "$ROOT_DIR/$PKGDB_DIR/$name/.pkginfo"
echo "release = $release" >> "$ROOT_DIR/$PKGDB_DIR/$name/.pkginfo"
install -m644 "$TMP_PKGINSTALL" "$ROOT_DIR/$PKGDB_DIR/$name/.files"

for ii in $(grep ^.pkg* $TMP_PKGADD); do
	pkgfiles="$pkgfiles $ii"
done

if [ "$pkgfiles" ]; then
	tar -x -f "$PKGNAME" -C "$ROOT_DIR/$PKGDB_DIR/$name" $pkgfiles >/dev/null 2>&1
fi

if [ -f "$ROOT_DIR/$PKGDB_DIR/$name/.pkginstall" ]; then
	if [ ! "$NO_POSTINSTALL" ] && [ ! "$UPGRADE_PKG" ]; then
		(cd "$ROOT_DIR"/
			run_scripts "$PKGDB_DIR/$name/.pkginstall" post-install "$version"
		)
	fi
	if [ "$UPGRADE_PKG" ] && [ ! "$NO_POSTUPGRADE" ]; then
		(cd "$ROOT_DIR"/
			run_scripts "$PKGDB_DIR/$name/.pkginstall" post-upgrade "$version" "$iversion"
		)
	fi
fi

# running ldconfig
if [ -x "$ROOT_DIR"/sbin/ldconfig ]; then
	"$ROOT_DIR"/sbin/ldconfig -r "$ROOT_DIR"/
fi

ret 0
