#! /usr/bin/env bash
## vim:set ts=4 sw=4 et:
set -e; set -o pipefail
argv0=$0; argv0abs=$(readlink -fn "$argv0"); argv0dir=$(dirname "$argv0abs")

#
# Copyright (C) Markus Franz Xaver Johannes Oberhumer
#
# very first version of the upx-testsuite; requires:
#   $upx_exe                (required, but with convenience fallback "./upx")
#   $upx_testsuite_SRCDIR   (required, but with convenience fallback)
#   $upx_testsuite_BUILDDIR (optional)
#
# optional settings:
#   $upx_exe_runner         (e.g. "qemu-x86_64 -cpu Nehalem" or "valgrind")
#   $UPX_TESTSUITE_VERBOSE
#   $UPX_TESTSUITE_LEVEL
#
# see https://github.com/upx/upx-testsuite.git
#

#***********************************************************************
# init & checks
#***********************************************************************

# upx_exe
[[ -z $upx_exe && -f ./upx && -x ./upx ]] && upx_exe=./upx # convenience fallback
if [[ -z $upx_exe ]]; then echo "UPX-ERROR: please set \$upx_exe"; exit 1; fi
if [[ ! -f $upx_exe ]]; then echo "UPX-ERROR: file '$upx_exe' does not exist"; exit 1; fi
upx_exe=$(readlink -fn "$upx_exe") # make absolute
[[ -f $upx_exe ]] || exit 1

# set emu and run_upx
emu=()
if [[ -n $upx_exe_runner ]]; then
    # usage examples:
    #   export upx_exe_runner="qemu-x86_64 -cpu Nehalem"
    #   export upx_exe_runner="valgrind --leak-check=no --error-exitcode=1 --quiet"
    #   export upx_exe_runner="wine"
    IFS=' ' read -r -a emu <<< "$upx_exe_runner" # split at spaces into array
elif [[ -n $CMAKE_CROSSCOMPILING_EMULATOR ]]; then
    IFS=';' read -r -a emu <<< "$CMAKE_CROSSCOMPILING_EMULATOR" # split at semicolons into array
fi
run_upx=( "${emu[@]}" "$upx_exe" )
echo "run_upx='${run_upx[*]}'"

# run_upx sanity check
if ! "${run_upx[@]}" --version-short >/dev/null; then echo "UPX-ERROR: FATAL: upx --version-short FAILED"; exit 1; fi
if ! "${run_upx[@]}" -L >/dev/null 2>&1; then echo "UPX-ERROR: FATAL: upx -L FAILED"; exit 1; fi
if ! "${run_upx[@]}" --help >/dev/null;  then echo "UPX-ERROR: FATAL: upx --help FAILED"; exit 1; fi

# upx_testsuite_SRCDIR
if [[ -z $upx_testsuite_SRCDIR ]]; then
    # convenience fallback: search standard locations below upx top-level directory
    if [[                 -d "$argv0dir/../../../upx--upx-testsuite.git/files/packed" ]]; then
        upx_testsuite_SRCDIR="$argv0dir/../../../upx--upx-testsuite.git"
    elif [[               -d "$argv0dir/../../../upx-testsuite.git/files/packed" ]]; then
        upx_testsuite_SRCDIR="$argv0dir/../../../upx-testsuite.git"
    elif [[               -d "$argv0dir/../../../upx-testsuite/files/packed" ]]; then
        upx_testsuite_SRCDIR="$argv0dir/../../../upx-testsuite"
    fi
fi
if [[ ! -d "$upx_testsuite_SRCDIR/files/packed" ]]; then
    echo "invalid or missing \$upx_testsuite_SRCDIR:"
    echo "  please git clone https://github.com/upx/upx-testsuite.git"
    echo "  and set (export) the envvar upx_testsuite_SRCDIR to the local file path"
    exit 1
fi
upx_testsuite_SRCDIR=$(readlink -fn "$upx_testsuite_SRCDIR") # make absolute
[[ -d $upx_testsuite_SRCDIR ]] || exit 1

# upx_testsuite_BUILDDIR
if [[ -z $upx_testsuite_BUILDDIR ]]; then
    upx_testsuite_BUILDDIR="./tmp-upx-testsuite"
fi
mkdir -p "$upx_testsuite_BUILDDIR" || exit 1
upx_testsuite_BUILDDIR=$(readlink -fn "$upx_testsuite_BUILDDIR") # make absolute
[[ -d $upx_testsuite_BUILDDIR ]] || exit 1
cd / && cd "$upx_testsuite_BUILDDIR" || exit 1
: > ./.mfxnobackup

# run_upx sanity check after "cd"
if ! "${run_upx[@]}" --version-short >/dev/null; then
    echo "UPX-ERROR: FATAL: upx --version-short FAILED"
    echo "please make sure that \$upx_exe contains ABSOLUTE file paths and can be run from any directory"
    echo "INFO: run_upx='${run_upx[*]}'"
    exit 1
fi

#***********************************************************************
# setup
#***********************************************************************

