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

verbose=
nl=$'\n'
shimdir="/usr/share/efi/$(uname -m)"
arg_esp_path="$SYSTEMD_ESP_PATH"
arg_entry_token=
arg_arch=
arg_all_entries=
arg_entry_keys=()
arg_no_variables=
arg_no_reuse_initrd=
arg_no_random_seed=
arg_portable=
arg_sync=
arg_only_default=
arg_default_snapshot=
arg_ask_key_pin_or_pw=
arg_method=
arg_signed_policy=
arg_measure_pcr=
have_snapshots=
# for x in vmlinuz image vmlinux linux bzImage uImage Image zImage; do
image=
unlock_method=

chroot_dir=

color_red=
color_end=
# The shell is interactive and the colors haven't been disabled
# forcefully or the shell is not connected to a terminal but the
# colors have been forcefully enabled
if { [ -t 1 ] \
    && [ "$SYSTEMD_COLORS" != "false" ] && [ "$SYSTEMD_COLORS" != "0" ]; } \
    || { [ ! -t 1 ] \
    && [ "$SYSTEMD_COLORS" = "true" ] || [ "$SYSTEMD_COLORS" = "1" ]; }; then
	color_red="\e[31m"
	color_green="\e[32m"
	color_yellow="\e[33m"
	color_bu="\e[1;4m" # bold underscore
	color_end="\e[m"
fi

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

update_predictions=

tracked_devices=()

rollback=()

declare -g -A eventlog

tmpdir=$(mktemp -d -t sdbootutil.XXXXXX)
cleanup()
{
	local i
	for i in "${rollback[@]}"; do
		if [ -e "$i.bak" ]; then
			info "Restoring $i"
			mv "$i.bak" "$i"
		else
			info "Removing $i"
			rm -f "$i"
		fi
	done
	dbg "Cleaning temporary directory $tmpdir"
	rm -rf "$tmpdir"

	[ -z "$chroot_dir" ] || umount_chroot "$chroot_dir"
}
trap cleanup EXIT

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

helpandquit()
{
	# Tabs are removed from the start of the line.  Use spaces to
	# indent
	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
		  --all			List all entries (inc. from other systems)
		  --entry-keys		Comma separated list of keys
		  --no-variables	Do not update UEFI variables
		  --no-reuse-initrd	Always regenerate initrd
		  --sync		Synchronize (update, downgrade) the bootloader
		  --portable		Handle bootloader on portable devices
		                        (also --removable possible)
		  --only-default	Only list the default entry
		  --default-snapshot	[SNAPSHOT] refers to the default snapshot
		  --ask-key		Ask recovery Key when initial enrollment
		                        (or randomly generated)
		  --ask-pin		Ask recovery PIN for re-enrollment
		                        Ask TPM2 PIN when initial enrollment
		  --ask-pw		Ask password when initial enrollment
		  --method		"tpm2", "tpm2+pin", "fido2", "password", "recovery-key"
		  --signed-policy	Use signed policy for TPM2 enrollment
		  --measure-pcr		Force update of PCR 15 prediction for LUKS2 volume key
		                        Requires LUKS2 password to accessing the volume key
		  --pcr			Comma seperated list of PCRs to enroll
		  --devices		Comma separated list of devices to enroll or unenroll
		                        By default all (not ignored) devices are [un]enrolled
		  -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

		cleanup [SNAPSHOT]
		           Remove boot entries with missing kernels from SNAPSHOT

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

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

		list-snapshots
		           List all snapshots

		list-devices
		           List encrypted devices that are tracked

		show-entry VERSION [SNAPSHOT]
		           Show fields for an entry with an specified kernel
		           version

		update-entry VERSION [SNAPSHOT]
		           Update "options" field from /etc/kernel/cmdline
		           for an entry

		update-all-entries [SNAPSHOT]
		           Update "options" field from /etc/kernel/cmdline
		           for all entries

		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 the bootloader and shim into ESP

		needs-update
		           Check whether the bootloader in ESP needs updating

		update
		           Update the bootloader in the ESP if a newer version
		           is available. Passing the --sync option will also
		           allow downgrades, ensuring that the version in the ESP
		           matches the one installed in the system.

		force-update
		           Update the bootloader in any case

		set-default ID
		           Set default boot loader entry

		get-default
		           Get default boot loader entry

		set-timeout SECONDS|menu-disabled|menu-hidden|menu-force
		           Set the menu timeout
		           menu-disabled or menu-hidden=0; menu-force=-1

		get-timeout
		           Get the menu timeout in seconds

		enroll
		           Enroll a TPM2 (+PIN), a FIDO2 key or a password for
		           all devices

		unenroll
		           Unenroll a TPM2 (+PIN), a FIDO2 key or a password for
		           all devices

		update-predictions
		           Update TPM2 predictions

		Variables:
		SYSTEMD_COLORS	Set 0 to disable colored output
		KEY		Recovery key (initial enrollment; %u:sdbootutil)
		PIN		TPM2 PIN (initial enrollment; %u:sdbootutil)
		PIN		Recovery PIN (re-enrollment; %u:sdbootutil)
		PW		Password / Recovery Key (initial enrollment; %u:sdbootutil)
		                (%u:cryptenroll for changes via systemd-cryptenroll)

		Misc:
		Ignoring Devices	A LUKS2 device can be un-tracked (ignored)
		                        by sdbootutil if is present in /etc/crypttab
		                        and has the "x-sdbootutil.ignore" option

	EOF
	exit 0
}

dbg()
{
	[ "${verbose:-0}" -gt 1 ] || return 0
	echo "DEBUG: $*" >&2
}

dbg_var()
{
	[ "${verbose:-0}" -gt 1 ] || return 0
	local v="${1:?}"
	echo "DEBUG: $v: ${!v}" >&2
}

dbg_cat()
{
	[ "${verbose:-0}" -gt 1 ] || return 0
	[ ! -e "$1" ] || { echo "DEBUG: $1" >&2; cat "$1" >&2; }
}

info()
{
	[ "${verbose:-0}" -gt 0 ] || return 0
	echo "$@" >&2
}

warn()
{
	echo "WARNING: $*" >&2
}

err()
{
	echo "ERROR: $*" >&2
	exit 1
}

is_config_file()
{
	[ -e /usr/etc/default/fde-tools ] ||
		[ -e /etc/default/fde-tools ] ||
		[ -e /etc/sysconfig/fde-tools ]
}

load_config_file()
{
	local f
	# Some old installations have /etc/sysconfig/fde-tools
	for f in /usr/etc/default/fde-tools /etc/default/fde-tools /etc/sysconfig/fde-tools; do
		[ ! -e "$f" ] || {
			# shellcheck disable=SC1090
			. "$f"
			info "Loading config file $f"
			dbg_cat "$f"
		}
	done
}

is_sdboot()
{
	local sdboot grub2_bls

	sdboot="$(find_sdboot "${1-$root_snapshot}")"
	grub2_bls="$(find_grub2_bls "${1-$root_snapshot}")"

	# If boot loader is not found, then we check LOADER_TYPE, but
	# if is not present and systemd-boot and grub2-bls are
	# co-installed, we favor grub2-bls in the detection
	if [ ! -e "$sdboot" ] && [ ! -e "$grub2_bls" ]; then
		[ -z "$LOADER_TYPE" ] || [ "$LOADER_TYPE" = "systemd-boot" ]
	else
		[ -e "$sdboot" ] && [ ! -e "$grub2_bls" ]
	fi
}

is_grub2_bls()
{
	local sdboot grub2_bls

	sdboot="$(find_sdboot "${1-$root_snapshot}")"
	grub2_bls="$(find_grub2_bls "${1-$root_snapshot}")"

	# If boot loader is not found, then we check LOADER_TYPE, but
	# if is not present and systemd-boot and grub2-bls are
	# co-installed, we favor grub2-bls in the detection
	if [ ! -e "$sdboot" ] && [ ! -e "$grub2_bls" ]; then
		[ -z "$LOADER_TYPE" ] || [ "$LOADER_TYPE" = "grub2-bls" ]
	else
		[ -e "$grub2_bls" ]
	fi
}

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

is_transactional()
{
	findmnt --fstab / -O ro >/dev/null
}

keyctl_add_with_timeout()
{
	local key="$1"
	local value="$2"

	info "Creating key $key with timeout"
	# When we create a new key with sudo, we do not create a new
	# login session, so the key is not accessible to the owner
	# See https://mjg59.dreamwidth.org/37333.html
	local keyid
	keyid="$(echo -n "$value" | keyctl padd user "$key" @s)"
	# Remove permission from the possesor.  For some reason that I
	# do not understand, this can fail in YaST when called via
	# cheetah
	keyctl setperm "$keyid" 0x003f0000 2> /dev/null || {
		warn "Failed to change the timeout for $key"
		return 0
	}
	keyctl timeout "$keyid" 120
	keyctl link "$keyid" @u
	# systemd tools are doing `keyctl request user "$key"`, and
	# for some reason this means that the key must still be in the
	# session keyring, so we cannot do `keyctl unlink "$keyid" @s`
}

ask_password()
{
	local msg="$1"
	local -n nameref_pw="$2"
	read -r -s -p "$msg: " nameref_pw
	echo >&2
}

ask_new_password()
{
	local msg="$1"
	local -n nameref_pw="$2"
	local pw1 pw2
	ask_password "New $msg" pw1
	ask_password "Re-type $msg" pw2
	while [ "$pw1" != "$pw2" ]; do
		warn "Inputs did't match!"
		ask_password "New $msg" pw1
		ask_password "Re-type $msg" pw2
	done
	# shellcheck disable=SC2034
	nameref_pw="$pw1"
}

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

	local parent_uuid
	parent_uuid="$(btrfs subvol show "${subvol#"${subvol_prefix}"}" | sed -ne 's/\s*Parent UUID:\s*//p')"
	[ "$parent_uuid" != '-' ] || return 0

	local -a parent_subvol_uuid
	# shellcheck disable=SC2207
	parent_subvol_uuid=($(btrfs subvol show -u "$parent_uuid" "${subvol#"${subvol_prefix}"}" 2> /dev/null))
	local btrfs_subvol_status=$?
	[ "$btrfs_subvol_status" = 0 ] || return 0
	parent_subvol="${parent_subvol_uuid[0]}"

	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

	dbg_var "parent_subvol"
	dbg_var "parent_snapshot"
}

sedrootflags()
{
	local subvol="$1"
	# - Delete everything before BOOT_IMAGE= and initrd=
	#   (see https://github.com/openSUSE/sdbootutil/issues/182)
	# - Delete BOOT_IMAGE= and initrd=
	# - Replace or add root= to refers to UUID or mapped device
	#   (if encrypted)
	# - 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 root_param="UUID=$root_uuid"
	[ -z "$root_device_is_crypt" ] || root_param="$root_device"
	local sed_arguments=("-e s/[ \t]\+/ /g"
		"-e s/^.*\(initrd=[^ ]*\|BOOT_IMAGE=[^ ]*\)\s*/\1 /"
		"-e s/\<\(BOOT_IMAGE\|initrd\)=[^ ]* \?//"
		"-e s/\$//;ta;:a"
		"-e s,\<root=[^ ]*,root=$root_param,;tb;s,\$, root=$root_param,;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"
	dbg "Entry filter: ${entry_filter[*]}"
	dbg_cat "$entryfile"
}

update_entries_for_subvol()
{
	local subvol="$1"
	local ext="${2:-}"

	[ -z "$ext" ] || ext="|$ext"
	update_entries jq "[.[]|select(has(\"options\"))|select(.options|test(\"root=(?:UUID=$root_uuid|$root_device) .*rootflags=subvol=$subvol\")$ext)]"
}

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

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

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

entry_conf_file()
{
	local kernel_version="${1:?}"
	local snapshot="$2"
	local tries="$3"

	# GRUB2 with the BLS patches does not follow the expected
	# ordering rules, using only rpmvercmp() with the entry
	# filename.  To provide an order we add a prefix ("system")
	# for entries that are not RO, making it newer that any other
	# entry name that start with the "snapper" prefix: ("system" >
	# "snapper").
	local prefix=""
	local subvol=""
	[ -z "$have_snapshots" ] || subvol="${subvol_prefix}/.snapshots/${snapshot}/snapshot"
	if ! is_transactional && is_grub2_bls; then
		if ! subvol_is_ro "$subvol"; then
			prefix="system"
		else
			prefix="snapper"
		fi
	fi

	echo "${prefix:+$prefix-}$entry_token-$kernel_version${snapshot:+-$snapshot}${tries:++$tries}.conf"
}

find_conf_file()
{
	local kernel_version="${1:?}"
	local snapshot="$2"
	local id
	id="$(entry_conf_file "$kernel_version" "$snapshot")"

	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"

	dbg_var "entry_token"
	return 0
}

remove_kernel()
{
	local snapshot="$1"
	local kernel_version="$2"
	[ -n "$kernel_version" ] || err "Missing kernel version"

	info "Removing kernel $kernel_version"
	dbg_var "snapshot"

	settle_entry_token "${snapshot}"
	local id
	id="$(entry_conf_file "$kernel_version" "$snapshot")"
	info "Removing boot entry $id"
	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
			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 || :
	info "Installed $dst"
}

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

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

	[ -s "$snapperfile" ] || 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
			[ -f "${boot_root}$v" ] || continue
			info "Found existing initrd $v"
			dstinitrd+=("$v")
		done < "$conf"
		[ "${#dstinitrd[@]}" -eq 0 ] || return 0
	fi

	return 1
}

