#!/bin/sh

# NOTE: use base shell syntax only, no bashims, for max portability and
# reduced dependencies.

# NOTE:
#
# Linux vfat filesystem driver will do a rename of a file over an existing
# file in the same directory by linking the inode (file data location in
# FAT) to the directory entry of the new name and then deleting the old
# directory entry, so the name is always pointing to valid file data,
# although for a short time there will be two directory entries pointing to
# the same file; which is still a benign error since the file name always
# exists (maybe more than once) and points to either the new or old file
# data. Using the dirsync option all updates to directory data are committed
# as soon as done, instead of being deferred.
#
# This means that we can safely do updates of bootloader files without the
# risk of the EFI BIOS not being able to boot without needing to switch to
# another partition for boot; as long as the new version of each file is
# compatible with the old version of the boot files not yet updated. This
# requires mounting the filesystem with the dirsync option, writing files to
# temporary names in the same directory as the final name, sync, rename the
# file to its final name, and sync again.

# The number of the EFI System Partition
ESP_PART_NUM=1

# The number of the EFI loader recovery partition
EFILDRRECO_PART_NUM=2

# If the machine has a menu boot entry (yes | no)
HAS_DSOS_MENU_BOOT_ENTRY=yes

is_nfsroot() {
    grep -q '\bnfsroot=' /proc/cmdline
}

# Do not do anything on NFS root
if is_nfsroot; then
    echo skipping bootloader since running on NFS root
    exit 0
fi

# checks if two files are equal, optionally updating the destination
# arg1: source path
# arg2: destination path
# arg3: mode: update (optional)
check_file() {
    [ -f "$2" ] && cmp -s "$1" "$2" && return 0
    [ "$3" != "update" ] && return 1
    # temporary file in same directory, we use a fixed "TMP" name to avoid
    # VFAT long file entries in the VFAT directory structure
    local tmpfile
    tmpfile="${2%/*}/TMP"
    cp "$1" "$tmpfile" && sync && mv -f "$tmpfile" "$2" && sync && \
        echo "updated $2" || echo "ERROR: failed updating $2" >&2
}

# checks all the boot files in a boot partition, optionally updating the destination
# arg1: directory where the boot partition is mounted
# arg2: type of partition: ESP | recovery
# arg3: mode: update (optional)
check_boot_files() {

    # NOTE: update order is important, as each updated file must be
    # compatible with the old files that will be updated later, in case the
    # update is interrupted.

    [ "$3" != "update" ] || mkdir -p "$1"/EFI/BOOT

    if [ "$2" = "ESP" ]; then
        check_file /boot/EFI/BOOT/bootx64.efi "$1"/EFI/BOOT/bootx64.efi $3 || return
    fi

    [ "$3" != "update" ] || mkdir -p "$1"/EFI/DSOS
    check_file /boot/EFI/BOOT/bootx64.efi "$1"/EFI/DSOS/grubx64.efi $3 || return

    if [ "$HAS_DSOS_MENU_BOOT_ENTRY" = "yes" ]; then
        [ "$3" != "update" ] || mkdir -p "$1"/EFI/DSOS/menu
        check_file /boot/EFI/BOOT/bootx64.efi "$1"/EFI/DSOS/menu/grubx64.efi $3 || return
    fi

    check_file /boot/EFI/BOOT/grub.cfg "$1"/EFI/BOOT/grub.cfg $3 || return
    check_file /boot/EFI/DSOS/grub.cfg "$1"/EFI/DSOS/grub.cfg $3 || return

    if [ -s /usr/share/resources/default/splash/boot.png ]; then
        check_file /usr/share/resources/default/splash/boot.png "$1"/EFI/DSOS/boot.png $3 || return
    fi

    check_file /boot/EFI/DSOS/CONFVER "$1"/EFI/DSOS/CONFVER $3 || return

    return 0
}