#set -x # debug

exit_code=0
num_errors=0
all_errors=

export UPX="--prefer-ucl --no-color --no-progress"
export UPX_DEBUG_DISABLE_GITREV_WARNING=1
export UPX_DEBUG_DOCTEST_DISABLE=1 # already checked above

case $UPX_TESTSUITE_LEVEL in
    [0-8]) ;;
    *) UPX_TESTSUITE_LEVEL=999 ;;
esac
if [[ $UPX_TESTSUITE_LEVEL == 0 ]]; then
    echo "UPX testsuite SKIPPED."
    exit 0
fi

rm -rf ./testsuite_1
mkdir testsuite_1 || exit 1
cd testsuite_1 || exit 1

#***********************************************************************
# support functions
#***********************************************************************

run_upx() {
    local flags="--disable-random-id --fake-stub-version=5.01 --fake-stub-year=2025"
    local ec=0
    if [[ $UPX_TESTSUITE_VERBOSE == 1 ]]; then
        echo "LOG: '${run_upx[*]}' $*"
    fi
    "${run_upx[@]}" $flags "$@" || ec=$?
    if [[ $ec != 0 ]]; then
        echo "FATAL: '${run_upx[*]}' $*"
        echo "  (exit code was $ec)"
        exit 42
    fi
}

testsuite_header() {
    local x='==========='; x="$x$x$x$x$x$x$x"
    echo -e "\n${x}\n${1}\n${x}\n"
}

testsuite_split_f() {
    fd=$(dirname "$1")
    fb=$(basename "$1")
    fsubdir=$(basename "$fd")
    # sanity checks
    if [[ ! -f $1 || -z $fsubdir || -z $fb ]]; then
        fd=''; fb=''; fsubdir=''
    fi
}

testsuite_check_sha() {
    (cd "$1" && sha256sum -b [0-9a-zA-Z]*/* | LC_ALL=C sort -k2) > $1/.sha256sums.current
    echo
    cat $1/.sha256sums.current
    if ! cmp -s $1/.sha256sums.expected $1/.sha256sums.current; then
        echo "UPX-ERROR: $1 FAILED: checksum mismatch"
        diff -u $1/.sha256sums.expected $1/.sha256sums.current || true
        exit_code=99
        let num_errors+=1 || true
        all_errors="${all_errors} $1"
        #exit 99
    fi
    echo
}

testsuite_check_sha_decompressed() {
    (cd "$1" && sha256sum -b [0-9a-zA-Z]*/* | LC_ALL=C sort -k2) > $1/.sha256sums.current
    if ! cmp -s $1/.sha256sums.expected $1/.sha256sums.current; then
        cat $1/.sha256sums.current
        echo "UPX-ERROR: FATAL: $1 FAILED: decompressed checksum mismatch"
        diff -u $1/.sha256sums.expected $1/.sha256sums.current || true
        exit 98
    fi
}

testsuite_use_canonicalized=1
testsuite_run_compress() {
    testsuite_header $testdir
    local files f
    if [[ $testsuite_use_canonicalized == 1 ]]; then
        files='t020_canonicalized/*/*'
    else
        files='t010_decompressed/*/*'
    fi
    for f in $files; do
        testsuite_split_f "$f"
        [[ -z $fb ]] && continue
        echo "# $f"
        mkdir -p "$testdir/$fsubdir" "$testdir/.decompressed/$fsubdir"
        run_upx -qq --prefer-ucl "$@" "$f" -o "$testdir/$fsubdir/$fb"
        run_upx -qq -d "$testdir/$fsubdir/$fb" -o "$testdir/.decompressed/$fsubdir/$fb"
    done
    testsuite_check_sha $testdir
    run_upx -qq -l "$testdir"/*/*
    run_upx -qq --file-info "$testdir"/*/*
    run_upx -q -t "$testdir"/*/*
    if [[ $testsuite_use_canonicalized == 1 ]]; then
        # check that after decompression the file matches the canonicalized version
        cp t020_canonicalized/.sha256sums.expected $testdir/.decompressed/
        testsuite_check_sha_decompressed $testdir/.decompressed
        rm -rf "./$testdir/.decompressed"
    fi
}

#***********************************************************************
# expected checksums
#
# To ease maintenance of this script in case of updates this section
# can be automatically re-created from the current checksums -
# see call of function recreate_expected_sha256sums below.
#***********************************************************************

recreate_expected_sha256sums() {
    local o="$1"
    local files f d
    echo "########## begin .sha256sums.recreate" > "$o"
    files='*/.sha256sums.current'
    for f in $files; do
        d=$(dirname "$f")
        echo "expected_sha256sums__${d}="'"\' >> "$o"
        cat "$f" >> "$o"
        echo '"' >> "$o"
    done
    echo "########## end .sha256sums.recreate" >> "$o"
}

source "$argv0dir/upx_testsuite_1-expected_sha256sums.sh" || exit 1

#***********************************************************************
# decompression tests
#***********************************************************************