mount_chroot()
{
	local snapshot_dir="$1"

	# We include the rootfs (the first line usually), as is needed
	# to appear in the mounts under the chroot, allowing dracut to
	# properly detect the fs type and load the relevant module.
	findmnt -o TARGET,FSTYPE -Rv --pairs / > "$tmpdir/mounts"
	mount --bind "$snapshot_dir" "$snapshot_dir"
	while read -r line; do
		eval "$line"
		# shellcheck disable=SC2153
		[ "$FSTYPE" = "btrfs" ] || [ "$FSTYPE" = "vfat" ] || [ "$FSTYPE" = "xfs" ] || [[ "$FSTYPE" == ext* ]] || continue
		[ "$TARGET" != "/" ] || continue
		[[ "$TARGET" != /.snapshots* ]] || continue
		mountpoint --quiet "$snapshot_dir$TARGET" || mount --bind "$TARGET" "$snapshot_dir$TARGET"
	done < "$tmpdir/mounts"
	rm "$tmpdir/mounts"

	mount -t tmpfs -o size=10m tmpfs "$snapshot_dir/run"
	for i in proc dev sys tmp; do
		mount --bind "/$i" "$snapshot_dir/$i"
	done
	chroot_dir="$snapshot_dir"
}

umount_chroot()
{
	local snapshot_dir="$1"

	umount -R "$snapshot_dir"
	chroot_dir=
}

mount_etc()
{
	local snapshot_dir="$1"

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

	# Only overlayfs needs special treatment
	[ "$(findmnt --tab-file "${snapshot_dir}/etc/fstab" --noheadings --nofsroot --output FSTYPE /etc)" = "overlay" ] || 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"
}

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

add_kernel_version_to_title()
{
	# grub2-bls does not show the `version` field
	title="$title ($snapshot@$kernel_version)"
}

pending_kernel_size()
{
	echo $(($(stat -c %s "$1") / 1024 + 1))
}

pending_initrds_size()
{
	local size=0
	local i=0
	while [ -e "$1/initrd-$i" ]; do
		size=$((size + $(stat -c %s "$1/initrd-$i")))
		((++i))
	done
	echo $((size / 1024 + 1))
}

boot_free_space()
{
	echo $(($(findmnt -n -b -o AVAIL --target "$boot_root" | head -n 1) / 1024))
}

regex_snapshot_ids_for_free_space()
{
	local snapshot="$1"

	[ -s "$snapperfile" ] || update_snapper

	# Select the default and the active snapshots.  Also include
	# one extra snapshot if it is passed as a parameter, that
	# refers to the snapshot from where we are installing the
	# kernel
	declare -A snapshots
	[ -z "$snapshot" ] || snapshots["$snapshot"]=1
	[ -z "$root_snapshot" ] || snapshots["$root_snapshot"]=1
	local id
	while read -r id; do
		snapshots[$id]=1
	done < <(jq -r '.root[]|select(.active==true or .default==true)|.number' "$snapperfile")

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

	echo "$re"
}

select_entries_for_free_space()
{
	local snapshot="$1"

	# Extend the entry file with a "priority" field, based on the
	# ordered snapshot IDs, and the "kernel" field, based on the
	# kernel version.  Note that the textual suffix (-default,
	# -slowroll) is removed and does not participate in the
	# ordering
	#
	# The final list is a join of two list.  One that contains all
	# the entries that belong to the safe-to-delete snapshots, and
	# another one that contain the entries from the default
	# snapshot, that is not the default entry.  This last sub-list
	# has the higher priority value, so when sorted get the last
	# order
	#
	# Expected order for removal:
	#
	# MicroOS
	#   - The default / active snapshot is always the last one,
	#     except when we do a rollback
	#   - The default boot entry should never be removed
	#   - The removal order is sort(snapshot_id, kernel), except
	#     for entries that belong to the default snapshot.  Those
	#     should be removed as a last resort
	#   - If there are snapshots after the default and active one,
	#     they are remainigs from a rollback, and they can be
	#     removed in the same order (ideally early)
	#
	# Tumbleweed
	#   - The default / active snapshot is always the first one,
	#     except when we do a rollback
	#   - The default boot entry should never be removed
	#   - The removal order is sort(snapshot_id, kernel), the same
	#     as in MicroOS
	#   - If there are snapshot before the default and active one,
	#     they are remainings from a rollback and they can be
	#     removed in the same order

	# Select the entries that are safe to remove, i.e. the ones
	# that are not the default, the active nor the one from where
	# we are extracting the kernel (that usually is the default or
	# the active one)
	update_entries_for_snapshot_invert "$(regex_snapshot_ids_for_free_space "$snapshot")"
	jq 'map(. + {"priority": .version | scan("(\\d+)@") | .[] | tonumber, "kernel": .version | scan(".*@(?:(\\d+).(\\d+).(\\d+)-(\\d+))") | map(. | tonumber)})' < "$entryfile" > "${entryfile}.ext-1"

	# The name is confusing.  The maximum priority is the minimal
	# one once is sorted by priority
	local max_priority
	read -r max_priority <<<"$(jq -r 'max_by(.priority)|.priority' < "${entryfile}.ext-1")"
	((++max_priority))

	# From the no-safe ones, we can remove some entries from the
	# default entry (the active is the one under development in
	# MicroOS)
	update_entries_for_snapshot "$root_snapshot"
	jq --arg max_priority "$max_priority" 'map(. + {"priority": $max_priority | tonumber, "kernel": .version | scan(".*@(?:(\\d+).(\\d+).(\\d+)-(\\d+))") | map(. | tonumber)})' < "$entryfile" > "${entryfile}.ext-2"

	# The default boot entry should be in the second list, but
	# puting the filter here guarantees that it will not appears
	# in the final list
	jq --slurp 'add|[.[]|select(.isDefault==false)]' "${entryfile}.ext-1" "${entryfile}.ext-2" > "$entryfile"
	dbg "Added priority and kernel version to entry file (for free space)"
	dbg_cat "$entryfile"
}

make_free_space()
{
	local snapshot="$1"
	local required_size="$2"

	info "Required free space in ESP: $required_size KB"

	# If there is already free space, shortcut the code
	free_space="$(boot_free_space)"
	[ "$required_size" -gt "$free_space" ] || return 0

	# "Cleaning /boot/efi" message is presented via stderr
	dbg "Calling bootctl cleanup"
	bootctl -q cleanup 2> /dev/null

	select_entries_for_free_space "$snapshot"

	local id
	while read -r id; do
		free_space="$(boot_free_space)"
		dbg "Free space in the ESP: $free_space KB"
		if [ "$required_size" -gt "$free_space" ]; then
			info "Removing boot entry $id"
			bootctl unlink "$id"
		else
			return 0
		fi
	done < <(jq -r 'sort_by(.priority, .kernel) | .[] | .id' < "$entryfile")

	free_space="$(boot_free_space)"
	dbg "Free space in the ESP after deallocation: $free_space KB"
	[ "$required_size" -lt "$free_space" ]
}

make_free_space_for_kernel()
{
	local snapshot="$1"

	# Calculate the free space and the required size.  All sizes
	# are in Kb to avoid big numbers
	local free_space total_size
	total_size=$(($(pending_kernel_size "$src") + $(pending_initrds_size "$tmpdir")))

	make_free_space "$snapshot" "$total_size"
}

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

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"
	[ -n "$kernel_version" ] || err "Missing kernel version"
	[ -e "$src" ] || err "Can't find $src"

	info "Installing kernel $kernel_version"
	dbg_var "snapshot"

	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=(
			'--reproducible'
			'--force'
			'--tmpdir' '/var/tmp'
		)
		if [ "${verbose:-0}" -le 1 ]; then
			dracut_args+=('--quiet')
		fi

		info "Generating new initrd"

		if [ "$subvol" != "$root_subvol" ] && [ -n "$have_snapshots" ]; then
			mount_chroot "${snapshot_dir}"
			# 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 /.snapshots is still the unmodified base
			is_transactional && mount_etc "${snapshot_dir}"
			chroot "${snapshot_dir}" dracut "${dracut_args[@]}" "$tmpdir/initrd-0" "$kernel_version"
			umount_chroot "${snapshot_dir}"
		else
			dracut "${dracut_args[@]}" "$tmpdir/initrd-0" "$kernel_version"
		fi
	fi

	make_free_space_for_kernel "$snapshot" || err "No free space in $boot_root for new kernel"

	local boot_options
	boot_options="$(create_boot_options "$subvol")"

	if [ "${#dstinitrd[@]}" -eq 0 ] && [ -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"

	add_version_to_title
	if ! is_transactional && subvol_is_ro "$subvol"; then
		set_snapper_title_and_sortkey "$snapshot"
	elif is_grub2_bls; then
		add_kernel_version_to_title
	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
	dbg "Generated new boot entry"
	dbg_cat "$tmpdir/entry.conf"

	local failed=
	if [ ! -e "$boot_root$dst" ]; then
		install_with_rollback "$src" "$boot_root$dst" || failed=kernel
	else
		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_conf_file "$kernel_version" "$snapshot" "$tries")"
		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

	# Do a final cleanup, as sometimes we are replacing an old
	# initrd
	bootctl -q cleanup 2> /dev/null

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

install_all_kernels()
{
	local snapshot="$1"

	info "Installing all kernels"
	dbg_var "snapshot"
	dbg_var "arg_no_reuse_initrd"

	find_kernels "$snapshot"
	for kv in "${!found_kernels[@]}"; do
		install_kernel "${snapshot}" "$kv"
	done
}

remove_all_kernels()
{
	local snapshot="$1"

	info "Removing all kernels"
	dbg_var "snapshot"

	find_kernels "$snapshot"
	for kv in "${!found_kernels[@]}"; do
		remove_kernel "${snapshot}" "$kv"
	done
}

cleanup_entries()
{
	info "Cleaning up boot entries"

	if [ ! -s "$entryfile" ]; then
		if [ -n "$1" ]; then
			update_entries_for_snapshot "$1"
		else
			update_entries_for_this_system
		fi
	fi

	local id snapshot kernel_version subvol src
	while read -r id; do
		read -r snapshot
		read -r kernel_version
		subvol="${subvol_prefix}/.snapshots/${snapshot}/snapshot"
		src="${subvol#"${subvol_prefix}"}/lib/modules/$kernel_version/$image"
		[ -e "$src" ] || {
			info "Cleaning boot entry $id"
			rm "${boot_root}/loader/entries/$id"
		}
	done < <(jq -r '.[] | .id, (.version | scan("(.*)@(.*)") | .[])' "$entryfile")

	dbg "Calling bootctl cleanup"
	bootctl -q cleanup 2> /dev/null
}

list_entries()
{
	info "Listing boot 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="$color_bu"
		elif [ -n "$arg_only_default" ]; then
			continue
		fi
		if [ "$isreported" = "false" ]; then
			color="$color${color_green}"
		fi
		if [ "$type" = "loader" ]; then
			color="$color${color_yellow}"
		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
					# shellcheck disable=SC2001
					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 [ "${#errors[@]}" -gt 0 ]; then
			echo -e "  ${color_red}${errors[*]}${color_end}" >&2
		fi
		echo -e "$color$id${verbose:+: $title}${color_end}"
	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_entry_fields()
{
	local snapshot="$1"
	local kernel_version="$2"
	[ -n "$kernel_version" ] || err "Missing kernel version"
	settle_entry_token "${snapshot}"
	local id
	id="$(entry_conf_file "$kernel_version" "$snapshot")"

	local conf
	conf="$(find_conf_file "$kernel_version" "$snapshot")"

	[ -z "$verbose" ] || echo -e "ID\t$id"
	local k
	local v
	while read -r k v; do
		case "$k" in
			title|version|sort-key|options|linux|initrd) ;;
			*) continue ;;
		esac

		if [ "${#arg_entry_keys[@]}" -eq 0 ] || [[ ${arg_entry_keys[*]} == *"all"* ]] || [[ ${arg_entry_keys[*]} == *"$k"* ]]; then
			echo -e "$k\t$v"
		fi
	done < "$conf"
}

