#!/bin/sh

# osv_version.sh
#   Wrapper tool to centralise calls to "get" current OSV system version, and to "compare"
#   version-string against system-version or against another version-string. Trims and
#   simplifies strings so comparisons are minimal semantic numerical comparisons (e.g.
#   "V9R4 equals 9.4", etc). use --help flag for more detail.
#   NB:
#    Handling of ancient OSV versions up to V4 has been obsoleted to keep the
#    version-comparison logic robust and simple (not relying on heuristics and "accidental
#    logic" like "[ 11.x \< V9.x ]" string comparisons (which were previously being used
#    because for e.g. OSV v3 & 4 where the node.cfg "srx_build_id" value is "11.xxx" and
#    "12.xxx" without the leading "V").
#
# Authors:
#  Rowan Thorpe, for Atos (c), 2019

# portability/compatibility...
if test -n "${ZSH_VERSION}"; then
    setopt shwordsplit
fi

#--8<--
# TODO: When we no longer support OSV on <= SLES 11 (even for upgrading from) then remove the
# backward-compat handling function and just use the portable "sed -E" directly
if sed -E -e '' </dev/null >/dev/null 2>&1; then
    sed_ext() { sed -E "${@}"; } # forward compatibility
else
    sed_ext() { sed -r "${@}"; } # backward compatibility
fi
#TODO: -->8-- up to here...

croak()
{
    printf "${@}" | sed -e "s/^/${scriptname}: /" >&2
}

die()
{
    croak "${@}"
    exit ${FALSE}
}

get_head()
{
    printf '%s' "${1}" | cut -d. -f1
}

get_tail()
{
    printf '%s' "${1}" | grep '\.' | cut -d. -f2-
}

get_parts()
{
    printf '%s' "${1}" | cut -d. -f${parts:-1-}
}

simplify_version()
{
    sed_ext \
        -e '1! d' \
        -e 's/^V//' \
        -e 's/^([0-9]+)R/\1./' \
        -e 's/\.ALL//' \
        -e 's/(^|\.)0+([1-9])/\1\2/g' \
        -e 's/(^|\.)0+(\.|$)/\10\2/g'
}

version_format_ok()
{
    expr "${1}" : '\([0-9]\+\(\.[0-9]\+\)*\)$' >/dev/null
}

scriptname="$(basename "${0}")"

#TODO: --8<-- ensure cfgread is delivered everywhere so this fallback is never needed, then remove it
if ! command -v cfgread >/dev/null 2>&1; then
    logger --id=${$} "${scriptname}: cfgread not found in \$PATH ($PATH), using internal fallback instead." >/dev/null 2>&1 || :
    cfgread() {
        case "${1}" in
            srx_build_id)
                sed -n -e 's/^srx_build_id:[ \t]*//' -e tP -e b -e :P -e 's/[ \t]*$//' -e p /etc/hiq8000/node.cfg 2>/dev/null
                ;;
            *)
                die 'cfgread executable was not found in $PATH and cfgread wrapper function only handles "srx_build_id" arg\n'
                ;;
        esac
    }
fi
#TODO: -->8-- up to here...

## main

TRUE=0
FALSE=1
parts=''
simplify=${FALSE}
while test ${#} -ne 0; do
    case "${1}" in
        --simplify|-s)
            simplify=${TRUE}
            shift
            ;;
        --parts|-p|--major|-M|--minor|-m|--patch|-P)
            test -z "${parts}" || die 'more than one --parts, --major, --minor, or --patch flag used\n'
            case "${1}" in
                --parts|-p)
                    parts="${2}"
                    shift 2
                    ;;
                --major|-M)
                    parts='1'
                    shift
                    ;;
                --minor|-m)
                    parts='2'
                    shift
                    ;;
                --patch|-P)
                    parts='3'
                    shift
                    ;;
            esac
            ;;
        --)
            shift
            break
            ;;
        --help|-h|--get|-g|--compare|-c|--selftest|selftest|--selftest-cpp|selftest-cpp)
            break
            ;;
        -*)
            die 'common-option "%s" is unrecognized (should be --parts|-p, --major|-M, --minor|-m, --P|--patch)\n' "${1}"
            ;;
        *)
            break
            ;;
    esac
