#!/bin/bash
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: Copyright 2023 SUSE LLC
set -e
shopt -s nullglob

# set to --keep-title to use altenate screen. Better for debugging but causes flicker
dialog_altenate_screen=
dialog_backtitle="sdbootutil"
interactive=
verbose=
nl=$'\n'
shimdir="/usr/share/efi/$(uname -m)"
grub2moddir="/usr/share/grub2/$(uname -m)-efi"
arg_esp_path="$SYSTEMD_ESP_PATH"
arg_entry_token=
arg_arch=
arg_all_entries=
arg_no_variables=
arg_no_reuse_initrd=
arg_no_random_seed=
have_snapshots=
# for x in vmlinuz image vmlinux linux bzImage uImage Image zImage; do
image=

# State file for transactional systems
state_file="/var/lib/misc/transactional-update.state"

update_predictions=

rollback=()

tmpdir=$(mktemp -d -t sdbootutil.XXXXXX)
cleanup()
{
	local i
	for i in "${rollback[@]}"; do
		if [ -e "$i.bak" ]; then
			log_info "restoring $i"
			mv "$i.bak" "$i"
		else
			log_info "removing $i"
			rm -f "$i"
		fi
	done
	rm -rf "$tmpdir"
}
trap cleanup EXIT

entryfile="$tmpdir/entries.json"
snapperfile="$tmpdir/snapper.json"
tmpfile="$tmpdir/tmp"

helpandquit()
{
	cat <<-EOF
		Usage: $0 [OPTIONS] [COMMAND]
		OPTIONS:
		  --esp-path		Manually specify path to ESP
		  --arch		Manually set architecture
		  --entry-token		Override entry token
		  --image		Specify Linux kernel file name
		  --no-variables	Do not update UEFI variables
		  --no-reuse-initrd	Always regenerate initrd
		  -v, --verbose		More verbose output
		  -h, --help		This screen

		COMMAND:
		bootloader [SNAPSHOT]
			   Print the detected bootloader

		add-kernel VERSION [SNAPSHOT]
			   Create boot entry for specified kernel

		add-all-kernels [SNAPSHOT]
			   Create boot entries for all kernels in SNAPSHOT

		mkinitrd [SNAPSHOT]
			   Create boot entries for all kernels in SNAPSHOT,
			   assumes --no-reuse-initrd to regenerate initrds

		remove-kernel VERSION [SNAPSHOT]
			   Remove boot entry for specified kernel

		remove-all-kernels [SNAPSHOT]
			   Remove boot entries for all kernels in SNAPSHOT

		list-kernels [SNAPSHOT]
			   List all kernels related to SNAPSHOT

		list-entries [SNAPSHOT]
			   List all entries related to SNAPSHOT

		list-snapshots
			   List all snapshots

		set-default-snapshot [SNAPSHOT]
			   Make SNAPSHOT the default for next boot.
			   Also install all kernels if needed

		is-bootable [SNAPSHOT]
			   Check whether SNAPSHOT has any kernels registered, ie
			   is potentially bootable

		install    Install systemd-boot and shim into ESP
		needs-update
			   Check whether the bootloader in ESP needs updating
		update
			    Update the bootloader if it's old
		force-update
			    Update the bootloader in any case
		update-predictions
			    Update TPM2 predictions

		UI commands:
		kernels    Open kernel menu
		snapshots  Open snapshots menu
		entries    Open entry menu

	EOF
	exit 0
}

log_info()
{
	[ "${verbose:-0}" -gt 0 ] || return 0
	echo "$@"
}

d(){
	local retval=0
	# Bash makes it a bit annoying to read the output of a different FD into a variable, it
	# only supports reading stdout by itself. So redirect 3 to stdout and 1 to the real stdout.
	exec {stdoutfd}>&1
	result="$(dialog $dialog_altenate_screen --backtitle "$dialog_backtitle" --output-fd 3 "$@" 3>&1 1>&${stdoutfd})" || retval=$?
	# Word splitting makes it necessary to use eval here.
	eval "exec ${stdoutfd}>&-"
	return "$retval"
}

err()
{
	if [ "$interactive" = 1 ]; then
		d --title 'Error' --ok-label "Quit" --colors --aspect 60 --msgbox "\Z1Error:\Zn $*" 0 0
	else
		echo "Error: $*" >&2
	fi
	exit 1
}

warn()
{
	if [ "$interactive" = 1 ]; then
		d --title 'Warning' --ok-label "Continue" --colors --aspect 60 --msgbox "\Z1Warning:\Zn $*" 0 0
	else
		echo "Warning: $*" >&2
	fi
}

is_sdboot()
{
	# If systemd-boot and grub2 are co-installed, we favor grub2
	# in the detection
	local sdboot grub2
	sdboot="$(find_sdboot "${1-$root_snapshot}")"
	grub2="$(find_grub2 "${1-$root_snapshot}")"
	[ -e "$sdboot" ] && [ ! -e "$grub2" ]
}

is_grub2()
{
	# If systemd-boot and grub2 are co-installed, we favor grub2
	# in the detection
	local grub2
	grub2="$(find_grub2 "${1-$root_snapshot}")"
	[ -e "$grub2" ]
}

reset_rollback()
{
	for i in "${rollback[@]}"; do
		[ -e "$i.bak" ] || continue
		log_info "removing $i.bak"
		rm -f "$i.bak"
	done
	rollback=()
}

run_command_live_output()
{
	if [ "$interactive" = 1 ]; then
		"$@" 2>&1 | dialog $dialog_altenate_screen --backtitle "$dialog_backtitle" --title "$1" --aspect 60 --progressbox 0 0
	else
		"$@"
	fi
}

run_command_output()
{
	if [ "$interactive" = 1 ]; then
		"$@" > "$tmpfile" 2>&1
		[ -s "$tmpfile" ] && d --textbox "$tmpfile" 0 0
	else
		"$@"
	fi
}

# Given the number of total item pairs, outputs the number of items to display at once
menuheight() {
	local height=$(($1 / 2))
	[ "$height" -le "$dh_menu" ] || height="$dh_menu"
	echo "$height"
}

stty_size() {
	# shellcheck disable=SC2046
	set -- $(stty size 2>/dev/null)
	LINES="$1"
	COLUMNS="$2"
	# stty size can return zero when not ready or
	# its a serial console
	if [ "$COLUMNS" = "0" ] || [ "$LINES" = "0" ]; then
		LINES=24
		COLUMNS=80
	fi

	dh_menu=$((LINES-15))
}

# check whether it's a transactional system
is_transactional()
{
	[ "$(stat -f -c %T /etc)" = "overlayfs" ]
}

subvol_is_ro()
{
	[ -n "$have_snapshots" ] || return 0
	local subvol="${1:?}"
	while read -r line; do
		[ "$line" = "ro=true" ] && return 0
	done < <(btrfs prop get -t s "${subvol#"${subvol_prefix}"}" ro)
	return 1
}