update_entry_conf()
{
	local conf="$1"
	local snapshot="$2"

	local subvol=""
	[ -z "$have_snapshots" ] || subvol="${subvol_prefix}/.snapshots/${snapshot}/snapshot"

	local boot_options
	boot_options="$(create_boot_options "$subvol")"

	cp "$conf" "$tmpdir/entry.conf"
	sed -i "s|^options\s*.*$|options    $boot_options|g" "$tmpdir/entry.conf"
	cp "$tmpdir/entry.conf" "$conf"
}

update_entry()
{
	local snapshot="$1"
	local kernel_version="$2"

	settle_entry_token "${snapshot}"
	local id
	id="$(entry_conf_file "$kernel_version" "$snapshot")"

	local conf
	conf="$(find_conf_file "$kernel_version" "$snapshot")"
	[ -f "$conf" ] || return 0

	info "Updating boot entry $id"
	update_entry_conf "$conf" "$snapshot"

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

update_all_entries()
{
	local snapshot="$1"

	make_free_space "$snapshot" 1024

	update_entries_for_snapshot "$1"
	while read -r conf; do
		update_entry_conf "$conf" "$snapshot"
	done < <(jq -r '.[]|.path' < "$entryfile")

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

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

	info "Listing snapshots"

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

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"
		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:?}"

	info "Listing kernels"

	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 "${color_yellow}missing /lib/modules/$kv/$image${color_end}"
		else
			echo -e "${color_green}ok /lib/modules/$kv/$image -> $id${color_end}"
		fi
	done
	kernelfiles=("${!stale_kernels[@]}")
	for k in "${kernelfiles[@]}"; do
		local id="${stale_kernels[$k]}"
		printf "${color_red}stale %s${color_end}\n" "$id"
	done
}

list_devices()
{
	info "Listing devices"

	detect_tracked_devices

	for dev in "${tracked_devices[@]}"; do
		echo "$dev"
	done
}

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

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

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

	dbg "Bootloader version $v"

	echo "$v"
}

is_installed()
{
	info "Checking if the bootloader 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_bls()
{
	local prefix=""
	[ -z "$have_snapshots" ] || prefix="/.snapshots/${1-$root_snapshot}/snapshot"

	local grub2_arch
	grub2_arch="$(uname -m)"
	case "$grub2_arch" in
		i[[3456]]86) grub2_arch=i386 ;;
		x86_64) grub2_arch=x86_64 ;;
		amd64) grub2_arch=x86_64 ;;
		sparc) grub2_arch=sparc64 ;;
		mipsel|mips64el) grub2_arch=mipsel ;;
		mips|mips64) grub2_arch=mips ;;
		arm*) grub2_arch=arm ;;
		aarch64*) grub2_arch=arm64 ;;
		loongarch64) grub2_arch=loongarch64 ;;
		riscv32*) grub2_arch=riscv32 ;;
		riscv64*) grub2_arch=riscv64 ;;
	esac

	# The old grub.efi will contain the BLS patches, but we cannot
	# use it because we also dropped the process of creating the
	# configuration file and installing bli.mod
	echo "$prefix/usr/share/grub2/${grub2_arch}-efi/grubbls.efi"
}

find_bootloader()
{
	if is_sdboot "${1-$root_snapshot}"; then
		find_sdboot "${1-$root_snapshot}"
	elif is_grub2_bls "${1-$root_snapshot}"; then
		find_grub2_bls "${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

	info "Checking if bootloader needs update"

	local bldr_name
	local v nv
	v="$(bootloader_version)"
	[ -n "$v" ] || return 1
	info "Deployed version $v"
	nv="$(bootloader_version "$(find_bootloader "$snapshot")")"
	[ -n "$nv" ] || return 1
	info "System version $nv"
	systemd-analyze compare-versions "$v" "$nv" 2>/dev/null
	local status="$?"
	bldr_name=$(bootloader_name "$snapshot")
	if [ "$status" = "11" ]; then
		info "$bldr_name is newer than system bootloader"
		return 2
	elif [ "$status" = "12" ]; then
		info "$bldr_name needs to be updated"
		return 0
	fi
	info "$bldr_name is already up-to-date"
	return 1
}

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

	info "Installing bootloader"
	dbg_var "$snapshot"

	local bootloader bldr_name blkpart drive partno
	settle_entry_token "${snapshot}"

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

	mkdir -p "$boot_root/loader/entries"

	# The ESP presence was checked early
	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
		info "Installing $bldr_name with shim into $boot_root"
		entry="$boot_dst/shim.efi"
		for i in MokManager shim; do
			[ -n "$arg_portable" ] || 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
		install -p -D "$prefix$shimdir/MokManager.efi" "$boot_root/EFI/BOOT/MokManager.efi"
		[ -n "$arg_portable" ] || install -p -D "$prefix$shimdir/fallback.efi" "$boot_root/EFI/BOOT/fallback.efi"
		install -p -D "$prefix$shimdir/shim.efi" "$boot_root/EFI/BOOT/BOOT${firmware_arch^^}.EFI"
	else
		info "Installing $bldr_name without shim into $boot_root"
		entry="$boot_dst/${bootloader##*/}"
		[ -n "$arg_portable" ] || 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
	[ -n "$arg_portable" ] || echo "${entry##*/},openSUSE Boot Manager" | { echo -ne "\xff\xfe"; iconv -f ascii -t ucs-2le; } > "$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"
	fi

	# Create boot menu entry if it does not exist
	local escaped_entry="${entry//\//\\\\}"
	[ -n "$arg_no_variables" ] || [ -n "$arg_portable" ] || efibootmgr | grep -q "Boot.*openSUSE Boot Manager.*${escaped_entry}" || efibootmgr -q --create --disk "$drive" --part "$partno" --label "openSUSE Boot Manager ($bldr_name)" --loader "$entry" || true

	# Make it the first option
	if [ -z "$arg_no_variables" ] && [ -z "$arg_portable" ]; then
		local boot_order
		boot_order="$(efibootmgr | grep BootOrder)"
		boot_order="${boot_order#BootOrder: }"

		local boot_entry
		boot_entry="$(efibootmgr | grep "Boot.*openSUSE Boot Manager.*${escaped_entry}")"
		boot_entry="${boot_entry%\* *}"
		boot_entry="${boot_entry#Boot}"

		efibootmgr -q -D -o "$boot_entry,$boot_order" || true
	fi

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

bootloader_update()
{
	local status=0

	info "Updating bootloader"

	bootloader_needs_update "${1:-$root_snapshot}" || status=$?
	if [ $status -eq 0 ]; then
		info "The bootloader needs to be updated"
		install_bootloader "${1:-$root_snapshot}"
	elif [ -n "$arg_sync" ] && [ $status -eq 2 ]; then
		info "The bootloader will be downgraded"
		install_bootloader "${1:-$root_snapshot}"
	fi
}