# Does the complete check of a boot partition, updating files if necessary
# arg1: mount point
# arg2: type of partition: ESP | recovery
# arg3: partition block device
check_boot_partition() {
    local ret
    local ver

    mkdir -p "$1" || return

    # we first check with the partition mounted read-only, and only mount
    # read-write if an update is needed.

    # ESP is always mounted, others should not and need mounting
    [ "$2" = "ESP" ] || mount -t vfat -o "$MNTOPTS,dirsync,ro" "$3" "$1" || return

    if [ ! -d "$1"/EFI/DSOS ]; then
        echo "ERROR: partition does not hold a DSOS bootloader configuration" >&2
    fi
    # Version 1.00 did not have any identifier saved in bootloader partition
    if [ ! -f "$1"/EFI/DSOS/CONFVER ]; then
        ver=1.0
    else
        ver="$(cat "$1"/EFI/DSOS/CONFVER)"
    fi

    # If the existing configuration has a higher version, or we cannot make
    # sense of the configuration version, we leave it alone; if versions are
    # equal we proceed as there may be compatible changes.
    if ! [ ${ver%%.*} -lt ${CONFVER%%.*} ] && ! [ ${ver%%.*} -eq ${CONFVER%%.*} -a ${ver#*.} -le ${CONFVER#*.} ]; then
        echo "bootloader configuration version ($ver) is higher, not updating"
        [ "$2" = "ESP" ] || umount "$1" || return
        return 0
    fi

    if check_boot_files "$1" "$2"; then
        echo all files are up to date
        [ "$2" = "ESP" ] || umount "$1" || return
        return 0
    fi

    echo "some files are out of date, updating"

    # repair any filesystem problems if needed
    umount "$1" || return
    if ! fsck.vfat -n "$3" > /dev/null 2>&1; then
        echo "doing filesystem recovery"
        fsck.vfat -a "$3"
    fi

    mount -t vfat -o "$MNTOPTS,dirsync,rw" "$3" "$1" || return

    # remove any data recovered by fsck to free the space
    rm -f "$1"/fsck*.rec

    if ! check_boot_files "$1" "$2" update; then
        echo "ERROR: failed to update bootloader files" >&2
        ret=1
    else
        echo bootloader files updated successfully
        ret=0
    fi
    umount "$1" || return

    # The ESP should always be mounted and has an entry in fstab
    [ "$2" != "ESP" ] || mount "$1" || return

    return $ret
}

#
# MAIN
#

# The configuration version
CONFVER="$(cat /boot/EFI/DSOS/CONFVER)"

echo "updating bootloader files to version $CONFVER"

# Load identifiers before starting
[ -f /etc/spinetix/identifiers ] && . /etc/spinetix/identifiers

# The disk block device where the bootloader partitions are found
MAIN_DISK_BDEV=/dev/"$SPX_MAIN_DISK_NAME"

# Only device names ending in a digit have "p" to separate the partition number
[ "${MAIN_DISK_BDEV%[0-9]}" != "${MAIN_DISK_BDEV}" ] && PARTSEP=p || PARTSEP=

ESP_BDEV="${MAIN_DISK_BDEV}${PARTSEP}${ESP_PART_NUM}"
EFILDRRECO_BDEV="${MAIN_DISK_BDEV}${PARTSEP}${EFILDRRECO_PART_NUM}"

if [ ! -b "$MAIN_DISK_BDEV" -o ! -b "$ESP_BDEV" -o ! -b "$EFILDRRECO_BDEV" ]; then
    echo "ERROR: main disk '$SPX_MAIN_DISK_NAME' not set, missing or EFI partitions missing" >&2
    exit 1
fi

if [ ! -d /efi ]; then
    echo "ERROR: /efi directory missing" >&2
    exit 1
fi

# Get the mount options from fstab
MNTOPTS="$(awk '/^[^#]*[[:space:]]\/efi[[:space:]]/ { print $4 }' /etc/fstab)"
if [ -z "$MNTOPTS" ]; then
    echo "ERROR: cannot find mount options for EFI partitions" >&2
    exit 1
fi

if ! type -t cmp fsck.vfat > /dev/null; then
    echo "ERROR: necessary commands are not available" >&2
fi

echo "checking bootloader in EFI loader recovery partition"
check_boot_partition /efi-recovery recovery "$EFILDRRECO_BDEV" || exit

echo "checking bootloader in EFI system partition"
check_boot_partition /efi ESP "$ESP_BDEV" || exit