detect_parent() {
	local subvol="$1"
	[ -n "$have_snapshots" ] || return 0
	parent_uuid="$(btrfs subvol show "${subvol#"${subvol_prefix}"}" | sed -ne 's/\s*Parent UUID:\s*//p')"
	[ "$parent_uuid" != '-' ] || parent_uuid=
	[ -n "$parent_uuid" ] || return 0
	parent_subvol="$(/sbin/btrfs subvol show -u "$parent_uuid" "${subvol#"${subvol_prefix}"}" | head -1)"
	parent_snapshot="${parent_subvol#"${subvol_prefix}"/.snapshots/}"
	if [ "$parent_subvol" = "$parent_snapshot" ]; then
		unset parent_subvol parent_snapshot
	else
		parent_snapshot="${parent_snapshot%/snapshot}"
	fi
}

sedrootflags()
{
	local subvol="$1"
	# - delete BOOT_IMAGE= and initrd=
	# - make sure root= refers to uuid
	# - replace or add rootflags to point at correct subvolume
	# - replace or add systemd.machine-id to match current machine-id
	#
	# from the sed manual:
	# ‘t’
	#     branch conditionally (that is: jump to a label) _only if_ a ‘s///’
	#     command has succeeded since the last input line was read or another
	#     conditional branch was taken.
	# we use the t command to jump over an expression that appends a
	# parameter if replacing the parameter succeeded (ie it was
	# already there). Since we always operate on the same line,
	# "empty" t jumps are used to reset the condition after very
	# s///.
	local sed_arguments=("-e s/[ \t]\+/ /g"\
		"-e s/\<\(BOOT_IMAGE\|initrd\)=[^ ]* \?//"\
		"-e s/\<root=[^ ]*/root=UUID=$root_uuid/;tb;s,\$, root=UUID=$root_uuid,;tc;:c;:b")
	[ -z "$have_snapshots" ] || sed_arguments+=("-e s,\<rootflags=subvol=[^ ]*,rootflags=subvol=$subvol,;td;s,\$, rootflags=subvol=$subvol,;te;:e;:d")
	[ -z "$machine_id" ] || sed_arguments+=("-e s,\<systemd.machine_id=[^ ]*,systemd.machine_id=$machine_id,;tf;s,\$, systemd.machine_id=$machine_id,;tg;:g;:f")
	sed "${sed_arguments[@]}"
}

entry_filter=("cat")
update_entries()
{
	[ -z "$1" ] || entry_filter=("$@")
	bootctl list --json=short | "${entry_filter[@]}" > "$entryfile"
}

update_entries_for_subvol()
{
	local subvol="$1"
	update_entries jq "[.[]|select(has(\"options\"))|select(.options|match(\"root=UUID=$root_uuid .*rootflags=subvol=$subvol\"))]"
}

update_entries_for_snapshot()
{
	local n="$1"
	update_entries_for_subvol "${subvol_prefix}/.snapshots/$n/snapshot"
}

update_entries_for_this_system()
{
	update_entries jq "[.[]|select(has(\"options\"))|select(.options|match(\"root=UUID=$root_uuid\"))]"
}

find_conf_file() {
	local kernel_version="${1:?}"
	local snapshot="$2"
	local id="$entry_token-$kernel_version${snapshot:+-$snapshot}.conf"

	update_entries_for_snapshot "$snapshot"

	while IFS= read -r path; do
		if [ -f "$path" ]; then
			echo "$path"
			return 0
		fi
	done < <(jq -r --arg id "$id" '.[] | select(.id == $id) | .path' < "$entryfile")

	return 1
}

settle_entry_token()
{
	local snapshot="$1"
	set_os_release "$snapshot"
	set_machine_id "$snapshot"
	case "$arg_entry_token" in
		""|auto)
			if [ -s '/etc/kernel/entry-token' ]; then
				read -r entry_token < '/etc/kernel/entry-token'
			else
				local var
				for var in machine_id os_release_IMAGE_ID os_release_ID; do
					entry_token="${!var}"
					[ -z "$entry_token" ] || break
				done
			fi
			;;
		machine-id)
			[ -n "$machine_id" ] || err "Couldn't determine machine-id"
			entry_token="$machine_id"
			;;
		os-id)
			# shellcheck disable=SC2154
			entry_token="$os_release_ID"
			[ -n "$entry_token" ] || err "Missing ID"
			;;
		os-image)
			# shellcheck disable=SC2154
			entry_token="$os_release_IMAGE_ID"
			[ -n "$entry_token" ] || err "Missing IMAGE_ID"
			;;
		literal:*)
			entry_token="${arg_entry_token#literal:}"
			;;
		*) err "Unexpected parameter for --entry-token=: $arg_entry_token" ;;
	esac
	[ -n "$entry_token" ] || err "Can't determine entry-token"
	return 0
}

remove_kernel()
{
	local snapshot="$1"
	local kernel_version="$2"
	settle_entry_token "${snapshot}"
	local id="$entry_token-$kernel_version${snapshot:+-$snapshot}.conf"
	run_command_output bootctl unlink "$id"

	# This action will require to update the PCR predictions
	update_predictions=1
}

install_with_rollback()
{
	local src="${1:?}"
	local dst="${2:?}"

	if [ -e "$dst" ]; then
		if cmp -s "$src" "$dst"; then
			log_info "$dst unchanged"
			return 0
		fi
		mv "$dst" "$dst.bak" || return "$?"
	fi
	rollback+=("$dst")
	install -p -m 0644 "$src" "$dst" || return "$?"
	chown root:root "$dst" 2>/dev/null || :
	log_info "installed $dst"
}

update_snapper()
{
	snapper --jsonout --no-dbus list --disable-used-space > "$snapperfile"
}

set_snapper_title_and_sortkey()
{
	[ -n "$have_snapshots" ] || return 0
	snapshot="${1:?}"
	local type date desc important pre_num
	local snapshot_info

	update_snapper

	# shellcheck disable=SC2046
	IFS="|" read -r type date desc important pre_num <<< \
		$(jq -r --arg snapshot "$snapshot" \
		'.["root"][]|select(.number==( $snapshot|tonumber))|[.type,.date,(.description|gsub("\\|";"_")),.userdata.important,."pre-number"//""]|join("|")'\
		< "$snapperfile")

	if [ -z "$desc" ] && [ "$type" = "post" ] && [ -n "$pre_num" ]; then
		read -r desc <<< "$(jq -r --arg snapshot "$pre_num" '.["root"][]|select(.number==($snapshot|tonumber))|.description' < "$snapperfile")"
	fi

	if [ "$important" = "yes" ]; then important="*"; else important=""; fi
	[ "$type" = "single" ] && type=""
	snapshot_info="$snapshot,$kernel_version,$date${type:+, $type}${desc:+, $desc}"

	# shellcheck disable=SC2154
	title="Snapper: ${important}$title ($snapshot_info)"
	sort_key="snapper-$sort_key"
}

set_os_release()
{
	local snapshot="$1"
	local subvol=""
	[ -z "$snapshot" ] || subvol="${subvol_prefix}/.snapshots/${snapshot}/snapshot"
	os_release_files=(
		"${subvol#"${subvol_prefix}"}/usr/lib/os-release"
		"${subvol#"${subvol_prefix}"}/etc/os-release"
	)

	for file in "${os_release_files[@]}"; do
		[ -f "$file" ] || continue
		eval $(sed -ne '/^[A-Z_]\+=/s/^/os_release_/p' < "$file")
		break
	done
}

set_machine_id()
{
	local snapshot="$1"
	local subvol=""
	[ -z "$snapshot" ] || subvol="${subvol_prefix}/.snapshots/${snapshot}/snapshot"
	machine_id_files=()
	if is_transactional && [ -z "$TRANSACTIONAL_UPDATE" ]; then
		[ -n "$snapshot" ] && machine_id_files+=("/var/lib/overlay/$snapshot/etc/machine-id")
	fi
	machine_id_files+=(
		"${subvol#"${subvol_prefix}"}/etc/machine-id"
	)

	for file in "${machine_id_files[@]}"; do
		if [ -s "$file" ]; then
			read -r machine_id < "$file"
			break
		fi
	done
}

reuse_initrd() {
	local snapshot="$1"
	local subvol="$2"
	local kernel_version="${3:?}"
	local conf

	[ -z "$arg_no_reuse_initrd" ] || return 1
	settle_entry_token "${snapshot}"

	conf="$(find_conf_file "$kernel_version" "${snapshot}")"
	local find_conf_status=$?

	if [ $find_conf_status -ne 0 ]; then
		# check if we can reuse the initrd from the parent
		# to avoid expensive regeneration
		detect_parent "$subvol"
		if [ -n "$parent_subvol" ]; then
			settle_entry_token "$parent_snapshot"
			conf="$(find_conf_file "$kernel_version" "$parent_snapshot")"
			find_conf_status=$?
		fi
	fi

	if [ "$find_conf_status" -eq 0 ]; then
		local k v
		while read -r k v; do
			[ "$k" = 'initrd' ] || continue
			log_info "found existing initrd $v"
			dstinitrd+=("$v")
		done < "$conf"
		[ -z "$dstinitrd" ] || return 0
	fi

	return 1
}