hex_to_binary()
{
	local s="$1"
	local i
	for ((i=0;i<${#s};i+=2)); do echo -ne "\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"
}

bli_efi_var_get()
{
	# BLI uses this vendor UUID
	local efi_var="/sys/firmware/efi/efivars/${1:?}-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
	if [ -e "$efi_var" ]; then
		dd "if=$efi_var" bs=2 skip=2 conv=lcase status=none | tr -d '\0'
	fi
}

loader_conf_set()
{
	local key="${1:?}"
	local value="${2:?}"

	[ -e "${boot_root}/loader/loader.conf" ] || touch "${boot_root}/loader/loader.conf"

	if grep -q "^$key " "${boot_root}/loader/loader.conf"; then
		sed -i -e "s/^$key .*/$key $value/" "${boot_root}/loader/loader.conf"
	else
		echo "$key $value" >> "${boot_root}/loader/loader.conf"
	fi
}

loader_conf_get()
{
	local key="${1:?}"
	if [ -e "${boot_root}/loader/loader.conf" ]; then
		IFS=' ' read -r key value < <(grep "^$key " "${boot_root}/loader/loader.conf")
		echo -n "$value"
	fi
}

grubenv_set()
{
	local key="${1:?}"
	local value="${2:?}"

	[ -e "${boot_root}${boot_dst}/grubenv" ] || touch "${boot_root}${boot_dst}/grubenv"

	grubenv="$(mktemp -t grubenv.XXXXXX)"
	echo "# GRUB Environment Block" > "$grubenv"
	while read -r line; do
		[[ "$line" == '#'* ]] && continue
		[[ "$line" == "$key"=* ]] && continue
		echo "$line" >> "$grubenv"
	done < "${boot_root}${boot_dst}/grubenv"
	echo "$key=$value" >> "$grubenv"

	local filler
	filler=$((1024 - $(stat -c %s "$grubenv")))
	printf '#%.0s' $(seq 1 $filler) >> "$grubenv"

	mv "$grubenv" "${boot_root}${boot_dst}/grubenv"
}

grubenv_get()
{
	local key="${1:?}"

	if [ -e "${boot_root}${boot_dst}/grubenv" ]; then
		IFS='=' read -r key value < <(grep "^$key=" "${boot_root}${boot_dst}/grubenv")
		echo -n "$value"
	fi
}

set_default_sdboot()
{
	local id="${1:?}"
	if ! bootctl set-default "$id" > "$tmpfile" 2>&1; then
		if grep -q "Failed to update EFI variable" "$tmpfile" ||
				grep -q "Not booted with a supported boot loader" "$tmpfile" ||
				grep -q "Not booted with UEFI" "$tmpfile"; then
			loader_conf_set "default" "$id"
		else
			err "$(cat "$tmpfile")"
		fi
	fi
}

set_default_grub2_bls()
{
	local id="${1:?}"
	set_default_sdboot "$id"
	grubenv_set "default" "$id"
}

set_default_entry()
{
	local id="${1:?}"

	info "Setting default entry $id"

	if [ ! -f "${boot_root}/loader/entries/$id" ] && [ ! -f "${boot_root}/loader/entries/$id.conf" ]; then
		err "Boot loader entry $id not found"
	fi
	if is_sdboot; then
		set_default_sdboot "$id"
	elif is_grub2_bls; then
		set_default_grub2_bls "$id"
	else
		err "Bootloader not detected"
	fi

	# Setting a different boot entry invalidates "$entryfile"
	rm -f "$entryfile"

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

get_default_sdboot()
{
	local val
	val="$(bli_efi_var_get "LoaderEntryDefault")"
	[ -n "$val" ] || val="$(loader_conf_get "default")"
	[ -n "$val" ] || {
		update_entries_for_this_system
		jq -r '.[] | select(.isDefault == true) | .id' < "$entryfile"
	}
	[ -z "$val" ] || echo "$val"
}

get_default_grub2_bls()
{
	local val
	val="$(grubenv_get "default")"
	[ -n "$val" ] || val="$(get_default_sdboot "default")"
	[ -z "$val" ] || echo "$val"
}

get_default_entry()
{
	if is_sdboot; then
		get_default_sdboot
	elif is_grub2_bls; then
		get_default_grub2_bls
	else
		err "Bootloader not detected"
	fi
}

set_timeout_sdboot()
{
	local timeout="${1:?}"
	[ "$timeout" = "-1" ] && timeout="menu-force"
	if ! bootctl set-timeout "$timeout" > "$tmpfile" 2>&1; then
		if grep -q "Failed to update EFI variable" "$tmpfile" ||
				grep -q "Not booted with a supported boot loader" "$tmpfile" ||
				grep -q "Not booted with UEFI" "$tmpfile"; then
			loader_conf_set "timeout" "$timeout"
		else
			err "$(cat "$tmpfile")"
		fi
	fi
}

set_timeout_grub2_bls()
{
	local timeout="${1:?}"
	set_timeout_sdboot "$timeout"
	[ "$timeout" = "menu-disabled" ] || [ "$timeout" = "menu-hidden" ] && timeout=0
	[ "$timeout" = "menu-force" ] && timeout=-1
	grubenv_set "timeout" "$timeout"
}

set_timeout()
{
	local timeout="${1:?}"
	info "Setting timeout $timeout"
	if is_sdboot; then
		set_timeout_sdboot "$timeout"
	elif is_grub2_bls; then
		set_timeout_grub2_bls "$timeout"
	else
		err "Bootloader not detected"
	fi

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

get_timeout_sdboot()
{
	local val
	val="$(bli_efi_var_get "LoaderConfigTimeout")"
	[ -n "$val" ] || val="$(loader_conf_get "timeout")"
	if [ "$val" = 4294967295 ]; then
		val=-1
	fi
	[ -z "$val" ] || echo "$val"
}

get_timeout_grub2_bls()
{
	local val
	val="$(grubenv_get "timeout")"
	[ -z "$val" ] || echo "$val"
}

get_timeout()
{
	if is_sdboot; then
		get_timeout_sdboot
	elif is_grub2_bls; then
		get_timeout_grub2_bls
	else
		err "Bootloader not detected"
	fi
}

set_default_snapshot()
{
	[ -n "$have_snapshots" ] || { info "System does not support snapshots."; return 0; }
	local num="${1:?}"
	local configs

	info "Setting default snapshot $num"

	update_entries_for_snapshot "$num"
	mapfile configs < <(jq '.[]|[.id]|join(" ")' -r < "$entryfile")
	configs=("${configs[@]%$nl}")
	if [ -z "${configs[0]}" ]; then
		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

	# The default snapshot is not the criteria used for the
	# bootloader to select the default boot entry, but for
	# coherence we synchronize it here, invalidating any previous
	# "$snapperfile"
	#
	# We do not use snapper, to avoid recursion and do not trigger
	# any plugin
	local subvolume_id
	read -r _ subvolume_id _ < <(btrfs subvolume list -o /.snapshots | grep "${subvol_prefix}/.snapshots/$num/snapshot")
	btrfs subvolume set-default "${subvolume_id}" /.snapshots
	rm -f "$snapperfile"

	set_default_entry "${configs[0]}"
}

have_pcrlock()
{
	[ -e /usr/bin/systemd-pcrlock ] || [ -e /usr/lib/systemd/systemd-pcrlock ]
}

have_pcr_oracle()
{
	[ -e /usr/bin/pcr-oracle ]
}

pcrlock()
{
	dbg "systemd-pcrlock $*"
	local pcrlock_cmd="/usr/bin/systemd-pcrlock"
	[ -e "$pcrlock_cmd" ] || pcrlock_cmd="/usr/lib/systemd/systemd-pcrlock"
	SYSTEMD_LOG_LEVEL="${SYSTEMD_LOG_LEVEL:-warning}" "$pcrlock_cmd" "$@"
}

is_pcr_oracle()
{
	[ -e /etc/systemd/tpm2-pcr-public-key.pem ] && \
	    [ -e /etc/systemd/tpm2-pcr-private-key.pem ] && \
	    have_pcr_oracle
}

snapshot_ids_for_prediction()
{
	# Select the ID of the snapshots that participate in the
	# prediction.  The order is important, so the most relevant
	# should be presented first

	# Get the numbers for the last three snapshots
	[ -s "$snapperfile" ] || update_snapper

	# Select the default and the active snapshots.
	local -A snapshots
	local -a snapshot_ids
	local id
	if [ -n "$root_snapshot" ]; then
		[ -n "${snapshots[$root_snapshot]}" ] || snapshot_ids+=("$root_snapshot")
		snapshots["$root_snapshot"]=1
	fi
	while read -r id; do
		[ -n "${snapshots[$id]}" ] || snapshot_ids+=("$id")
		snapshots[$id]=1
	done < <(jq -r '.root[]|select(.default==true)|.number' "$snapperfile")
	while read -r id; do
		[ -n "${snapshots[$id]}" ] || snapshot_ids+=("$id")
		snapshots[$id]=1
	done < <(jq -r '.root[]|select(.active==true)|.number' "$snapperfile")

	if is_transactional && [ -e "${state_file}" ]; then
		# shellcheck disable=SC1090
		. "${state_file}"
		for id in $LAST_WORKING_SNAPSHOTS; do
			if [ "${#snapshots[@]}" -lt 3 ]; then
				[ -n "${snapshots[$id]}" ] || snapshot_ids+=("$id")
				snapshots[$id]=1
			fi
		done
	else
		while read -r id; do
			if [ "${#snapshots[@]}" -lt 3 ]; then
				[ -n "${snapshots[$id]}" ] || snapshot_ids+=("$id")
				snapshots[$id]=1
			fi
		done < <(jq -r '.root|sort_by(.date)[-2:]|reverse|.[]|.number' "$snapperfile")
	fi

	echo "${snapshot_ids[@]}"
}

regex_snapshot_ids_for_prediction()
{
	local re
	re="$(snapshot_ids_for_prediction)"
	re="${re// /|}"
	echo "(:?$re)"
}

select_entries_for_prediction()
{
	# The regex of ids is ordered by relevance, but the returns
	# set is not.  We need to add a "priority" and "kernel"
	# (version) field that can be used to order the set
	local ids
	ids="$(snapshot_ids_for_prediction)"

	update_entries_for_snapshot "$(regex_snapshot_ids_for_prediction)"

	# Extend the entry file with a "priority" field, based on the
	# ordered snapshot IDs, and "kernel" field, based on the
	# kernel version.  Note that the textual suffix (-default,
	# -slowroll) is removed and does not participate in the
	# ordering
	#
	# Expected order for prediction:
	#
	# MicroOS
	#   - The default snapshot usually has the top priority (0),
	#     then the active (1).  The order is set by
	#     `snapshot_ids_for_prediction`
	#   - Last working snapshots are added after those.  Also set
	#     by `snapshot_ids_for_prediction`
	#   - If the snapshot contains multiple kernel, the higher
	#     version has more priority
	#
	# Tumbleweed
	#   - Same than MicroOS (`snapshot_ids_for_prediction`)
	#   - Newest working snapshot are added after those.  Also set
	#     by `snapshot_ids_for_prediction`
	#   - If the snapshot contains multiple kernel, the higher
	#     version has more priority
	#
	jq --arg ids "$ids" 'def priority(id): id as $id | $ids | split(" ") | index($id); map(. + {"priority": priority(.version | scan("(\\d+)@") | .[]), "kernel": .version | scan(".*@(?:(\\d+).(\\d+).(\\d+)-(\\d+))") | map(. | tonumber)})' < "$entryfile" > "${entryfile}.ext"
	mv "${entryfile}.ext" "$entryfile"
	dbg "Added priority and kernel version to entry file (for prediction)"
	dbg_cat "$entryfile"
}

backup_initial_components()
{
	select_entries_for_prediction
	mv "$entryfile" "$initialentryfile"
	cp -a "$boot_root/." "$tmpdir"
}

parse_eventlog()
{
	[ "${#eventlog[@]}" -eq 0 ] || return 0

	while read -r line; do
		eventlog["$line"]=1
	done < <(pcrlock log --json=short | jq -r '.log | .[] | .sha256' | sort -u)
}

find_matching_variation()
{
	local component="$1"

	local hashes=()
	for variation in /var/lib/pcrlock.d/"$component".pcrlock.d/*.pcrlock; do
		mapfile -t hashes < <(jq -r '.records | .[] | .digests | .[] | select(.hashAlg == "sha256") | .digest' < "$variation")
		for h in "${hashes[@]}"; do
			[ "${eventlog["$h"]+_}" ] || continue 2
		done
		echo "$variation"
		break
	done
}

shift_component()
{
	local component="$1"

	parse_eventlog

	dbg "Shifting component $component"
	[ -d "/var/lib/pcrlock.d/$component.pcrlock.d" ] || {
		dbg "The component is not a directory or is missing"
		return 0
	}

	# Instead of moving all the variations of a component (as this
	# will increase the amount of combinations, reaching early the
	# PolicyOR limitation), we keep the one that matches the
	# current eventlog.  If the variation is also duplicated, it
	# will be dropped by `systemd-pcrlock`
	local variation vname
	variation="$(find_matching_variation "$component")"
	if [ -n "$variation" ]; then
		vname="$(basename "$variation")"
		dbg "$variation found in the eventlog"
		[[ "$vname" == shift-* ]] || {
			# Drop any previous shifted variation, as
			# there is no guarantee that the new one will
			# share the name
			find /var/lib/pcrlock.d/"$component".pcrlock.d -name 'shift-*.pcrlock' -delete
			mv "$variation" "/var/lib/pcrlock.d/$component.pcrlock.d/shift-$vname"
		}
	else
		dbg "No matching variation found for $component"
		return 0
	fi

	# Remove the rest of the variations
	find /var/lib/pcrlock.d/"$component".pcrlock.d -name '*.pcrlock' ! -name 'shift-*.pcrlock' -delete
}

pcrlock_manual_raw()
{
	local pcr="$1"
	local pcrlock="$2"
	local element="$3"

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

pcrlock_sdboot_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" \
		"$initrd" 2> /dev/null || pcrlock_manual_raw 9 "$tmpdir/initrd.pcrlock" "$initrd"
	mkdir -p /var/lib/pcrlock.d/710-kernel-cmdline-initrd-entry.pcrlock.d
	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"
}

pcrlock_grub2_bls_kernel_initrd()
{
	local linux="$1"
	local initrd="$2"
	local suffix="$3"

	local elements=("$linux" "$initrd")
	local locks=()
	local n=0
	for element in "${elements[@]}"; do
		n=$((n+1))
		pcrlock \
			lock-raw \
			--pcr=9 \
			--pcrlock "$tmpdir/element-$n.pcrlock" \
			"$element" 2> /dev/null || pcrlock_manual_raw 9 "$tmpdir/element-$n.pcrlock" "$element"
		locks+=("$tmpdir/element-$n.pcrlock")
	done
	mkdir -p /var/lib/pcrlock.d/710-grub2-bls-kernel-initrd-entry.pcrlock.d
	jq --slurp '{"records": [.[].records[0]]}' \
	   "${locks[@]}" \
	   > "/var/lib/pcrlock.d/710-grub2-bls-kernel-initrd-entry.pcrlock.d/kernel-initrd-$suffix.pcrlock"
	rm "${locks[@]}"
}

pcrlock_grub2_bls_cmdline()
{
	local linux="$1"
	local cmdline="$2"
	local initrd="$3"
	local suffix="$4"
	local lines=("$linux" "$cmdline" "$initrd")

	local locks=()
	local n=0
	for line in "${lines[@]}"; do
		n=$((n+1))
		echo -n "$line" > "$tmpdir/line"
		pcrlock \
			lock-raw \
			--pcr=8 \
			--pcrlock "$tmpdir/line-$n.pcrlock" \
			"$tmpdir/line"
		locks+=("$tmpdir/line-$n.pcrlock")
		rm "$tmpdir/line"
	done
	mkdir -p /var/lib/pcrlock.d/650-grub2-bls-entry-cmdline.pcrlock.d
	jq --slurp '{"records": [.[].records[0]]}' \
	   "${locks[@]}" \
	   > "/var/lib/pcrlock.d/650-grub2-bls-entry-cmdline.pcrlock.d/cmdline-$suffix.pcrlock"
	rm "${locks[@]}"
}

pcrlock_grub2_bls_entry_files()
{
	local suffix="${1:+-$1}"
	local base="${2:-$boot_root}"
	local locks=()
	local n=0
	for i in "$base"/loader/entries/*.conf; do
		n=$((n+1))
		pcrlock \
			lock-raw \
			--pcr=9 \
			--pcrlock="$tmpdir/entry-$n.pcrlock" \
			"$i"
		locks+=("$tmpdir/entry-$n.pcrlock")
	done
	mkdir -p /var/lib/pcrlock.d/643-grub2-bls-entry-files.pcrlock.d
	jq --slurp '{"records": [.[].records[0]]}' \
	   "${locks[@]}" \
	   > "/var/lib/pcrlock.d/643-grub2-bls-entry-files.pcrlock.d/generated$suffix.pcrlock"
	rm "${locks[@]}"
}

pcrlock_sdboot()
{
	info "Generating TPM2 predictions with systemd-pcrlock (systemd-boot)"

	# 641-sdboot-loader-conf.pcrlock is not part of the pcrlock
	# standards
	if [ -e "${boot_root}/loader/loader.conf" ]; then
		shift_component 641-sdboot-loader-conf
		pcrlock \
			lock-raw "${boot_root}/loader/loader.conf" \
			--pcr=5 \
			--pcrlock=/var/lib/pcrlock.d/641-sdboot-loader-conf.pcrlock.d/generated.pcrlock
	fi

	# 650-kernel-efi-application.pcrlock is not part of the
	# pcrlock standards
	# TODO: move to kernel-TYPE-pcrlock.rpm
	shift_component 650-kernel-efi-application
	local n=0
	local -A kernels
	while read -r linux; do
		[ -f "${boot_root}$linux" ] || {
			info "Missing ${boot_root}$linux, ignoring entry for prediction"
			continue
		}
		[ -z "${kernels["$linux"]}" ] || continue
		kernels["$linux"]=1
		n=$((n+1))
		# Limit to 4 because of the separator
		[ "$n" -le 4 ] || {
			info "More than 4 variations for 650-kernel-efi-application"
			continue
		}
		pcrlock \
			lock-pe \
			--pcrlock=/var/lib/pcrlock.d/650-kernel-efi-application.pcrlock.d/linux-"$n".pcrlock \
			"${boot_root}/$linux"
	done < <(jq --raw-output 'sort_by(.priority, (.kernel | map(-.))) | map(.linux) | .[]' "$entryfile")

	# Join the cmdline and the initrd in a single component
	shift_component 710-kernel-cmdline-initrd-entry
	shift_component 710-kernel-cmdline-boot-loader
	n=0
	while read -r cmdline; do
		read -r initrd
		[ -f "${boot_root}$initrd" ] || {
			info "Missing ${boot_root}$initrd, ignoring entry for prediction"
			continue
		}
		n=$((n+1))
		[ "$n" -le 8 ] || {
			info "More than 8 variations for 710-kernel-cmdline-initrd-entry"
			continue
		}
		pcrlock_sdboot_cmdline_initrd "initrd=$cmdline" "${boot_root}$initrd" "$n"
	done < <(jq --raw-output 'sort_by(.priority, (.kernel | map(-.))) | .[] | ([(.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
	n=0
	if [ "$SDB_ADD_INITIAL_COMPONENT" = "1" ]; then
		while read -r cmdline; do
			read -r initrd
			n=$((n+1))
			pcrlock_sdboot_cmdline_initrd "initrd=$cmdline" "${tmpdir}$initrd" "0-$n"
		done < <(jq --raw-output '.[] | ([(.initrd[0] | sub("/"; "\\"; "g")), .options] | join(" ")), .initrd[0]' "$initialentryfile")
	fi
}

pcrlock_grub2_bls()
{
	info "Generating TPM2 predictions with systemd-pcrlock (grub2-bls)"

	# 641-grub2-bls-grubenv.pcrlock is not part of the pcrlock
	# standards
	if [ -e "${boot_root}${boot_dst}/grubenv" ]; then
		shift_component 641-grub2-bls-grubenv
		pcrlock \
			lock-raw "${boot_root}${boot_dst}/grubenv" \
			--pcr=9 \
			--pcrlock=/var/lib/pcrlock.d/641-grub2-bls-grubenv.pcrlock.d/generated.pcrlock
	fi

	# 643-grub2-bls-entry-files.pcrlock is not part of the pcrlock
	# standards
	shift_component 643-grub2-bls-entry-files
	pcrlock_grub2_bls_entry_files
	if [ "$SDB_ADD_INITIAL_COMPONENT" = "1" ]; then
		pcrlock_grub2_bls_entry_files "0" "$tmpdir"
	fi

	blkpart="$(findmnt -nvo SOURCE "$boot_root")"
	read -r partno < "/sys/class/block/${blkpart##*/}"/partition
	# Once we are out of the BIOS / EFI, the numeration cannot be
	# done without device.map.  It is safe to assume that the ESP
	# is always the first disk (hd0)
	grub2_bls_drive="(hd0,gpt$partno)"

	# Join linux, initrd and cmdline in a single pcrlock file
	shift_component 650-grub2-bls-entry-cmdline
	n=0
	while read -r options; do
		read -r linux
		read -r initrd
		[ -f "${boot_root}$linux" ] || {
			info "Missing ${boot_root}$linux, ignoring entry for prediction"
			continue
		}
		[ -f "${boot_root}$initrd" ] || {
			info "Missing ${boot_root}$initrd, ignoring entry for prediction"
			continue
		}
		n=$((n+1))
		[ "$n" -le 8 ] || {
			info "More than 8 variations for 650-grub2-bls-entry-cmdline"
			continue
		}
		pcrlock_grub2_bls_cmdline "linux ${grub2_bls_drive}$linux $options" \
					  "${grub2_bls_drive}$linux $options" \
					  "initrd ${grub2_bls_drive}$initrd" "$n"
	done < <(jq --raw-output 'sort_by(.priority, (.kernel | map(-.))) | .[] | .options, .linux, .initrd[0]' "$entryfile")

	# Generate variation for 650-grub2-bls-entry-cmdline 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
	n=0
	if [ "$SDB_ADD_INITIAL_COMPONENT" = "1" ]; then
		while read -r options; do
			read -r linux
			read -r initrd
			n=$((n+1))
			pcrlock_grub2_bls_cmdline "linux ${grub2_bls_drive}$linux $options" \
						  "${grub2_bls_drive}$linux $options" \
						  "initrd ${grub2_bls_drive}$initrd" "0-$n"
		done < <(jq --raw-output '.[] | .options, .linux, .initrd[0]' "$initialentryfile")
	fi

	# Join the kernel and the initrd in a single component
	shift_component 710-grub2-bls-kernel-initrd-entry
	n=0
	while read -r linux; do
		read -r initrd
		[ -f "${boot_root}$linux" ] || continue
		[ -f "${boot_root}$initrd" ] || continue
		n=$((n+1))
		[ "$n" -le 8 ] || continue
		pcrlock_grub2_bls_kernel_initrd "${boot_root}$linux" "${boot_root}$initrd" "$n"
	done < <(jq --raw-output 'sort_by(.priority, (.kernel | map(-.))) | .[] | .linux, .initrd[0]' "$entryfile")

	# Generate variation for 710-grub2-bls-kernel-initrd-entry for the
	# same reason than before.
	n=0
	if [ "$SDB_ADD_INITIAL_COMPONENT" = "1" ]; then
		while read -r linux; do
			read -r initrd
			n=$((n+1))
			pcrlock_grub2_bls_kernel_initrd "${tmpdir}$linux" "${tmpdir}$initrd" "0-$n"
		done < <(jq --raw-output '.[] | .linux, .initrd[0]' "$initialentryfile")
	fi
}

clean_pcrlock_d()
{
	[ -d /var/lib/pcrlock.d ] || return 0

	# Remove the shifted measurements since the last reboot.  They
	# are used to link the current components with the event log,
	# so pcrlock can work with the aligments.  For example, if a
	# file gets replaced (loader.conf) the new measurement cannot
	# be found in the event log, as contains the old hash, making
	# the aligment fail.
	local btime
	read -r _ btime < <(grep btime /proc/stat)
	local minutes=$((1 + ($(date +%s) - btime) / 60))
	dbg "Cleaning shifted measurements older than $minutes minutes"
	find /var/lib/pcrlock.d -name 'shift-*.pcrlock' -type f -cmin +"$minutes" -delete

	# Remove older (1 week) generated measurements.  This will
	# keep the predictions at minimum and decrease the
	# combinations.  Removing all can be a problem in certain
	# conditions.  For example, after the first boot some pcrlock
	# files contain hashes for the original ESP assets, that are
	# required for the event log aligment.
	find /var/lib/pcrlock.d -name '*.pcrlock' -type f -mtime +7 -delete

	# Sometimes, like in tests, the user will generate new entries
	# and reboot in a short period of time
	if [ "$(find /var/lib/pcrlock.d -type f -name '*-7.pcrlock' | wc -l)" -gt 0 ]; then
		rm -fr /var/lib/pcrlock.d
	fi
}

generate_tpm2_predictions_pcrlock()
{
	local pcrs="$FDE_SEAL_PCR_LIST"

	info "Generating TPM2 predictions with systemd-pcrlock"

	# Select the affected entries
	select_entries_for_prediction

	clean_pcrlock_d

	shift_component 250-firmware-code-early
	shift_component 550-firmware-code-late
	pcrlock lock-firmware-code

	shift_component 250-firmware-config-early
	shift_component 550-firmware-config-late
	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
	shift_component 240-secureboot-policy
	pcrlock lock-secureboot-policy &> /dev/null || true
	shift_component 620-secureboot-authority
	pcrlock lock-secureboot-authority &> /dev/null || true

	# Uses / by default
	shift_component 600-gpt
	pcrlock lock-gpt

	# 630-shim-efi-application is not part of the pcrlock standards
	# TODO: move to shim-pcrlock.rpm
	shift_component 630-shim-efi-application
	local entry="shim.efi"
	[ -f "${boot_root}${boot_dst}/${entry}" ] || entry="BOOT${firmware_arch^^}.EFI"
	pcrlock \
		lock-pe \
		--pcrlock=/var/lib/pcrlock.d/630-shim-efi-application.pcrlock.d/generated.pcrlock \
		"${boot_root}${boot_dst}/${entry}"

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

	if is_sdboot; then
		pcrlock_sdboot
	elif is_grub2_bls; then
		pcrlock_grub2_bls
	fi

	# If the prediction fails, the system will ask for a password,
	# but we can do a re-enrollment using the recovery PIN.  To
	# register a recovery PIN the installer (sdbootutil-enroll,
	# YaST) will call this script deploying in the %u keyring
	# "sdbootutil[-pin]" entry.  For re-enrollments we can use the
	# same entry, the PIN environment variable, or the
	# --ask-{key,pin,pw} parameter.
	local pin
	local extra=()
	local keyid keyid_int
	keyid="$(keyctl id %user:sdbootutil 2> /dev/null)" || true
	keyid_int="$(keyctl id %user:sdbootutil-pin 2> /dev/null)" || true
	if [ -n "$arg_ask_key_pin_or_pw" ]; then
		ask_password "Recovery PIN" pin
		extra=("--recovery-pin=yes")
	elif [ -n "$PIN" ]; then
		pin="$PIN"
		extra=("--recovery-pin=yes")
	elif [ -n "$keyid_int" ]; then
		pin="$(keyctl pipe "$keyid_int")"
		extra=("--recovery-pin=yes")
	elif [ -n "$keyid" ]; then
		pin="$(keyctl pipe "$keyid")"
		extra=("--recovery-pin=yes")
	else
		# No PIN was provided, systemd-pcrlock will generate
		# one
		extra=("--recovery-pin=show")
	fi

	local output
	output="$(PIN="$pin" pcrlock --pcr="$pcrs" "${extra[@]}" make-policy)"
	local pcrlock_status=$?

	if [ $pcrlock_status -ne 0 ]; then
		echo "Error creating the systemd-pcrlock policy!"
		return 1
	elif echo "$output" | grep -q "recovery PIN"; then
		local split
		IFS=":" read -r -a split <<<"$output"
		pin="${split[1]}"
		pin="${pin## }"
		pin="${pin%% }"

		echo "Recovery PIN: $pin"
		if [ -x /usr/bin/qrencode ]; then
			echo "You can also scan it with your mobile phone:"
			qrencode -t utf8i "$pin"
		fi

		# Add the generated recovery PIN to the kernel
		# keyring, so that it is available to `sdbootutil
		# enroll --method=recovery-key`
		keyctl_add_with_timeout "sdbootutil-pin" "$pin"
	fi

	# 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}" && {
			echo "NVIndex policy created"
		}
}

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)"
}

generate_tpm2_predictions_pcr_oracle()
{
	local entry
	local all_pcrs

	info "Generating TPM2 predictions with pcr-oracle"

	# Select the affected entries
	select_entries_for_prediction

	all_pcrs=$(get_pcrs)
	[ -n "$all_pcrs" ] || all_pcrs="$FDE_SEAL_PCR_LIST"

	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.
	local -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
			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}" && {
			echo "Signed policy created"
		}
}

