#!/bin/bash

# This script unpacks an install package, verifying its validity and
# starts the install script in the package. The install package can be
# provided as the first argument or on stdin.

INSTALL_LOCK=/var/lock/spxpkg-install
INSTALL_STATUS=/var/run/install-status
DIAGNOSTICS_LOCK=/var/lock/diagnostics
GPGKEYRING=/usr/share/resources/default/pki/installer/keys.db

# The resources file
RCFILE="/usr/share/resources/default/recovery/main"
BLOCKLISTFILE="/usr/share/resources/default/recovery/pkg-blocklist"

LIBDIR=/usr/libexec/spxrecovery

# Load the resources
[ -f "$RCFILE" ] && . "$RCFILE"
# Load identifiers
[ -f /etc/spinetix/identifiers ] && . /etc/spinetix/identifiers

# Be sure there is no install in progress
(set -o noclobber; echo $$ > "$INSTALL_LOCK") 2>/dev/null
LOCK_PID="$(< "$INSTALL_LOCK")"
if [ "$LOCK_PID" != "$$" ]; then
    echo "ERROR: another install is already in progress." 1>&2
    exit 1
fi

#
# Prints the value of a firmware (U-Boot / GRUB) environment variable
# arguments: <variable name>
fw_envvar_print() {
    local var="$1"

    if [ "$HOSTTYPE" = "arm" ]; then
        fw_printenv -n "$var" 2>/dev/null
    else
        $LIBDIR/grub-env-wrapper get "$var" 2>/dev/null
    fi
}

#
# Deletes a firmware (U-Boot / GRUB) environment variable, if set
# arguments: <variable name>
fw_envvar_delete() {
    local var="$1"
    local cval

    if [ "$HOSTTYPE" = "arm" ]; then
        cval="$(fw_printenv -n "$var" 2>/dev/null)"
        if [ -n "$cval" ]; then
            fw_setenv "$var" > /dev/null
        else
            return 0
        fi
    else
        $LIBDIR/grub-env-wrapper unset "$var"
    fi
}

#
# Saves a firmware (U-Boot / GRUB) environment variable, if not already saved with the given value
# arguments: <variable name> <value>
fw_envvar_save() {
    local var="$1"
    local val="$2"
    local cval

    if [ "$HOSTTYPE" = "arm" ]; then
        cval="$(fw_printenv -n "$var" 2>/dev/null)"
        if [ "$cval" = "$val" ]; then
            return 0
        fi
        fw_setenv "$var" "$val" > /dev/null
    else
        cval="$($LIBDIR/grub-env-wrapper get "$var" 2>/dev/null)"
        if [ "$cval" = "$val" ]; then
            return 0
        fi
        $LIBDIR/grub-env-wrapper set "$var=$val"
    fi
}

WORKDIR=
cleanup() {
    rm -f "$INSTALL_LOCK"
    [ -z "$WORKDIR" ] || rm -rf "$WORKDIR"
    # Clear any automated upgrade request saved in the bootloader environment,
    # otherwise we could end up in a boot loop or override another install.
    if [ -n "$INRECOVERY" ]; then
        if [ "$(fw_envvar_print fwconfig)" = "upgrade" ]; then
            fw_envvar_delete fwconfig
        fi
    fi
}
trap cleanup EXIT

# Tests if the package for which the header was read matches an entry in the blocklist
package_blocklisted() {
    [ -f "$BLOCKLISTFILE" ] || return 1

    local bl_type bl_ver bl_rel bl_product junk

    while read -r bl_type bl_ver bl_rel bl_product junk; do
        # skip if comment or empty line
        [ -n "${bl_type%%#*}" ] || continue

        # match mandatory type and version, exact match only, no wildcards
        [ "$bl_type" = "$pkg_type" ] || continue
        [ "$bl_ver" = "$pkg_version" ] || continue

        # match product and type (* and empty match any)
        [ -z "$bl_rel" -o "$bl_rel" = '*' -o "$bl_rel" = "$pkg_release" ] || continue
        [ -z "$bl_product" -o "$bl_product" = '*' -o "$bl_product" = "$pkg_product" ] || continue

        # all criteria match
        return 0

    done < "$BLOCKLISTFILE"

    # none of the entries matched all criteria
    return 1
}