mount_etc()
{
	local snapshot_dir="$1"

	# don't mount if we are within a transactional-update shell
        [ -z "$TRANSACTIONAL_UPDATE" ] || return 0

	IFS=',' read -ra fields <<< \
	   $(findmnt --tab-file "${snapshot_dir}/etc/fstab" --noheadings --nofsroot --output OPTIONS /etc | sed 's#/sysroot##g' | sed 's#:/etc,#:'"${snapshot_dir}"'/etc,#g')

	local lower=""
	local upper=""
	for element in "${fields[@]}"; do
		IFS='=' read -r key value <<< "$element"
		[ "$key" = "lowerdir" ] && lower="$value"
		[ "$key" = "upperdir" ] && upper="$value"
	done

	mount overlay -t overlay -o ro,"lowerdir=${upper}:${lower}" "${snapshot_dir}/etc"
}

umount_etc()
{
	local snapshot_dir="$1"
	# don't umount if we are within a transactional-update shell
        [ -z "$TRANSACTIONAL_UPDATE" ] || return 0
	umount "${snapshot_dir}/etc"
}

add_version_to_title()
{
	# TW pretty name does not include the version
	[ -n "$os_release_VERSION" ] || title="$title $os_release_VERSION_ID"
}

install_kernel()
{
	local snapshot="$1"
	local subvol=""
	[ -z "$have_snapshots" ] || subvol="${subvol_prefix}/.snapshots/${snapshot}/snapshot"
	local kernel_version="$2"
	local dstinitrd=()
	local src="${subvol#"${subvol_prefix}"}/lib/modules/$kernel_version/$image"
	local initrddir="${subvol#"${subvol_prefix}"}/usr/lib/initrd"
	[ -e "$src" ] || err "Can't find $src"

	calc_chksum "$src"
	settle_entry_token "${snapshot}"
	local dst="/$entry_token/$kernel_version/linux-$chksum"

	local initrd="${src%/*}/initrd"

	mkdir -p "$boot_root${dst%/*}"

	if [ -e "$initrd" ]; then
		ln -s "$initrd" "$tmpdir/initrd-0"
	elif [ -d "$initrddir" ] && [ -x "/usr/bin/mkmoduleinitrd" ]; then
		local f i
		i=0
		for f in "$initrddir"/*; do
			ln -s "$f" "$tmpdir/initrd-$i"
			((++i))
		done
		/usr/bin/mkmoduleinitrd "${subvol#"${subvol_prefix}"}" "$kernel_version" "$tmpdir/initrd-$i"
	elif ! reuse_initrd "$snapshot" "$subvol" "$kernel_version"; then
		local snapshot_dir="/.snapshots/$snapshot/snapshot"
		local dracut_args=()
		dracut_args=('--force' '--tmpdir' '/var/tmp')
		if [ "$subvol" != "$root_subvol" ] && [ -n "$have_snapshots" ]; then
			dracut_args+=('--sysroot' "${snapshot_dir}" '--add-device' "$root_device")
		fi
		log_info "generating new initrd"

		# In MicroOS we need to be sure to have the same /etc
		# inside the snapshot.  For example, /etc/crypttab can
		# have modifications in the overlay that will be
		# visible once the snapshot is active, but the version
		# in /.snashots is still the unmodified base
		is_transactional && mount_etc "${snapshot_dir}"
		run_command_live_output dracut --quiet --reproducible "${dracut_args[@]}" "$tmpdir/initrd-0" "$kernel_version"
		is_transactional && umount_etc "${snapshot_dir}"
	fi

	local boot_options=
	for i in /etc/kernel/cmdline /usr/lib/kernel/cmdline /proc/cmdline; do
		[ -f "$i" ] || continue
		boot_options="$(sedrootflags "$subvol" < "$i")"
		break
	done

	if [ -z "$dstinitrd" ] && [ -e "$tmpdir/initrd-0" ]; then
		i=0
		while [ -e "$tmpdir/initrd-$i" ]; do
			calc_chksum "$tmpdir/initrd-$i"
			dstinitrd+=("${dst%/*}/initrd-$chksum")
			((++i))
		done
	fi

	title="${os_release_PRETTY_NAME:-Linux $kernel_version}"
	# shellcheck disable=SC2154
	sort_key="$os_release_ID"

	if is_transactional; then
		add_version_to_title
	elif subvol_is_ro "$subvol"; then
		add_version_to_title
		set_snapper_title_and_sortkey "$snapshot"
	fi

	local entry_machine_id=
	[ "$entry_token" = "$machine_id" ] && entry_machine_id="$machine_id"

	cat > "$tmpdir/entry.conf" <<-EOF
	# Boot Loader Specification type#1 entry
	title      $title
	version    $snapshot@$kernel_version${entry_machine_id:+${nl}machine-id $entry_machine_id}${sort_key:+${nl}sort-key   $sort_key}
	options    $boot_options
	linux      $dst
	EOF
	for i in "${dstinitrd[@]}"; do
		echo "initrd     $i" >> "$tmpdir/entry.conf"
	done

	local failed=
	if [ ! -e "$boot_root$dst" ]; then
		install_with_rollback "$src" "$boot_root$dst" || failed=kernel
	else
		log_info "reusing $boot_root$dst"
	fi
	if [ -z "$failed" ] && [ -e "$tmpdir/initrd-0" ]; then
		i=0
		while [ -e "$tmpdir/initrd-$i" ]; do
			if [ ! -e "$boot_root${dstinitrd[$i]}" ]; then
				install_with_rollback "$tmpdir/initrd-$i" "$boot_root${dstinitrd[$i]}" || { failed=initrd; break; }
				rm -f "$tmpdir/initrd-$i"
			fi
			((++i))
		done
	fi
	if [ -z "$failed" ]; then
		local tries
		if [ -f /etc/kernel/tries ]; then
			read -r tries < /etc/kernel/tries
		fi

		if ! [[ "$tries" =~ ^[0-9]+$ ]]; then
			tries=
		fi

		loader_entry="$boot_root/loader/entries/$entry_token-$kernel_version${snapshot:+-$snapshot}${tries:++$tries}.conf"
		install_with_rollback "$tmpdir/entry.conf" "$loader_entry" || failed="bootloader entry"
		rm -f "$tmpdir/entry.conf"
	fi
	[ -z "$failed" ] || err "Failed to install $failed"
	reset_rollback

	# This action will require to update the PCR predictions
	update_predictions=1
}

install_all_kernels()
{
	local snapshot="$1"
	find_kernels "$snapshot"
	for kv in "${!found_kernels[@]}"; do
		log_info "installing $kv"
		install_kernel "${snapshot}" "$kv"
	done

}

remove_all_kernels()
{
	local snapshot="$1"
	find_kernels "$snapshot"
	for kv in "${!found_kernels[@]}"; do
		remove_kernel "${snapshot}" "$kv"
	done

}

list_entries()
{
	if [ ! -s "$entryfile" ]; then
		if [ -n "$1" ]; then
			update_entries_for_snapshot "$1"
		elif [ -n "$arg_all_entries" ]; then
			update_entries
		else
			update_entries_for_this_system
		fi
	fi

	local isdefault isreported type id root conf title
	while read -r isdefault isreported type id root conf title; do
		color=
		if [ "$isdefault" = "true" ]; then
			color="\e[1;4m"
		fi
		if [ "$isreported" = "false" ]; then
			color="$color\e[32m"
		fi
		if [ "$type" = "loader" ]; then
			color="$color\e[33m"
		fi
		local errors=()
		if [ -n "$verbose" ] && [ -n "$conf" ] && [  -e "$conf" ]; then
			local k
			local v
			while read -r k v; do
				if [ "$k" = 'linux' ] || [ "$k" = 'initrd' ] ; then
					if [ ! -e "$root$v" ]; then
						errors+=("$root/$v does not exist")
					fi
				fi
				[ -n "$have_snapshots" ] || break
				if [ "$k" = 'options' ]; then
					local snapshot
					read -r snapshot <<<"$(echo "$v" | sed -e "s,.*rootflags=subvol=${subvol_prefix}/.snapshots/\([0-9]\+\)/snapshot.*,\1,")"
					if [ ! -d "/.snapshots/$snapshot/snapshot" ]; then
						errors+=("/.snapshot/$snapshot/snapshot does not exist")
					fi
				fi
			done < "$conf"
		fi
		if [ -n "$errors" ]; then
			echo -e "  \e[31m${errors[*]}\e[m" >&2
		fi
		echo -e "$color$id${verbose:+: $title}\e[m"
	done < <(jq '.[]|[.isDefault, if has("isReported") then .isReported else 0 end, if has("type") then .type else "unknown" end, .id, .root, .path, .showTitle]|join(" ")' -r < "$entryfile")
}

show_entries()
{
	local dialogtitle="${1:-Entries}"

	[ -s "$entryfile" ] || update_entries_for_this_system

	while true; do
		local list=()
		local n=0
		local default=
		while read -r isdefault isreported type title; do
			color=
			if [ "$isdefault" = "true" ]; then
				default="$n"
				color="\\Zb\Zu"
			fi
			if [ "$isreported" = "false" ]; then
				color="$color\\Z2"
			fi
			if [ "$type" = "loader" ]; then
				color="$color\\Z5"
			fi
			list+=("$n" "$color$title\\Zn")
			n=$((++n))
		done < <(jq '.[]|[.isDefault, if has("isReported") then .isReported else 0 end, if has("type") then .type else "unknown" end, .showTitle]|join(" ")' -r < "$entryfile")
		if [ "${#list}" = 0 ]; then
			d --msgbox "No entries" 0 0
			return 0
		fi
		local d_args=(--no-hot-list --colors --ok-label "Options" --cancel-label "Back")
		[ -n "$arg_all_entries" ] || d_args+=(--extra-button --extra-label "All")
		d "${d_args[@]}" --menu "$dialogtitle" 0 0 "$(menuheight ${#list[@]})" "${list[@]}"  || {
			if [ "$?" = 3 ]; then
				arg_all_entries=1
				update_entries cat
				continue
			fi
			return 0
		}
		n="$result"

		show_entry ".[$n]"
	done
}

show_entry()
{
	local filter="$1"
	local type
	local isreported
	local isdefault
	local new=

	read -r isdefault isreported type title < <(jq "$filter"'|[.isDefault, if has("isReported") then .isReported else 0 end, if has("type") then .type else "unknown" end, .showTitle]|join(" ")' -r < "$entryfile")

	[ "$isdefault" = true ] || isdefault=
	[ "$isreported" = true ] || new=1

	if [ -n "$isdefault$new" ]; then
		title="$title ["
		[ -z "$isdefault" ] || title="${title}default"
		[ -z "$new" ] || title="${title}${isdefault:+,}new"
		title="$title]"
	fi

	while true; do
		local list=(show json)
		if [ "$type" = "type1" ]; then
			list+=(cat Raw edit Edit)
		fi
		if [ -z "$isdefault" ]; then
			list+=(set-default "set as default" oneshot "set as one-shot")
			if [ "$type" != "loader" ]; then
				list+=(delete delete)
			fi
		fi
		d --no-tags --menu "Entry #$title" 0 0 "$(menuheight ${#list[@]})" "${list[@]}" || break
		action="$result"

		case "$action" in
			show)
				jq "$filter" < "$entryfile" > "$tmpfile"
				d --textbox "$tmpfile" 0 0
				;;
			cat)
				read -r fn < <(jq -r "$filter|.path" < "$entryfile")
				d --textbox "$fn" 0 0
				;;
			edit)
				read -r fn < <(jq -r "$filter|.path" < "$entryfile")
				${EDITOR:-vim} "$fn"
				update_entries
				;;
			delete)
				read -r id < <(jq -r "$filter|.id" < "$entryfile")
				bootctl unlink "$id" > "$tmpfile" 2>&1
				[ -s "$tmpfile" ] && d --textbox "$tmpfile" 0 0
				update_entries
				break
				;;
			set-default)
				read -r id < <(jq -r "$filter|.id" < "$entryfile")
				set_default_entry "$id"
				update_entries
				break
				;;
			oneshot)
				read -r id < <(jq -r "$filter|.id" < "$entryfile")
				bootctl set-oneshot "$id" > "$tmpfile" 2>&1
				[ -s "$tmpfile" ] && d --textbox "$tmpfile" 0 0
				update_entries
				break
				;;
		esac
	done
}

list_snapshots()
{
	[ -n "$have_snapshots"  ] || { log_info "System does no support snapshots."; return 0; }
	update_snapper 2>"$tmpfile" || err "$(cat "$tmpfile")"

	local n=0
	while read -r n isdefault title; do
		[ "$n" != "0" ] || continue
		local id="$n"
		if [ "$isdefault" = "true" ]; then
			id="\e[1;4m$id\e[m"
		fi
		update_kernels "$n"
		[ "$is_bootable" = 1 ] || id="!$id"
		echo -e "$id $title"
	done < <(jq '.root|.[]|[.number, .default, .description]|join(" ")' -r < "$snapperfile")
}

show_snapper()
{
	[ -n "$have_snapshots" ] || { log_info "System does no support snapshots."; return 0; }
	if ! update_snapper 2>"$tmpfile"; then
		d --title "Error" --textbox "$tmpfile" 0 0
		exit 1
	fi

	while true; do
		local list=()
		local n=0
		local default=
		while read -r n isdefault title; do
			[ "$n" != "0" ] || continue
			if [ "$isdefault" = "true" ]; then
				default="$n"
				title="\\Zb\Zu$title\\Zn"
			fi
			update_kernels "$n"
			[ "$is_bootable" = 1 ] || title="!$title"
			list+=("$n" "$title")
		done < <(jq '.root|.[]|[.number, .default, .description]|join(" ")' -r < "$snapperfile")
		if [ "${#list}" = 0 ]; then
			d --msgbox "No snapshots" 0 0
			return 0
		fi
		d --no-hot-list --colors --ok-label "Options" --cancel-label "Back" --menu "Snapshots" 0 0 "$(menuheight ${#list[@]})" "${list[@]}" || return 0
		n="$result"

		while true; do
			list=(kernels kernels entries entries show json)
			if [ "$n" != "$default" ]; then
				list+=(delete delete)
			fi
			d --no-tags --menu "Snapshot #$n" 0 0 "$(menuheight ${#list[@]})" "${list[@]}" || break
			action="$result"

			case "$action" in
				show)
					jq ".root|.[]|select(.number==$n)" < "$snapperfile" > "$tmpfile"
					d --textbox "$tmpfile" 0 0
					;;
				entries)
					update_entries_for_snapshot "$n"
					show_entries "Entries for Snapshot $n"
					;;
				kernels)
					show_kernels "$n"
					;;
			esac
		done
	done
}

calc_chksum() {
    # shellcheck disable=SC2046
    set -- $(sha1sum "$1")
    chksum="$1"
}

# map with kernel version as key and checksum as value
declare -A found_kernels
find_kernels()
{
	local subvol=""
	[ -z "$have_snapshots" ] || subvol="${subvol_prefix}/.snapshots/${1:?}/snapshot"
	local fn kv
	found_kernels=()

	for fn in "${subvol#"${subvol_prefix}"}"/usr/lib/modules/*/"$image"; do
		kv="${fn%/*}"
		kv="${kv##*/}"
		calc_chksum "$fn"
		found_kernels["$kv"]="$chksum"
		log_info "found kernel $kv = $chksum"
	done
}