get_volume_password()
{
	local dev="$1"
	local pw keyid
	# If we are enrolling for the first time, sdbootutil-pin can
	# contain the recovery PIN, that cannot be used to unlock the
	# device (unless later it is done an enrollment of a new
	# recovery key)
	keyid="$(keyctl id %user:sdbootutil 2> /dev/null)" || true
	keyid_ce="$(keyctl id %user:cryptenroll 2> /dev/null)" || true
	if [ -n "$keyid_ce" ]; then
		pw="$(keyctl pipe "$keyid_ce")"
	elif [ -n "$keyid" ]; then
		pw="$(keyctl pipe "$keyid")"
	else
		ask_password "Password for $dev" pw
		# If the key was missing for all the keyrings put back
		# into the cryptenroll keyring, as there is a chance
		# that this is part or a re-enrollment
		keyctl_add_with_timeout "cryptenroll" "$pw"
	fi
	echo "$pw"
}

get_volume_key()
{
	local dev="$1"
	local pw out
	pw="$(get_volume_password "$dev")"
	out="$(cryptsetup luksDump --batch-mode --dump-master-key "$dev" <<<"$pw")" || {
		# If luksDump fails, remove the password from the
		# keyring.  Can be that the password was wrong, and
		# systemd-cryptenroll ask later for the password.
		# Both passwords can appear in the keyring separated
		# by NULL
		keyctl revoke %user:cryptenroll 2> /dev/null || true
		keyctl reap 2> /dev/null || true
		return 1
	}
	echo "$out" | sed -n '/MK dump:/,$p' | sed -E 's/MK dump:|[[:blank:]]+//g' | sed -z 's/\n//g'
}