done
test -n "${parts}" && simplify=${TRUE}

action="${1}"
shift
case "${action}" in
    --help|-h|help)
        cat <<EOF
Usage: ${scriptname} [COMMON_OPTS] ACTION [ARGS]

COMMON_OPTS:
  -s|--simplify: Output/compare simplified/normalised version of the string (implied for compare)
  -p|--parts XX: Comma-separated part-numbers of the dot-separated string to process ("get" output
                 is still dot-separated, implies -s)
  -M|--major, -m|--minor, -P|--patch:
                 Shortcuts for '-p 1', '-p 2', and '-p 3' respectively (incompatible with -p and
                 each other)

ACTION:
 --help|-h|help:       This message
 --get|-g|get:         Get the system OSV version
 --compare|-c|compare: Compare two versions ("sys" placeholder uses system-version)

(MAINTAINER ACTION):
 --selftest|selftest:         Test-run the shellscript examples from --help output
                              (run on an OSV system to have node.cfg available at runtime)
 --selftest-cpp|selftest-cpp: Compile and test-run the c++ example from the --help output
                              (run on a build server to have compiler available at runtime,
                              and avoid referencing system-version because node.cfg isn't
                              available on the build servers)

ARGS:
 compare:
  arg1:   Version or "sys" placeholder as arg to compare
  arg2:   gt|lt|ge|le|eq|ne (comparator as used by "test" builtin with preceding dash, but
          additionally handles version-strings)
  [arg3]: Version or "sys" placeholder as matcher to compare against (if omitted implies "sys")

DETAILS:
 "--get" deliberately doesn't output a trailing newline for easier inline use (see examples).
 "--compare" uses "--get" logic internally when the system-version is compared against.

EXAMPLES:

 # "get"
 % ${scriptname} get
 V9.00.01.ALL.12

 # "get" simplified
 % ${scriptname} --simplify get
 9.0.1.12

 # "get" parts
 % ${scriptname} --major get
 9
 % ${scriptname} --patch get
 1
 % ${scriptname} --parts '1,2,3' get
 9.0.1

 # "compare" with last arg omitted (implies system version)
 % ${scriptname} compare 9.0.1.13 gt; echo \$?
 0

 # "compare" using "sys" placeholder (implies system version)
 % ${scriptname} compare 9.0.1.13 gt sys; echo \$?
 0
 % ${scriptname} compare 9.0.1.12 le sys; echo \$?
 0
 % ${scriptname} compare 9.0.1.12 eq sys; echo \$?
 0
 % ${scriptname} --major compare 9 eq sys; echo \$?
 0
 % ${scriptname} compare 9 lt sys; echo \$?
 0

 # "compare" versions including syntactic-sugar
 % ${scriptname} compare sys gt V9R4.45; echo \$?
 1
 % ${scriptname} compare sys gt V9.00.01.ALL.11; echo \$?
 0

 # "compare" two specified versions
 % ${scriptname} compare V9 lt V9R4; echo \$?
 0
 % ${scriptname} compare 9.0.1.11 eq V9.00.01.ALL.11; echo \$?
 0

 # using "get" output inline
 % printf '"%s" is the system version\\n' "\$(${scriptname} get)"
 "V9.00.01.ALL.12" is the system version
 % printf '"%s" is the system major-version\\n' "\$(${scriptname} --simplify --major get)"
 "9" is the system major-version

 # using "compare" in a conditional without subshell-capture-output-hell
 % if ${scriptname} compare 8 lt sys; then echo "system is 9-or-greater"; fi
 system is 9-or-greater
 % if ${scriptname} compare sys ge 9; then echo "system is 9-or-greater"; fi
 system is 9-or-greater
 % if ${scriptname} --major compare sys lt 10; then echo "system major-version is less than 10"; fi
 system major-version is less than 10
 % if ${scriptname} --major compare sys eq 9; then echo "system major-version equals 9"; fi
 system major-version equals 9

 # calling "${scriptname} compare V10.1 ge 9" from c++
 ---8< c++---
 extern "C"
 {
   #include <sys/wait.h>
   #include <stdlib.h>
   #include <unistd.h>
   #include <stdio.h>
 }
 #include <iostream>
 int main()
 {
   pid_t cpid = -1, w = -1;
   int status = -1;

   if ((cpid = fork()) == -1) {
     perror("fork");
     exit(EXIT_FAILURE);
   }
   if (cpid == 0) {
     execlp("$(readlink -e "${0}")", "${scriptname}", "compare", "V10.1", "ge", "9", static_cast<char *>(NULL));
     perror("execlp"); // should never reach here
     exit(EXIT_FAILURE);
   }
   do {
     w = waitpid(cpid, &status, 0);
     if (w == -1) {
       perror("waitpid");
       exit(EXIT_FAILURE);
     }
     if (WIFEXITED(status)) {
       if (WEXITSTATUS(status) == 0) {
         std::cout << "V10.1 is 9-or-greater" << std::endl;
         exit(EXIT_SUCCESS);
       } else
         exit(EXIT_FAILURE);
     }
   } while (!WIFEXITED(status) && !WIFSIGNALED(status));
   exit(EXIT_SUCCESS);
 }
 --->8 c++---
 V10.1 is 9-or-greater