# map that uses expected path on the ESP for each installed kernel as key. The
# value is the entry id if an entry exists.
declare -A installed_kernels
# map of ESP path to id of kernels that are not in the subvol
declare -A stale_kernels
is_bootable=
update_kernels()
{
	local snapshot="$1"
	local path id
	installed_kernels=()
	stale_kernels=()
	is_bootable=
	find_kernels "$snapshot"
	settle_entry_token "${snapshot}"
	for kv in "${!found_kernels[@]}"; do
		installed_kernels["/$entry_token/$kv/linux-${found_kernels[$kv]}"]=''
	done
	update_entries_for_snapshot "$snapshot"

	# XXX: maybe we should parse the actual path in the entry
	while read -r path id; do
		if [ "${installed_kernels[$path]+none}" = 'none' ]; then
			installed_kernels["$path"]="$id"
			is_bootable=1
		else
			# kernel in ESP that is not installed
			stale_kernels["$path"]="$id"
		fi
	done < <(jq -r '.[]|select(has("linux"))|[.linux,.id]|join(" ")'< "$entryfile")
}

list_kernels()
{
	local snapshot=""
	[ -z "$have_snapshots" ] || snapshot="${1:?}"
	update_kernels "$snapshot"
	local kernelfiles=("${!installed_kernels[@]}")
	for k in "${kernelfiles[@]}"; do
		local id="${installed_kernels[$k]}"
		local kv="${k%/*}"
		kv="${kv##*/}"
		if [ -z "$id" ]; then
			echo -e "\e[33mmissing /lib/modules/$kv/$image\e[m"
		else
			echo "ok /lib/modules/$kv/$image -> $id"
		fi
	done
	kernelfiles=("${!stale_kernels[@]}")
	for k in "${kernelfiles[@]}"; do
		local id="${stale_kernels[$k]}"
		printf "\e[31mstale %s\e[m\n" "$id"
	done
}

