# vim: set ts=4 sw=4 sts=4 et:
# Print the tool name and version banner
show_header() {
    pr_info "Spectre and Meltdown mitigation detection tool v$VERSION"
    pr_info
}

# Convert Family-Model-Stepping triplet to a CPUID value (base-10 to stdout)
# Args: $1=family $2=model $3=stepping
fms2cpuid() {
    local family model stepping extfamily lowfamily extmodel lowmodel
    family="$1"
    model="$2"
    stepping="$3"

    if [ "$((family))" -le 15 ]; then
        extfamily=0
        lowfamily=$((family))
    else
        # when we have a family > 0xF, then lowfamily is stuck at 0xF
        # and extfamily is ADDED to it (as in "+"), to ensure old software
        # never sees a lowfamily < 0xF for newer families
        lowfamily=15
        extfamily=$(((family) - 15))
    fi
    extmodel=$(((model & 0xF0) >> 4))
    lowmodel=$(((model & 0x0F) >> 0))
    echo $(((stepping & 0x0F) | (lowmodel << 4) | (lowfamily << 8) | (extmodel << 16) | (extfamily << 20)))
}

# Download a file using wget, curl, or fetch (whichever is available)
# Args: $1=url $2=output_file
download_file() {
    local ret url file
    url="$1"
    file="$2"
    if command -v wget >/dev/null 2>&1; then
        wget -q "$url" -O "$file"
        ret=$?
    elif command -v curl >/dev/null 2>&1; then
        curl -sL "$url" -o "$file"
        ret=$?
    elif command -v fetch >/dev/null 2>&1; then
        fetch -q "$url" -o "$file"
        ret=$?
    else
        echo ERROR "please install one of \`wget\`, \`curl\` of \`fetch\` programs"
        unset file url
        return 1
    fi
    unset file url
    if [ "$ret" != 0 ]; then
        echo ERROR "error $ret"
        return $ret
    fi
    echo DONE
}