EOF
        exit ${TRUE}
        ;;
    --get|-g|get)
        action='get'
        ;;
    --compare|-c|compare)
        action='compare'
        ;;
    --selftest|selftest)
        test_success=${TRUE}
        "${0}" -h | \
            sed -n \
                -e '/^EXAMPLES:$/,$! b' \
                -e 's/^ % //' \
                -e 'tP' \
                -e 'b' \
                -e ':P' \
                -e "s:${scriptname}:./${scriptname}:g" \
                -e 'N' \
                -e 's/\(\n\) /\1/' \
                -e 'p' | \
            while IFS= read -r line; do
                IFS= read -r output_match;
                printf '* %s\n' "${line}"
                output="$(eval "${line}")"
                printf -- '--> %s\n' "${output}"
                if test "x${output}" = "x${output_match}"; then
                    printf '==> OK\n'
                else
                    printf '==> FAILED, should be:\n--> %s\n' "${output_match}"
                    test_success=${FALSE}
                fi
                printf '\n'
            done
        exit ${test_success}
        ;;
    --selftest-cpp|selftest-cpp) #TODO: make this handle multiple c++ examples, presently just one
        test_success=${TRUE}
        tempfile="$(mktemp XXXXXX.cpp)"
        mytrap='rm -f "${tempfile}"'
        trap "${mytrap}" EXIT
        "${0}" -h | \
            sed -n \
                -e 's/^ # /* /' \
                -e 'tP' \
                -e 'b' \
                -e ':P' \
                -e 'x' \
                -e 'n' \
                -e '/^ ---8< c++---$/! b' \
                -e 'x' \
                -e 's/$/ (see --help output to view the example-code)/' \
                -e 'p' \
                -e 'q'
        output_match="$(
            "${0}" -h | \
                sed -n \
                    -e '/^ --->8 c++---$/! b' \
                    -e 'n' \
                    -e 's/^ //' \
                    -e 'p' \
                    -e 'q'
        )"
        "${0}" -h | \
            sed -n \
                -e '/^ ---8< c++---$/,/^ --->8 c++---$/! b' \
                -e 's/^ //' \
                -e '/^---\(8<\|>8\) c++---$/! p' >"${tempfile}"
        g++ -o "${tempfile%.cpp}" "${tempfile}"
        mytrap="rm -f ${tempfile%.cpp}; ${mytrap}"
        trap "${mytrap}" EXIT
        output="$("./${tempfile%.cpp}")"
        printf -- '--> %s\n' "${output}"
        if test "x${output}" = "x${output_match}"; then
            printf '==> OK\n'
        else
            printf '==> FAILED, should be:\n--> %s\n' "${output_match}"
            test_success=${FALSE}
        fi
        printf '\n'
        exit ${test_success}
        ;;
    *)
        die 'action "%s" is unrecognized (should be --help|-h|help, --get|-g|get, --compare|-c|compare)\n' "${action}"
        ;;
esac