is_bootable()
{
	local snapshot="$1"
	update_kernels "$snapshot"

	[ "$is_bootable" = 1 ] || return 1
	return 0
}

show_kernels()
{
	local subvol=""
	local snapshot="$1"
	[ -z "$have_snapshots" ] || subvol="${subvol_prefix}/.snapshots/${1:?}/snapshot"
	while true; do
		update_kernels "$snapshot"
		local list=()
		local n=0
		local default=
		local id
		local kernelfiles=("${!installed_kernels[@]}" "${!stale_kernels[@]}")
		local ids=()
		for k in "${kernelfiles[@]}"; do
			if [  "${installed_kernels[$k]+yup}" = yup ]; then
				id="${installed_kernels[$k]}"
				if [ -z "$id" ]; then
					state="missing"
				else
					state="ok"
				fi
			else
				id="${stale_kernels[$k]}"
				state='stale'
			fi
			ids+=("$id")
			s="${k#/*/}"
			list+=("$n" "$(printf "%-10s %s" "$state" "$s")")
			n=$((++n))
		done
		if [ "${#list}" = 0 ]; then
			d --msgbox "No kernels" 0 0
			return 1
		fi
		d --no-tags --no-hot-list --colors --ok-label "Options" --cancel-label "Back" --menu "Kernels associated with $subvol" 0 0 "$(menuheight ${#list[@]})" "${list[@]}" || return 0
		n="$result"

		list=()
		id="${ids[$n]}"
		if [ -z "$id" ]; then
			list+=(install "Install")
		else
			list+=(show "Entry")
		fi
		list+=(entries "Other Entries")

		local kv="${kernelfiles[$n]%/*}"
		kv="${kv##*/}"
		local title="Kernel $kv"
		while true; do
			d --no-tags --no-hot-list --colors --ok-label "Ok" --cancel-label "Back" --menu "$title" 0 0 "$(menuheight ${#list[@]})" "${list[@]}" || break
			action="$result"

			case "$action" in
				entries)
					update_entries jq "[.[]|select(has(\"linux\"))|select(.linux|match(\"${kernelfiles[$n]}\"))]"
					show_entries "Entries for kernel ${kernelfiles[$n]#/*/}"
					;;
				show)
					show_entry ".[]|select(.id|match(\"$id\"))"
					break # might have selected delete so refresh
					;;
				install)
					install_kernel "$snapshot" "$kv"
					break
					;;
			esac
		done
	done
}

bootloader_version()
{
	local fn="$1"
	if [ -z "$1" ]; then
		if [ -e "$shimdir/shim.efi" ]; then
			fn="$boot_root$boot_dst/grub.efi"
		else
			local bootloader
			bootloader="$(find_bootloader)"
			fn="$boot_root$boot_dst/${bootloader##*/}"
		fi
	fi
	[ -e "$fn" ] || return 1
	if is_sdboot; then
		read -r _ _ _ v _ < <(grep -ao '#### LoaderInfo: systemd-boot [^#]\+ ####' "$fn")
	else
		# Useless as it reports mayor.minor, so append the
		# last update time until the minutes, as the FAT store
		# dates differently than other filesystems
		read -r _ _ _ v _ < <(grep -aoP 'GNU GRUB  version %s\x00[^\x00]+\x00' "$fn")
		v="${v:2}-$(date -r "$fn" +'%Y%m%d%H%M')"
	fi
	[ -n "$v" ] || return 1
	echo "$v"
}

is_installed()
{
	bootloader_version > /dev/null && [ -e "$boot_root/$boot_dst/installed_by_sdbootutil" ]
}

find_sdboot()
{
	local prefix=""
	[ -z "$have_snapshots" ] || prefix="/.snapshots/${1-$root_snapshot}/snapshot"
	# XXX: this is a hack in case we need to inject a signed
	# systemd-boot from a separate package
	local sdboot="$prefix/usr/lib/systemd-boot/systemd-boot$firmware_arch.efi"
	[ -e "$sdboot" ] || sdboot="$prefix/usr/lib/systemd/boot/efi/systemd-boot$firmware_arch.efi"
	echo "$sdboot"
}

find_grub2()
{
	local prefix=""
	[ -z "$have_snapshots" ] || prefix="/.snapshots/${1-$root_snapshot}/snapshot"
	local grub2="$prefix/usr/share/efi/$(uname -m)/grub.efi"
	[ -e "$grub2" ] || grub2="$prefix/usr/share/grub2/$(uname -m)-efi/grub.efi"
	echo "$grub2"
}

find_bootloader()
{
	if is_sdboot "${1-$root_snapshot}"; then
		find_sdboot "${1-$root_snapshot}"
	elif is_grub2 "${1-$root_snapshot}"; then
		find_grub2 "${1-$root_snapshot}"
	else
		err "Bootloader not detected"
	fi
}

bootloader_needs_update()
{
	local prefix=""
	local snapshot=""
	if [ -n "$have_snapshots" ]; then
		snapshot="${1-$root_snapshot}"
		prefix="/.snapshots/${snapshot}/snapshot"
	fi
	local bldr_name
	local v nv
	v="$(bootloader_version)"
	[ -n "$v" ] || return 1
	log_info "deployed version $v"
	nv="$(bootloader_version "$(find_bootloader "$snapshot")")"
	[ -n "$v" ] || return 1
	log_info "system version $nv"
	systemd-analyze compare-versions "$v" lt "$nv" 2>/dev/null || return 1
	bldr_name=$(bootloader_name "$snapshot")
	log_info "$bldr_name needs to be updated"
	return 0
}