# Be sure there is no diagnostics already running
if [ -f "$DIAGNOSTICS_LOCK" ]; then
    echo "ERROR: a diagnostic test is in progress, cannot install." 1>&2
    exit 1
fi

# Detect if running from the recovery console or not
INRECOVERY=1
[ -d /usr/share/spxrecovery ] || INRECOVERY=


# Cleanup the status file
rm -f "$INSTALL_STATUS"

has_install_cap() {
    echo "$pkg_installcaps" | tr '\t ' '\n' | fgrep -qx "$1"
}

if [ "$#" -lt 1 ]; then
    echo "ERROR: passing package file from stdin no longer supported." 1>&2
    exit 1
fi

# Any options starting with "--" are considered local options,
# until a lone "--" is found.
extraopts=()
localopts=1
testonly=
keepuserdata=
keepconfig=
upgradeprep=
while [ "$#" -gt 1 ]; do
    if [ "${1#--}" != "$1" -a -n "$localopts" ]; then
        case "$1" in
            --)
                localopts=
                ;;
            --test)
                testonly=1
                ;;
            *)
                echo "ERROR: unknown option '$1'" >&2
                exit 1
                ;;
        esac
    else
        case "$1" in
            -keepuserdata)
                keepuserdata=1
                ;;
            -keepconfig)
                keepconfig=1
                ;;
            -upgradeprep)
                upgradeprep=1
                ;;
        esac
        extraopts+=("$1")
    fi
    shift
done
fwpkg="$1"

if [ -z "$fwpkg" ]; then
    echo "ERROR: no image package file name given." 1>&2
    exit 1
fi
if [ ! -r "$fwpkg" ]; then
    echo "ERROR: image package file not found or not readable." 1>&2
    exit 1
fi

# Make the image file be stdin for stream processing
exec < "$fwpkg"
if [ $? -ne 0 ]; then
    echo "ERROR: failed to start reading from image package." 1>&2
    exit 1
fi

# Move to /tmp to avoid problems with files that we
# may leave behind, etc.
WORKDIR="$(mktemp -dt install.XXXXXX)"
cd "$WORKDIR"

# The file were we accumulate all chunks for verification
chunks=chunks

# The file where to store the signature
fwpkgsig=fwpkgsig

# Clean essential verification files
rm -f "$chunks" "$fwpkgsig"
if [ $? -ne 0 ]; then
    echo "ERROR: failed preparing verification files" 1>&2
    exit 1
fi

# Check image package header
if ! read -r -n 1024; then
    echo "ERROR: image package is empty or non-readable." 1>&2
    exit 1
fi
if [ "$REPLY" != "--__===== $rc_fw_pkg_magic =====__--" ]; then
    echo "ERROR: not an installation image package or package corrupted." 1>&2
    exit 1
fi
echo "$REPLY" >> "$chunks"