case "${action}:${3}" in
    get:* | compare:sys | compare: )
        if [ -f /etc/osv-release ]
        then
           source /etc/osv-release
           formatted_string="V$OSV_VERSION_MAJOR""R$OSV_VERSION_MINOR.$OSV_VERSION_PATCH.$OSV_VERSION_HOTFIX"
           version_matcher=$formatted_string
        else
           version_matcher="$(cfgread srx_build_id)" || die 'failed to parse the srx_build_id entry from /etc/hiq8000/node.cfg\n'
        fi
        ;;
    compare:* )
        version_matcher="${3}"
        ;;
esac
version_matcher_simplified="$(printf '%s' "${version_matcher}" | simplify_version)"
if ! version_format_ok "${version_matcher_simplified}"; then
    die 'version-matcher "%s" has an invalid format (one or more digits [0-9] with possible interspersed decimal points)\n' "${version_matcher}"
fi

case "${action}" in
    get)
        test ${simplify} -eq ${TRUE} && version_matcher="${version_matcher_simplified}"
        get_parts "${version_matcher}"
        exit ${TRUE}
        ;;
    compare)
        version_matcher="${version_matcher_simplified}"
        if [ -f /etc/osv-release ]
        then
            source /etc/osv-release
            formatted_string="V$OSV_VERSION_MAJOR""R$OSV_VERSION_MINOR.$OSV_VERSION_PATCH.$OSV_VERSION_HOTFIX"
            version_arg="$(printf '%s' ${1} | sed -e "s/^sys$/$formatted_string/" | simplify_version)"
        else
            version_arg="$(printf '%s' ${1} | sed -e "s/^sys$/$(cfgread srx_build_id)/" | simplify_version)"
        fi
        if version_format_ok "${version_arg}"; then
            shift
        else
            die 'version-arg "%s" has an invalid format (one or more digits [0-9] with possible interspersed decimal points)\n' "${version_arg}"
        fi
        version_matcher="$(get_parts "${version_matcher}")"
        version_arg="$(get_parts "${version_arg}")"
        comparator="${1}"
        case "${comparator}" in
            eq|ne|lt|gt|le|ge)
                shift
                ;;
            *)
                die 'action "%s" is unrecognized (should be eq, ne, lt, gt, le, ge)\n' "${comparator}"
        esac
        case "${comparator}" in
            eq)
                test "${version_arg}" = "${version_matcher}"
                exit ${?}
                ;;
            ne)
                ! "${0}" compare "${version_arg}" eq "${version_matcher}"
                exit ${?}
                ;;
            lt|gt)
                case "${comparator}" in
                    lt)
                        opposite_comparator='gt'
                        ;;
                    gt)
                        opposite_comparator='lt'
                        ;;
                esac
                arg_tail="${version_arg}"
                matcher_tail="${version_matcher}"
                while : ; do
                    arg_head="$(get_head "${arg_tail}")"
                    matcher_head="$(get_head "${matcher_tail}")"
                    arg_tail="$(get_tail "${arg_tail}")"
                    matcher_tail="$(get_tail "${matcher_tail}")"
                    if test -z "${arg_head}${matcher_head}"; then
                        exit ${FALSE}
                    elif test -z "${matcher_head}"; then
                        if test "${comparator}" = 'gt'; then
                            exit ${TRUE}
                        else
                            exit ${FALSE}
                        fi
                    elif test -z "${arg_head}"; then
                        if test "${comparator}" = 'lt'; then
                            exit ${TRUE}
                        else
                            exit ${FALSE}
                        fi
                    elif test "${arg_head}" "-${opposite_comparator}" "${matcher_head}"; then
                        exit ${FALSE}
                    elif test "${arg_head}" "-${comparator}" "${matcher_head}"; then
                        exit ${TRUE}
                    fi
                done
                exit ${FALSE} # this should never be reached, but would probably indicate equality even if buggy logic, meaning FALSE
                ;;
            le|ge)
                case "${comparator}" in
                    le)
                        orthogonal_comparator='gt'
                        ;;
                    ge)
                        orthogonal_comparator='lt'
                        ;;
                esac
                ! "${0}" compare "${version_arg}" "${orthogonal_comparator}" "${version_matcher}"
                exit ${?}
                ;;
        esac
        ;;
esac
