#!/bin/bash

# Copyright (C) 2011-2014 Red Hat, Inc.
#
# This file is part of csmock.
#
# csmock 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
# any later version.
#
# csmock 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 csmock.  If not, see <http://www.gnu.org/licenses/>.

SELF="$0"
ARGS="`for i in "$@"; do printf " '%s'" "$i"; done`"

set -o pipefail

export LC_ALL=C

if tty 0>&2 >/dev/null; then
    R="\033[1;31m"
    G="\033[1;32m"
    N="\033[0m"
else
    R=
    G=
    N=
fi

printf "${R}
***************************************************************
*                                                             *
*   ${N}cov-mockbuild is deprecated, please use ${G}csmock${N} instead!${R}   *
*                                                             *
***************************************************************
${N}\n" >&2

test -z "$TMPDIR" && TMPDIR="/tmp"

QUICK_BUILD_CMD="./configure; make -kj13"

OPTS="bcd:fhilm::nq:rtw:x"

usage(){
    cat << EOF
Usage: $(basename $SELF) [-$OPTS] MOCK_PROFILE my-package.src.rpm [COV_OPTS]

  -b  Do not use Coverity Static Analysis (default for csmock, the new cli).

  -c  Enable the Cppcheck tool (uses the version installed on the host).

  -d  A space-separated list of manually added dependences needed for build.

  -f  Overwrite the file with results if it already exists.  By default, this
      would fail.

  -h  Provide HTML output of cov-format-errors.  [Coverity only]

  -i  Keep Coverity intermediate directory for further processing.  By default,
      only build-log.txt is kept.  [Coverity only]

  -l  Enable clang analyzer.  It will use the clang-analyzer package from your
      mock chroot, or fail if the package is not installable.

  -m  Commit the results to Integrity Manager specified by the following
      format: user:passwd@host:port/stream

  -n  Do not clean mock root after the build.

  -q  A shell comand used to build the given tarball.

  -r  Expect a tarball with sources instead of source RPM.

  -t  Do not compress the resulting tarball.

  -wN  Adjust compiler warning level.  -w0 means default flags, -w1 appends
       -Wall and -Wextra, and -w2 enables some other useful warnings.

  -x  Skip patches not annotated by %{?_rawbuild}.  That is, do a vanilla build.

  MOCK_PROFILE  Only name of the mock profile, without the path and without
                the .cfg suffix.

  COV_OPTS      Options passed to cov-analyze.
EOF
    exit 1
}

if test "x--version" = "x$1"; then
    rpm -qf $SELF
    exit $?
fi

EC=0
update_ec() {
    last_ec=$?
    test -z "$1" || last_ec="$1"
    if test 128 -le "$last_ec" -a "$last_ec" -lt 192; then
        echo "<<< $SELF: child signalled to die by $(($last_ec - 128))" >&2
        exit $last_ec
    fi

    echo "--- $SELF: exit code was: $last_ec" >&2
    test 0 -ne "$EC" || EC=$last_ec
}

die() {
    if test -n "$1"; then
        echo "$SELF: error: $1" >&2
    else
        echo "$SELF: generic error" >&2
    fi
    update_ec 2
    exit 1
}

parse_im_opts() {
    python - << EOF
import re
m = re.match('^([^@:]+):([^@]+)(?:@([^:/]+)(?::([^/]+)(?:/(.+))?)?)?\$', '$*')
print "export COV_IM_USER=%s" % m.group(1)
print "export COV_IM_PASS=%s" % m.group(2)
if m.group(3) is not None:
    print "export COV_IM_HOST=%s" % m.group(3)
if m.group(4) is not None:
    print "export COV_IM_PORT=%s" % m.group(4)
if m.group(5) is not None:
    print "export PROJ_NAME=%s" % m.group(5)
EOF
}

COV_USED=yes
USE_CLANG=no
USE_CPPCHECK=no

CSMOCK_ARGS=
CSMOCK_TOOLS="gcc"