read_chunk() {
    local name="$1"
    local tag="$(echo "$name" | tr '[a-z]' '[A-Z]')"

    rm -f "$name"
    if [ "$name" = "header" ]; then
	rm -f header.nosig
	if [ $? -ne 0 ]; then
	    echo "ERROR: failed preparing aux header files" 1>&2
	    exit 1
	fi
    fi
    read -r -n 1024
    if [ "$REPLY" != "--__===== $tag START =====__--" ]; then
	echo "ERROR: missing $name in image package." 1>&2
	exit 1
    fi
    echo "$REPLY" >> "$chunks"
    while read -r -n 1024; do
	if [ "$REPLY" = "--__===== $tag END =====__--" ]; then
	    break
	fi
	echo "$REPLY" >> $name
	if [ "$name" != "header" -o "${REPLY%%=*}" != "SIGNATURE" ]; then
	    echo "$REPLY" >> "$chunks"
	    [ "$name" = "header" ] && echo "$REPLY" >> header.nosig
	else
	    echo "${REPLY#*=}" >> "$fwpkgsig"
	fi
    done
    echo "$REPLY" >> "$chunks"
    if ! read -r -n 1024; then
	echo "ERROR: early termination of image package." 1>&2
	exit 1
    fi
    sum="${REPLY%% *}"
    if [ "$name" = "header" ]; then
	md5sum < header.nosig >> "$chunks"
    else
	echo "$REPLY" >> "$chunks"
    fi
    # NOTE: md5sum format is MD5 sum, two spaces, file name
    echo "$sum  $name" > $name.sum
    if ! md5sum -c "$name".sum >/dev/null 2>&1; then
	echo "ERROR: corrupted image package $name." 1>&2
	exit 1
    fi
}

# Read header and extract known values into shell variables
read_chunk header

pkg_product=
pkg_version=
pkg_release=
pkg_type=
pkg_settings=
pkg_installcaps=
pkg_description=
pkg_timestamp=
pkg_min_hwrev="$rc_fw_pkg_default_min_hwrev"
pkg_max_hwrev="$rc_fw_pkg_default_max_hwrev"
is_compat_pkg="no"

while read -r -n 1024; do
    name="${REPLY%%=*}"
    value="${REPLY#*=}"
    case "$name" in
	"PRODUCT") # there may be multiple of these
	    if [ "$value" = "$rc_model" ]; then
		is_compat_pkg="no"
		pkg_product="$value"
	    elif [ -z "$pkg_product" -a "$value" = "$rc_model_compat" ]; then
		is_compat_pkg="yes"
		pkg_product="$value"
	    fi
	    ;;
	"VERSION")
	    pkg_version="$value"
	    ;;
	"RELEASE")
	    pkg_release="$value"
	    ;;
	"TYPE")
	    pkg_type="$value"
	    ;;
	"SETTINGS")
	    pkg_settings="$value"
	    ;;
	"DESCRIPTION")
	    pkg_description="$value"
	    ;;
	"INSTALLCAPS") # there may be multiple of these, space separated tags
	    pkg_installcaps="${pkg_installcaps}${pkg_installcaps:+ }$value"
	    ;;
	"TIMESTAMP")
	    pkg_timestamp="$value"
	    ;;
	"MIN_HWREV")
	    pkg_min_hwrev="$value"
	    ;;
	"MAX_HWREV")
	    pkg_max_hwrev="$value"
	    ;;
    esac
done < header

# Check that we have a target product match
if [ -z "$pkg_product" ]; then
    if [ -z "$rc_model_compat" ]; then
	echo "ERROR: image package does not support $rc_model product" 1>&2
    else
	echo "ERROR: image package does not support $rc_model nor $rc_model_compat products" 1>&2
    fi
    exit 1
fi

# If no type is set and that is allowed, then we assume, for backwards
# compat, that any package 32 MiB or larger in size is a firmware
# package and otherwise it is a miscellaneous function package
if [ -z "$pkg_type" ]; then
    if [ "$rc_fw_pkg_type_guess" != "yes" ]; then
	echo "ERROR: image package is missing the TYPE header" 1>&2
	exit 1
    fi
    if [ "$(stat -L -c '%s' "$fwpkg")" -lt 33554432 ]; then
	pkg_type="misc"
    else
	pkg_type="firmware"
    fi
fi

# If no settings action specified assume "clear" for backwards compat
if [ -z "$pkg_settings" ]; then
    pkg_settings="clear"
fi

# If no description included assume firmware standard package for backwards compat
if [ -z "$pkg_description" ]; then
    if [ "$pkg_type" = "firmware" ]; then
	pkg_description="firmware"
    else
	pkg_description="miscellaneous package"
    fi