testdir=t010_decompressed
mkdir $testdir; v=expected_sha256sums__$testdir; echo -n "${!v}" >$testdir/.sha256sums.expected

testsuite_header $testdir
for f in "$upx_testsuite_SRCDIR"/files/packed/*/upx-3.9[15]*; do
    testsuite_split_f "$f"
    [[ -z $fb ]] && continue
    echo "# $f"
    mkdir -p "$testdir/$fsubdir"
    run_upx -qq -d "$f" -o "$testdir/$fsubdir/$fb"
done
testsuite_check_sha $testdir

# run one pack+unpack step to canonicalize the files
testdir=t020_canonicalized
mkdir $testdir; v=expected_sha256sums__$testdir; echo -n "${!v}" >$testdir/.sha256sums.expected

testsuite_header $testdir
for f in t010_decompressed/*/*; do
    testsuite_split_f "$f"
    [[ -z $fb ]] && continue
    echo "# $f"
    mkdir -p "$testdir/$fsubdir/.packed"
    run_upx -qq --prefer-ucl -1 "$f" -o "$testdir/$fsubdir/.packed/$fb"
    run_upx -qq -d "$testdir/$fsubdir/.packed/$fb" -o "$testdir/$fsubdir/$fb"
done
testsuite_check_sha $testdir

#***********************************************************************
# compression tests
# info: we use fast compression levels because we want to
#   test UPX and not the compression libraries
#***********************************************************************

if [[ $UPX_TESTSUITE_LEVEL -ge 2 ]]; then
testdir=t110_compress_ucl_nrv2b_3_no_filter
mkdir $testdir; v=expected_sha256sums__$testdir; echo -n "${!v}" >$testdir/.sha256sums.expected
time testsuite_run_compress --nrv2b -3 --no-filter
fi

if [[ $UPX_TESTSUITE_LEVEL -ge 3 ]]; then
testdir=t120_compress_ucl_nrv2d_3_no_filter
mkdir $testdir; v=expected_sha256sums__$testdir; echo -n "${!v}" >$testdir/.sha256sums.expected
time testsuite_run_compress --nrv2d -3 --no-filter
fi

if [[ $UPX_TESTSUITE_LEVEL -ge 4 ]]; then
testdir=t130_compress_ucl_nrv2e_3_no_filter
mkdir $testdir; v=expected_sha256sums__$testdir; echo -n "${!v}" >$testdir/.sha256sums.expected
time testsuite_run_compress --nrv2e -3 --no-filter
fi

if [[ $UPX_TESTSUITE_LEVEL -ge 5 ]]; then
testdir=t140_compress_lzma_2_no_filter
mkdir $testdir; v=expected_sha256sums__$testdir; echo -n "${!v}" >$testdir/.sha256sums.expected
time testsuite_run_compress --lzma -2 --no-filter
fi

if [[ $UPX_TESTSUITE_LEVEL -ge 6 ]]; then
testdir=t150_compress_ucl_2_all_filters
mkdir $testdir; v=expected_sha256sums__$testdir; echo -n "${!v}" >$testdir/.sha256sums.expected
time testsuite_run_compress -2 --all-filters
fi

if [[ $UPX_TESTSUITE_LEVEL -ge 7 ]]; then
testdir=t160_compress_all_methods_1_no_filter
mkdir $testdir; v=expected_sha256sums__$testdir; echo -n "${!v}" >$testdir/.sha256sums.expected
time testsuite_run_compress --all-methods -1 --no-filter
fi

if [[ $UPX_TESTSUITE_LEVEL -ge 8 ]]; then
testdir=t170_compress_all_methods_no_lzma_5_no_filter
mkdir $testdir; v=expected_sha256sums__$testdir; echo -n "${!v}" >$testdir/.sha256sums.expected
time testsuite_run_compress --all-methods --no-lzma -5 --no-filter
fi

#***********************************************************************
# summary
#***********************************************************************

# recreate checksums from current version for an easy update in case of changes
recreate_expected_sha256sums .sha256sums.recreate

testsuite_header "UPX testsuite summary: level $UPX_TESTSUITE_LEVEL"
run_upx --version-short
echo
echo "upx_exe='$upx_exe'"
ls -l "$upx_exe"
if command -v file >/dev/null; then
    file "$upx_exe" || true
fi
echo "run_upx='${run_upx[*]}'"
echo "upx_testsuite_SRCDIR='$upx_testsuite_SRCDIR'"
echo "upx_testsuite_BUILDDIR='$upx_testsuite_BUILDDIR'"
echo ".sha256sums.{expected,current} counts:"
cat ./*/.sha256sums.expected | LC_ALL=C sort | wc
cat ./*/.sha256sums.current  | LC_ALL=C sort | wc
echo
if [[ $exit_code == 0 ]]; then
    echo "UPX testsuite passed. All done."
else
    echo "UPX-ERROR: UPX testsuite FAILED:${all_errors}"
    echo "UPX-ERROR: UPX testsuite FAILED with $num_errors error(s). See log file."
fi
exit $exit_code