while getopts $OPTS opt; do
    case "$opt" in
        b)  COV_USED=no
            ;;

        c)  CSMOCK_ARGS="$CSMOCK_ARGS --use-host-cppcheck"
            USE_CPPCHECK=yes
            ;;

        d)  CSMOCK_ARGS="$CSMOCK_ARGS --install '$OPTARG'"
            ;;

        f)  OVERWRITE_RESULTS=yes
            ;;

        h)  CSMOCK_ARGS="$CSMOCK_ARGS --cov-html-output"
            ;;

        i)  CSMOCK_ARGS="$CSMOCK_ARGS --cov-keep-int-dir"
            ;;

        l)  CSMOCK_TOOLS="${CSMOCK_TOOLS},clang"
            USE_CLANG=yes
            ;;

        m)  CSMOCK_ARGS="$CSMOCK_ARGS --cov-commit-to '$OPTARG'"
            COMMIT_TO_IM=yes
            COMMIT_TO_IM_OPTS="$OPTARG"
            if test -n "$COMMIT_TO_IM_OPTS"; then
                parse_im_opts "$COMMIT_TO_IM_OPTS" >/dev/null 2>&1 \
                    || die "invalid argument given to -m: $COMMIT_TO_IM_OPTS"
            fi
            ;;

        n)  CSMOCK_ARGS="$CSMOCK_ARGS --no-clean"
            ;;

        q)
            QUICK_BUILD_CMD="$OPTARG"
            ;;

        r)
            USE_TARBALL=yes
            ;;

        t)  TAR_ONLY=yes
            ;;

        w)
            case "$OPTARG" in
                [0-3])
                    CSMOCK_ARGS="$CSMOCK_ARGS -w$OPTARG"
                    ;;

                *)
                    usage
                    ;;
            esac
            ;;

        x)  CSMOCK_ARGS="$CSMOCK_ARGS --skip-patches"
            RAWBUILD=yes
            ;;

        *)
            usage
            ;;
    esac
done
shift $(($OPTIND - 1))

# the first operand is the mock profile to use
MOCK_PROFILE="$1"
MOCK_CFG="/etc/mock/$MOCK_PROFILE.cfg"
test -f "$MOCK_CFG" || usage

# the second operand is the SRPM to build/analyze
SRPM="`readlink -f "$2"`"
if test xyes = "x$USE_TARBALL"; then
    BASE="`basename "$SRPM" | sed -e 's|\.[^.]*$||' -e 's|\.tar$||'`"
else
    BASE="`basename "$SRPM" .src.rpm`"
    test -n "$SRPM" || usage
    test -r "$SRPM" || die "failed to open $SRPM"
    rpm -pq "$SRPM" >/dev/null 2>&1 \
        || die "failed to open an RPM package: $SRPM"
    rpm -lpq "$SRPM" 2>/dev/null | grep '\.spec$' >/dev/null \
        || die "no specfile found in $SRPM"
fi

if test xyes = "x$TAR_ONLY"; then
    TGZ="$PWD/$BASE.tar"
else
    TGZ="$PWD/$BASE.tar.xz"
    TAR_OPTS=-J
fi

if test xyes = "x$OVERWRITE_RESULTS"; then
    rm -fv "$TGZ"
fi

test -e "$TGZ" && die "'$TGZ' already exists"

if test xyes = "x$RAWBUILD"; then
    RUN=run0
else
    RUN=run1
fi

cmd_wrap() {
    printf "\n>>> %s\t%s\n" "`date`" "$*" >&2
    "$@"
}

shift
shift
COV_OPTS="$*"
# prepare $TMP directory
TMP="`mktemp -d $TMPDIR/csmock.XXXXXX`"
BASE_DIR="$TMP/$BASE/$RUN"
CSMOCK_RESDIR="$TMP/csmock-output"
mkdir -p "$BASE_DIR" || die "mktemp failed"

# from now on, do the pack/cleanup on exit, no matter if we succeeded or not
trap "test 0 -eq \"\$EC\" || touch \"${BASE_DIR}.FAILED\"
    cmd_wrap tar $TAR_OPTS -cvf '$TGZ' -C '$TMP' '$BASE'
    echo --- $SELF: removing $TMP... 2>&1; rm -rf '$TMP'" EXIT

if test xyes = "x$COV_USED"; then
    CSMOCK_TOOLS="${CSMOCK_TOOLS},coverity"

    COV_BIN_DIR="$(dirname "`which cov-analyze`")"
    COV_WRAP="$COV_BIN_DIR/cov-build --dir /builddir/cov"
    COV_DIR="`dirname "$COV_BIN_DIR"`"
    if test -x "$COV_BIN_DIR/cov-analyze"; then
        CSMOCK_ARGS="$CSMOCK_ARGS --cov-use-instance $COV_DIR"
    fi
else
    COV_OPTS=
fi
if test -n "$COV_OPTS"; then
    CSMOCK_ARGS="$CSMOCK_ARGS --cov-analyze-opts='$COV_OPTS'"
fi
if test xyes = "x$USE_TARBALL"; then
    CSMOCK_ARGS="$CSMOCK_ARGS --shell-cmd '$QUICK_BUILD_CMD'"
fi