extend_pcr()
{
	local dgst="$1"
	local pcr="$2"
	local val="$3"
	local digest

	hash "${dgst}sum" || return
	hex_to_binary "$pcr$val" > "$tmpdir/pcr"
	read -r digest _ < <("${dgst}sum" "$tmpdir/pcr")
	echo "$digest"
}

generate_tpm2_predictions_pcr_15()
{
	local devs=()
	local msgs=()
	local vks=()
	local name dev opts fstype uuid pw

	info "Generating predictions for PCR15"

	# Read /etc/crypttab lines that contains tpm2-device and
	# tpm2-measure-pcr.  This code is the similar from
	# measure-pcr-generator.sh, so we guarantee the same ordering
	# for PCR 15 extension
	while read -r name dev _ opts; do
		# Only the entries in /etc/crypttab in the initrd
		# (marked with x-initrd.attach) should participate
		# from the extension for now.  The reason is that
		# extensions after the switch root cannot participate
		# in abort the boot process from initrd itself
		#
		# Note that dracut will add the cr_swap partition even
		# if it is not marked as x-initrd.attach
		[[ "$name" = \#* ]] && continue
		[[ "$opts" != *"tpm2-device="* ]] && continue
		[[ "$opts" != *"tpm2-measure-pcr="* ]] && continue

		# If the device name is UUID= convert as a real device
		# name, and if not, retrieve the UUID
		if [[ "$dev" = "UUID="* ]]; then
			uuid="${dev#"UUID="}"
			dev="$(blkid --uuid "$uuid")"
		else
			uuid="$(blkid "$dev" -o value -s UUID)"
		fi

		# Get the FSTYPE of the real device (crypto_LUKS) and
		# the slave / holder one (btrfs, swap, etc)
		fstype="$(lsblk --noheadings -o FSTYPE "$dev")"
		if [[ "$fstype" != *"swap"* ]]; then
			[[ "$opts" != *"x-initrd.attach"* ]] && continue
		fi

		dbg "Adding $dev (cryptsetup:$name:$uuid) for PCR15"

		devs+=("$dev")
		msgs+=("cryptsetup:$name:$uuid")
	done < /etc/crypttab
	# We need to separate this into a different loop because we
	# cannot nest two reads (one for crypttab and another for the
	# password)
	for dev in "${devs[@]}"; do
		local vk
		vk="$(get_volume_key "$dev")"
		[ -n "$vk" ] || { warn "Volume key cannot be extracted. Dropping PCR 15"; return 0; }
		vks+=("$vk")
	done

	rm -f /var/lib/sdbootutil/measure-pcr-prediction
	rm -f /var/lib/sdbootutil/measure-pcr-prediction.sha256
	local dgsts=("sha1" "sha256" "sha384" "sha512")
	local sizes=(40 64 96 128)
	local pcr15 hmac
	for i in "${!dgsts[@]}"; do
		pcr15="$(printf '0%.0s' $(seq 1 "${sizes[i]}"))"
		for j in "${!msgs[@]}"; do
			hmac="$(echo -ne "${msgs[j]}" | openssl mac -digest "${dgsts[i]}" -macopt hexkey:"${vks[j]}" HMAC)"
			pcr15="$(extend_pcr "${dgsts[i]}" "$pcr15" "$hmac")"
			dbg "${msgs[j]} (${dgsts[i]}): $hmac"
		done
		echo "$pcr15" >> "/var/lib/sdbootutil/measure-pcr-prediction"
	done

	if [ -f "/var/lib/sdbootutil/measure-pcr-prediction" ] && [ -f "/var/lib/sdbootutil/measure-pcr-private.pem" ]; then
		openssl dgst -sha256 \
			-sign /var/lib/sdbootutil/measure-pcr-private.pem \
			-out /var/lib/sdbootutil/measure-pcr-prediction.sha256 \
			/var/lib/sdbootutil/measure-pcr-prediction
	fi

	# Register the hash of the parsed crypttab
	local crypttab_sha1
	read -r crypttab_sha1 _ < <(sha1sum /etc/crypttab)
	echo "$crypttab_sha1" > /var/lib/sdbootutil/crypttab.sha1

	# Publish the assets in the ESP, so can be imported by
	# dracut-pcr-signature
	[ ! -e /var/lib/sdbootutil/measure-pcr-prediction ] || \
		cp /var/lib/sdbootutil/measure-pcr-prediction "${boot_root}${boot_dst}"
	[ ! -e /var/lib/sdbootutil/measure-pcr-prediction.sha256 ] || \
		cp /var/lib/sdbootutil/measure-pcr-prediction.sha256 "${boot_root}${boot_dst}"
}

updated_crypttab()
{
	local crypttab_sha1
	read -r crypttab_sha1 _ < <(sha1sum /etc/crypttab)
	grep -Fixq "$crypttab_sha1" /var/lib/sdbootutil/crypttab.sha1 2> /dev/null
}

generate_tpm2_predictions()
{
	[ -e /etc/crypttab ] || return 0
	grep -q "tpm2-device" /etc/crypttab || return 0
	! in_lockout || err "The TPM2 is in lockout. Use 'tpm2_dictionarylockout -c [ -p passwd ]' to clear the DA lockout and re-try the sdbootutil command"

	# The PCR list is used by both models (pcr-oracle,
	# systemd-pcrlock).  The first one will try first to get the
	# PCRs from the LUKS2 header, that will succeed in the normal
	# situation but fail during the initial enrollment.  This is
	# because we generate the prediction first and later we do the
	# enrollment
	is_config_file || err "/etc/sysconfig/fde-tools not found"
	[ -n "$FDE_SEAL_PCR_LIST" ] || load_config_file

	if is_pcr_oracle; then
		generate_tpm2_predictions_pcr_oracle
	elif have_pcrlock; then
		generate_tpm2_predictions_pcrlock || return 1
	fi

	# Generate a PCR 15 prediction only in certain cases, as for
	# now this will ask the password (can be resolved by an
	# external tool that extract the password from the TPM2 if the
	# policy is still valid)
	updated_crypttab || { generate_tpm2_predictions_pcr_15; arg_measure_pcr=; }
	# shellcheck disable=SC2015
	[ -f "/var/lib/sdbootutil/measure-pcr-prediction" ] && [ -z "$arg_measure_pcr" ] || generate_tpm2_predictions_pcr_15
}

have_tracked_devices()
{
	[ "${#tracked_devices[@]}" -gt 0 ]
}

detect_tracked_devices()
{
	# A LUKS2 device can be un-tracked (ignored) by sdbootutil if
	# is present in /etc/crypttab and has the
	# "x-sdbootutil.ignore" option
	local dev fstype uuid
	! have_tracked_devices || return 0

	dbg_cat "/etc/crypttab"

	while read -r dev fstype uuid; do
		[ "$fstype" = 'crypto_LUKS' ] || continue
		cryptsetup isLuks --type luks2 "$dev" || continue
		[ -e /etc/crypttab ] && grep -E -q "${dev}[[:space:]].*x-sdbootutil.ignore" /etc/crypttab && continue
		[ -e /etc/crypttab ] && grep -E -q "${uuid}[[:space:]].*x-sdbootutil.ignore" /etc/crypttab && continue
		dbg "Tracking encrypted device $dev"
		tracked_devices+=("$dev")
	done < <(lsblk --noheadings -o PATH,FSTYPE,UUID)
	have_tracked_devices
}

have_tpm2()
{
	[ -n "$(systemd-cryptenroll --tpm2-device=list 2>/dev/null)" ]
}

have_fido2()
{
	[ -n "$(systemd-cryptenroll --fido2-device=list 2>/dev/null)" ]
}

have_slot()
{
	local dev="${1:?}"
	local kind="${2:?}"
	grep -q "$kind" < <(systemd-cryptenroll "$dev")
}

in_lockout()
{
	hash tpm2_getcap &> /dev/null || { warn "tpm2_getcap not found"; return 1; }
	tpm2_getcap properties-variable | grep -q 'inLockout: *1'
}

add_crypttab_option()
{
	# This version will share the same options for all crypto_LUKS
	# devices.  This imply that all of them will be unlocked by
	# the same TPM2, or the same FIDO2 key
	local option="$1"

	dbg "Adding \"$option\" to /etc/crypttab"
	dbg_cat "/etc/crypttab"

	local crypttab
	crypttab="$(mktemp -t crypttab.XXXXXX)"
	echo "# File created by sdbootutil.  Comments will be removed" > "$crypttab"
	echo "# Add the 'x-sdbootutil.ignore' option to un-track a device" >> "$crypttab"

	local name
	local device
	local key
	local opts
	while read -r name device key opts; do
		[[ "$name" = \#* ]] && continue
		if [[ "$opts" != *"$option"* ]]; then
			[ -z "$opts" ] && opts="$option" || opts="$opts,$option"
			# crypttab has changed so initrd needs to be
			# updated
			arg_no_reuse_initrd=1
		fi
		echo "$name $device ${key:-none} $opts" >> "$crypttab"
	done < /etc/crypttab

	mv -Z "$crypttab" /etc/crypttab
	chmod 644 /etc/crypttab

	dbg "Added \"$option\" to /etc/crypttab"
	dbg_cat "/etc/crypttab"
}

remove_crypttab_option()
{
	# Similar to add_crypttab_option, the option will be removed
	# from all the entries
	local option="$1"

	dbg "Removing \"$option\" to /etc/crypttab"
	dbg_cat "/etc/crypttab"

	local crypttab
	crypttab="$(mktemp -t crypttab.XXXXXX)"
	echo "# File created by sdbootutil.  Comments will be removed" > "$crypttab"

	local name
	local device
	local key
	local opts
	while read -r name device key opts; do
		[[ "$name" = \#* ]] && continue
		if [[ "$opts" == *"$option"* ]]; then
			opts="${opts#"$option",}"
			opts="${opts//,"$option"}"
			opts="${opts//"$option"}"
			# crypttab has changed so initrd needs to be
			# updated
			arg_no_reuse_initrd=1
		fi
		[ -n "$opts" ] && echo "$name $device ${key:-none} $opts" >> "$crypttab"
		[ -z "$opts" ] && echo "$name $device ${key:-none}" >> "$crypttab"
	done < /etc/crypttab

	mv -Z "$crypttab" /etc/crypttab
	chmod 644 /etc/crypttab

	dbg "Removed \"$option\" to /etc/crypttab"
	dbg_cat "/etc/crypttab"
}

generate_rsa_key()
{
	pcr-oracle \
		--rsa-generate-key \
		--private-key /etc/systemd/tpm2-pcr-private-key.pem \
		--public-key /etc/systemd/tpm2-pcr-public-key.pem \
		store-public-key
}

set_unlock_method()
{
	local dev="$1"

	unlock_method=

	# If %user:cryptenroll is set, use it as an automatic
	# unlocker, if not, try the TPM2 or the FIDO2 key
	local keyid
	keyid="$(keyctl id %user:cryptenroll 2> /dev/null)" || true
	[ -z "$keyid" ] || return 0

	# Do not use TPM2 slot for enrolling TPM2
	if [ "$arg_method" != "tpm2" ] && [ "$arg_method" != "tpm2+pin" ] && have_slot "$dev" "tpm2"; then
		info "Unlocking using TPM2"
		unlock_method="--unlock-tpm2-device=auto"
	# Same for FIDO2
	elif [ "$arg_method" != "fido2" ] && have_slot "$dev" "fido2"; then
		info "Unlocking using FIDO2"
		unlock_method="--unlock-fido2-device=auto"
	fi
}

enroll_pcrlock()
{
	local dev="$1"
	local tpm2_pin="$2"
	local extra_args=()

	if [ -z "$tpm2_pin" ]; then
		info "Enrolling with TPM2 (pcrlock): $dev"
	else
		info "Enrolling with TPM2+PIN (pcrlock): $dev"
		extra_args+=(--tpm2-with-pin=1)
	fi

	if [ ! -f /var/lib/systemd/pcrlock.json ]; then
		warn "Could not find /var/lib/systemd/pcrlock.json"
	fi

	set_unlock_method "$dev"
	if [ -n "$unlock_method" ]; then
		extra_args+=("$unlock_method")
	fi

	# Note that the PCRs are now not stored in the LUKS2 header
	if NEWPIN="$tpm2_pin" systemd-cryptenroll \
		 --wipe-slot=tpm2 \
		 --tpm2-device=auto \
		 "${extra_args[@]}" \
		 --tpm2-pcrlock=/var/lib/systemd/pcrlock.json \
		 "$dev"; then
		# systemd-cryptenroll exits successfully even if the
		# token was not enrolled.  Manually check if the
		# device has a tpm2 slot enrolled
		systemd-cryptenroll "$dev" | grep -q "tpm2"
	else
		return 1
	fi
}

enroll_pcroracle()
{
	local dev="$1"
	local tpm_pin="$2"
	local extra_args=()

	if [ -z "$tpm_pin" ]; then
		info "Enrolling with TPM2 (pcr-oracle): $dev"
	else
		info "Enrolling with TPM2+PIN (pcr-oracle): $dev"
		extra_args+=(--tpm2-with-pin=1)
	fi

	if [ ! -f /etc/systemd/tpm2-pcr-signature.json ]; then
		warn "Could not find /etc/systemd/tpm2-pcr-signature.json"
	fi

	set_unlock_method "$dev"
	if [ -n "$unlock_method" ]; then
		extra_args+=("$unlock_method")
	fi

	# Note that the PCRs are now not stored in the LUKS2 header
	if NEWPIN="$tpm_pin" systemd-cryptenroll \
		 --wipe-slot=tpm2 \
		 --tpm2-device=auto \
		 "${extra_args[@]}" \
		 --tpm2-public-key=/etc/systemd/tpm2-pcr-public-key.pem \
		 --tpm2-public-key-pcrs="${FDE_SEAL_PCR_LIST}" \
		 "$dev"; then
		# systemd-cryptenroll exits successfully even if the
		# token was not enrolled.  Manually check if the
		# device has a tpm2 slot enrolled
		systemd-cryptenroll "$dev" | grep -q "tpm2"
	else
		return 1
	fi
}

enroll_fido2()
{
	local dev="$1"

	info "Enrolling with FIDO2: $dev"

	local extra_args=()
	set_unlock_method "$dev"
	if [ -n "$unlock_method" ]; then
		extra_args+=("$unlock_method")
	fi

	systemd-cryptenroll --wipe-slot=fido2 --fido2-device=auto "${extra_args[@]}" "$dev"
}

enroll_password()
{
	local dev="$1"
	local pw="$2"

	info "Enrolling with password: $dev"

	local extra_args=()
	set_unlock_method "$dev"
	if [ -n "$unlock_method" ]; then
		extra_args+=("$unlock_method")
	fi

	NEWPASSWORD="$pw" systemd-cryptenroll --wipe-slot=password --password "${extra_args[@]}" "$dev"
}

enroll_recovery_key()
{
	local dev="$1"

	info "Enrolling with recovery key: $dev"

	local extra_args=()
	set_unlock_method "$dev"
	if [ -n "$unlock_method" ]; then
		extra_args+=("$unlock_method")
	fi

	# If no recovery key is provided, systemd-cryptenroll will
	# generate one
	local key keyid keyid_int
	keyid="$(keyctl id %user:sdbootutil 2> /dev/null)" || true
	keyid_int="$(keyctl id %user:sdbootutil-pin 2> /dev/null)" || true
	if [ -n "$arg_ask_key_pin_or_pw" ]; then
		ask_new_password "recovery key" key
	elif [ -n "$KEY" ]; then
		key="$KEY"
	elif [ -n "$keyid_int" ]; then
		key="$(keyctl pipe "$keyid_int")"
	elif [ -n "$keyid" ]; then
		key="$(keyctl pipe "$keyid")"
	fi

	local generated_key=
	# This function will be called for every device.  Let systemd
	# generate a secure recovery key only the first time if there
	# is no recovery pin selected (%user:sdbootutil[-pin])
	if [ -z "$key" ]; then
		# systemd-cryptenroll will put in stdout the recovery
		# key, and the rest of the information in stderr
		key="$(systemd-cryptenroll --wipe-slot=recovery --recovery-key "${extra_args[@]}" "$dev" 2> /dev/null)"
		generated_key=1
	else
		# A recovery key has already been generated, use it
		# for all the devices.  systemd-cryptenroll always
		# generates a random recovery key, but we want $key.
		# Replace it by using cryptsetup, so we can still use
		# %u:cryptenroll in systemd-cryptenroll, and the
		# temporary recovery key in cryptsetup, to avoid
		# requesting a password
		local tmp_key
		tmp_key="$(systemd-cryptenroll --wipe-slot=recovery --recovery-key "${extra_args[@]}" "$dev" 2> /dev/null)"
		local split
		read -r -a split < <(systemd-cryptenroll "$dev" | grep recovery)
		local keyslot="${split[0]}"
		# cryptsetup can only read the new passphrase from a
		# keyfile
		local tmp_key_file
		tmp_key_file="$(mktemp -t key_file.XXXXXX)"
		echo -n "$key" > "$tmp_key_file"
		cryptsetup luksChangeKey --key-slot "$keyslot" --force-password "$dev" "$tmp_key_file" <<<"$tmp_key"
		shred "$tmp_key_file"
	fi

	# If we enroll a recovery key first, we can use the generated
	# key as a recovery PIN later when we enroll a TPM2[+PIN]
	# (note that the recovery PIN is not the same PIN for the
	# tpm2+pin method).
	#
	# If we enroll the recovery key after the TPM2 enrollment and
	# we send the recovery PIN via the keyring, then we can make
	# the recovery key the same as the recovery PIN.  But if the
	# PIN is missing from the keyring, then we missed the
	# synchronization and the key and the PIN are different.
	if [ -z "$keyid_int" ] && have_slot "$dev" "tpm2"; then
		warn "There is already a recovery PIN for the TPM2"
		warn "The recovery key and the recovery PIN are now different"
	fi

	if [ -n "$generated_key" ]; then
		echo "Recovery key: $key"
		if [ -x /usr/bin/qrencode ]; then
			echo "You can also scan it with your mobile phone:"
			qrencode -t utf8i "$key"
		fi
	fi

	# Make sure that the registered recovery key is in the kernel
	# keyring, so that it is available to systemd-cryptenroll
	keyid="$(keyctl id %user:cryptenroll 2> /dev/null)" || true
	if [ -z "$keyid" ] && [ -z "$unlock_method" ]; then
		keyctl_add_with_timeout "cryptenroll" "$key"
	fi
	# ... and to --method=tpm2[+pin] via a private keyring
	keyctl_add_with_timeout "sdbootutil-pin" "$key"
}

enroll_device()
{
	local dev="$1"
	local pin_or_pw="$2"

	case "$arg_method" in
		"tpm2"|"tpm2+pin")
			if ! is_pcr_oracle && have_pcrlock; then
				enroll_pcrlock "$dev" "$pin_or_pw" || {
					warn "Enrollment with systemd-pcrlock failed"
					warn "Re-trying with pcr-oracle"
					unenroll_pcrlock
					# This function generates
					# /etc/systemd/tpm2-pcr-public-key.pem
					# which is needed by
					# enroll_pcroracle
					generate_rsa_key
					enroll_pcroracle "$dev" "$pin_or_pw"
				}
			elif have_pcr_oracle; then
				enroll_pcroracle "$dev" "$pin_or_pw"
			else
				info "No TMP2 enrollment mechanism found"
			fi
			;;

		"fido2")
			enroll_fido2 "$dev"
			;;

		"password")
			enroll_password "$dev" "$pin_or_pw"
			;;

		"recovery-key")
			enroll_recovery_key "$dev"
			;;

		*)
			err "Unexpected parameter for --method=: $arg_method"
			;;
	esac
}

enroll()
{
	[ -e /etc/crypttab ] || { info "/etc/crypttab not found. No encrypted devices?"; return 0; }
	[ -e /usr/bin/systemd-cryptenroll ] || { info "systemd-cryptenroll not found"; return 0; }
	detect_tracked_devices || { info "No LUKS2 devices found"; return 0; }

	info "Enrolling devices ($arg_method): ${tracked_devices[*]}"

	have_pcrlock || have_pcr_oracle || err "Not systemd-pcrlock nor pcr-oracle found"

	# Prepare /etc/crypttab and update initrd if required
	case "$arg_method" in
		"tpm2"|"tpm2+pin")
			have_tpm2 || err "No TPM2 found found"
			! in_lockout || err "The TPM2 is in lockout. Use 'tpm2_dictionarylockout -c [ -p passwd ]' to continue"
			add_crypttab_option 'tpm2-device=auto'
			add_crypttab_option 'tpm2-measure-pcr=yes'
			;;

		"fido2")
			have_fido2 || err "No FIDO2 key found"
			add_crypttab_option 'fido2-device=auto'
			;;
	esac

	# For predicting PCR 15 we need to sign a file.  Create the
	# public and private key if missing.  This (as the
	# /etc/crypttab change) can require a new initrd
	if [ "$arg_method" = "tpm2" ] || [ "$arg_method" = "tpm2+pin" ]; then
		local private="/var/lib/sdbootutil/measure-pcr-private.pem"
		local public="/var/lib/sdbootutil/measure-pcr-public.pem"
		[ -f "$private" ] || openssl genrsa -out "$private" 4096
		# Writes "writing RSA key" in stderr and -noout is not
		# doing what I was expecting
		[ -f "$public" ] || openssl rsa -in "$private" -pubout -out "$public" 2> /dev/null
	fi

	# If the crypttab file changed (that is expected), we need to
	# generate a new initrd
	if [ "$arg_no_reuse_initrd" = "1" ]; then
		install_all_kernels "$root_snapshot"
		# Avoid the call of generate_tpm2_predictions at the
		# end of the script
		update_predictions=
	fi

	# If systemd-pcrlock is missing then we need to use
	# pcr-oracle.  For this we need to generate the RSA keys
	# early.  Also we need to set the PCRs used to assest the
	# system status
	if [ "$arg_method" = "tpm2" ] || [ "$arg_method" = "tpm2+pin" ]; then
		if [ -z "${FDE_SEAL_PCR_LIST}" ]; then
			if systemd-detect-virt -q; then
				info "Virtualized systemd detected ($(systemd-detect-virt)). Dropping PCR0 and PCR2"
				FDE_SEAL_PCR_LIST=""
			else
				FDE_SEAL_PCR_LIST="0,2,"
			fi
			if is_sdboot; then
				FDE_SEAL_PCR_LIST+="4,7,9"
			elif is_grub2_bls; then
				FDE_SEAL_PCR_LIST+="4,7,8,9"
			else
				err "Bootloader not detected"
			fi
		fi

		is_config_file || echo "FDE_SEAL_PCR_LIST=${FDE_SEAL_PCR_LIST}" >> /etc/default/fde-tools

		if { ! have_pcrlock || [ -n "$arg_signed_policy" ]; } && have_pcr_oracle; then
			# Once the RSA key is present, then
			# is_pcr_oracle is true
			generate_rsa_key
		fi

		# During the initial enrollment it is expected that
		# for systemd-pcrlock the recovery PIN will be
		# extracted from the %u keyring "sdbootutil[-pin]"
		# entry
		generate_tpm2_predictions || {
			# If the generation of prediction fails and we
			# are trying with systemd-pcrlock, we try
			# again with pcr-oracle
			if ! is_pcr_oracle; then
				echo "Predictions with systemd-pcrlock failed"
				echo "Re-trying with pcr-oracle"
				unenroll_pcrlock
				# This function generates
				# /etc/systemd/tpm2-pcr-public-key.pem
				# which is needed by
				# generate_tpm2_predictions
				generate_rsa_key
				generate_tpm2_predictions
			fi
		}
	fi

	# For the PIN (tpm2+pin) or password (password), we can get it
	# from the %u keyring "sdbootutil" entry, the PIN or PW
	# environment variable, or introduced by the user
	local pin_or_pw keyid
	if [ "$arg_method" = "tpm2+pin" ]; then
		keyid="$(keyctl id %user:sdbootutil 2> /dev/null)" || true
		if [ -n "$arg_ask_key_pin_or_pw" ]; then
			ask_new_password "TPM2 PIN" pin_or_pw
		elif [ -n "$PIN" ]; then
			pin_or_pw="$PIN"
		elif [ -n "$keyid" ]; then
			pin_or_pw="$(keyctl pipe "$keyid")"
		else
			err "Use %u:sdbootutil, PIN or --ask-pin to provide the TPM2 PIN"
		fi
	elif [ "$arg_method" = "password" ]; then
		keyid="$(keyctl id %user:sdbootutil 2> /dev/null)" || true
		if [ -n "$arg_ask_key_pin_or_pw" ]; then
			ask_new_password "password" pin_or_pw
		elif [ -n "$PW" ]; then
			pin_or_pw="$PW"
		elif [ -n "$keyid" ]; then
			pin_or_pw="$(keyctl pipe "$keyid")"
		else
			err "Use %u:sdbootutil, PW or --ask-pw to provide the password"
		fi
	fi

	for dev in "${tracked_devices[@]}"; do
		enroll_device "$dev" "$pin_or_pw"
	done
}