[ -z "$HOME" ] && HOME="$(getent passwd "$(whoami)" | cut -d: -f6)"
g_mcedb_cache="$HOME/.mcedb"
# Download and update the local microcode firmware database cache
# Sets: g_mcedb_tmp (temp file, cleaned up on exit)
update_fwdb() {
    local previous_dbversion dbversion mcedb_revision iucode_tool nbfound linuxfw_hash mcedb_url intel_url linuxfw_url newfile line cpuid pfmask date version intel_timestamp intel_latest_date family model stepping sqlstm

    show_header

    set -e

    if [ -r "$g_mcedb_cache" ]; then
        previous_dbversion=$(awk '/^# %%% MCEDB / { print $4 }' "$g_mcedb_cache")
    fi

    # first, download the MCE.db from the excellent platomav's MCExtractor project
    g_mcedb_tmp="$(mktemp -t smc-mcedb-XXXXXX)"
    mcedb_url='https://github.com/platomav/MCExtractor/raw/master/MCE.db'
    pr_info_nol "Fetching MCE.db from the MCExtractor project... "
    download_file "$mcedb_url" "$g_mcedb_tmp" || return $?

    # second, get the Intel firmwares from GitHub
    g_intel_tmp="$(mktemp -d -t smc-intelfw-XXXXXX)"
    intel_url="https://github.com/intel/Intel-Linux-Processor-Microcode-Data-Files/archive/main.zip"
    pr_info_nol "Fetching Intel firmwares... "
    ## https://github.com/intel/Intel-Linux-Processor-Microcode-Data-Files.git
    download_file "$intel_url" "$g_intel_tmp/fw.zip" || return $?

    # now extract MCEdb contents using sqlite
    pr_info_nol "Extracting MCEdb data... "
    if ! command -v sqlite3 >/dev/null 2>&1; then
        echo ERROR "please install the \`sqlite3\` program"
        return 1
    fi
    mcedb_revision=$(sqlite3 "$g_mcedb_tmp" "SELECT \"revision\" from \"MCE\"")
    if [ -z "$mcedb_revision" ]; then
        echo ERROR "downloaded file seems invalid"
        return 1
    fi
    sqlite3 "$g_mcedb_tmp" "ALTER TABLE \"Intel\" ADD COLUMN \"origin\" TEXT"
    sqlite3 "$g_mcedb_tmp" "ALTER TABLE \"Intel\" ADD COLUMN \"pfmask\" TEXT"
    sqlite3 "$g_mcedb_tmp" "ALTER TABLE \"AMD\" ADD COLUMN \"origin\" TEXT"
    sqlite3 "$g_mcedb_tmp" "ALTER TABLE \"AMD\" ADD COLUMN \"pfmask\" TEXT"
    sqlite3 "$g_mcedb_tmp" "UPDATE \"Intel\" SET \"origin\"='mce'"
    sqlite3 "$g_mcedb_tmp" "UPDATE \"Intel\" SET \"pfmask\"='FF'"
    sqlite3 "$g_mcedb_tmp" "UPDATE \"AMD\" SET \"origin\"='mce'"
    sqlite3 "$g_mcedb_tmp" "UPDATE \"AMD\" SET \"pfmask\"='FF'"

    echo OK "MCExtractor database revision $mcedb_revision"

    # parse Intel firmwares to get their versions
    pr_info_nol "Integrating Intel firmwares data to db... "
    if ! command -v unzip >/dev/null 2>&1; then
        echo ERROR "please install the \`unzip\` program"
        return 1
    fi
    (cd "$g_intel_tmp" && unzip fw.zip >/dev/null)
    if ! [ -d "$g_intel_tmp/Intel-Linux-Processor-Microcode-Data-Files-main/intel-ucode" ]; then
        echo ERROR "expected the 'intel-ucode' folder in the downloaded zip file"
        return 1
    fi

    if ! command -v iucode_tool >/dev/null 2>&1; then
        if ! command -v iucode-tool >/dev/null 2>&1; then
            echo ERROR "please install the \`iucode-tool\` program"
            return 1
        else
            iucode_tool="iucode-tool"
        fi
    else
        iucode_tool="iucode_tool"
    fi
    #  079/001: sig 0x000106c2, pf_mask 0x01, 2009-04-10, rev 0x0217, size 5120
    #  078/004: sig 0x000106ca, pf_mask 0x10, 2009-08-25, rev 0x0107, size 5120
    $iucode_tool -l "$g_intel_tmp/Intel-Linux-Processor-Microcode-Data-Files-main/intel-ucode" | grep -wF sig | while read -r line; do
        cpuid=$(echo "$line" | grep -Eio 'sig 0x[0-9a-f]+' | awk '{print $2}')
        cpuid=$((cpuid))
        cpuid=$(printf "%08X" "$cpuid")
        pfmask=$(echo "$line" | grep -Eio 'pf_mask 0x[0-9a-f]+' | awk '{print $2}')
        pfmask=$((pfmask))
        pfmask=$(printf "%02X" $pfmask)
        date=$(echo "$line" | grep -Eo '(19|20)[0-9][0-9]-[01][0-9]-[0-3][0-9]' | tr -d '-')
        version=$(echo "$line" | grep -Eio 'rev 0x[0-9a-f]+' | awk '{print $2}')
        version=$((version))
        version=$(printf "%08X" "$version")
        # ensure the official Intel DB always has precedence over mcedb, even if mcedb has seen a more recent fw
        sqlite3 "$g_mcedb_tmp" "DELETE FROM \"Intel\" WHERE \"origin\" != 'intel' AND \"cpuid\" = '$cpuid';"
        # then insert our version
        sqlite3 "$g_mcedb_tmp" "INSERT INTO \"Intel\" (\"origin\",\"cpuid\",\"pfmask\",\"version\",\"yyyymmdd\") VALUES ('intel','$cpuid','$pfmask','$version','$date');"
    done
    intel_timestamp=$(stat -c %Y "$g_intel_tmp/Intel-Linux-Processor-Microcode-Data-Files-main/license" 2>/dev/null || stat -f %m "$g_intel_tmp/Intel-Linux-Processor-Microcode-Data-Files-main/license" 2>/dev/null)
    if [ -n "$intel_timestamp" ]; then
        # use this date, it matches the last commit date
        intel_latest_date=$(date -d @"$intel_timestamp" +%Y%m%d 2>/dev/null || date -r "$intel_timestamp" +%Y%m%d)
    else
        echo "Falling back to the latest microcode date"
        intel_latest_date=$(sqlite3 "$g_mcedb_tmp" "SELECT \"yyyymmdd\" FROM \"Intel\" WHERE \"origin\"='intel' ORDER BY \"yyyymmdd\" DESC LIMIT 1;")
    fi
    echo DONE "(version $intel_latest_date)"

    # now parse the most recent linux-firmware amd-ucode README file
    pr_info_nol "Fetching latest amd-ucode README from linux-firmware project... "
    linuxfw_url="https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git/plain/amd-ucode/README"
    g_linuxfw_tmp=$(mktemp -t smc-linuxfw-XXXXXX)
    download_file "$linuxfw_url" "$g_linuxfw_tmp" || return $?

    pr_info_nol "Parsing the README... "
    nbfound=0
    for line in $(grep -E 'Family=0x[0-9a-f]+ Model=0x[0-9a-f]+ Stepping=0x[0-9a-f]+: Patch=0x[0-9a-f]+' "$g_linuxfw_tmp" | tr " " ","); do
        pr_debug "Parsing line $line"
        family=$(echo "$line" | grep -Eoi 'Family=0x[0-9a-f]+' | cut -d= -f2)
        model=$(echo "$line" | grep -Eoi 'Model=0x[0-9a-f]+' | cut -d= -f2)
        stepping=$(echo "$line" | grep -Eoi 'Stepping=0x[0-9a-f]+' | cut -d= -f2)
        version=$(echo "$line" | grep -Eoi 'Patch=0x[0-9a-f]+' | cut -d= -f2)
        version=$(printf "%08X" "$((version))")
        cpuid=$(fms2cpuid "$family" "$model" "$stepping")
        cpuid=$(printf "%08X" "$cpuid")
        sqlstm="INSERT INTO \"AMD\" (\"origin\",\"cpuid\",\"pfmask\",\"version\",\"yyyymmdd\") VALUES ('linux-firmware','$cpuid','FF','$version','20000101')"
        pr_debug "family $family model $model stepping $stepping cpuid $cpuid"
        pr_debug "$sqlstm"
        sqlite3 "$g_mcedb_tmp" "$sqlstm"
        nbfound=$((nbfound + 1))
        unset family model stepping version cpuid date sqlstm
    done
    echo "found $nbfound microcodes"
    unset nbfound

    dbversion="$mcedb_revision+i$intel_latest_date"
    linuxfw_hash=$(md5sum "$g_linuxfw_tmp" 2>/dev/null | cut -c1-4)
    if [ -n "$linuxfw_hash" ]; then
        dbversion="$dbversion+$linuxfw_hash"
    fi

    if [ "$1" != builtin ] && [ -n "$previous_dbversion" ] && [ "$previous_dbversion" = "v$dbversion" ]; then
        echo "We already have this version locally, no update needed"
        return 0
    fi

    pr_info_nol "Building local database... "
    {
        echo "# Spectre & Meltdown Checker"
        echo "# %%% MCEDB v$dbversion"
        # we'll use the more recent fw for Intel and AMD
        sqlite3 "$g_mcedb_tmp" "SELECT '# I,0x'||\"t1\".\"cpuid\"||',0x'||\"t1\".\"pfmask\"||',0x'||MAX(\"t1\".\"version\")||','||\"t1\".\"yyyymmdd\" FROM \"Intel\" AS \"t1\" LEFT OUTER JOIN \"Intel\" AS \"t2\" ON \"t2\".\"cpuid\"=\"t1\".\"cpuid\" AND \"t2\".\"pfmask\"=\"t1\".\"pfmask\" AND \"t2\".\"yyyymmdd\" > \"t1\".\"yyyymmdd\" WHERE \"t2\".\"yyyymmdd\" IS NULL GROUP BY \"t1\".\"cpuid\",\"t1\".\"pfmask\" ORDER BY \"t1\".\"cpuid\",\"t1\".\"pfmask\" ASC;" | grep -v '^# .,0x00000000,'
        sqlite3 "$g_mcedb_tmp" "SELECT '# A,0x'||\"t1\".\"cpuid\"||',0x'||\"t1\".\"pfmask\"||',0x'||MAX(\"t1\".\"version\")||','||\"t1\".\"yyyymmdd\" FROM \"AMD\"   AS \"t1\" LEFT OUTER JOIN \"AMD\"   AS \"t2\" ON \"t2\".\"cpuid\"=\"t1\".\"cpuid\" AND \"t2\".\"pfmask\"=\"t1\".\"pfmask\" AND \"t2\".\"yyyymmdd\" > \"t1\".\"yyyymmdd\" WHERE \"t2\".\"yyyymmdd\" IS NULL GROUP BY \"t1\".\"cpuid\",\"t1\".\"pfmask\" ORDER BY \"t1\".\"cpuid\",\"t1\".\"pfmask\" ASC;" | grep -v '^# .,0x00000000,'
    } >"$g_mcedb_cache"
    echo DONE "(version $dbversion)"

    if [ "$1" = builtin ]; then
        newfile=$(mktemp -t smc-builtin-XXXXXX)
        awk '/^# %%% MCEDB / { exit }; { print }' "$0" >"$newfile"
        awk '{ if (NR>1) { print } }' "$g_mcedb_cache" >>"$newfile"
        cat "$newfile" >"$0"
        rm -f "$newfile"
    fi
}