CSMOCK_ARGS="$CSMOCK_ARGS -t '$CSMOCK_TOOLS'"
CSMOCK_ARGS="$CSMOCK_ARGS -r '$MOCK_PROFILE'"
CSMOCK_ARGS="$CSMOCK_ARGS -o '$CSMOCK_RESDIR'"
CSMOCK_ARGS="$CSMOCK_ARGS '$SRPM'"

ERR="$BASE_DIR/$BASE.err"
HTML="$BASE_DIR/$BASE.html"
INI="$BASE_DIR/$BASE.ini"
 JS="$BASE_DIR/$BASE.js"
LOG="$BASE_DIR/$BASE.log"

cov_version() {
    cov-analyze --ident \
        | perl -p -e 's/\n/\\n/' \
        | perl -p -e 's/(\\n)*$//'
}

self_version() {
    echo $(rpm -q csmock csdiff) 2>&1
}

print_if_readable() {
    if test -r "$1"; then
        echo "$1"
    fi
}

write_link_if_exists() {
    test -r "$2" || return
    printf "    <li><a href='%s'>%s</a></li>\n" "$2" "$1"
}

write_links_if_exist() {
    file="$2"
    file_js="${file}.js"
    file_err="${file}.err"
    file_html="${file}.html"
    test -r "$file_html" || return

    cnt_def="?"
    style=
    if test -r "$file_err"; then
        cnt_def="$(grep "^Error: " "$file_err" | wc -l)"
        test 0 -lt "$cnt_def" && style=" style='font-weight: bold;'"
    fi

    printf "  <li><a%s href='%s'>%s (%d)</a><ul>\n" \
        "$style" "$file_html" "$1" "$cnt_def"

    write_link_if_exists "as TXT"               "$file_err"
    write_link_if_exists "as JSON"              "$file_js"

    printf "</ul><br/></li>\n"
}

write_contents_if_exists() {
    test -r "$1" || return
    printf "<pre>\n%s</pre>\n" "$(<$1)"
}

write_index() {
    title="Scan of $BASE"
    printf "<?xml version='1.0' encoding='utf-8'?>\n"
    printf "<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.1//EN' \
'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'>\n"
    printf "<html xmlns='http://www.w3.org/1999/xhtml'>\n"
    printf "<head><title>%s</title></head>\n" "$title"
    printf "<body>\n<h1>%s</h1>\n" "$title"

    printf "<h2>All Results</h2>\n<ul>\n"
    write_links_if_exist "All Defects"                 "results"

    printf "</ul>\n<h2>Per Tool Results</h2>\n<ul>\n"
    write_links_if_exist "Compiler Warnings"           "compiler-warnings"
    write_links_if_exist "Defects Found by Cppcheck"   "cppcheck-warnings"
    write_links_if_exist "Defects Found by CLang"      "clang-warnings"
    write_links_if_exist "Defects Found by Coverity"   "$BASE"

    printf "</ul>\n<h2>Results Summary</h2>\n"
    write_contents_if_exists                           "results-summary.txt"

    printf "<h2>Debugging Information</h2>\n<ul>\n"
    write_link_if_exists "Scan Log"                    "$BASE.log"
    write_link_if_exists "Scan Properties"             "$BASE.ini"
    write_link_if_exists "RPMs on the host"            "debug/rpm-list-host.txt"
    write_link_if_exists "RPMs in the chroot"          "debug/rpm-list-mock.txt"
    write_link_if_exists "Coverity build log"          "debug/build-log.txt"
    printf "</ul>\n"
    write_contents_if_exists                           "$BASE.ini"

    printf "</body>\n</html>\n"
}

# extra options for cov-analyze
printf "[scan]
tool = cov-mockbuild
tool-version = %s
tool-args =%s
analyzer = coverity
analyzer-version = %s
analyzer-args = %s
host = %s
mock-config = %s
time-created = %s\n" \
    "`self_version`" "$ARGS" "`cov_version`" "$COV_OPTS" "`hostname`" \
    "$MOCK_PROFILE" "`date '+%Y-%m-%d %H:%M:%S'`" \
    | tee "$INI"

PROJ_NAME="$BASE"
test xyes = "x$RAWBUILD" && PROJ_NAME+="-rawbuild"
printf "project-name = %s\n" "$PROJ_NAME" | tee --append "$INI"

eval csmock "$CSMOCK_ARGS" || update_ec