fi

# Now that we have all characteristics update status file
echo "PRODUCT=$pkg_product" >> "$INSTALL_STATUS"
echo "VERSION=$pkg_version" >> "$INSTALL_STATUS"
echo "RELEASE=$pkg_release" >> "$INSTALL_STATUS"
echo "TYPE=$pkg_type" >> "$INSTALL_STATUS"
echo "SETTINGS=$pkg_settings" >> "$INSTALL_STATUS"
echo "DESCRIPTION=$pkg_description" >> "$INSTALL_STATUS"
echo "INSTALLCAPS=$pkg_installcaps" >> "$INSTALL_STATUS"
echo "TIMESTAMP=$pkg_timestamp" >> "$INSTALL_STATUS"
echo "MIN_HWREV=$pkg_min_hwrev" >> "$INSTALL_STATUS"
echo "MAX_HWREV=$pkg_max_hwrev" >> "$INSTALL_STATUS"

# Output a message about actual package
echo "Starting installation of $pkg_product $pkg_description version $pkg_version build $pkg_release."

if package_blocklisted; then
    echo "ERROR: installation of this pkg is forbidden as it matches entry in the blocklist, retry with latest version."
    exit 1
fi

# Enforce some restrictions if not running from the recovery console
if [ -z "$INRECOVERY" ]; then
    if has_install_cap upgradeprep; then
        # upgradeprep is automatically enabled if supported and not running from recovery console
        if [ -n "${extraopts[*]}" -a "${extraopts[*]}" != "-upgradeprep" ]; then
            echo "ERROR: cannot specify any install options when installing upgradeprep capable pkg from outside the recovery console" >&2
            exit 1
        fi
        [ "${extraopts[*]}" != "-upgradeprep" ] && extraopts=( "-upgradeprep" )
        keepconfig=1
        keepuserdata=1
        upgradeprep=1
    elif [ -n "$upgradeprep" ]; then
        echo "ERROR: installing with upgradeprep but not supported by package" >&2
        exit 1
    else
        if [ "$pkg_type" = "firmware" ]; then
            echo "ERROR: a firmware package can only be installed from the recovery console" >&2
            exit 1
        fi
        if [ "$pkg_settings" = "clear" ]; then
            echo "ERROR: a package that clears the settings can only be installed from the recovery console" >&2
            exit 1
        fi
    fi
else
    if [ -n "$upgradeprep" ]; then
        echo "ERROR: installing with upgradeprep is not supported from the recovery console" >&2
        exit 1
    fi
fi

if [ -n "$keepuserdata" ] && ! has_install_cap keepuserdata; then
    echo "ERROR: installing with keepuserdata but not supported by package" >&2
    exit 1
fi

if [ -n "$keepconfig" ] && ! has_install_cap keepconfig; then
    echo "ERROR: installing with keepconfig but not supported by package" >&2
    exit 1
fi

# When the package is for a compatible model we only accept them if they are firmware packages
if [ "$is_compat_pkg" = "yes" -a "$pkg_type" != "firmware" ]; then
    echo "ERROR: package is for a compatible model but is not a firmware package" 1>&2
    exit 1
fi

# Check that package is compatible with this hardware revision
hwrev=0
if [ -n "$pkg_min_hwrev" -o -n "$pkg_max_hwrev" ]; then
    # on ARM platforms cpuinfo has revision name
    if [ "$HOSTTYPE" = "arm" ]; then
        hwrev="$(sed -n -e '/^Revision\s\+:\s*/{s/.*:\s*//;p}' < /proc/cpuinfo)"
    else
        # Add support for reading hardware revision on a per-platform basis as required
        hwrev=
    fi
    if [ -z "$hwrev" ]; then
        echo "ERROR: could not read hardware revision" 1>&2
        exit 1
    fi
    # value from cpuinfo is hexadecimal, but normally without leading 0x
    [ "${hwrev#0x}" != "${hwrev}" ] || hwrev="0x${hwrev}"