install_bootloader()
{
	local snapshot=""
	local prefix=""
	if [ -n "$have_snapshots" ]; then
		snapshot="${1:-$root_snapshot}"
		prefix="/.snapshots/${root_snapshot}/snapshot"
	fi
	local bootloader bldr_name blkpart drive partno
	settle_entry_token "${snapshot}"

	bootloader=$(find_bootloader "$snapshot")
	bldr_name=$(bootloader_name "$snapshot")

	mkdir -p "$boot_root/loader/entries"

	mountpoint -q "$boot_root" || err "$boot_root is not a valid mountpoint"
	blkpart="$(findmnt -nvo SOURCE "$boot_root")"
	[ -L "/sys/class/block/${blkpart##*/}" ] || err "$blkpart is not a partition"
	drive="$(readlink -f "/sys/class/block/${blkpart##*/}")"
	drive="${drive%/*}"
	drive="/dev/${drive##*/}"
	read -r partno < "/sys/class/block/${blkpart##*/}"/partition

	if [ -e "$prefix$shimdir/shim.efi" ]; then
		log_info "Installing $bldr_name with shim into $boot_root"
		entry="$boot_dst/shim.efi"
		for i in MokManager shim; do
			install -p -D "$prefix$shimdir/$i.efi" "$boot_root$boot_dst/$i.efi"
		done
		install -p -D "$bootloader" "$boot_root$boot_dst/grub.efi"

		# boot entry point
		for i in MokManager fallback; do
			install -p -D "$prefix$shimdir/$i.efi" "$boot_root/EFI/BOOT/$i.efi"
		done
		install -p -D "$prefix$shimdir/shim.efi" "$boot_root/EFI/BOOT/BOOT${firmware_arch^^}.EFI"
	else
		log_info "Installing $bldr_name into $boot_root"
		entry="$boot_dst/${bootloader##*/}"
		install -p -D "$bootloader" "$boot_root$entry"
		install -p -D "$bootloader" "$boot_root/EFI/BOOT/BOOT${firmware_arch^^}.EFI"
	fi
	# this is for shim to create the entry if missing
	echo "${entry##*/},openSUSE Boot Manager" | iconv -f ascii -t ucs2 > "$boot_root/$boot_dst/boot.csv"

	mkdir -p "$boot_root/$entry_token"
	echo "$entry_token" > "$boot_root/$boot_dst/installed_by_sdbootutil"
	mkdir -p "/etc/kernel"
	[ -s /etc/kernel/entry-token ] || echo "$entry_token" > /etc/kernel/entry-token
	update_random_seed

	if is_sdboot "$snapshot"; then
		[ -s "$boot_root/loader/entries.srel" ] || echo type1 > "$boot_root/loader/entries.srel"
		[ -e "$boot_root/loader/loader.conf" ] || echo -e "#timeout 3\n#console-mode keep\n" > "$boot_root/loader/loader.conf"
	elif is_grub2 "$snapshot"; then
		# Minimal configuration file for now.  The theme can
		# come later with:
		#   * rsync the theme in /boot/efi
		#   * grub memdisk, removing unused modules (fs,
		#     crypto, LVM, RAID)
	        [ -e "$boot_root$boot_dst/grub.cfg" ] || cat > "$boot_root$boot_dst/grub.cfg" <<-EOF
		timeout=8
		function load_video {
		  # A load_video call is added for each bls entry
		  true
		}
		# Required to populate EFI varidables
		insmod bli
		blscfg
		EOF

		[ -e "$boot_root/EFI/BOOT/grub.cfg" ] || cp "$boot_root$boot_dst/grub.cfg" "$boot_root/EFI/BOOT/grub.cfg"
		mkdir -p "$boot_root$boot_dst/$(uname -m)-efi"
		cp -a "$prefix$grub2moddir/bli.mod" "$boot_root$boot_dst/$(uname -m)-efi"
	fi

	# Create boot menu entry if it does not exist
	[ -n "$arg_no_variables" ] || efibootmgr | grep -q 'Boot.*openSUSE Boot Manager' || efibootmgr -q --create --disk "$drive" --part "$partno" --label "openSUSE Boot Manager" --loader "$entry" || true

	# This action will require to update the PCR predictions
	update_predictions=1
}

hex_to_binary()
{
	local s="$1"
	local i
	for ((i=0;i<${#s};i+=2)); do eval echo -n "\$'\x${s:$i:2}'"; done
}

update_random_seed()
{
	[ -z "$arg_no_random_seed" ] || return 0
	local s _p
	read -r s _p < <({ dd if=/dev/urandom bs=32 count=1 status=none; [ -e "$boot_root/loader/random-seed" ] && dd if="$boot_root/loader/random-seed" bs=32 count=1 status=none; } | sha256sum)
	[ "${#s}" = 64 ] || { warn "Invalid random seed"; return 0; }
	hex_to_binary "$s" > "$boot_root/loader/random-seed.new"
	mv "$boot_root/loader/random-seed.new" "$boot_root/loader/random-seed"
}

install_bootloader_interactive()
{
	local v
	v="$(bootloader_version)"
	if [ -n "$v" ]; then
		if bootloader_needs_update; then
			local nv
			nv="$(bootloader_version "$(find_bootloader)")"
			d --aspect 60 --yesno "Update systemd-boot from $v to $nv?" 0 0 || return 0
		else
			d --aspect 60 --yesno "systemd-boot already at current version $v. Install again?" 0 0 || return 0
		fi
	else
		d --aspect 60 --yesno "Are you sure you want to install systemd-boot into $boot_root?\n
			This will overwrite any existing bootloaders" 0 0 || return 0
	fi
	install_bootloader
	d --aspect 60 --msgbox "Installed into $boot_root" 0 0
}

set_default_entry()
{
	local id="${1:?}"
	log_info "setting default entry ${id}"
	if ! bootctl set-default "$id" > "$tmpfile" 2>&1; then
		if grep -q "Failed to update EFI variable .*LoaderEntryDefault.* Read-only file system" "$tmpfile"; then
			if grep -q "^default " "$boot_root/loader/loader.conf"; then
				sed -i -e "s/^default .*/default $id/" "$boot_root/loader/loader.conf"
			else
				echo "default $id" >> "$boot_root/loader/loader.conf"
			fi
		else
			err "$(cat "$tmpfile")"
		fi
	fi
}

set_default_snapshot()
{
	[ -n "$have_snapshots" ] || { log_info "System does no support snapshots."; return 0; }
	local num="${1:?}"
	local configs
	update_entries_for_snapshot "$num"
	mapfile configs < <(jq '.[]|[.id]|join(" ")' -r < "$entryfile")
	configs=("${configs[@]%$nl}")
	if [ -z "${configs[0]}" ]; then
		log_info "snapshot $num has no configs, trying to create them..."
		install_all_kernels "$num"
		update_entries_for_snapshot "$num"
		mapfile configs < <(jq '.[]|[.id]|join(" ")' -r < "$entryfile")
		configs=("${configs[@]%$nl}")
		if [ -z "${configs[0]}" ]; then
			err "snapshot $num has no kernels"
		fi
	fi
	set_default_entry "${configs[0]}"
}

# TODO: Maybe share this code outside
have_pcrlock() {
	[ -e /usr/lib/systemd/systemd-pcrlock ]
}

pcrlock() {
	SYSTEMD_LOG_LEVEL="${SYSTEMD_LOG_LEVEL:-warning}" /usr/lib/systemd/systemd-pcrlock "$@"
}

# TODO: Maybe share this code outside
is_pcr_oracle() {
	[ -e /etc/systemd/tpm2-pcr-public-key.pem ] && \
	    [ -e /etc/systemd/tpm2-pcr-private-key.pem ] && \
	    [ -e /usr/bin/pcr-oracle ]
}

# TODO: Maybe share this code outside
select_entries_for_prediction()
{
	# Get the numbers for the last three snapshots
	[ -s "$snapperfile" ] || update_snapper

	# Select the default and the active snapshots
	declare -A snapshots
	local id
	while read -r id; do
		snapshots[$id]=1
	done < <(jq -r '.root[]|select(.active==true or .default==true)|.number' "$snapperfile")

	if is_transactional && [ -e "${state_file}" ]; then
		# shellcheck disable=SC1090
		. "${state_file}"
		for id in $LAST_WORKING_SNAPSHOTS; do
			snapshots[$id]=1
		done
	fi

	log_info "Creating predictions for snapshots: ${!snapshots[*]}"
	local re
	if [ "${#snapshots[@]}" = 1 ]; then
		re="${!snapshots[*]}"
	else
		IFS='|' eval re='"(:?${!snapshots[*]})"'
	fi

	update_entries_for_snapshot "$re"
}


# TODO: Maybe share this code outside
pcrlock_manual_initrd() {
	local pcr="$1"
	local pcrlock="$2"
	local initrd="$3"

	echo -n '{"records":[{"pcr":'"$pcr"',"digests":[' > "$pcrlock"
	local separator=0
	local digest
	for a in sha1 sha256 sha384 sha512; do
		[ "$separator" = "0" ] || echo -n "," >> "$pcrlock"
		separator=1
		hash "${a}sum" || continue
		read -r digest _ < <("${a}sum" "$initrd")
		echo -n '{"hashAlg":"'"$a"'","digest":"'"$digest"'"}' >> "$pcrlock"
	done
	echo ']}]}' >> "$pcrlock"
}

# TODO: Maybe share this code outside
pcrlock_cmdline_initrd() {
	local cmdline="$1"
	local initrd="$2"
	local suffix="$3"

	# 710-kernel-cmdline-initrd-entry.pcrlock.d is not part of the
	# pcrlock standards
	echo "$cmdline" > "$tmpdir/cmdline"
	pcrlock \
		lock-kernel-cmdline \
		--pcrlock="$tmpdir/cmdline.pcrlock" \
		"$tmpdir/cmdline"
	pcrlock \
		lock-kernel-initrd \
		--pcrlock="$tmpdir/initrd.pcrlock" \
		"${boot_root}/$initrd" 2> /dev/null || pcrlock_manual_initrd 9 "$tmpdir/initrd.pcrlock" "${boot_root}/$initrd"
	jq --slurp '{"records": [.[].records[0]]}' \
	   "$tmpdir/cmdline.pcrlock" \
	   "$tmpdir/initrd.pcrlock" \
	   > "/var/lib/pcrlock.d/710-kernel-cmdline-initrd-entry.pcrlock.d/cmdline-initrd-$suffix.pcrlock"
	rm "$tmpdir/cmdline"
	rm "$tmpdir/cmdline.pcrlock"
	rm "$tmpdir/initrd.pcrlock"

	# 710-kernel-cmdline-boot-loader.pcrlock.d is not part of the
	# pcrlock standards
	echo -ne "$cmdline\0" > "$tmpdir/cmdline"
	iconv -t UTF-16LE -o "$tmpdir/cmdline.utf16" "$tmpdir/cmdline"
	pcrlock \
		lock-raw \
		--pcr=12 \
		--pcrlock="/var/lib/pcrlock.d/710-kernel-cmdline-boot-loader.pcrlock.d/cmdline-$suffix.pcrlock" \
		"$tmpdir/cmdline.utf16"
	rm "$tmpdir/cmdline.utf16"
}

# TODO: Maybe share this code outside
generate_tpm2_predictions_pcrlock()
{
	local pcrs="$1"

	# Select the affected entries
	select_entries_for_prediction

	# Remove all the generated measurements.  This will keep the
	# predictions at minimum and decrease the combinations
	rm -fr /var/lib/pcrlock.d/*

	pcrlock lock-firmware-code
	pcrlock lock-firmware-config
	# If secure boot is disabled, this can fail.  There is patch
	# for the policy generation, and for the authority is planned
	/usr/lib/systemd/systemd-pcrlock lock-secureboot-policy || true
	/usr/lib/systemd/systemd-pcrlock lock-secureboot-authority || true
	# uses / by default
	pcrlock lock-gpt

	# 630-shim-efi-application is not part of the pcrlock standards
	# TODO: move to shim-pcrlock.rpm
	pcrlock \
	    lock-pe \
	    --pcrlock=/var/lib/pcrlock.d/630-shim-efi-application.pcrlock.d/generated.pcrlock \
	    "${boot_root}${boot_dst}/shim.efi"

	# 640-boot-loader-efi-application is not part of the pcrlock
	# standards
	# This is measuing the systemd-boot EFI binary (named grub.efi)
	# TODO: move to systemd-boot-pcrlock.rpm
	pcrlock \
	    lock-pe \
	    --pcrlock=/var/lib/pcrlock.d/640-boot-loader-efi-application.pcrlock.d/generated.pcrlock \
	    "${boot_root}${boot_dst}/grub.efi"

	if [ -e "$boot_root/loader/loader.conf" ]; then
		pcrlock \
			lock-raw /boot/efi/loader/loader.conf \
			--pcr=5 \
			--pcrlock=/var/lib/pcrlock.d/641-sdboot-loader-conf.pcrlock
	fi

	# 650-kernel-efi-application.pcrlock is not part of the
	# pcrlock standards
	# TODO: move to kernel-TYPE-pcrlock.rpm
	local n=0
	while read -r i; do
		n=$((n+1))
		pcrlock \
		    lock-pe \
		    --pcrlock=/var/lib/pcrlock.d/650-kernel-efi-application.pcrlock.d/linux-"$n".pcrlock \
		    "${boot_root}/$i"
	done < <(jq --raw-output 'map(.linux) | unique | .[]' "$entryfile")

	# Join the cmdline and the initrd in a single component
	mkdir -p /var/lib/pcrlock.d/710-kernel-cmdline-initrd-entry.pcrlock.d
	n=0
	while read -r cmdline; do
		read -r initrd
		n=$((n+1))
		pcrlock_cmdline_initrd "initrd=$cmdline" "$initrd" "$n"
	done < <(jq --raw-output '.[] | ([(.initrd[0] | sub("/"; "\\"; "g")), .options] | join(" ")), .initrd[0]' "$entryfile")

	# Generate variation for 710-kernel-cmdline-initrd-entry
	# component that contains the current cmdline and the current
	# initrd, even if this will never be used again.  This is
	# required because disk-encryption-tool generates a new initrd
	# during the first boot, making the event log impossible to
	# align for systemd-pcrlock
	if [ "$SDB_ADD_INITIAL_CMDLINE" = "1" ]; then
		read -r cmdline < /proc/cmdline
		local initrd="${cmdline#*initrd=}"; initrd="${initrd%% *}"; initrd="${initrd//\\//}"
		pcrlock_cmdline_initrd "$cmdline" "$initrd" "0" || warn "Not all PCRs are registered in the policy"
	fi

	pcrlock --pcr="$pcrs" make-policy

	# Publish the assets in the ESP, so can be imported by
	# dracut-pcr-signature
	[ -e /var/lib/systemd/pcrlock.json ] && cp /var/lib/systemd/pcrlock.json "${boot_root}${boot_dst}"
}

get_pcrs() {
	local pcrs
	local jq_pcr='.tokens[]|select(.type == "systemd-tpm2")|."tpm2_pubkey_pcrs"|join(",")'
	# We can have multiple devices, each one of them with
	# different PCRs
	while read -r dev; do
		pcrs=$(cryptsetup luksDump --dump-json-metadata "$dev" | jq -r "$jq_pcr")
		[ -z "$pcrs" ] || echo "$pcrs"
	done <<<$(blkid -t TYPE=crypto_LUKS -o device)
}

# TODO: Maybe share this code outside
generate_tpm2_predictions_pcr_oracle()
{
	local entry
	local all_pcrs

	# Select the affected entries
	select_entries_for_prediction

	all_pcrs=$(get_pcrs)
	if [ -z "$all_pcrs" ]; then
		warn "PCR Oracle configured but not enrolled in any LUKS header"
		return 0
	fi

	rm -f /etc/systemd/tpm2-pcr-signature.json

	# We make as many predictions as |all_pcrs| * |entries| to
	# cover all the combinations.  pcr-oracle is smart to include
	# the entry only one time, so we will not have duplications.
	# This is a step for multi device configurations.
	declare -a entries
	mapfile -t entries < <(jq -r '.[]|.id' "$entryfile")
	if [ -z "${entries[0]}" ]; then
		err "No bootloader entries found"
	fi
	for pcrs in $all_pcrs; do
		for entry in "${entries[@]}"; do
			log_info "Generate prediction for $entry with PCRs $pcrs"
			if ! pcr-oracle \
				--private-key /etc/systemd/tpm2-pcr-private-key.pem \
				--from eventlog \
				--output /etc/systemd/tpm2-pcr-signature.json \
				--target-platform=systemd \
				--boot-entry "${entry}" \
				sign "$pcrs"; then
				err "Failed to install TPM predictions for ${entry}"
			fi
		done
	done

	# Publish the assets in the ESP, so can be imported by
	# dracut-pcr-signature
	cp /etc/systemd/tpm2-pcr-public-key.pem "${boot_root}${boot_dst}"
	[ -e /etc/systemd/tpm2-pcr-signature.json ] && cp /etc/systemd/tpm2-pcr-signature.json "${boot_root}${boot_dst}"
}


# TODO: Maybe share this code outside
generate_tpm2_predictions()
{
	[ -e /etc/crypttab ] || return 0
	grep -q "tpm2-device" /etc/crypttab || return 0

	if is_pcr_oracle; then
		generate_tpm2_predictions_pcr_oracle
	elif have_pcrlock; then
		[ -e /etc/sysconfig/fde-tools ] || return 0
		# shellcheck disable=SC1091
		. /etc/sysconfig/fde-tools

		generate_tpm2_predictions_pcrlock "${FDE_SEAL_PCR_LIST}"
	fi
}

bootloader_name()
{
	if is_sdboot "${1-$root_snapshot}"; then
		echo "systemd-boot"
	elif is_grub2 "${1-$root_snapshot}"; then
		echo "grub2"
	else
		err "Bootloader not detected"
	fi
}

main_menu()
{
	while true; do
		list=(kernels Kernels snapper Snapshots sd-boot Entries install "Install/Update")
		d --no-tags --cancel-label "Quit"  --menu "Main Menu" 0 0 "$(menuheight ${#list[@]})" "${list[@]}" || return 0
		action="$result"

		case "$action" in
			snapper) show_snapper ;;
			sd-boot) update_entries cat; show_entries ;;
			kernels) show_kernels "$root_snapshot";;
			install) install_bootloader_interactive ;;
		esac
	done
}

####### main #######

getopttmp=$(getopt -o hc:v --long help,flicker,verbose,esp-path:,entry-token:,arch:,image:,no-variables,no-reuse-initrd,no-random-seed,all -n "${0##*/}" -- "$@")
eval set -- "$getopttmp"

while true ; do
        case "$1" in
                -h|--help) helpandquit ;;
		--flicker) dialog_altenate_screen=--keep-tite; shift ;;
		-v|--verbose) verbose=$((++verbose)); shift ;;
		--esp-path) arg_esp_path="$2"; shift 2 ;;
		--arch) arg_arch="$2"; shift 2 ;;
		--entry-token) arg_entry_token="$2"; shift 2 ;;
		--image) image="$2"; shift 2 ;;
		--no-variables) arg_no_variables=1; shift ;;
		--no-reuse-initrd) arg_no_reuse_initrd=1; shift ;;
		--no-random-seed) arg_no_random_seed=1; shift ;;
		--all) arg_all_entries=1; shift ;;
                --) shift ; break ;;
                *) echo "Internal error!" ; exit 1 ;;
        esac
