#!/usr/bin/sh -e

# Copyright (c) 2017, Mellanox Technologies
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
#    list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and documentation are those
# of the authors and should not be interpreted as representing official policies,
# either expressed or implied, of the FreeBSD Project.

PROGNAME=$(basename "$0")
TMP_DIR=$(mktemp -d)
trap "rm -rf $TMP_DIR" EXIT
trap "rm -rf $TMP_DIR; exit 1" TERM INT

# Build number to use in case it can't be retrieved from BFB.
DEFAULT_BUILD_ID=999

usage ()
{
    cat <<EOF
Usage: $PROGNAME [--help|-h]
       $PROGNAME [--part|-p PARTITIONS]
       $PROGNAME [--file|-f FILE]

Prints version information for currently installed software.

Options:
  --help,-h              print this message
  --part,-p PARTITIONS   print version information for all boot partitions
                         specified in a comma-delimited list. For example,
                         "-p0,1" will print bootloader versions for
                         partition 0, and then partition 1. "-p1" will
                         print only partition 1. If -p is not specified,
                         the current primary partition will be printed.
  --file,-f FILE         print BSP version information from a BFB file instead
                         of what is installed.
EOF
}

err () {
    echo "$PROGNAME: $@" >&2
}

print_path_vers () {
    # Print the versions of an image at a given path.
    # For any file, the versions contained in the BFB will be printed.
    # For /dev/mmcblk0boot*, mlxbf-bootctl will be employed to read the
    # partition.
    STREAM_PATH="$1"

    case $STREAM_PATH in
        /dev/mmcblk0boot*) print_bfb_installed_vers "$STREAM_PATH";;
        *)
            if ! [ -e $STREAM_PATH ]; then
                err "warn: $STREAM_PATH does not exist, skipping"
            elif ! [ -f $STREAM_PATH ]; then
                err "warn: $STREAM_PATH is not file, skipping"
            else
                print_bfb_file_vers "$STREAM_PATH"
            fi
            ;;
    esac
}

print_bfb_installed_vers () {
    DEV_PATH=$1
    CURRENT_BFB="$TMP_DIR/$(basename $DEV_PATH).bfb"

    # Retrieve default.bfb from the eMMC
    mlxbf-bootctl -r "$DEV_PATH" -b "$CURRENT_BFB" 2>&1 >/dev/null

    print_bfb_file_vers "$CURRENT_BFB" "$DEV_PATH"
}

print_capsule_file_vers () {
    # Print versions stored in files.
    BFB_PATH="$1"

    # UEFI image header
    pattern="4266021321003005"
    uefi_image_file="$TMP_DIR/temp_uefi_image.bin"

    atf_version=$(strings "$BFB_PATH" | grep -m 1 "(\(release\|debug\))")
    if [ -n "$atf_version" ]; then
        echo BlueField ATF version: "$atf_version"
    fi

    # Search for the UEFI header pattern
    xxd -p "$BFB_PATH" | tr -d "\n" | grep -b -o "$pattern" | cut -d: -f1 | while read -r header_offset2; do
        header_offset=$(($header_offset2/2))
        image_offset=$((header_offset + 24))

        header_string=$(xxd -p -s $header_offset -l 24 "$BFB_PATH")

        if echo "$header_string" | grep -q "^$pattern"; then
            # Extract the image length (next 4 bytes after the header)
            image_len_hex=$(echo "$header_string" | cut -c 17-24)
            image_len=$(echo "$image_len_hex" | sed 's/\(..\)\(..\)\(..\)\(..\)/\4\3\2\1/')
            image_len_dec=$(printf "%d" "0x${image_len#0*}")

            # Check the image offset and length against the input file length
            input_file_length=$(stat -c%s "$BFB_PATH")
            end_position=$((image_offset + image_len_dec))
            if [ "$end_position" -gt "$input_file_length" ]; then
                echo "Error: Image offset + length is greater than input file length"
                exit 1
            fi

            # Extract the UEFI image
            tail -c +$((image_offset + 1)) "$BFB_PATH" | head -c $image_len_dec > "$uefi_image_file"

            # Fetch the version from the UEFI image
            gzipped=$(file $uefi_image_file | grep gzip)
            if [ -n "$gzipped" ]; then
                mv $uefi_image_file $uefi_image_file.gz
                gunzip $uefi_image_file.gz
                uefi_version="$(strings -el $uefi_image_file | grep "BlueField" | cut -d':' -f 2)"
            else
                echo "Warning: UEFI image not compressed and no version info"
            fi

            rm $uefi_image_file

            echo BlueField UEFI version: $uefi_version
            echo
        fi
    done
}