unenroll_pcrlock()
{
	pcrlock remove-policy &> /dev/null || true
	rm -fr /var/lib/pcrlock.d
	rm -f /var/lib/systemd/pcrlock.json
	rm -f "${boot_root}${boot_dst}/pcrlock.json"
	rm -f /var/lib/sdbootutil/measure-pcr-prediction
	rm -f /var/lib/sdbootutil/measure-pcr-prediction.sha256
	rm -f "${boot_root}${boot_dst}/measure-pcr-prediction"
	rm -f "${boot_root}${boot_dst}/measure-pcr-prediction.sha256"
}

unenroll_pcr_oracle()
{
	rm -f /etc/systemd/tpm2-pcr-private-key.pem
	rm -f /etc/systemd/tpm2-pcr-public-key.pem
	rm -f /etc/systemd/tpm2-pcr-signature.json
	rm -f "${boot_root}${boot_dst}/tpm2-pcr-public-key.pem"
	rm -f "${boot_root}${boot_dst}/tpm2-pcr-signature.json"
	rm -f /var/lib/sdbootutil/measure-pcr-prediction
	rm -f /var/lib/sdbootutil/measure-pcr-prediction.sha256
	rm -f "${boot_root}${boot_dst}/measure-pcr-prediction"
	rm -f "${boot_root}${boot_dst}/measure-pcr-prediction.sha256"
}