done

if [ -z "$SYSTEMD_LOG_LEVEL" -a -n "$verbose" ]; then
	if [ "$verbose" -gt 1 ]; then
		SYSTEMD_LOG_LEVEL=debug
	else
		SYSTEMD_LOG_LEVEL=info
	fi
	export SYSTEMD_LOG_LEVEL
fi

case "$1" in
	install|needs-update|update|force-update|add-kernel|remove-kernel|set-default-snapshot|add-all-kernels|mkinitrd|remove-all-kernels|is-installed|list-snapshots|list-entries|list-kernels|is-bootable|update-predictions|bootloader) ;;
	kernels|snapshots|entries|"") stty_size; interactive=1 ;;
	*) err "unknown command $1" ;;
esac

[ -n "$arg_esp_path" ] && export SYSTEMD_ESP_PATH="$arg_esp_path"

# XXX: bootctl should have json output for that too
eval "$(bootctl 2>/dev/null | sed -ne 's/Firmware Arch: *\(\w\+\)/firmware_arch="\1"/p;s/ *token: *\(\w\+\)/entry_token="\1"/p;s, *\$BOOT: *\([^ ]\+\).*,boot_root="\1",p')"
read -r root_uuid root_device < <(findmnt / -v -r -n -o UUID,SOURCE)
root_subvol=""
subvol_prefix=""
if [ "$(stat -f -c %T /)" = "btrfs" ] && [ -d /.snapshots ]; then
	have_snapshots=1
	root_subvol=$(btrfs subvol show / 2>/dev/null|head -1)
	subvol_prefix="${root_subvol%/.snapshots/*}"