fi

if [ -n "$pkg_min_hwrev" ] && (( hwrev < pkg_min_hwrev )) ; then
    echo "ERROR: device hardware revision is lower than what package supports" 1>&2
    exit 1
fi
if [ -n "$pkg_max_hwrev" ] && (( hwrev > pkg_max_hwrev )) ; then
    echo "ERROR: device hardware revision is higher than what package supports" 1>&2
    exit 1
fi

read_chunk script
chmod +x script
if [ $? -ne 0 ]; then
    echo "ERROR: could not make image package script executable." 1>&2
    exit 1
fi

# Check image package header
if ! read -r -n 1024; then
    echo "ERROR: early termination of image package." 1>&2
    exit 1
fi
if [ "$REPLY" != "--__===== ARCHIVE =====__--" ]; then
    echo "ERROR: missing image package archive." 1>&2
    exit 1
fi
echo "$REPLY" >> "$chunks"

# Verify signature
if [ ! -s "$fwpkgsig" ]; then
    echo "ERROR: missing signature in image package file." 1>&2
    exit 1
fi
if [ ! -s "$GPGKEYRING" ]; then
    echo "ERROR: missing keys DB for verification." 1>&2
    exit 1
fi
echo "Verifying package..."
cat "$chunks" - 2> /dev/null | \
    gpgv --homedir /dev/null --ignore-time-conflict --keyring "$GPGKEYRING" "$fwpkgsig" - > /dev/null 2>&1
if [ $? -ne 0 ]; then
    echo "ERROR: package corrupted or forged" 1>&2
    exit 1
fi
echo "Package signature validated successfully"
echo

# Reopen package and position at start of archive
exec < "$fwpkg"
if [ $? -ne 0 ]; then
    echo "ERROR: failed reopening package file" 1>&2
    exit 1
fi
while read -r -n 1024; do
    [ "$REPLY" = "--__===== ARCHIVE =====__--" ] && break;
done

if [ -n "$testonly" ]; then
    exit 0
fi

#
# Here starts the actual installation
#

# Add /sbin dirs to PATH
PATH="${PATH}":/usr/local/sbin:/usr/sbin:/sbin

# Change from native to compatible mode and back as required for firmware package
if [ "$pkg_type" = "firmware" ]; then
    if [ "$is_compat_pkg" = "yes" ]; then
	if [ -n "$rc_compat_del_fwenv" -o -n "$rc_compat_set_fwenv" ]; then
	    echo "Firmware package is for a compatible product, enabling compatibility mode."
	fi
	if [ -n "$rc_compat_del_fwenv" ]; then
	    fw_envvar_delete "$rc_compat_del_fwenv"
	fi
	if [ -n "$rc_compat_set_fwenv" -a -n "$rc_compat_set_fwenv_val" ]; then
	    fw_envvar_save "$rc_compat_set_fwenv" "$rc_compat_set_fwenv_val"
	fi
    else
	if [ -n "$rc_native_del_fwenv" -o -n "$rc_native_set_fwenv" ]; then
	    echo "Firmware package is for native product, disabling compatibility mode."
	fi
	if [ -n "$rc_native_del_fwenv" ]; then
	    fw_envvar_delete "$rc_native_del_fwenv"
	fi
	if [ -n "$rc_native_set_fwenv" -a -n "$rc_native_set_fwenv_val" ]; then
	    fw_envvar_save "$rc_native_set_fwenv" "$rc_native_set_fwenv_val"
	fi
    fi
fi

if [ "$pkg_settings" = "clear" -a -z "$keepconfig" ]; then
    if [ -z "$INRECOVERY" ]; then
        echo "ERROR: attempt to clear settings when installing outside recovery console" >&2
        exit 1
    fi
    if [ "$HOSTTYPE" = "arm" ]; then
        # Erase the mainexargs and mem U-Boot variables so that it is
        # possible to revert to older firmware versions if need be.
        fw_envvar_delete "mem"
        fw_envvar_delete "mainexargs"
    fi