print_bfb_file_vers () {
    # Print versions stored in files.
    BFB_PATH="$1"

    # Second argument is optional, specifies a display name for the file.
    # Defaults to $1.
    if [ -n "$2" ]; then
        DISPLAY_NAME="$2"
    else
        DISPLAY_NAME="$BFB_PATH"
    fi

    echo "--$DISPLAY_NAME"

    # Check for EFI Firmware Management Capsule GUID
    # EFI_FIRMWARE_MANAGEMENT_CAPSULE_ID_GUID: 6dcbd5ed-e82d-4c44-bda1-7194199ad92a
    local capguid=$(dd if="${BFB_PATH}" skip=0 count=16 bs=1 2>/dev/null | xxd -p)
    if [ "$capguid" = "edd5cb6d2de8444cbda17194199ad92a" ]; then
        print_capsule_file_vers "$DISPLAY_NAME"
        exit 0
    fi

    # Find and print the ATF version
    echo BlueField ATF version: "$(strings "$BFB_PATH" | grep -m 1 "(\(release\|debug\))")"

    # Keep UEFI version and use it to determine BSP version as well
    UEFI_VER="$(strings -el "$BFB_PATH" | grep "BlueField" | cut -d':' -f 2)"

    # The UEFI image may be compressed. If it is, decompress before searching
    # for version strings
    UEFI_IMAGE="$BFB_PATH"
    if [ -z "$UEFI_VER" ]; then
      mkdir -p "$TMP_DIR"/uefi
      cd "$TMP_DIR"/uefi
      mlx-mkbfb -x "$BFB_PATH"
      gzipped=$(file dump-bl33-v0 | grep gzip)
      if [ -n "$gzipped" ]; then
        mv dump-bl33-v0 dump-bl33-v0.gz
        gunzip dump-bl33-v0.gz
        UEFI_VER="$(strings -el dump-bl33-v0 | grep "BlueField" | cut -d':' -f 2)"
      else
        echo "$PROGNAME: warn: UEFI image not compressed and no version info"
      fi
      UEFI_IMAGE=$(pwd)/dump-bl33-v0
      cd - >/dev/null
    fi

    echo BlueField UEFI version: $UEFI_VER

    # sed regex needs to be escaped heavily, so in more normal terms:
    # (.+\..+\..+).*-[0-9]+-g[0-9a-fA-F]+
    # Essentially, match the string as Major.Minor.Patch in front,
    # the git describe tag from the back, and anything in the middle.
    BSP_MAJOR_MINOR_PATCH="$(echo "$UEFI_VER" | sed 's/\(.\+\..\+\..\+\).*-[0-9]\+-g[0-9a-fA-F]\+/\1/')"

    # cleanup temp files if needed.
    if [ -z "$UEFI_VER" ]; then
      rm -rf "$TMP_DIR"/uefi
    fi

    # Find build ID. Note that when locating strings in the UEFI_IMAGE binary it's possible that
    # the BId string can contain extra characters before and after the build ID that need to be
    # cut out. This sed regex takes only the numbers directly following "BId".
    BUILD_ID="$(strings -el "$UEFI_IMAGE" | grep "BId" | sed "s/.*BId\([0-9]*\).*/\1/")"

    if [ -z "$BUILD_ID" ]; then
        err "warn: could not find UEFI build ID (likely bootloader too old, using placeholder value)"
        BUILD_ID="$DEFAULT_BUILD_ID"
    fi

    if [ "$BUILD_ID" = 0 ]; then
        err "warn: build id of 0 (likely development image)"
    fi

    echo BlueField BSP version: ${BSP_MAJOR_MINOR_PATCH}.$BUILD_ID
    echo
}


PARSED_OPTIONS=$(getopt -n "$PROGNAME" -o hp:f: -l help,part:,file: -- "$@")
eval set -- "$PARSED_OPTIONS"

PARTLIST=
FILE=

while true
do
    case $1 in
        -h | --help)
            usage
            exit 0
            ;;
        -p | --part)
            PARTLIST="$2"
            shift 2
            ;;
        -f | --file)
            FILE="$2"
            shift 2
            ;;
        --)
            shift
            break
            ;;
    esac
done

if [ -n "$FILE" ]; then
    if [ ! -r $FILE ]; then
        err "fatal: $FILE does not exist or is not readable by the current user"
        exit 1
    fi

    print_path_vers $FILE

    # File mode, do not print any installation info
    exit 0
fi

m_uid=$(id -u)

if [ "$m_uid" -ne 0 ]; then
    err "fatal: must be run as root to check currently installed software"
    exit 1
fi

# Print bootloader versions
if [ -z "$PARTLIST" ]; then
    # Identify current primary boot partition and print versions
    print_path_vers $(mlxbf-bootctl | grep "primary" | cut -d ' ' -f 2)
else
    for part in $(echo $PARTLIST | cut -d"," -f1- --output-delimiter=" "); do
        if [ "$part" -ne 0 ] && [ "$part" -ne 1 ]; then
            err "err: bad partition $part"
            continue
        fi

        print_path_vers /dev/mmcblk0boot$part
    done
fi

# Print BlueField version number if Yocto version file is present
if [ -e "/etc/mlnx-release" ]; then
    echo OS Release Version: "$(cat /etc/mlnx-release)"
elif [ -e "/etc/bluefield_version" ]; then
	echo Yocto Release Version: "$(cat /etc/bluefield_version)"
else
	echo No distribution version file present
fi