cmd_wrap mv "$CSMOCK_RESDIR"/* "$BASE_DIR"
mv -v "$BASE_DIR/scan.log" "$LOG"

printf "time-finished = %s\n" "`date '+%Y-%m-%d %H:%M:%S'`" >> "$INI"

if test xyes = "x$COV_USED"; then
    # get the line stating count of successfully parsed compilation units
    PSTAT="`grep 'C/C++ compilation units (.*%) are ready for analysis$' \
        "$BASE_DIR/debug/build-log.txt"`"

    # deal with the build-log.txt format introduced in CSA 6.6.x
    PSTAT="`echo "$PSTAT" | sed -r 's/^[^>]+> //'`"

    # parse the absolute/relative count of successfully parsed compilation units
    U_COUNT="`printf '%s\n' "$PSTAT" | sed -r 's|^[^0-9]*([0-9]+) C/C\+\+ .*$|\1|'`"
    U_RATIO="`printf '%s\n' "$PSTAT" | sed -r 's|^.* \(([0-9]+)%\) .*$|\1|'`"

    # assume zero successfully parsed compilation units unless proven otherwise
    test 0 -lt "$U_COUNT" 2>/dev/null || U_COUNT=0
    test 0 -lt "$U_RATIO" 2>/dev/null || U_RATIO=0

    # write the parsing statistics to .ini
    printf "compilation-unit-count = %d\ncompilation-unit-ratio = %d\n" \
        "$U_COUNT" "$U_RATIO" | tee --append "$INI"

    ALOG="$BASE_DIR/debug/analysis-log.txt"

    # write the 'lines-processed' field to .ini
    LSTAT="`grep 'Total LoC input to cov-analyze' "$ALOG"`"
    L_CNT="`printf '%s\n' "$LSTAT" | sed -r 's|^[^0-9]*||'`"
    printf "lines-processed = %d\n" "$L_CNT" | tee --append "$INI"

    # write the 'time-elapsed-analysis' field to to .ini
    TSTAT="`grep 'Time taken by .*analysis' "$ALOG"`"
    ATIME="`printf '%s\n' "$TSTAT" | sed -r 's|^[^0-9]*||'`"
    printf "time-elapsed-analysis = %s\n" "$ATIME" | tee --append "$INI"
fi

cmd_wrap cslinker --inifile "$INI" "$BASE_DIR/scan-results.js" \
    > "$BASE_DIR/results.js"

cmd_wrap sh -c "csgrep --mode=json --invert-match \
    --checker '(CLANG|COMPILER|CPPCHECK)_WARNING' \"$BASE_DIR/results.js\" \
    | cssort --key checker > \"$JS\""

# regenerate .err from .js to get the CWE numbers in place and drop trace events
cmd_wrap csgrep --prune-events=1 "$JS" > "$ERR"

# generate .html from .js without trace events
cmd_wrap sh -c "csgrep --prune-events=1 --mode=json \"$JS\" \
    | cshtml $CSHTML_OPTS - > \"$HTML\""

cmd_wrap csgrep --mode=grep --prune-events=1 "$BASE_DIR/results.js" \
    > "$BASE_DIR/results.err"

cmd_wrap sh -c "csgrep --mode=json --prune-events=1 \"$BASE_DIR/results.js\" \
    | cshtml $CSHTML_OPTS - > \"$BASE_DIR/results.html\""

# filter out compiler warnings
cmd_wrap csgrep --checker "COMPILER_WARNING" "$BASE_DIR/results.js" \
    > "$BASE_DIR/compiler-warnings.err"
cmd_wrap cshtml "$BASE_DIR/compiler-warnings.err" \
    > "$BASE_DIR/compiler-warnings.html"

if test xyes = "x$USE_CLANG"; then
    # filter out clang warnings
    cmd_wrap csgrep --checker "CLANG_WARNING" "$BASE_DIR/results.js" \
        > "$BASE_DIR/clang-warnings.err"
    cmd_wrap cshtml "$BASE_DIR/clang-warnings.err" \
        > "$BASE_DIR/clang-warnings.html"
fi

if test xyes = "x$USE_CPPCHECK"; then
    # filter out cppcheck warnings
    cmd_wrap csgrep --checker "CPPCHECK_WARNING" "$BASE_DIR/results.js" \
        > "$BASE_DIR/cppcheck-warnings.err"
    cmd_wrap cshtml "$BASE_DIR/cppcheck-warnings.err" \
        > "$BASE_DIR/cppcheck-warnings.html"
fi

mkdir "$BASE_DIR/debug/csmock"
cmd_wrap mv -v "$BASE_DIR"/scan* "$BASE_DIR/debug/csmock/"

# print a brief summary of the defects found
cmd_wrap csgrep --mode=stat "$BASE_DIR/results.err" \
    | tee "$BASE_DIR/results-summary.txt"

(cd "$BASE_DIR" && { write_index > "index.html"; }) 

printf "\n<<< %s has finished, exit code is %s\n" "$SELF" "${EC:=0}"

exit $EC
