#!/bin/sh -eu

. shell-error
. shell-getopt
. shell-var

BOOTDEVICE=
ROOTDIR=
ADD_KERNELS='yes'
KERNELS_GROUP='Details Menu'
KERNEL_APPEND=
TEST=

show_help() {
	printf '%s\n' \
	"Usage: $PROG [options] [extlinux options]" \
	"" \
	"[Options]" \
	"  -b, --boot=DEVICE  specify the boot device;" \
	"  -r, --root=DIR     use the system rooted at DIR;" \
	"  -d, --dry-run      show what will be done and exit;" \
	"  -v, --verbose      print a message for each action;" \
	"  -V, --version      print program version and exit;" \
	"  -h, --help         show this text and exit." \
	"" \
	"Report bugs to authors." \
	""
	exit
}

print_version() {
	printf '%s\n' \
	"$PROG version 2.0" \
	"Written by Alexey Gladkov." \
	"" \
	"Copyright (C) 2010-2018  Alexey Gladkov <gladkov.alexey@gmail.com>" \
	"This is free software; see the source for copying conditions.  There is NO" \
	"warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." \
	exit
}

# Read 32-bit little-endian from GPT header and output its as decimal number.
# GPT header begins in the second sector of the disk. $1 is offset.
#
read_gpt() {
	local offset=$(($sector_size + $1))
	dd if="$dev" bs=1 skip=$offset count=4 2>/dev/null |
		hexdump '-e"%u"'
}

# Read single byte from partition table element and output its as decimal number.
# GPT partition table begins in the third sector of the disk. $1 is offset.
#
read_entry() {
	local offset=$(($sector_size * 2 + $1))
	local x="$(dd if="$dev" bs=1 skip=$offset count=1 2>/dev/null)"
	printf "%u" "'$x"
}

GETOPT_ALLOW_UNKNOWN=1
TEMP=`getopt -n $PROG -o b:,d,r:,v,V,h -l boot:,dry-run,root:,verbose,version,help -- "$@"` ||
	show_help
eval set -- "$TEMP"

while :; do
	case "$1" in
		-b|--boot) shift
			BOOTDEVICE="$1"
			;;
		-r|--root) shift
			ROOTDIR="${1%/}"
			;;
		-d|--dry-run)
			TEST=message
			verbose=-v
			;;
		-v|--verbose) verbose=-v
			;;
		-V|--version) show_version
			;;
		-h|--help) show_help
			;;
		--) shift; break
			;;
	esac
	shift
done

. "$ROOTDIR"/etc/sysconfig/extlinux

[ -z "${APPEND-}" ] ||
	KERNEL_APPEND="${KERNEL_APPEND:+KERNEL_APPEND }$APPEND"

boot_majmin="$(mountpoint -d "$ROOTDIR/boot")" ||:
root_majmin="$(mountpoint -d "$ROOTDIR/")" ||:

[ "$boot_majmin" = "$root_majmin" -o -L "$ROOTDIR/boot/boot" ] ||
	fatal "$ROOTDIR/boot a separate partition, but there's no '/boot/boot' symlink"

sysfs_path="$(readlink -fv "/sys/dev/block/$boot_majmin" 2>/dev/null)" ||
	fatal "Unable to get sysfs path"

[ -f "$sysfs_path/partition" ] ||
	fatal "Boot device must be a partition"

read partition < "$sysfs_path/partition"