fi

if [ -n "$INRECOVERY" -a -n "$pkg_timestamp" ]; then
    systimestamp=$(date -u +%4Y%2m%2d%2H%2M%2S)
    if [ $pkg_timestamp -gt $systimestamp ]; then
	echo "System time older than package timestamp, setting to package timestamp"
	mmddhhmmss=${pkg_timestamp#????}
	mmddhhmm=${mmddhhmmss%??}
	ss=${mmddhhmmss#????????}
	ccyy=${pkg_timestamp%??????????}
	date -u ${mmddhhmm}${ccyy}.${ss}
    fi
fi

# Launch script, which inherits image package file from
# stdin at current position (start of archive)
# Pass the package image as well so that it may be saved.
./script "${extraopts[@]}" "$fwpkg"
RET=$?

# Erase the failsafe data if successful, so that it is
# in sync with main system. Also erase the fwconfig U-Boot
# environment variable to cancel any pending requests for
# firmware configuration
if [ "$RET" -eq 0 -a "$pkg_settings" = "clear" -a -z "$keepconfig" ]; then
    if [ -z "$INRECOVERY" ]; then
        echo "ERROR: attempt to clear settings when installing outside recovery console" >&2
        exit 1
    fi
    if [ "$HOSTTYPE" = "arm" -o "$SPX_HW_TYPE" = "fukiran" ]; then
        # failsafe data may be in MTD (arm only) or eMMC boot partitions
        if [ "$HOSTTYPE" = "arm" ]; then
            mtd="$(sed -e '/"failsafe-data"$/{s,:.*,,;p};d' /proc/mtd)"
            if [ -n "$mtd" ]; then
                flash_erase -q /dev/"$mtd" 0 0
            fi
        fi
        for bdev in mmcblk0boot0 mmcblk0boot1; do
            [ -b /dev/"$bdev" ] || continue
            blkid -c /dev/null -w /dev/null -s none /dev/"$bdev" || continue
            if [ -f /sys/block/"$bdev"/force_ro ]; then
                force_ro="$(< /sys/block/"$bdev"/force_ro)"
                echo 0 > /sys/block/"$bdev"/force_ro
                dd if=/dev/zero of=/dev/"$bdev" bs=4096 count=1 conv=fsync 2> /dev/null
                echo "$force_ro" > /sys/block/"$bdev"/force_ro
            fi
        done
    else
        # Partitions may change, so data in the identifiers file may be outdated,
        # we need to find the failsafe-data partitions anew.
        FAILSAFE_DATA_BDEV1=
        FAILSAFE_DATA_BDEV2=
        # Find the partition within that device that matches the GPT partition type UUID and label
        while read name partuuid partlabel junk; do
            [ "$partuuid" = "0fc63daf-8483-4772-8e79-3d69d8477de4" ] || continue
            case "$partlabel" in
                "failsafe-data-1")
                    FAILSAFE_DATA_BDEV1=/dev/"$name"
                    ;;
                "failsafe-data-2")
                    FAILSAFE_DATA_BDEV2=/dev/"$name"
                    ;;
            esac
        done < <(lsblk  --raw --noheadings -o NAME,PARTTYPE,PARTLABEL /dev/"$SPX_MAIN_DISK_NAME")
        for bdev in $FAILSAFE_DATA_BDEV1 $FAILSAFE_DATA_BDEV2; do
            [ -b "$bdev" ] && dd if=/dev/zero of="$bdev" bs=4096 count=1 conv=fsync 2> /dev/null
        done
    fi
    # Erase the fwconfig variable
    fw_envvar_delete "fwconfig"
    # Remove this file for web interface to display correct data
    rm -f /etc/spinetix/network
fi

# Return installation script exit status
exit "$RET"