unenroll_device()
{
	local dev="$1"

	case "$arg_method" in
		"tpm2"|"tpm2+pin")
			systemd-cryptenroll \
				--wipe-slot=tpm2 \
				"$dev"
			;;

		"fido2")
			systemd-cryptenroll \
				--wipe-slot=fido2 \
				"$dev"
			;;

		"password")
			systemd-cryptenroll \
				--wipe-slot=password \
				"$dev"
			;;

		"recovery-key")
			systemd-cryptenroll \
				--wipe-slot=recovery \
				"$dev"
			;;

		*)
			err "Unexpected parameter for --method=: $arg_method"
			;;
	esac
}

unenroll()
{
	[ -e /etc/crypttab ] || { info "/etc/crypttab not found. No encrypted devices?"; return 0; }
	[ -e /usr/bin/systemd-cryptenroll ] || { info "systemd-cryptenroll not found"; return 0; }
	detect_tracked_devices || { info "No LUKS2 devices found"; return 0; }

	info "Unenrolling devices ($arg_method): ${tracked_devices[*]}"

	# Prepare /etc/crypttab and update initrd if required
	case "$arg_method" in
		"tpm2"|"tpm2+pin")
			have_tpm2 || err "No TPM2 found found"
			remove_crypttab_option 'tpm2-device=auto'
			remove_crypttab_option 'tpm2-measure-pcr=yes'
			;;

		"fido2")
			have_fido2 || err "No FIDO2 key found"
			remove_crypttab_option 'fido2-device=auto'
			;;
	esac
	if [ "$arg_no_reuse_initrd" = "1" ]; then
		install_all_kernels "$root_snapshot"
		# Avoid the call of generate_tpm2_predictions at the
		# end of the script
		update_predictions=
	fi

	if [ "$arg_method" = "tpm2" ] || [ "$arg_method" = "tpm2+pin" ]; then
		unenroll_pcrlock
		unenroll_pcr_oracle
	fi

	for dev in "${tracked_devices[@]}"; do
		unenroll_device "$dev"
	done
}

eval_bootctl()
{
	# XXX: bootctl should have json output for that too
	# shellcheck disable=SC2016
	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')"
}

bootloader_name()
{
	info "Checking the bootloader name"

	if is_sdboot "${1:-$root_snapshot}"; then
		echo "systemd-boot"
	elif is_grub2_bls "${1:-$root_snapshot}"; then
		echo "grub2-bls"
	else
		err "Bootloader not detected"
	fi
}

set_image_name() {
	[ -z "$image" ] || return

	declare -gA arch_image_map=(
		[x64]="vmlinuz"
		[aa64]="Image"
	)

	if [ -n "${arch_image_map[$firmware_arch]}" ]; then
		image="${arch_image_map[$firmware_arch]}"
	else
		err "Unsupported architecture $firmware_arch"
	fi
}

define_commands() {
	declare -gA commands=(
		[install]=""
		[needs-update]=""
		[update]=""
		[force-update]=""
		[add-kernel]="kernel"
		[remove-kernel]="kernel"
		[cleanup]=""
		[set-default-snapshot]=""
		[add-all-kernels]=""
		[mkinitrd]=""
		[remove-all-kernels]=""
		[is-installed]=""
		[list-snapshots]=""
		[list-entries]=""
		[list-kernels]=""
		[list-devices]=""
		[show-entry]="kernel"
		[update-entry]="kernel"
		[update-all-entries]=""
		[is-bootable]=""
		[set-default]="id"
		[get-default]=""
		[set-timeout]="seconds"
		[get-timeout]=""
		[enroll]=""
		[unenroll]=""
		[update-predictions]=""
		[bootloader]=""
	)
}

define_options() {
	declare -gA options_with_arg=(
		[help]=""
		[verbose]=""
		[esp-path]="_path"
		[entry-token]="_path"
		[arch]="_arch_name"
		[image]="_image_name"
		[entry-keys]="_find_kernels"
		[no-variables]=""
		[no-reuse-initrd]=""
		[no-random-seed]=""
		[all]=""
		[sync]=""
		[portable]=""
		[removable]=""
		[only-default]=""
		[default-snapshot]=""
		[ask-key]=""
		[ask-pin]=""
		[ask-pw]=""
		[method]="_method"
		[signed-policy]=""
		[measure-pcr]=""
		[pcr]="_none"
		[devices]="_devices"
	)
	opts_long=""
	for opt in "${!options_with_arg[@]}"; do
		if [ "${options_with_arg[$opt]}" ]; then
			opts_long+="${opt}:,"
		else
			opts_long+="${opt},"
		fi
	done
	opts_long="${opts_long%,}"
}

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

if [ "$1" = "_print_bash_completion_data" ]; then
	declare -f set_image_name
	declare -f eval_bootctl
	declare -f define_commands
	declare -f define_options
	exit 0
fi

define_options
getopt_tmp=$(getopt -o hv --long "$opts_long" -n "${0##*/}" -- "$@")
eval set -- "$getopt_tmp"

while true ; do
	case "$1" in
		-h|--help) helpandquit ;;
		-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 ;;
		--entry-keys) IFS=',' read -r -a arg_entry_keys <<<"$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 ;;
		--sync) arg_sync=1; shift ;;
		--portable) arg_portable=1; shift ;;
		--removable) arg_portable=1; shift ;;
		--only-default) arg_only_default=1; shift ;;
		--default-snapshot) arg_default_snapshot=1; shift ;;
		--ask-key|--ask-pin|--ask-pw) arg_ask_key_pin_or_pw=1; shift ;;
		--method) arg_method="$2"; shift 2 ;;
		--signed-policy) arg_signed_policy=1; shift ;;
		--measure-pcr) arg_measure_pcr=1; shift ;;
		--pcr) FDE_SEAL_PCR_LIST="$2"; shift 2 ;;
		--devices) IFS=',' read -r -a tracked_devices <<<"$2"; shift 2 ;;
		--) shift ; break ;;
		*) echo "Internal error!" ; exit 1 ;;
	esac
done

if [ -z "$SYSTEMD_LOG_LEVEL" ] && [ "${verbose:-0}" -gt 1 ]; then
	if [ "$verbose" -gt 2 ]; then
		SYSTEMD_LOG_LEVEL=debug
	else
		SYSTEMD_LOG_LEVEL=info
	fi
	export SYSTEMD_LOG_LEVEL
fi

define_commands
if [ -z "$1" ]; then
	helpandquit
elif [ -z ${commands["$1"]+yes} ]; then
	err "unknown command $1"
fi

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

eval_bootctl
read -r root_uuid root_device < <(findmnt / -v -r -n -o UUID,SOURCE)
root_device_is_crypt=
[ "$(lsblk --noheadings -o TYPE "$root_device")" = "crypt" ] && root_device_is_crypt=1
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
	if [ -n "$arg_default_snapshot" ]; then
		[ -s "$snapperfile" ] || update_snapper
		read -r root_snapshot <<<"$(jq -r '.root[]|select(.default==true)|.number' < "$snapperfile")"
	else
		root_snapshot="${root_subvol#"${subvol_prefix}"/.snapshots/}"
		root_snapshot="${root_snapshot%/snapshot}"
	fi
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"
set_image_name

mountpoint -q "$boot_root" || err "$boot_root is not a valid mountpoint"

dbg_var "root_snapshot"
dbg_var "boot_root"

# shellcheck disable=SC1091
[ -e /etc/sysconfig/bootloader ] && . /etc/sysconfig/bootloader

# XXX: Unify both in /EFI/opensuse?
if [ -n "$arg_portable" ]; then
	if [ ! -d "$boot_root/EFI/systemd" ] && [ ! -d "$boot_root/EFI/opensuse" ]; then
		boot_dst="/EFI/BOOT"
	else
		err "Bootloader is already installed permanently"
	fi
elif is_sdboot; then
	boot_dst="/EFI/systemd"
elif is_grub2_bls; then
	set_os_release "${root_snapshot}"
	# shellcheck disable=SC2154
	read -r -a name <<<"${os_release_NAME,,}"
	boot_dst="/EFI/${name[0]}"
else
	msg="Bootloader not detected"
	[ -z "$LOADER_TYPE" ] || msg+=". /etc/sysconfig/bootloader has LOADER_TYPE=\"$LOADER_TYPE\", but only \"systemd-boot\" or \"grub2-bls\" are recognized."
	err "$msg"
fi

dbg_var "boot_dst"

# Keep initial components before they are replaced by some actions
# (new initrd, new entry, etc)
if [ "$SDB_ADD_INITIAL_COMPONENT" = "1" ]; then
	backup_initial_components
fi

case "$1" in
	install)
		install_bootloader "${2:-$root_snapshot}" ;;
	needs-update)
		bootloader_needs_update "${2:-$root_snapshot}" ;;
	update)
		bootloader_update "${2:-$root_snapshot}" ;;
	force-update)
		if is_installed; then
			install_bootloader "${2:-$root_snapshot}"
		else
			:
		fi ;;
	bootloader)
		bootloader_name "${2:-$root_snapshot}" ;;
	add-kernel)
		install_kernel "${3:-$root_snapshot}" "$2" ;;
	add-all-kernels)
		install_all_kernels "${2:-$root_snapshot}" ;;
	mkinitrd)
		arg_no_reuse_initrd=1
		install_all_kernels "${2:-$root_snapshot}" ;;
	remove-kernel)
		remove_kernel "${3:-$root_snapshot}" "$2" ;;
	remove-all-kernels)
		remove_all_kernels "${2:-$root_snapshot}" ;;
	cleanup)
		cleanup_entries "${2:-}" ;;
	set-default-snapshot)
		set_default_snapshot "${2:-$root_snapshot}" ;;
	is-installed)
		if is_installed; then
			info "systemd-boot was installed using sdbootutil"
			exit 0
		else
			info "not installed using this tool"
			exit 1
		fi ;;
	list-kernels)
		list_kernels "${2:-$root_snapshot}" ;;
	list-entries)
		list_entries "${2:-}" ;;
	list-snapshots)
		list_snapshots ;;
	list-devices)
		list_devices ;;
	show-entry)
		show_entry_fields "${3:-$root_snapshot}" "$2" ;;
	update-entry)
		update_entry "${3:-$root_snapshot}" "$2" ;;
	update-all-entries)
		update_all_entries "${2:-$root_snapshot}" ;;
	is-bootable)
		is_bootable "${2:-$root_snapshot}" ;;
	set-default)
		set_default_entry "$2" ;;
	get-default)
		get_default_entry "$2" ;;
	set-timeout)
		set_timeout "$2" ;;
	get-timeout)
		get_timeout "$2" ;;
	enroll)
		enroll ;;
	unenroll)
		unenroll ;;
	update-predictions)
		update_predictions=1 ;;
	*)
		helpandquit ;;
esac

[ -z "$update_predictions" ] || generate_tpm2_predictions