dev="$(sed -n -e 's,^DEVNAME=,/dev/,p' "${sysfs_path%/*}/uevent")"
sector_size=$(blockdev --getss "$dev")

verbose "Device name: $dev"
verbose "Partition number: $partition"
verbose "Sector size: $sector_size"

#
# First, 2-bytes MBR-signature at address 510 (end of the first sector) is read.
# It should be read as 0x55, 0xAA.
# https://en.wikipedia.org/wiki/Master_boot_record#Sector_layout
#
magic="$(dd if="$dev" bs=2 skip=255 count=1 2>/dev/null | base64)"
[ "$magic" = "Vao=" ] ||
	fatal "Unknown partition table"

#
# Then 8-bytes GPT-signature "EFI PART" is checked at the beginning of the second
# sector of the disk.
# https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_table_header_(LBA_1)
#
magic="$(dd if="$dev" bs=1 skip=$sector_size count=8 2>/dev/null)"
if [ "$magic" = "EFI PART" ]; then
	partition_table='gpt'
	# Number of partition entries, 4 bytes little-endian.
	magic=$(read_gpt 80)
else
	partition_table='msdos'
	magic=4
fi

verbose "Partition table: $partition_table"

[ $partition -ge 1 ] && [ $partition -le $magic ] ||
	fatal "invalid partition number: $partition"

if [ "$partition_table" = "msdos" ]; then
	# MBR partition table starts at offset 446.
	# Each element of the table occupying 16 bytes.
	magic=$((16 * ($partition - 1) + 446))
	magic="$(dd if="$dev" bs=1 skip=$magic count=1 2>/dev/null | base64)"
	# First byte of the element is 0x80 for the boot partition.
	# https://en.wikipedia.org/wiki/Master_boot_record#PTE
	[ "$magic" = "gA==" ] ||
		fatal "partition not bootable"
	mbr_bin='mbr.bin'
else
	# Size of a single partition entry, 4 bytes little-endian.
	magic=$(read_gpt 84)
	# Attribute flags (8 bytes little-endian) starts at offset 48 of the entry.
	# https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_entries_(LBA_2-33)
	magic=$(($magic * ($partition - 1) + 48))
	# We interested only low byte of the attribute flags and only bit "2" (mask 0x04).
	magic=$(read_entry $magic)
	magic=$(($magic & 4))
	# If the bit "2" of the attribute flags is set,
	# partition is marked as bootable in the Legacy BIOS.
	[ "$magic" = "4" ] ||
		fatal "partition not bootable"
	mbr_bin='gptmbr.bin'
fi

verbose "Boot code: $mbr_bin"

DEVNAME=
dir="$(readlink -m "$ROOTDIR/")"
while read dev mpoint dummy; do
	[ "$mpoint" = "$dir" ] && [ "$dev" != 'rootfs' ] ||
		continue
	DEVNAME="$dev"
	break
done < /proc/mounts

[ -n "$DEVNAME" ] ||
	fatal "Unable to get device info '$ROOTDIR/'"

UUID="$(blkid -o value -s UUID -c /dev/null "$DEVNAME")"

cd "$ROOTDIR/boot/extlinux"

if [ -z "$TEST" ]; then
	verbose "Replace 'root=@ROOTDEV@' by 'root=UUID=$UUID'"

	for f in extlinux.conf extlinux.conf.d/*.conf; do
		[ ! -f "$f" ] ||
			sed -i -e "s#@ROOTDEV@#UUID=$UUID#ig" "$f"
	done
fi

rm $verbose -f -- extlinux.conf.d/kernels.conf
if shell_var_is_yes "$ADD_KERNELS"; then
	verbose "Creating new extlinux.conf.d/kernels.conf ..."

	printf > extlinux.conf.d/kernels.conf \
		'# Generated by %s. Do not edit manually.\n' "$PROG"

	[ -z "$KERNELS_GROUP" ] ||
		printf >> extlinux.conf.d/kernels.conf '%s\n' \
		"MENU BEGIN" \
		"MENU TITLE $KERNELS_GROUP" \
		""

	for f in "$ROOTDIR"/boot/vmlinuz*; do
		[ -f "$f" ] || continue

		vmlinuz="${f##*/}"
		version="${vmlinuz#vmlinuz}"
		initrd="initrd$version.img"

		[ -f "$ROOTDIR/boot/$initrd" ] ||
			initrd=

		[ -n "$version" ] &&
			label="Kernel ${version:+(${version#-})}" ||
			label='Kernel image'

		printf >> extlinux.conf.d/kernels.conf '%s\n' \
		"LABEL ${vmlinuz##*/}" \
		"MENU LABEL $label" \
		"KERNEL /boot/$vmlinuz${KERNEL_APPEND:+ $KERNEL_APPEND}${initrd:+
INITRD /boot/$initrd}" \
		""
	done

	[ -z "$KERNELS_GROUP" ] ||
		printf >> extlinux.conf.d/kernels.conf '%s\n' \
		"MENU SEPARATOR" \
		"LABEL return_main" \
		"MENU LABEL ^Return to main Menu" \
		"MENU EXIT" \
		"" \
		"MENU END"
fi

verbose "Creating new extlinux.conf.d/.index.conf ..."
printf > extlinux.conf.d/.index.conf \
	'# Generated by %s. Do not edit manually.\n' "$PROG"

def_label=
for f in extlinux.conf.d/*.conf; do
	[ -f "$f" ] || continue
	if [ -z "$def_label" ]; then
		while read name value; do
			case "$name" in
				[Ll][Aa][Bb][Ee][Ll])
					def_label="$value"
					break
					;;
			esac
		done < "$f"
		printf 'DEFAULT %s\n' "$def_label"
	fi
	printf 'INCLUDE %s\n' "$f"
done >> extlinux.conf.d/.index.conf

mode='install'
if [ -b "$BOOTDEVICE" ]; then
	ldr="$(sha1sum < "$mbr_bin")"
	siz="$(stat -c '%s' "$mbr_bin")"
	mbr="$(dd ibs="$siz" count=1 if="$BOOTDEVICE" 2>/dev/null |sha1sum)"

	[ "$mbr" != "$ldr" ] || mode='update'
	verbose "Using $mode mode"
fi

if [ "$mode" = 'install' ]; then
	if [ -b "$BOOTDEVICE" ]; then
		verbose "installing master boot record: $BOOTDEVICE"
		$TEST dd if="$mbr_bin" of="$BOOTDEVICE"
	else
		message "WARNING: MBR not installed. BOOTDEVICE not specified."
	fi
fi

$TEST extlinux --$mode "$PWD" "$@"