fi
root_snapshot=""
if [ -n "$have_snapshots" ]; then
	root_snapshot="${root_subvol#"${subvol_prefix}"/.snapshots/}"
	root_snapshot="${root_snapshot%/snapshot}"
fi

if [ -n "$arg_esp_path" ] && [ "$boot_root" != "$arg_esp_path" ]; then
	err "mismatch of esp path"
fi
[ -n "$arg_arch" ] && firmware_arch="$arg_arch"

[ -n "$boot_root" ] || err "No ESP detected. Legacy system?"
[ -n "$root_uuid" ] || err "Can't determine root UUID"
[ -n "$root_subvol" ] || [ -z "$have_snapshots" ] || err "Can't determine root subvolume"
[ -n "$root_device" ] || err "Can't determine root device"
[ -n "$firmware_arch" ] || err "Can't determine firmware arch"
case "$firmware_arch" in
	x64) image=vmlinuz ;;
	aa64) image=Image ;;
	*) err "Unsupported architecture $firmware_arch" ;;
esac

# XXX: Unify both in /EFI/opensuse?
if is_sdboot; then
	boot_dst="/EFI/systemd"
elif is_grub2; then
	boot_dst="/EFI/opensuse"
else
	err "Bootloader not detected"
fi

if [ "$1" = "install" ]; then
	install_bootloader "${2:-$root_snapshot}"
elif [ "$1" = "needs-update" ]; then
	bootloader_needs_update "${2:-$root_snapshot}"
elif [ "$1" = "update" ]; then
	if bootloader_needs_update "${2:-$root_snapshot}"; then install_bootloader "${2:-$root_snapshot}"; else :; fi
elif [ "$1" = "force-update" ]; then
	if is_installed; then install_bootloader "${2:-$root_snapshot}"; else :; fi
elif [ "$1" = "bootloader" ]; then
	bootloader_name "${2:-$root_snapshot}"
elif [ "$1" = "add-kernel" ]; then
	install_kernel "${3:-$root_snapshot}" "$2"
elif [ "$1" = "add-all-kernels" ]; then
	install_all_kernels "${2:-$root_snapshot}"
elif [ "$1" = "mkinitrd" ]; then
	arg_no_reuse_initrd=1
	install_all_kernels "${2:-$root_snapshot}"
elif [ "$1" = "remove-kernel" ]; then
	remove_kernel "${3:-$root_snapshot}" "$2"
elif [ "$1" = "remove-all-kernels" ]; then
	remove_all_kernels "${2:-$root_snapshot}"
elif [ "$1" = "set-default-snapshot" ]; then
	set_default_snapshot "${2:-$root_snapshot}"
elif [ "$1" = "is-installed" ]; then
	if is_installed; then
		log_info "systemd-boot was installed using sdbootutil"
		exit 0
	else
		log_info "not installed using this tool"
		exit 1
	fi
elif [ "$1" = "list-kernels" ]; then
	list_kernels "${2:-$root_snapshot}"
elif [ "$1" = "list-entries" ]; then
	list_entries "${2:-}"
elif [ "$1" = "list-snapshots" ]; then
	list_snapshots
elif [ "$1" = "is-bootable" ]; then
	is_bootable "${2:-$root_snapshot}"
elif [ "$1" = "update-predictions" ]; then
	update_predictions=1
elif [ "$1" = "kernels" ]; then
	show_kernels "${2:-$root_snapshot}"
elif [ "$1" = "snapshots" ]; then
	show_snapper
elif [ "$1" = "entries" ]; then
	show_entries
else
	main_menu
fi

[ -z "$update_predictions" ] || generate_tpm2_predictions
