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

export LC_ALL=C

DEBUG_LOG="/var/log/sdbootutil.log"
verbose=

if [[ "$*" =~ "--start-trace-code" ]] && ! touch "$DEBUG_LOG" 2>/dev/null; then
	echo "Cannot enable the code trace, $DEBUG_LOG is not writable" >&2
	exit 1
fi
if [[ "$*" =~ "--stop-trace-code" ]] && ! rm -f "$DEBUG_LOG" 2>/dev/null; then
	echo "Cannot disable the code trace, $DEBUG_LOG cannot be removed" >&2
	exit 1
fi
# The trace is only enabled if the log can be opened for writing, so a
# trace left behind by root does not break the tool for the other users.
# The completion data is also excluded, as the messages and the trace
# itself interfere with the shell completion
if [ "$1" != "_print_bash_completion_data" ] && [ -f "$DEBUG_LOG" ] && { exec 3>>"$DEBUG_LOG"; } 2>/dev/null; then
	verbose=3
	echo "The trace of the code is being stored in $DEBUG_LOG" >&2
	echo "Remove the file or use --stop-trace-code to stop tracing the code" >&2
	echo "Audit the file before sharing, as secrets can be leaked" >&2
	export BASH_XTRACEFD=3
	export PS4='+ \D{%F %T} ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}: '
	set -x
fi

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_secure_boot=
arg_sync=
arg_only_default=
arg_default_snapshot=
arg_ask_key_pin_or_pw=
arg_method=
arg_no_measure_pcr=
arg_measure_pcr=
arg_pcr=
arg_rootfs=
arg_rootfs_data=
arg_esp_free_space=
arg_devicetree_source=
arg_force=
arg_disable_predictions=
have_snapshots=
in_buildroot=
# Snapshot that owns the "/" subvolume, that is not always the one
# that provides /usr (see `detect_applied_snapshot`)
running_snapshot=
# Snapshot mapped over the running system by `transactional-update
# apply` (see `detect_applied_snapshot`)
applied_snapshot=
# Possible values: vmlinuz image vmlinux linux bzImage uImage Image zImage
image=
unlock_method=

chroot_dir=

# The output goes to a terminal, so it is read by a person and can
# carry colors and extra decorations.  When it is captured by another
# program only the plain data is printed: `transactional-update` reads
# `list-entries` with `popen` and opens the result as a file name, and
# `weak-modules2` parses `show-entry` with a command substitution
interactive=
[ ! -t 1 ] || interactive=1

color_red=
color_end=
# SYSTEMD_COLORS can only disable the colors, as documented in the help
# message.  Honouring a forced "true" would leak escape sequences into
# the programs above, that inherit the variable from the environment of
# the caller
if [ -n "$interactive" ] \
    && [ "$SYSTEMD_COLORS" != "false" ] && [ "$SYSTEMD_COLORS" != "0" ]; 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=
# Set only when "update-predictions" is the command, to tell it apart
# from the predictions that are a side effect of another command
explicit_predictions=

tracked_devices=()

rollback=()

declare -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"
devicefile="$tmpdir/devices.json"

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)
		  --secure-boot		When installing the bootloader, use the shim
		  --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 or ask TPM2 PIN
		                        when initial enrollment
		  --ask-pw		Ask password when initial enrollment
		  --method		"tpm2", "tpm2+pin", "fido2", "password", "recovery-key"
		  --no-measure-pcr	During enrollment, do not include PCR 15 protection
		  --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)
		  --rootfs		During bootloader installation, select the kind of 'root'
		                        valid selectors
		                        (uuid, partuuid, label, partlabel, device)
		  --rootfs-data		Extra data when --rootfs is used
		  --esp-free-space	Percentage of free space in the ESP
		  --devicetree-source	Absolute source path of a device tree blob to copy
		                        into the boot partition and reference from generated
		                        BLS entries. The source path is resolved in the
		                        target rootfs/snapshot. %K expands to the full
		                        kernel version and %V to the kernel package version
		                        without the flavor suffix. If configured, the
		                        resolved file must exist and be readable, and
		                        must be usable without Secure Boot, or kernel
		                        installation is aborted.
		  --force		Force certain operations inside a transaction, and
		                        force the regeneration of the TPM2 policy
		  --disable-predictions	Disable automatic update predictions
		  -v, --verbose		More verbose output
		  --start-trace-code	Create /var/log/sdbootutil.log to trace the code.
		                        (verbose output is enabled)
		  --stop-trace-code	Remove /var/log/sdbootutil.log to stop tracing the code
		  -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.  On a
		           terminal the entry is marked with "+" when it
		           is the default one, "-" when the system booted
		           from it, and "*" when it is both

		list-snapshots
		           List all snapshots.  On a terminal the snapshot
		           is marked with "+" when it is the default one,
		           "-" when the system is running from it, and "*"
		           when it is both

		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

		print-loader-path
		           Print path to currently booted boot loader binary

		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		Set / use recovery key (initial enrollment; %u:sdbootutil)
		PW		Set / use password / recovery key (initial enrollment; %u:sdbootutil)
		PIN		Set / use TPM2 PIN (initial enrollment; %u:sdbootutil)
		PIN		Use recovery PIN (re-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
		Trace Code		Create /var/log/sdbootutil.log to debug deep
		                        sdbootutil calls.  Remove the file to stop the
		                        trace, and audit the content before sharing
		Applied Snapshots	After a "transactional-update apply" the running
		                        system is a mix of two snapshots.  [SNAPSHOT] has
		                        no meaningful default then, so commands that
		                        change the ESP require an explicit snapshot
		                        number or --default-snapshot


	EOF
	exit 0
}

dbg()
{
	[ "${verbose:-0}" -gt 1 ] || return 0
	echo -e "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 -e "$@" >&2
}

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

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

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

set_default_config_values()
{
	FDE_SEAL_PCR_LIST="$arg_pcr"
	if is_installed && [ -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

	if is_rootfs_crypt; then
		# `measure-pcr-validator` (PRC15) protect from
		# impostor LUKS2 volumes, as it measures the volume
		# key.  But if there is a volume in a non-encrypted
		# device (USB, for example) that has the same metadata
		# (referenced by /etc/fstab) as the internal volume in
		# the encrypted root device, then there is a chance
		# that this will get mounted in /sysroot.  systemd can
		# measure elements from the file system (like UUID or
		# /etc/machine-id) via systemd-validatefs@.service,
		# but this data is public and can be replicated
		#
		# See bsc#1229934 for more details
		ROOTFS="device"
	else
		ROOTFS="uuid"
	fi
	ROOTFS="${arg_rootfs:-$ROOTFS}"

	ESP_FREE_SPACE="${arg_esp_free_space:-10}"
	DEVICETREE_SOURCE="${arg_devicetree_source:-}"
}

create_default_config_file()
{
	! is_config_file || return 0

	set_default_config_values
	cat <<-EOF > /etc/default/sdbootutil
	# Default sdbootutil configuration file generated automatically

	# List of PCRs.  For VMs PCRs 0 and 2 are dropped
	FDE_SEAL_PCR_LIST="$FDE_SEAL_PCR_LIST"

	# Parameter used in the cmdline to find the root (device, [part]uuid, [part]label)
	# For encrypted devices it is forced "device"
	ROOTFS="$ROOTFS"

	# Percentage (%) of free space in the ESP that sdbootutil should guarantee
	# Default values is 10%
	ESP_FREE_SPACE="$ESP_FREE_SPACE"

	# Optional absolute path to a device tree blob. If set, it is copied to the
	# boot partition and a devicetree field is added to generated BLS entries.
	# The source path is resolved in the target rootfs/snapshot. %K expands to
	# the full kernel version, %V to the kernel package version without the
	# flavor suffix. If set, the resolved file must exist and be readable,
	# otherwise kernel installation aborts. Currently supported only with
	# Secure Boot disabled.
	DEVICETREE_SOURCE="$DEVICETREE_SOURCE"
	EOF
}

load_config_file()
{
	local f

	set_default_config_values

	# Prioritize sdbootutil configuration file, and use fde-tools
	# as a fallback
	for f in /usr/etc/default/sdbootutil \
			 /etc/default/sdbootutil \
			 /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

	# Force back "device" for ROOTFS if is encrypted
	if is_rootfs_crypt; then
		[ "$ROOTFS" = "device" ] || info "Fix the /etc/default/sdbootutil to set ROOTFS as 'device'"
		ROOTFS="device"
	fi

	# If arguments are passed, use them
	FDE_SEAL_PCR_LIST="${arg_pcr:-$FDE_SEAL_PCR_LIST}"
	ROOTFS="${arg_rootfs:-$ROOTFS}"
	ESP_FREE_SPACE="${arg_esp_free_space:-$ESP_FREE_SPACE}"
	DEVICETREE_SOURCE="${arg_devicetree_source:-$DEVICETREE_SOURCE}"
}

is_secure_boot()
{
	grep -q $'\x01' /sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c 2> /dev/null
}

bootloader_type()
{
	# If LOADER_TYPE is set, use that
	if [ -n "$LOADER_TYPE" ]; then
		echo "$LOADER_TYPE"
		return
	fi

	local sdboot grub2_bls

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

	# If LOADER_TYPE is unset and only systemd-boot is installed, use that
	if [ -e "$sdboot" ] && ! [ -e "$grub2_bls" ]; then
		echo "systemd-boot"
		return
	fi

	# Otherwise, use grub2-bls
	echo "grub2-bls"
}

is_sdboot()
{
	[ "$(bootloader_type "${1-$root_snapshot}")" = "systemd-boot" ]
}

is_grub2_bls()
{
	[ "$(bootloader_type "${1-$root_snapshot}")" = "grub2-bls" ]
}

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 -p` prints the prompt only when the input is a
	# terminal, so without this check a non-interactive caller
	# gets an empty password and no indication of why
	[ -t 0 ] || err "\"$msg\" is required, but there is no terminal to ask for it"
	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 1
	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
	local btrfs_subvol_status=0
	# shellcheck disable=SC2207
	parent_subvol_uuid=($(btrfs subvol show -u "$parent_uuid" "${subvol#"${subvol_prefix}"}" 2> /dev/null)) || 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"
}

snapshot_from_fsroot()
{
	local fsroot="${1#/}"
	local snapshot="${fsroot#"${subvol_prefix}"/.snapshots/}"

	[ "$snapshot" != "$fsroot" ] || return 0
	echo "${snapshot%%/*}"
}

detect_applied_snapshot()
{
	local fsroot
	local usr_snapshot=""

	[ -n "$have_snapshots" ] || return 0

	# With more than one `apply` the mounts stack, and the last
	# line is the visible one
	while read -r fsroot; do
		usr_snapshot="$(snapshot_from_fsroot "$fsroot")"
	done < <(findmnt --noheadings --output FSROOT /usr 2> /dev/null)

	[ -n "$usr_snapshot" ] || return 0
	[ "$usr_snapshot" != "$running_snapshot" ] || return 0
	echo "$usr_snapshot"
}

assert_snapshot()
{
	[ -n "$applied_snapshot" ] || return 0
	[ -z "$arg_default_snapshot" ] || return 0
	[ -z "$1" ] || return 0

	err "The running system is a mix of snapshot $running_snapshot (mounted in /) and" \
	    "snapshot $applied_snapshot (mounted in /usr), as left by 'transactional-update apply'." \
	    "\nRe-run the command with an explicit snapshot number, or with --default-snapshot" \
	    "to select the snapshot that will be booted next."
}

is_rootfs_crypt()
{
	local device
	read -r device < <(findmnt / -v -n -o SOURCE 2> /dev/null)
	[ -n "$device" ] && [ "$(lsblk --noheadings -o TYPE "$device" 2> /dev/null)" = "crypt" ]
}

get_rootfs()
{
	local rootfs_data

	case "$ROOTFS" in
		uuid) read -r rootfs_data < <(findmnt / -v -n -o UUID 2> /dev/null) ;;
		label) read -r rootfs_data < <(findmnt / -v -n -o LABEL 2> /dev/null) ;;
		partuuid) read -r rootfs_data < <(findmnt / -v -n -o PARTUUID 2> /dev/null) ;;
		partlabel) read -r rootfs_data < <(findmnt / -v -n -o PARTLABEL 2> /dev/null) ;;
		device) read -r rootfs_data < <(findmnt / -v -n -o SOURCE 2> /dev/null) ;;
		*)
			info "Can't determine rootfs ($ROOTFS). Using UUID as default"
			ROOTFS="uuid"
			read -r rootfs_data < <(findmnt / -v -n -o UUID 2> /dev/null)
			;;
	esac
	rootfs_data="${arg_rootfs_data:-$rootfs_data}"

	if [ "$ROOTFS" = "device" ]; then
		echo "$rootfs_data"
	else
		echo "${ROOTFS^^}=$rootfs_data"
	fi
}

get_all_rootfs()
{
	local rootfs rootfs_data

	read -r rootfs_data < <(findmnt / -v -n -o SOURCE)
	rootfs="$rootfs_data"

	read -r rootfs_data < <(findmnt / -v -n -o UUID)
	[ -z "$rootfs_data" ] || rootfs="$rootfs|UUID=$rootfs_data"

	read -r rootfs_data < <(findmnt / -v -n -o LABEL)
	[ -z "$rootfs_data" ] || rootfs="$rootfs|LABEL=$rootfs_data"

	read -r rootfs_data < <(findmnt / -v -n -o PARTUUID)
	[ -z "$rootfs_data" ] || rootfs="$rootfs|PARTUUID=$rootfs_data"

	read -r rootfs_data < <(findmnt / -v -n -o PARTLABEL)
	[ -z "$rootfs_data" ] || rootfs="$rootfs|PARTLABEL=$rootfs_data"

	echo "$rootfs"
}

sedrootflags()
{
	local subvol="$1"
	local root_param
	root_param="$(get_rootfs)"

	# - 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 [PART]{UUID, LABEL}, or
	#   mapped device
	# - Replace or add rootflags to point at correct subvolume
	# - Delete systemd.machine_id if /etc/machine-id is present
	#
	# From the sed manual:
	# ‘t’
	#     branch conditionally (that is: jump to a label) _only
	#     if_ a ‘s///’ command has succeeded since the last input
	#     line was read or another conditional branch was taken.
	#
	# We use the t command to jump over an expression that appends
	# a parameter if replacing the parameter succeeded (ie it was
	# already there). Since we always operate on the same line,
	# "empty" t jumps are used to reset the condition after very
	# s///.
	local sed_arguments=("-e s/[ \t]\+/ /g"
		"-e s/^.*\(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 "$subvol" ] || sed_arguments+=("-e s,\<rootflags=subvol=[^ ]*,rootflags=subvol=$subvol,;td;s,\$, rootflags=subvol=$subvol,;te;:e;:d")
	[ -n "$machine_id" ] || sed_arguments+=("-e s/\<systemd.machine_id=[^ ]* \?//")
	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:-}"

	local root
	root="$(get_all_rootfs)"

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

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

update_entries_for_this_system()
{
	local root
	root="$(get_all_rootfs)"

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

update_entries_for_extra()
{
	update_entries jq "[.[]|select((.linux//.efi//\"\")|startswith(\"/EFI/extra\"))]"
}

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

	if [ -n "$snapshot" ]; then
		update_entries_for_snapshot "$snapshot"
	else
		update_entries_for_this_system
	fi

	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
}

# There is no type#1 entry in the ESP.  Note that "nullglob" makes no
# difference between an empty and a missing directory, which is what
# the caller expects: both mean that no kernel was installed yet
no_boot_entries()
{
	local confs=("${boot_root}/loader/entries/"*.conf)

	[ "${#confs[@]}" -eq 0 ]
}

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"

	[ -z "$TRANSACTIONAL_UPDATE" ] || [ -n "$arg_force" ] || {
		warn "Inside transactional-update. Removing kernels must be done outside the transaction"
		return 0
	}

	settle_entry_token "${snapshot}"
	local id
	id="$(entry_conf_file "$kernel_version" "$snapshot")"
	info "Removing boot entry $id"
	bootctl unlink "$id"

	# If we remove the default entry, `bootctl` will mark a new
	# default, but we still need to update the EFI var (or the
	# grubenv value)
	local default
	default="$(get_default_bootctl)"
	if [ -z "$default" ]; then
		warn "Can't determine the new default entry"
	else
		set_default_entry "$default"
	fi

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

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

	# Install `src` as `dst` if the file is new or different.  If
	# the file is present with the `old` name, if will be kept if
	# the file did not change, else it will be installed with the
	# new name (`dst`)
	if [ -e "$old" ]; then
		if cmp -s "$src" "$old"; then
			info "$old unchanged"
			return 0
		fi
		mv "$old" "$old.bak" || return "$?"
	fi
	rollback+=("$old")
	install -p -m 0644 "$src" "$dst" || return "$?"
	chown root:root "$dst" 2> /dev/null || true
	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 "$have_snapshots" ] || 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 "$have_snapshots" ] || 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"
			[ "$machine_id" != "uninitialized" ] || machine_id=
			[ -z "$machine_id" ] || 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"

	local find_conf_status=0
	conf="$(find_conf_file "$kernel_version" "$snapshot")" || 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"
			find_conf_status=0
			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,FSROOT -Rv --pairs / > "$tmpdir/mounts"
	mount --bind "$snapshot_dir" "$snapshot_dir"
	# Register the chroot before mounting anything else, so a
	# failure in the middle of the loop is unwound by `cleanup`
	chroot_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" = "/etc" ] && [ "$FSTYPE" = "btrfs" ] && continue
		[[ "$TARGET" != /.snapshots* ]] || continue
		[[ "$TARGET" != /run/media/* ]] || continue
		# After a `transactional-update apply` the running
		# system has /usr and /boot bind mounted from the new
		# default snapshot.  Those belong to a different
		# snapshot and must not shadow the ones that
		# "$snapshot_dir" provides
		# shellcheck disable=SC2153
		[ -z "$(snapshot_from_fsroot "$FSROOT")" ] || continue
		# Not every mount point of the running system is present
		# in the snapshot.  For example the directory that
		# `transactional-update` mounts under /tmp while a
		# transaction is open, or any external media.  They are
		# not needed to generate the initrd, and the snapshot can
		# be read-only, so the directory cannot be created
		if [ ! -d "$snapshot_dir$TARGET" ]; then
			dbg "Skipping $TARGET, not present in $snapshot_dir"
			continue
		fi
		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"
	if [ -e /run/systemd/journal ]; then
		mkdir -p "$snapshot_dir/run/systemd/journal"
		mount --bind /run/systemd/journal "$snapshot_dir/run/systemd/journal"
	fi
	for i in proc dev sys tmp; do
		mount --bind "/$i" "$snapshot_dir/$i"
	done
}

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

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

pending_devicetree_size()
{
	local devicetree="$1"
	[ -n "$devicetree" ] || {
		echo 0
		return 0
	}
	echo $(($(stat -L -c %s "$devicetree") / 1024 + 1))
}

boot_space()
{
	local size
	size="$(findmnt --noheadings --bytes -o SIZE --target "${boot_root}" | head -n 1)"
	# Called from a command substitution, so `err` only ends the
	# sub-shell and the caller can detect the failure via the exit
	# status
	[[ "$size" =~ ^[0-9]+$ ]] || err "Can't determine the size of ${boot_root}"
	echo $((size / 1024))
}

boot_free_space()
{
	local avail
	avail="$(findmnt --noheadings --bytes -o AVAIL --target "${boot_root}" | head -n 1)"
	[[ "$avail" =~ ^[0-9]+$ ]] || err "Can't determine the free space in ${boot_root}"
	echo $((avail / 1024))
}

# The snapshots whose boot entries must be preserved: the default and
# the active ones, plus the snapshot from where the kernel is being
# installed, if any.  Printed one number per line
snapshots_in_use()
{
	local snapshot="$1"

	[ -n "$have_snapshots" ] || return 0

	{
		[ -z "$snapshot" ] || echo "$snapshot"
		[ -z "$root_snapshot" ] || echo "$root_snapshot"
		[ -s "$snapperfile" ] || update_snapper
		jq -r '.root[]|select(.active==true or .default==true)|.number' "$snapperfile"
	} | sort -un
}

# Print the ids of the boot entries that can be removed to make room in
# the ESP, in the order in which they should be removed.
#
# `bootctl unlink` garbage collects the kernel and the initrd only when
# the last entry that references them is removed, so entries are peeled
# off one snapshot at a time and the free space is re-evaluated after
# each removal.  Removing more than needed would leave older snapshots
# without a bootable entry.
#
# The order is:
#
#   1. entries of the snapshots that are not in use, oldest snapshot
#      first and, inside a snapshot, oldest kernel first
#   2. entries of "root_snapshot" (the default snapshot, or the running
#      one), as a last resort
#
# Never returned, and so never removed:
#
#   - the default and the selected entries
#   - the entries of the snapshots in use, except the ones of
#     "root_snapshot", that are last in the list
#
# This covers both layouts.  In MicroOS the default snapshot is the
# newest one and in Tumbleweed it is the oldest one, but in both cases
# the snapshots that are not in use are leftovers that can be removed
# from the oldest to the newest.  Snapshots created after a rollback
# fall in the same group and are removed the same way.
#
# The snapshot and the kernel version are read from "options" and
# "linux", which sdbootutil writes itself, and only then from "version"
# and "id".  An entry where none of them can be parsed (a kernel from a
# third party repository, an entry written by another tool, a system
# without snapshots) is still a candidate, but is sorted last inside
# its group: it can be removed, yet only after the entries that can be
# ordered with confidence
removable_entries()
{
	local snapshot="$1"

	local in_use
	mapfile -t in_use < <(snapshots_in_use "$snapshot")
	dbg "Snapshots that keep their boot entries: ${in_use[*]:-none}"

	update_entries_for_this_system

	# Six sort columns are emitted, and the last one is the id:
	#
	#   1. 0 for a snapshot that is not in use, 1 for "root_snapshot"
	#   2. 0 if the snapshot number is known, 1 if it is not
	#   3. the snapshot number
	#   4. 0 if the kernel version is known, 1 if it is not
	#   5. the kernel version
	#   6. the id, to make the order stable
	#
	# The columns are never empty, as `read` and `sort` would
	# collapse consecutive tabs and shift the fields
	jq -r --arg in_use "${in_use[*]}" --arg last "$root_snapshot" '
		def snapshot_of:
			((.options // "") | capture("rootflags=subvol=[^ ]*/\\.snapshots/(?<n>[0-9]+)/snapshot") | .n)
			// ((.version // "") | capture("^(?<n>[0-9]+)@") | .n)
			// ((.id // "") | capture("-(?<n>[0-9]+)(\\+[0-9]+(-[0-9]+)?)?\\.conf$") | .n)
			// "";
		def kernel_of:
			((.linux // "") | capture("^/[^/]+/(?<k>[^/]+)/[^/]+$") | .k)
			// ((.version // "") | capture("@(?<k>.+)$") | .k)
			// "";

		($in_use | split(" ") | map(select(. != ""))) as $in_use
		| .[]
		| select(.isDefault != true and .isSelected != true)
		| snapshot_of as $s
		| kernel_of as $k
		| select(($in_use | index($s)) == null or $s == $last)
		| [ (if $s == $last then 1 else 0 end),
		    (if $s == "" then 1 else 0 end),
		    (if $s == "" then 0 else ($s | tonumber) end),
		    (if $k == "" then 1 else 0 end),
		    (if $k == "" then "-" else $k end),
		    .id ]
		| @tsv
	' < "$entryfile" |
		sort -t$'\t' -k1,1n -k2,2n -k3,3n -k4,4n -k5,5V -k6,6 |
		cut -f6
}

# Try to make room in the ESP for "required_size" KB.  Note that the
# caller collects the exit status (`make_free_space ... || err ...`),
# so `set -e` is not active here and every failure must be reported,
# or it will be presented as a lack of space
make_free_space()
{
	local snapshot="$1"
	local required_size="$2"

	[[ "$required_size" =~ ^[0-9]+$ ]] || {
		warn "Can't determine the size of the new kernel and initrd"
		return 1
	}

	local total_space free_space
	total_space="$(boot_space)" || return 1
	free_space="$(boot_free_space)" || return 1

	# The extra size is a reserve that sdbootutil tries to keep
	# free in the ESP.  It is a goal, not a requirement: it must
	# never block the installation of a kernel that still fits
	local required_size_extra wanted_size
	required_size_extra=$((total_space * ESP_FREE_SPACE / 100))
	wanted_size=$((required_size + required_size_extra))

	info "Required free space in ESP: ${required_size}KB + ${required_size_extra}KB (${ESP_FREE_SPACE}% ESP size)"

	# If there is already free space, shortcut the code
	[ "$wanted_size" -gt "$free_space" ] || return 0

	# "Cleaning /boot/efi" message is presented via stderr
	dbg "Calling bootctl cleanup"
	bootctl -q cleanup 2> /dev/null || warn "Failed to clean up ${boot_root}"

	# Remove the entries one by one, from the least to the most
	# valuable one, until there is enough space.  It is re-evaluated
	# after every removal, as `bootctl unlink` frees the kernel and
	# the initrd only when the last entry that references them is
	# gone
	local id
	while read -r id; do
		free_space="$(boot_free_space)" || return 1
		dbg "Free space in the ESP: $free_space KB"
		[ "$wanted_size" -gt "$free_space" ] || return 0
		info "Removing boot entry $id"
		bootctl unlink "$id" || warn "Failed to remove the boot entry $id"
	done < <(removable_entries "$snapshot")

	free_space="$(boot_free_space)" || return 1
	dbg "Free space in the ESP after deallocation: $free_space KB"

	[ "$wanted_size" -gt "$free_space" ] || return 0

	# The reserve could not be honoured, but the new kernel and
	# initrd still fit.  Warn about it, as the next installation
	# can fail, but do not block this one
	[ "$required_size" -gt "$free_space" ] || {
		warn "Only ${free_space}KB free in ${boot_root}, below the ${ESP_FREE_SPACE}% reserve"
		warn "Remove the old kernels, or increase the size of ${boot_root}"
		return 0
	}

	warn "Needed ${required_size}KB in ${boot_root}, but only ${free_space}KB are free"
	warn "There are no more boot entries that can be removed automatically"
	return 1
}

make_free_space_for_kernel()
{
	local snapshot="$1"
	local kernel="$2"
	local initrds="$3"
	local devicetree="${4:-}"

	# 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 "$kernel") + $(pending_initrds_size "$initrds") + $(pending_devicetree_size "$devicetree")))

	make_free_space "$snapshot" "$total_size"
}

resolve_devicetree_source()
{
	local subvol="$1"
	local kernel_version="$2"
	local kernel_package_version="${kernel_version%-*}"
	local devicetree_source="${DEVICETREE_SOURCE:-}"

	[ -n "$devicetree_source" ] || return 0
	devicetree_source="${devicetree_source//%K/$kernel_version}"
	devicetree_source="${devicetree_source//%V/$kernel_package_version}"
	[[ "$devicetree_source" == '/'* ]] || err "DEVICETREE_SOURCE must be an absolute path"

	local snapshot_prefix="${subvol#"${subvol_prefix}"}"
	devicetree_source="$snapshot_prefix$devicetree_source"
	[ -f "$devicetree_source" ] || err "Can't find device tree blob $devicetree_source"
	[ -r "$devicetree_source" ] || err "Device tree blob is not readable: $devicetree_source"
	echo "$devicetree_source"
}

# Detect when the script is not operating over the system that is
# running, but over one that is mounted somewhere else and entered via
# `chroot` (the installer, or a `zypper --root` transaction), or over
# an image that is being built inside a container
foreign_system()
{
	[ -z "$in_buildroot" ] || return 0

	# `--chroot` compares "/" with "/proc/1/root", and reports a
	# chroot also when "/proc" is not mounted and the comparison
	# cannot be made.  Both answers are the ones that the caller
	# expects: without "/proc" there is no "/proc/cmdline" to take a
	# decision about either
	systemd-detect-virt -q --chroot && return 0
	systemd-detect-virt -q --container && return 0

	return 1
}

# List every file that contributes to the kernel command line of the
# entry of "subvol", in the order that they have to be aggregated: the
# first configuration file that is found, followed by the drop-in
# snippets that extend it.  Fails when the system does not configure a
# command line in any of them
kernel_cmdline_files()
{
	local subvol="$1"

	local sources=("${subvol:1}/etc/kernel/cmdline" "${subvol:1}/usr/lib/kernel/cmdline" /etc/kernel/cmdline /usr/lib/kernel/cmdline)
	# "/proc/cmdline" is the command line of the kernel that is
	# running, so it is a valid last resort only when the entry
	# belongs to the system that is running.  Inside the installer it
	# describes the installation medium, and parameters like
	# "Y2DEBUG=1", "linuxrc.debug" or "console=ttyS0" would be
	# inherited by the installed system, that would also miss the
	# ones that the installer is expected to provide, like "quiet"
	if foreign_system; then
		dbg "Ignoring /proc/cmdline: not the running system"
	else
		sources+=(/proc/cmdline)
	fi

	local found=
	local i
	for i in "${sources[@]}"; do
		[ -f "$i" ] || continue
		echo "$i"
		found=1
		break
	done

	# A drop-in in "/etc" replaces the one with the same name in
	# "/usr", and an empty one is a way of masking it
	for i in "${subvol:1}"/usr/kernel/cmdline.d/* /usr/kernel/cmdline.d/*; do
		[ -e "${subvol:1}/etc/kernel/cmdline.d/$(basename "$i")" ] && continue
		[ -s "$i" ] || continue
		echo "$i"
		found=1
	done

	for i in "${subvol:1}"/etc/kernel/cmdline.d/* /etc/kernel/cmdline.d/*; do
		[ -s "$i" ] || continue
		echo "$i"
		found=1
	done

	[ -n "$found" ]
}

create_boot_options() {
	local subvol="$1"
	local cmdline
	local boot_options

	local cmdline_files=()
	mapfile -t cmdline_files < <(kernel_cmdline_files "$subvol")

	local i
	for i in "${cmdline_files[@]}"; do
		dbg_cat "$i"
		while read -r line; do
			[[ "$line" == '#'* ]] && continue
			cmdline="${cmdline:+$cmdline }${line}"
		done < "$i"
	done

	# Now that "/proc/cmdline" is not always a valid fallback there
	# can be no command line at all.  Generate the options anyway:
	# "sedrootflags" adds "root=" and "rootflags=", which are the
	# minimum required to boot the system
	boot_options="$(echo "$cmdline" | sedrootflags "$subvol")"
	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"
	[ -n "$kernel_version" ] || err "Missing kernel version"
	[ -e "$src" ] || err "Can't find $src"

	info "Installing kernel $kernel_version"
	dbg_var "snapshot"

	# The RPM triggers install the kernel while the system is still
	# being installed, before the installer wrote
	# "/etc/kernel/cmdline".  The entry created now would carry a
	# command line that is not the one of the installed system, and
	# the initrd would be generated from a system that is not yet
	# configured.  Skip the installation: the installer calls
	# "add-all-kernels" once the system is set up.  An empty ESP is
	# what tells this situation apart from a configured system where
	# the administrator removed "/etc/kernel/cmdline"
	#
	# The subvolume is resolved like the one used later to generate
	# the options, so both look at the same files
	local cmdline_subvol="$subvol"
	[ -z "$in_buildroot" ] || cmdline_subvol="${subvol_prefix}/.snapshots/${snapshot}/snapshot"
	[ -n "$arg_force" ] || kernel_cmdline_files "$cmdline_subvol" > /dev/null || ! no_boot_entries || {
		# Building an image is a single pass: nothing will
		# install the kernel later, so a missing command line
		# is a configuration error and not a transient state
		[ -z "$in_buildroot" ] || err "No kernel command line configured for $kernel_version"
		warn "The system seems to be not fully configured (no kernel command line)"
		warn "Kernel $kernel_version will not be installed"
		return 0
	}

	[ -z "$TRANSACTIONAL_UPDATE" ] || [ -n "$arg_force" ] || {
		warn "Inside transactional-update. Installing kernels must be done outside the transaction"
		return 0
	}

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

	local initrds=("${boot_root}/$entry_token/$kernel_version/initrd-"*)
	[ -z "${boot_root}${dst}" ] \
		|| [ "${#initrds[@]}" -eq 0 ] \
		|| ! find_conf_file "$kernel_version" "$snapshot" > /dev/null \
		|| [ -n "$arg_no_reuse_initrd" ] \
		|| [ -n "$arg_force" ] \
		|| {
		info "Kernel $kernel_version already in the ESP"
		return 0
	}

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

	# Resolve the device tree before creating any ESP directory, so a
	# missing (but configured) blob aborts without leaving an empty
	# "$entry_token/$kernel_version" directory behind
	devicetree_src="$(resolve_devicetree_source "$subvol" "$kernel_version")" || err "Failed to resolve DEVICETREE_SOURCE"
	if [ -n "$devicetree_src" ]; then
		! is_secure_boot || err "External devicetree files are not supported when Secure Boot is enabled"
		calc_chksum "$devicetree_src"
		devicetree_dst="${dst%/*}/devicetree-$chksum.dtb"
	fi

	if [ -e "$initrd" ]; then
		ln -s "$initrd" "$tmpdir/initrd-0"
	elif ! reuse_initrd "$snapshot" "$subvol" "$kernel_version"; then
		local dracut_args=(
			'--reproducible'
			'--force'
			'--tmpdir' '/var/tmp'
		)
		if [ "${verbose:-0}" -le 1 ]; then
			dracut_args+=('--quiet')
		fi

		info "Generating new initrd"

		# When the running system is a mix of snapshots the live
		# /usr is not the one of "$root_subvol", so the initrd
		# needs to be generated inside the snapshot in any case
		if [ -n "$have_snapshots" ] && { [ "$subvol" != "$root_subvol" ] || [ -n "$applied_snapshot" ]; }; then
			mount_chroot "${subvol#"${subvol_prefix}"}"
			# 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 "${subvol#"${subvol_prefix}"}"
			chroot "${subvol#"${subvol_prefix}"}" dracut "${dracut_args[@]}" "$tmpdir/initrd-0" "$kernel_version" \
				|| err "Failed to generate the initrd for $kernel_version"
			umount_chroot "${subvol#"${subvol_prefix}"}"
		else
			dracut "${dracut_args[@]}" "$tmpdir/initrd-0" "$kernel_version" \
				|| err "Failed to generate the initrd for $kernel_version"
		fi
	fi

	make_free_space_for_kernel "$snapshot" "$src" "$tmpdir" "$devicetree_src" || err "No free space in ${boot_root} for new kernel"

	local boot_options
	[ -z "$in_buildroot" ] || subvol="${subvol_prefix}/.snapshots/${snapshot}/snapshot"
	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

	# A boot entry without an initrd will not boot.  This can only
	# happen if the initrd generation failed in a context where
	# `set -e` does not apply, like a command substitution
	[ "${#dstinitrd[@]}" -ne 0 ] || err "No initrd found for $kernel_version in snapshot $snapshot"

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

	add_version_to_title
	if is_transactional && [ "$snapshot" = 1 ]; then
		title="${title} (Initial Installation)"
	elif ! is_transactional && subvol_is_ro "$subvol"; then
		set_snapper_title_and_sortkey "$snapshot"
	fi

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

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

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

	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" ] && [ -n "$devicetree_src" ]; then
		if [ ! -e "${boot_root}${devicetree_dst}" ]; then
			install_with_rollback "$devicetree_src" "${boot_root}${devicetree_dst}" || failed=devicetree
		else
			info "Reusing ${boot_root}${devicetree_dst}"
		fi
	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

		# The directory can be missing if the user removed it,
		# or if the kernel is installed before the bootloader.
		# Note that `find` fails when the directory is not
		# present, and `install` does not create it
		mkdir -p "${boot_root}/loader/entries"

		# The entry can be already present with or without
		# boot counter.  In any case, if a new `initrd` was
		# created with a different `sha1`, then this boot
		# entry needs to be replaced, but under the new name
		# (with the boot counter configured in
		# `/etc/kernel/tries`)
		#
		# This replacement is done to avoid duplication.
		# Hopefully doing this check this late give the chance
		# of adding the kernel or initrd in case that the user
		# removes them
		local entry_name loader_entry loader_entry_current
		entry_name="$(entry_conf_file "$kernel_version" "$snapshot")"
		loader_entry="${boot_root}/loader/entries/$(entry_conf_file "$kernel_version" "$snapshot" "$tries")"
		# Only the boot counter can differ ("$entry+3.conf" and
		# "$entry+3-0.conf").  A trailing "*" would also match a
		# different snapshot, as the number is not delimited
		# (snapshot 1 matches 10, 11, ...), and the entry of that
		# other snapshot will be silently replaced
		loader_entry_current="$(find "${boot_root}/loader/entries" -maxdepth 1 -type f \
			\( -name "$entry_name" -o -name "${entry_name%.conf}+*" \) -print -quit)"
		install_with_rollback "$tmpdir/entry.conf" "$loader_entry" "$loader_entry_current" || 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"

	[ -z "$TRANSACTIONAL_UPDATE" ] || [ -n "$arg_force" ] || {
		warn "Inside transactional-update. Installing kernels / initrds must be done outside the transaction"
		return 0
	}

	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"

	[ -z "$TRANSACTIONAL_UPDATE" ] || [ -n "$arg_force" ] || {
		warn "Inside transactional-update. Removing kernels must be done outside the transaction"
		return 0
	}

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

cleanup_entries()
{
	local snapshot="$1"

	info "Cleaning up boot entries"

	[ -z "$TRANSACTIONAL_UPDATE" ] || [ -n "$arg_force" ] || {
		warn "Inside transactional-update. Cleaning up boot entries must be done outside the transaction"
		return 0
	}

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

	local id path snapshot kernel_version subvol src
	while read -r id; do
		read -r path
		read -r snapshot
		read -r kernel_version
		subvol=
		if [ -n "$snapshot" ] && [ "$snapshot" != "null" ]; then
			subvol="${subvol_prefix}/.snapshots/${snapshot}/snapshot"
		fi
		src="${subvol#"${subvol_prefix}"}/lib/modules/$kernel_version/$image"
		[ -e "$src" ] || {
			info "Cleaning boot entry $id"
			rm "$path"
		}
	done < <(jq -r '.[] | .id, .path, (.version | capture("((?<snapshot>[0-9]*)@)?(?<kernel>.*)")) as $v | $v.snapshot, $v.kernel' "$entryfile")

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

	# Guarantee ESP_FREE_SPACE.  Not being able to reach it is not
	# an error here: nothing is going to be installed
	make_free_space "${1:-$root_snapshot}" "0" || warn "Can't free space in ${boot_root}"
}

list_entries()
{
	local snapshot="$1"

	info "Listing boot entries"

	if [ ! -s "$entryfile" ]; then
		if [ -n "$snapshot" ]; then
			update_entries_for_snapshot "$snapshot"
		elif [ -n "$arg_all_entries" ]; then
			update_entries
		else
			update_entries_for_this_system
		fi
	fi

	# `bootctl` builds "isSelected" by matching the entry ID against
	# "LoaderEntrySelected", but grub2-bls writes the variable without
	# the ".conf" suffix, so nothing matches there and "isSelected" is
	# always null.  Read the variable to compare it here too, both
	# spellings, until grub2-bls is fixed.  Note that the value is
	# lower-cased by `bli_efi_var_get`
	local selected=
	[ -z "$interactive" ] || selected="$(bli_efi_var_get "LoaderEntrySelected")"

	local isdefault isselected isreported type id root conf title marker booted
	while read -r isdefault isselected isreported type id root conf title; do
		color=
		if [ "$isdefault" = "true" ]; then
			color="$color_bu"
		elif [ -n "$arg_only_default" ]; then
			continue
		fi
		# The entry that the boot loader reported as booted is
		# not necessarily the default one, so both are marked
		# separately.  The marker is kept out of the highlight,
		# as "list_snapshots" does
		marker=
		if [ -n "$interactive" ]; then
			booted=
			if [ "$isselected" = "true" ] || [ "${id,,}" = "$selected" ] \
			    || [ "${id,,}" = "$selected.conf" ]; then
				booted=1
			fi
			if [ "$isdefault" = "true" ]; then
				marker="+"
				[ -z "$booted" ] || marker="*"
			elif [ -n "$booted" ]; then
				marker="-"
			fi
		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' ] || [ "$k" = 'devicetree' ]; 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 "$marker$color$id${verbose:+: $title}${color:+$color_end}"
	done < <(jq '.[]|[.isDefault, .isSelected // false, 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
	# Without collecting the status here `set -e` ends the script
	# with an empty message when there is no entry for the kernel
	conf="$(find_conf_file "$kernel_version" "$snapshot")" || \
		err "Missing boot entry for kernel $kernel_version"

	[ -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|devicetree) ;;
			*) 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"
	[ -n "$kernel_version" ] || err "Missing kernel version"

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

	local conf
	# A missing entry is not an error, but `set -e` would end the
	# script before the check below
	conf="$(find_conf_file "$kernel_version" "$snapshot")" || :
	[ -f "$conf" ] || return 0

	info "Updating boot entry $id"

	[ -z "$TRANSACTIONAL_UPDATE" ] || [ -n "$arg_force" ] || {
		warn "Inside transactional-update. Updating boot entries must be done outside the transaction"
		return 0
	}

	update_entry_conf "$conf" "$snapshot"

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

update_all_entries()
{
	local snapshot="$1"

	info "Updating all boot entries"

	settle_entry_token "${snapshot}"

	[ -z "$TRANSACTIONAL_UPDATE" ] || [ -n "$arg_force" ] || {
		warn "Inside transactional-update. Updating boot entries must be done outside the transaction"
		return 0
	}

	make_free_space "$snapshot" 1024 || warn "Can't free space in ${boot_root}"

	if [ -n "$snapshot" ]; then
		update_entries_for_snapshot "$snapshot"
	else
		update_entries_for_this_system
	fi

	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
	local marker
	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"
		# The snapshot the system is running from is not
		# necessarily the default one, so both are marked
		# separately
		marker=
		if [ -n "$interactive" ]; then
			if [ "$isdefault" = "true" ]; then
				marker="+"
				[ "$n" != "$running_snapshot" ] || marker="*"
			elif [ "$n" = "$running_snapshot" ]; then
				marker="-"
			fi
		fi
		echo -e "$marker$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
	if [ -n "$snapshot" ]; then
		update_entries_for_snapshot "$snapshot"
	else
		update_entries_for_this_system
	fi

	# 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_shim_installed()
{
	# Check if shim is installed in the ESP
	# In installed systems there should be a shim.efi
	[ ! -f "${esp_root}${esp_dst}/shim.efi" ] || return 0
	# In removable media there is only BOOT${arch}.EFI
	! grep -q "UEFI SHIM" "${esp_root}/EFI/BOOT/BOOT${firmware_arch^^}.EFI" 2> /dev/null || return 0
	return 1
}

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 is_shim_installed; then
			fn="${esp_root}${esp_dst}/grub.efi"
		elif [ -n "$arg_portable" ]; then
			fn="${esp_root}/EFI/BOOT/BOOT${firmware_arch^^}.EFI"
		else
			local bootloader
			bootloader="$(find_bootloader)"
			fn="${esp_root}${esp_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 "${esp_root}/${esp_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
	# The status of the next commands is checked below, so it must
	# be collected in the same line: `set -e` would end the script
	# before the check, and without any message
	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"
	local status=0
	systemd-analyze compare-versions "$v" "$nv" 2> /dev/null || 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
}

esp_root_drive_partno_dseq()
{
	local blkpart drive disk_seq_min
	blkpart="$(findmnt -nvo SOURCE "$esp_root")"
	drive="${blkpart##*/}"
	lsblk -o NAME,PKNAME,PARTN,DISK-SEQ --json > "$devicefile"
	# Read the json file with all the devices and, recursively,
	# inspect the childrens looking for the partition that mounts
	# /boot/efi.  Once it is found gets the partition number and
	# the (kernel) name of the parent.
	#
	# Use DISK-SEQ to guess the ordering of devices that GRUB2
	# will see during boot (hdX,partY).  I saw that this value not
	# always start with 0 for the kernel, but with 0 in GRUB2, so
	# rebase it using the minimal found value.
	disk_seq_min="$(jq -r '[.. | ."disk-seq"? | numbers] | min' < "$devicefile")"
	jq -r --arg name "$drive" --arg base "$disk_seq_min" \
	   'first(
	     .blockdevices[]
	     | recurse(.children[]?)
	     | select(.children? | arrays | map(.name == $name) | any)
	     | {
		 "partn": .children[] | select(.name == $name).partn | tostring,
		 "name": .name,
		 "pkname": .pkname,
		 "disk-seq": ."disk-seq" - ($base | tonumber) | tostring
	       })
	   | if .pkname then "/dev/" + .pkname + " " + .partn + " " + ."disk-seq" else "/dev/" + .name + " " + .partn + " " + ."disk-seq" end
	   ' < "$devicefile"
}

extra_entry_needs_update()
{
	local entry="$1"

	# If we do not have a version in the entry file (or the entry
	# file is missing), then we assume that an update is required
	[ -n "$entry" ] || return 0
	read -r _ nv < <(grep -ao '^version .*$' "$entry")
	[ -n "$nv" ] || return 0

	entry="${boot_root}/loader/entries/$(basename "$entry")"
	[ -f "$entry" ] || return 0
	read -r _ v < <(grep -ao '^version .*$' "$entry")
	[ -n "$v" ] || return 0

	systemd-analyze compare-versions "$v" "<" "$nv" 2> /dev/null
}

install_extra_entry()
{
	local subvol="$1"
	local entry="$2"

	local -A e
	local -a l
	while read -r line; do
		[[ "$line" == '#'* ]] && continue
		IFS="=" read -r -a l <<<"$line"
		[ "${#l[@]}" -eq 2 ] || err "Cannot parse line $line"
		e["${l[0]}"]="${l[1]}"
	done < "$entry"

	[ -n "${e[EFI]}" ] || return 0
	[ -f "${subvol:1}${e[EFI]}" ] || err "Missing extra entry file ${e[EFI]}"
	[ -z "${e[ENTRY]}" ] || [ -f "${subvol:1}${e[ENTRY]}" ] || err "Missing extra entry file ${e[ENTRY]}"

	# If the file can be installed, echo the name so it can be
	# registered as installed (even if does not needs to be
	# updated)
	echo "/EFI/extra/$(basename "${e[EFI]}")"

	extra_entry_needs_update "${subvol:1}${e[ENTRY]}" || return 0

	mkdir -p "${esp_root}/EFI/extra"
	install -p -D "${subvol:1}${e[EFI]}" "${esp_root}/EFI/extra"
	[ -z "${e[ENTRY]}" ] || install -p -D "${subvol:1}${e[ENTRY]}" "${boot_root}/loader/entries"
}

install_extra_entries()
{
	local snapshot="$1"

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

	update_entries_for_extra

	local efi install_extra_entry_status
	local -A entries
	for i in "${subvol:1}"/usr/lib/sdbootutil/entries.d/*.conf; do
		[ -e "${subvol:1}/etc/sdbootutil/entries.d/$(basename "$i")" ] && continue
		dbg_cat "$i"
		# A broken entry is reported and skipped, but `set -e`
		# would end the script before the status is checked
		install_extra_entry_status=0
		efi="$(install_extra_entry "$subvol" "$i")" || install_extra_entry_status=$?
		[ "$install_extra_entry_status" -eq 0 ] || {
			warn "Error installing extra entry $(basename "$i")"
		}
		[ -z "$efi" ] || entries["$efi"]=1
	done

	for i in "${subvol:1}"/etc/sdbootutil/entries.d/*.conf; do
		dbg_cat "$i"
		install_extra_entry_status=0
		efi="$(install_extra_entry "$subvol" "$i")" || install_extra_entry_status=$?
		[ "$install_extra_entry_status" -eq 0 ] || {
			warn "Error installing extra entry $(basename "$i")"
		}
		[ -z "$efi" ] || entries["$efi"]=1
	done

	# Remove the entry + efi pairs not installed
	local entry
	while read -r efi; do
		read -r entry
		[ -n "${entries[$efi]}" ] || {
			rm -f "${esp_root}/EFI${efi}"
			rm -f "$entry"
		}
	done < <(jq -r '.[] | .linux // .efi, .path' < "$entryfile")

	# Finally, remove the single efi files not installed
	for i in "${esp_root}"/EFI/extra/*; do
		efi="${i#"${esp_root}"}"
		[ "${entries[$efi]:-0}" -eq 1 ] || rm -f "${esp_root}${efi}"
	done

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

install_bootloader()
{
	local snapshot=""
	local prefix=""
	if [ -n "$have_snapshots" ]; then
		snapshot="${1:-$root_snapshot}"
		# The binaries are taken from the same snapshot that
		# `bootloader_needs_update` inspected, that is not
		# necessarily the running one
		prefix="/.snapshots/${snapshot}/snapshot"
	fi

	info "Installing bootloader"
	dbg_var "$snapshot"

	[ -z "$TRANSACTIONAL_UPDATE" ] || [ -n "$arg_force" ] || {
		warn "Inside transactional-update. Installing bootloader must be done outside the transaction"
		return 0
	}

	local bootloader bldr_name
	settle_entry_token "${snapshot}"

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

	mkdir -p "${esp_root}/loader"
	mkdir -p "${boot_root}/loader/entries"

	if [ -n "$arg_secure_boot" ] && [ ! -e "$prefix$shimdir/shim.efi" ]; then
		warn "A secure boot installation cannot be done. The shim package is not installed"
	fi

	if [ -n "$arg_secure_boot" ] && [ -e "$prefix$shimdir/shim.efi" ]; then
		info "Installing $bldr_name with shim into $esp_root"
		entry="$esp_dst/shim.efi"
		for i in MokManager shim; do
			[ -n "$arg_portable" ] || install -p -D "$prefix$shimdir/$i.efi" "${esp_root}${esp_dst}/$i.efi"
		done
		install -p -D "$bootloader" "${esp_root}${esp_dst}/grub.efi"

		# boot entry point
		install -p -D "$prefix$shimdir/MokManager.efi" "${esp_root}/EFI/BOOT/MokManager.efi"
		[ -n "$arg_portable" ] || install -p -D "$prefix$shimdir/fallback.efi" "${esp_root}/EFI/BOOT/fallback.efi"
		install -p -D "$prefix$shimdir/shim.efi" "${esp_root}/EFI/BOOT/BOOT${firmware_arch^^}.EFI"
	else
		info "Installing $bldr_name without shim into $esp_root"
		entry="$esp_dst/${bootloader##*/}"
		[ -n "$arg_portable" ] || install -p -D "$bootloader" "${esp_root}$entry"
		install -p -D "$bootloader" "${esp_root}/EFI/BOOT/BOOT${firmware_arch^^}.EFI"
	fi

	# shellcheck disable=SC2154
	[ -n "${os_release_NAME}" ] || set_os_release "${snapshot}"
	local split
	IFS=" " read -r -a split <<<"${os_release_NAME}"
	local boot_manager
	boot_manager="${split[0]} Boot Manager"

	# This is for shim to create the entry if missing
	[ -n "$arg_portable" ] || echo "${entry##*/},$boot_manager" | { echo -ne "\xff\xfe"; iconv -f ascii -t ucs-2le; } > "${esp_root}${esp_dst}/boot.csv"

	mkdir -p "${boot_root}/${entry_token}"
	echo "$entry_token" > "${esp_root}${esp_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
		[ -e "${esp_root}/loader/loader.conf" ] || echo -e "#timeout 3\n#console-mode keep\n" > "${esp_root}/loader/loader.conf"
		[ -s "${boot_root}/loader/entries.srel" ] || echo type1 > "${boot_root}/loader/entries.srel"
	fi

	# Create boot menu entry if it does not exist
	if [ -z "$arg_no_variables" ] && [ -z "$arg_portable" ] && mountpoint -q "$esp_root"; then
		local drive partno
		read -r drive partno _ < <(esp_root_drive_partno_dseq)

		local escaped_entry="${entry//\//\\\\}"
		efibootmgr | grep -q "Boot.*${boot_manager}.*${escaped_entry}" || efibootmgr -q --create --disk "$drive" --part "$partno" --label "${boot_manager} ($bldr_name)" --loader "$entry" || true

		# Make it the first option.  A system that never booted
		# from disk can have no `BootOrder` at all
		local boot_order
		boot_order="$(efibootmgr | grep BootOrder)" || :
		boot_order="${boot_order#BootOrder: }"

		# There can be more than one match, for example a stale
		# entry left behind by a previous installation.
		# `efibootmgr` expects a single ID, so take the first
		# active one (the `*` marker), as an inactive entry is
		# never booted by the firmware
		local boot_entry
		boot_entry="$(efibootmgr | grep -m1 "Boot[0-9A-Fa-f]\{4\}\*.*${boot_manager}.*${escaped_entry}")" || :
		# The ID is printed as `BootXXXX`
		boot_entry="${boot_entry:4:4}"

		[ -z "$boot_entry" ] || efibootmgr -q -D -o "$boot_entry${boot_order:+,$boot_order}" || true
	fi

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

bootloader_update()
{
	local status=0

	info "Updating bootloader"

	[ -z "$TRANSACTIONAL_UPDATE" ] || [ -n "$arg_force" ] || {
		warn "Inside transactional-update. Updating bootloader must be done outside the transaction"
		return 0
	}

	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

	install_extra_entries "${1:-$root_snapshot}"
}

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 "${esp_root}/loader/random-seed" ] && dd if="${esp_root}/loader/random-seed" bs=32 count=1 status=none; } | sha256sum)
	[ "${#s}" = 64 ] || { warn "Invalid random seed"; return 0; }
	hex_to_binary "$s" > "${esp_root}/loader/random-seed.new"
	mv "${esp_root}/loader/random-seed.new" "${esp_root}/loader/random-seed"
}

has_efivars()
{
	[ -w /sys/firmware/efi/efivars ]
}

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

bli_efi_var_set()
{
	# BLI uses this vendor UUID
	local efi_var="/sys/firmware/efi/efivars/${1:?}-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
	local value="${2:?}"
	has_efivars || return 1
	[ -e "$efi_var" ] && chattr -i "$efi_var"
	echo -ne "\x07\x00${value}\x00" | iconv -t UTF-16LE > "$efi_var"
	chattr +i "$efi_var"
}

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

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

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

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

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

	[ -e "${esp_root}${esp_dst}/grubenv" ] || touch "${esp_root}${esp_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 < "${esp_root}${esp_dst}/grubenv"
	echo "$key=$value" >> "$grubenv"

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

	mv "$grubenv" "${esp_root}${esp_dst}/grubenv"
}

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

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

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

	if ! has_efivars || [ -n "$arg_no_variables" ]; then
		loader_conf_set "default" "$id"
	elif ! bli_efi_var_set "LoaderEntryDefault" "$id"; then
		warn "EFI variable LoaderEntryDefault cannot be set.  Falling back to configuration file"
		loader_conf_set "default" "$id"
	fi
}

set_default_grub2_bls()
{
	local id="${1:?}"
	if systemd-analyze compare-versions "$(bootloader_version)" "<" "2.13" || ! has_efivars || [ -n "$arg_no_variables" ]; then
		grubenv_set "default" "$id"
	elif ! bli_efi_var_set "LoaderEntryDefault" "$id"; then
		warn "EFI variable LoaderEntryDefault cannot be set.  Falling back to configuration file"
		grubenv_set "default" "$id"
	fi
}

get_top_entry_id()
{
	local snapshot="$1"

	if [ -n "$snapshot" ]; then
		update_entries_for_snapshot "$snapshot"
	else
		update_entries_for_this_system
	fi

	mapfile -t ids < <(jq -r '.[]|.id' < "$entryfile")
	if [ -z "${ids[0]}" ]; then
		info "No boot entries found, trying to create them..."
		install_all_kernels "$snapshot"
		if [ -n "$snapshot" ]; then
			update_entries_for_snapshot "$snapshot"
		else
			update_entries_for_this_system
		fi

		mapfile -t ids < <(jq -r '.[]|.id' < "$entryfile")
		if [ -z "${ids[0]}" ]; then
			err "snapshot $snapshot has no kernels"
		fi
	fi

	echo "${ids[0]}"
}

set_default_entry()
{
	local id="$1"
	# `get_top_entry_id` can install kernels, and inside a command
	# substitution `set -e` does not abort this shell, so the
	# result needs to be validated
	[ -n "$1" ] || id=$(get_top_entry_id "$root_snapshot")
	[ -n "$id" ] || err "Can't determine the top boot entry for snapshot $root_snapshot"

	info "Setting default entry $id"

	[ -z "$TRANSACTIONAL_UPDATE" ] || [ -n "$arg_force" ] || {
		warn "Inside transactional-update. Setting default entries must be done outside the transaction"
		return 0
	}

	if [ ! -f "${boot_root}/loader/entries/$id" ] && [ ! -f "${boot_root}/loader/entries/$id.conf" ]; then
		update_entries
		while read -r path; do
			if [ ! -f "$path" ]; then
				err "Boot loader entry $id not found"
 			fi
		done < <(jq -r --arg id "$id" '.[] | select(.id == $id) | .path' < "$entryfile")
	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_bootctl()
{
	update_entries_for_this_system
	jq -r '.[] | select(.isDefault == true) | .id' < "$entryfile"
}

get_default_sdboot()
{
	local val
	if has_efivars && [ -z "$arg_no_variables" ]; then
		val="$(bli_efi_var_get "LoaderEntryDefault")"
	fi
	[ -n "$val" ] || val="$(loader_conf_get "default")"
	[ -n "$val" ] || val="$(get_default_bootctl)"
	[ -z "$val" ] || echo "$val"
}

get_default_grub2_bls()
{
	local val
	if has_efivars && [ -z "$arg_no_variables" ]; then
		val="$(bli_efi_var_get "LoaderEntryDefault")"
	fi
	[ -n "$val" ] || val="$(grubenv_get "default")"
	[ -n "$val" ] || val="$(get_default_bootctl)"
	[ -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:?}"
	# `bootctl set-timeout menu-hidden` writes "0", and
	# "menu-disabled" write the full text.  In both cases the menu
	# is hidden.
	#
	# `bootctl set-timeout menu-force` writes 2^32-1 in the EFI
	# variable (fix needs to be backported) (bsc#1258944)
	[ "$timeout" = "0" ] && timeout="menu-disabled"
	[ "$timeout" = "-1" ] && timeout="menu-force"

	# `bootctl set-timeout menu-force` writes 2^32-1 in the EFI
	# variable (fix needs to be backported) (bsc#1258944)
	if ! has_efivars || [ -n "$arg_no_variables" ]; then
		loader_conf_set "timeout" "$timeout"
	elif ! bli_efi_var_set "LoaderConfigTimeout" "$timeout"; then
		warn "EFI variable LoaderConfigTimeout cannot be set.  Falling back to configuration file"
		loader_conf_set "timeout" "$timeout"
	fi
}

set_timeout_grub2_bls()
{
	local timeout="${1:?}"
	# `bootctl set-timeout menu-hidden` writes "0", and
	# "menu-disabled" write the full text.  In both cases the menu
	# is hidden.
	#
	# `bootctl set-timeout menu-force` writes 2^32-1 in the EFI
	# variable (fix needs to be backported) (bsc#1258944)
	#
	# We map the same string that works in grub2-bls with the
	# downstream patch
	[ "$timeout" = "menu-disabled" ] && timeout="menu-hidden"
	[ "$timeout" = "0" ] && timeout="menu-hidden"
	[ "$timeout" = "-1" ] && timeout="menu-force"

	if systemd-analyze compare-versions "$(bootloader_version)" "<" "2.13" || ! has_efivars || [ -n "$arg_no_variables" ]; then
		[ "$timeout" = "menu-disabled" ] || [ "$timeout" = "menu-hidden" ] && timeout=0
		[ "$timeout" = "menu-force" ] && timeout=-1
		grubenv_set "timeout" "$timeout"
	elif ! bli_efi_var_set "LoaderConfigTimeout" "$timeout"; then
		warn "EFI variable LoaderConfigTimeout cannot be set.  Falling back to configuration file"
		[ "$timeout" = "menu-disabled" ] || [ "$timeout" = "menu-hidden" ] && timeout=0
		[ "$timeout" = "menu-force" ] && timeout=-1
		grubenv_set "timeout" "$timeout"
	fi
}

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
	if has_efivars && [ -z "$arg_no_variables" ]; then
		val="$(bli_efi_var_get "LoaderConfigTimeout")"
	fi
	[ -n "$val" ] || val="$(loader_conf_get "timeout")"
	[ "$val" = "menu-disabled" ] || [ "$val" = "menu-hidden" ] && val=0
	[ "$val" = 4294967295 ] || [ "$val" = "menu-force" ] && val=-1
	[ -z "$val" ] || echo "$val"
}

get_timeout_grub2_bls()
{
	local val
	if has_efivars && [ -z "$arg_no_variables" ]; then
		val="$(bli_efi_var_get "LoaderConfigTimeout")"
	fi
	[ -n "$val" ] || val="$(grubenv_get "timeout")"
	[ "$val" = "menu-disabled" ] || [ "$val" = "menu-hidden" ] && val=0
	[ "$val" = 4294967295 ] || [ "$val" = "menu-force" ] && val=-1
	[ -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
}

print_loader_path()
{
	local loader_path
	loader_path="$(bootctl --print-loader-path 2> /dev/null)" && {
		echo "${loader_path}"
		return 0
	} || true

	# Simple approach: try the different options
	local bootloader
	bootloader=$(find_bootloader "$snapshot")
	for i in "grub.efi" "${bootloader##*/}" "BOOT${firmware_arch^^}.EFI"; do
		if [ -e "${esp_root}${esp_dst}/$i" ]; then
			echo "${esp_root}${esp_dst}/$i"
			return 0
		fi
	done

	echo "No loader EFI binary path found"
	return 1
}

set_default_snapshot()
{
	[ -n "$have_snapshots" ] || {
		info "System does not support snapshots."
		# suse-module-tools is calling `sdbootutil
		# set-default-snapshot` to set the top entry as the
		# default entry.  If the system does not support
		# snapshots we set the top entry as default and exit
		# early
		set_default_entry
		return 0
	}
	local snapshot="${1:?}"
	local id

	info "Setting default snapshot $snapshot"

	[ -z "$TRANSACTIONAL_UPDATE" ] || [ -n "$arg_force" ] || {
		warn "Inside transactional-update. Setting default snapshots must be done outside the transaction"
		return 0
	}

	# `get_top_entry_id` can install kernels, and inside a command
	# substitution `set -e` does not abort this shell, so the
	# result needs to be validated
	id="$(get_top_entry_id "$snapshot")"
	[ -n "$id" ] || err "Can't determine the top boot entry for snapshot $snapshot"
	set_default_entry "$id"

	# 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/$snapshot/snapshot")
	btrfs subvolume set-default "${subvolume_id}" /.snapshots
	rm -f "$snapperfile"
}

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

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

# Warnings that `systemd-pcrlock` emits, once per event log
# record, when the firmware pads the UEFI device paths stored in
# the log.  All of them are produced while collecting the
# description of the record, that is only used to display the log,
# so neither the policy nor the predictions are affected by them
pcrlock_noise="^(Garbage after device path end|Device path too short|Device path size does not match|Device path element too short), ignoring\.$"

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

	if [ -n "$verbose" ]; then
		SYSTEMD_LOG_LEVEL="${SYSTEMD_LOG_LEVEL:-warning}" "$pcrlock_cmd" "$@"
		return
	fi

	local status=0
	SYSTEMD_LOG_LEVEL="${SYSTEMD_LOG_LEVEL:-warning}" "$pcrlock_cmd" "$@" \
		2>"$tmpdir/pcrlock.err" || status=$?
	grep -Ev "$pcrlock_noise" "$tmpdir/pcrlock.err" >&2 || :
	rm -f "$tmpdir/pcrlock.err"

	return "$status"
}

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

pcr_oracle_public_key()
{
	# The public key that pcr-oracle used to sign the PCR policy
	# is not removed when the package goes away, and
	# systemd-cryptenroll picks it up from any of the
	# configuration directories.  Print the first one found.
	local dir
	for dir in /etc/systemd /run/systemd /usr/local/lib/systemd /usr/lib/systemd; do
		if [ -e "$dir/tpm2-pcr-public-key.pem" ]; then
			echo "$dir/tpm2-pcr-public-key.pem"
			return 0
		fi
	done

	return 1
}

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"
	[ "$esp_root" = "$boot_root" ] || cp -a "${esp_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
}

uint64_le()
{
	# 64 bit little endian representation of a number, as escape
	# sequences for `echo -ne`.  Nothing that is measured here is
	# big enough to reach the high 32 bits
	local value="$1"

	printf '\\x%02x' \
	       "$((value & 255))" \
	       "$((value >> 8 & 255))" \
	       "$((value >> 16 & 255))" \
	       "$((value >> 24 & 255))" \
	       0 0 0 0
}

pcrlock_secureboot_sbatlevel()
{
	# shim measures the `SbatLevel` variable into PCR 7 as an
	# EV_EFI_VARIABLE_AUTHORITY event.  When Secure Boot is
	# enabled `systemd-pcrlock lock-secureboot-authority` collects
	# every EV_EFI_VARIABLE_AUTHORITY record of PCR 7 in event log
	# order, without filtering by variable name, so this
	# measurement is already covered and there is nothing to do.
	# But when Secure Boot is disabled that command removes the
	# component and generates nothing, leaving the shim measurement
	# without an owner.  A measurement that no component can claim
	# drops the full PCR from the policy, so PCR 7 can never be
	# sealed in a system that boots via shim without Secure Boot.
	#
	# The prediction is written under the name that the failing
	# verb owns, and not under a new one.  `SbatLevel` is exactly
	# the record that `lock-secureboot-authority` would have
	# claimed, so the position in the replay order is the correct
	# one, and if Secure Boot is enabled later the verb overwrites
	# the file with the complete set of authorities.  A component
	# with a name of our own would survive that transition and
	# replay `SbatLevel` twice, breaking the PCR that it is meant
	# to fix
	local component_dir=/var/lib/pcrlock.d/620-secureboot-authority.pcrlock.d

	! is_secure_boot || {
		dbg "Secure Boot enabled, SbatLevel is locked by lock-secureboot-authority"
		return 0
	}

	local guid="605dab50-e046-4300-abb6-3dd810dd8b23"
	# The same GUID in binary form: the first three fields are
	# stored in little endian, and the last two as they are
	# written
	local guid_le='\x50\xab\x5d\x60\x46\xe0\x00\x43\xab\xb6\x3d\xd8\x10\xdd\x8b\x23'

	# The variable is measured as `SbatLevel`, but is exposed at
	# runtime as `SbatLevelRT`
	local name="SbatLevel"
	local var="/sys/firmware/efi/efivars/${name}RT-$guid"

	# Without shim there is no `SbatLevel` measurement to claim, and
	# `lock-secureboot-authority` already removed the component
	[ -e "$var" ] || {
		dbg "Missing $var, skipping the SbatLevel component"
		return 0
	}

	# What is measured is a UEFI_VARIABLE_DATA structure: the
	# GUID, the length of the name in characters, the length of
	# the content in bytes, the name in UTF-16LE, and the content.
	# The first four bytes of the efivarfs file are the
	# attributes, and are not part of the variable content
	local size
	size="$(stat -c%s "$var")"
	size=$((size - 4))

	{
		echo -ne "$guid_le$(uint64_le "${#name}")$(uint64_le "$size")"
		echo -n "$name" | iconv -t UTF-16LE
		tail -c "$size" "$var"
	} > "$tmpdir/sbatlevel"

	mkdir -p "$component_dir"
	pcrlock \
		lock-raw \
		--pcr=7 \
		--pcrlock="$component_dir/generated.pcrlock" \
		"$tmpdir/sbatlevel"
	rm "$tmpdir/sbatlevel"
	dbg_cat "$component_dir/generated.pcrlock"
}

pcrlock_cmdline_initrd()
{
	local cmdline="$1"
	local initrd="$2"
	local suffix="$3"

	# 710-kernel-cmdline-initrd-entry.pcrlock.d is not part of the
	# pcrlock standards
	#
	# When using systemd-boot or GRUB2-BLS >= 2.14, the cmdline is
	# send to the kernel via UEFI with null termination (double
	# \x00, as is UTF-16 enconding), but this is missing in
	# GRUB2-BLS <= 2.12
	echo -n "$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
	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

	# We cannot use lock-kernel-cmdline, as it ignore --pcr=12,
	# and assign PCR 9 in any case
	echo -ne "$cmdline\x00" > "$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"
	rm "$tmpdir/cmdline"
}

pcrlock_grub2_bls_kernel_initrd_cmdline_initrd()
{
	local linux="$1"
	local initrd="$2"
	local devicetree="$3"
	local cmdline="$4"
	local suffix="$5"

	local elements=("$linux" "$initrd")
	[ -z "$devicetree" ] || elements+=("$devicetree")
	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
		locks+=("$tmpdir/element-$n.pcrlock")
	done

	# When using systemd-boot or GRUB2-BLS >= 2.14, the cmdline is
	# send to the kernel via UEFI with null termination (double
	# \x00, as is UTF-16 enconding), but this is missing in
	# GRUB2-BLS <= 2.12
	echo -n "$cmdline" > "$tmpdir/cmdline"
	n=$((n+1))
	if systemd-analyze compare-versions "$(bootloader_version)" "<" "2.13"; then
		iconv -t UTF-16LE -o "$tmpdir/cmdline.utf16" "$tmpdir/cmdline"
		pcrlock \
			lock-raw \
			--pcr=9 \
			--pcrlock="$tmpdir/element-$n.pcrlock" \
			"$tmpdir/cmdline.utf16"
		rm "$tmpdir/cmdline.utf16"
	else
		pcrlock \
			lock-kernel-cmdline \
			--pcrlock="$tmpdir/element-$n.pcrlock" \
			"$tmpdir/cmdline"
	fi
	locks+=("$tmpdir/element-$n.pcrlock")
	rm "$tmpdir/cmdline"

	n=$((n+1))
	pcrlock \
		lock-kernel-initrd \
		--pcrlock="$tmpdir/element-$n.pcrlock" \
		"$initrd" 2> /dev/null
	locks+=("$tmpdir/element-$n.pcrlock")

	mkdir -p /var/lib/pcrlock.d/710-grub2-bls-kernel-initrd-kernel-cmdline-initrd-entry.pcrlock.d
	jq --slurp '{"records": [.[].records[0]]}' \
	   "${locks[@]}" \
	   > "/var/lib/pcrlock.d/710-grub2-bls-kernel-initrd-kernel-cmdline-initrd-entry.pcrlock.d/kernel-initrd-cmdline-initrd-$suffix.pcrlock"
	rm "${locks[@]}"
}

pcrlock_grub2_bls_cmdline()
{
	local linux="$1"
	local cmdline="$2"
	local initrd="$3"
	local devicetree="$4"
	local suffix="$5"
	local lines=("$linux" "$cmdline" "$initrd")
	[ -z "$devicetree" ] || lines+=("$devicetree")

	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
	[ "${#locks[@]}" -eq 0 ] || {
		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 "${esp_root}/loader/loader.conf" ]; then
		shift_component 641-sdboot-loader-conf
		pcrlock \
			lock-raw \
			--pcr=5 \
			--pcrlock=/var/lib/pcrlock.d/641-sdboot-loader-conf.pcrlock.d/generated.pcrlock \
			"${esp_root}/loader/loader.conf"
	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_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_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 "${esp_root}${esp_dst}/grubenv" ]; then
		shift_component 641-grub2-bls-grubenv
		pcrlock \
			lock-raw \
			--pcr=9 \
			--pcrlock=/var/lib/pcrlock.d/641-grub2-bls-grubenv.pcrlock.d/generated.pcrlock \
			"${esp_root}${esp_dst}/grubenv"
	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

	local grub2_bls_drive
	if systemd-analyze compare-versions "$(bootloader_version)" "<" "2.13"; then
		local partno
		read -r _ partno dseq < <(esp_root_drive_partno_dseq)
		# We are assuming that the disk sequence (that start
		# with 1) is related with the disk sequence that GRUB2
		# see during boot time (that start with 0).
		# esp_root_drive_partno_dseq already set the correct
		# start
		#
		# Also if for GRUB2 >= 2.14 this section is not
		# included
		grub2_bls_drive="(hd$dseq,gpt$partno)"
	fi

	# 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
		read -r devicetree
		[ -f "${esp_root}$linux" ] || {
			info "Missing ${esp_root}$linux, ignoring entry for prediction"
			continue
		}
		[ -f "${esp_root}$initrd" ] || {
			info "Missing ${esp_root}$initrd, ignoring entry for prediction"
			continue
		}
		if [ -n "$devicetree" ] && [ ! -f "${esp_root}$devicetree" ]; then
			info "Missing ${esp_root}$devicetree, ignoring entry for prediction"
			continue
		fi
		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" \
					  "${devicetree:+devicetree ${grub2_bls_drive}$devicetree}" \
					  "$n"
	done < <(jq --raw-output 'sort_by(.priority, (.kernel | map(-.))) | .[] | .options, .linux, .initrd[0], (.devicetree // "")' "$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
			read -r devicetree
			n=$((n+1))
			pcrlock_grub2_bls_cmdline "linux ${grub2_bls_drive}$linux $options" \
						  "${grub2_bls_drive}$linux $options" \
						  "initrd ${grub2_bls_drive}$initrd" \
						  "${devicetree:+devicetree ${grub2_bls_drive}$devicetree}" \
						  "0-$n"
		done < <(jq --raw-output '.[] | .options, .linux, .initrd[0], (.devicetree // "")' "$initialentryfile")
	fi

	# If shim is installed, grub2-bls invokes shim to extend PCR4
	if is_shim_installed || is_secure_boot; then
		# 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 "${esp_root}$linux" ] || {
				info "Missing ${esp_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" \
				"${esp_root}/$linux"
		done < <(jq --raw-output 'sort_by(.priority, (.kernel | map(-.))) | map(.linux) | .[]' "$entryfile")
	fi

	# In MicroOS with GRUB2-BLS we can easily exceed to 8 arms
	# limit for PolicyOR in PCR#9.  For each update:
	#  - 641-grub2-bls-grubenv: `grubenv` change after the
	#    `set-default` for the new boot entry
	#  - 643-grub2-bls-entry-files: a new boot entry is added wrt
	#    the current state
	#  - 710-kernel-cmdline-initrd-entry: the new snapshot
	#    requires a new cmdline (rootflags), so there is one per
	#    snapshot
	#  - 710-grub2-bls-kernel-initrd-entry: if the kernel or
	#    initrd gets updated, a new entry with different hashes
	#    will appear here
	#
	# If not kernel/initrd gets updated, the possible values for
	# PCR#9 are 2 for 641 (the shift and the next one that
	# includes the new grubenv), 2 for 643 (again, the shift and
	# the variation that includes the hash of the new entry), n
	# for 710-kernel (one per snapshot), and 2 for 710-grub2 (only
	# if there is a new kernel or initrd): [2x]2xn[x2]
	#
	# Solutions:
	#  - GRUB2-BLS use BLI and drops `grubenv`
	#  - MicroOS uses only systemd-boot
	#  - PolicyOR limit is resolved in systemd-pcrlock
	#  - Merge both 710 components, to decrease the variations
	#  - Make n=2 (710-kernel-cmdline-initrd-entry)
	#
	# For GRUB2 2.14 we drop grubenv with the BLI patches.
	#
	# The merge solution makes sense.  The order would be:
	#  - GRUB2: full sha256 of the kernel
	#  - GRUB2: sha256 of the initrd
	#  - Kernel: sha256 of the cmdline
	#  - Kernel: sha256 of the initrd (rep)
	#
	# The kernel is again measured a-la pesign in PCR#4 via
	# 650-kernel-efi-application (by UEFI/shim)

	# Remove old components before the merge
	rm -fr /var/lib/pcrlock.d/710-grub2-bls-kernel-initrd-entry.pcrlock.d
	rm -fr /var/lib/pcrlock.d/710-kernel-cmdline-initrd-entry.pcrlock.d

	# Join the kernel, initrd (GRUB2) and cmdline, initrd (kernel)
	# in a single component
	shift_component 710-grub2-bls-kernel-initrd-kernel-cmdline-initrd-entry
	n=0
	while read -r cmdline; do
		read -r linux
		read -r initrd
		read -r devicetree
		[ -f "${esp_root}$linux" ] || {
			info "Missing ${esp_root}$linux, ignoring entry for prediction"
			continue
		}
		[ -f "${esp_root}$initrd" ] || {
			info "Missing ${esp_root}$initrd, ignoring entry for prediction"
			continue
		}
		if [ -n "$devicetree" ] && [ ! -f "${esp_root}$devicetree" ]; then
			info "Missing ${esp_root}$devicetree, ignoring entry for prediction"
			continue
		fi
		n=$((n+1))
		[ "$n" -le 4 ] || {
			info "More than 4 variations for 710-grub2-bls-kernel-initrd-entry"
			continue
		}
		pcrlock_grub2_bls_kernel_initrd_cmdline_initrd \
			"${esp_root}$linux" "${esp_root}$initrd" \
			"${devicetree:+${esp_root}$devicetree}" \
			"BOOT_IMAGE=${grub2_bls_drive}$linux $cmdline" \
			"$n"
	done < <(jq --raw-output 'sort_by(.priority, (.kernel | map(-.))) | .[] | .options, .linux, .initrd[0], (.devicetree // "")' "$entryfile")

	# Generate variation for
	# 710-grub2-bls-kernel-initrd-kernel-cmdline-initrd-entry for
	# the same reason than before.
	n=0
	if [ "$SDB_ADD_INITIAL_COMPONENT" = "1" ]; then
		while read -r cmdline; do
			read -r linux
			read -r initrd
			read -r devicetree
			n=$((n+1))
			pcrlock_grub2_bls_kernel_initrd_cmdline_initrd \
				"${tmpdir}$linux" "${tmpdir}$initrd" \
				"${devicetree:+${tmpdir}$devicetree}" \
				"BOOT_IMAGE=${grub2_bls_drive}$linux $cmdline" \
				"0-$n"
		done < <(jq --raw-output '.[] | .options, .linux, .initrd[0], (.devicetree // "")' "$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.
	#
	# The masked components (see `mask_unmatched_components`) are
	# also a statement about the current event log, and expire in
	# the same way.  If the measurement that is missing today is
	# done in a later boot, for example because the initrd gained
	# `systemd-pcrextend`, the mask would hide the component that
	# owns it, and the new records would drop the PCR from the
	# policy.  Unlike a missing component, that is reported and
	# can be masked, an unclaimed record cannot be recovered from,
	# so the masks are dropped and re-evaluated after every reboot.
	# They cannot be recognized by name, as a mask is required to
	# be named after the component that it hides, but they are the
	# only empty files in the directory
	local btime
	read -r _ btime < <(grep btime /proc/stat)
	local minutes=$((1 + ($(date +%s) - btime) / 60))
	dbg "Cleaning shifted and masked measurements older than $minutes minutes"
	find /var/lib/pcrlock.d -type f \
	     \( -name 'shift-*.pcrlock' -o \( -name '*.pcrlock' -empty \) \) \
	     -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 openQA 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/* || true
	fi
}

get_predicted_hashes()
{
	find /var/lib/pcrlock.d/ -name "*.pcrlock" -type f -exec jq -r '.records[].digests[] | select(.hashAlg == "sha256") | .digest' {} + | sort -u
}

mask_unmatched_components()
{
	# `systemd-pcrlock` drops from the protection mask every PCR
	# that is touched by a component that cannot be found in the
	# event log.  Some of the components shipped in
	# /usr/lib/pcrlock.d describe measurements that the system
	# never does.  For example `750-enter-initrd` and
	# `750-os-separator` are measured by `systemd-pcrextend`, that
	# is not always present in the initrd.  As `750-os-separator`
	# touches PCRs 0-7, 9 and 12-14, a single missing component is
	# enough to leave the policy without a single PCR.
	#
	# A component is masked with an empty file placed in a
	# directory with more priority.  The name of the mask has to be
	# the name of the component, as `systemd-pcrlock` collects the
	# components with `conf_files_list_strv`, that resolves the
	# priority and the masking by file name.  A symlink to
	# /dev/null is also understood as a mask, but the SELinux
	# policy does not allow `lnk_file` in the pcrlock directory.
	# Only the components provided by the system are masked: the
	# ones generated here are expected to align, and if they do not
	# the problem is somewhere else.
	#
	# The mask describes the current event log, so it is dropped
	# after a reboot by `clean_pcrlock_d`, like the shifted
	# measurements.
	#
	# Returns 0 if something was masked, so the caller knows that
	# the prediction is stale
	local component masked=1
	while read -r component; do
		[ -e "/usr/lib/pcrlock.d/$component.pcrlock" ] || \
		    [ -d "/usr/lib/pcrlock.d/$component.pcrlock.d" ] || {
			warn "Component $component cannot be found in the event log"
			continue
		}
		[ ! -e "/var/lib/pcrlock.d/$component.pcrlock" ] || continue
		info "Masking component $component (not found in the event log)"
		: > "/var/lib/pcrlock.d/$component.pcrlock"
		masked=0
	done < <(sed -n "s/^Couldn't find component '\(.*\)' in event log\.\$/\1/p" "$tmpfile")

	return "$masked"
}

predict_pcrs()
{
	local pcrs="$1"

	# The components that cannot be aligned are reported at
	# `notice` level, and `mask_unmatched_components` needs them
	SYSTEMD_LOG_LEVEL=notice pcrlock --pcr="$pcrs" --json=short predict 2>"$tmpfile"
}

get_final_pcrs()
{
	local pcrs="$1"

	# Generate a JSON prediction, to get the final PCRs that will
	# participate in the policy.  To avoid the PolicyOR limit, we
	# filter the PCRs with more that 8 values
	#
	# The status is collected in the same command: with `set -e` a
	# failing command substitution ends the shell before the next
	# line can read `$?`, and the error message that is in $tmpfile
	# would never be printed
	local output
	local pcrlock_status=0
	output="$(predict_pcrs "$pcrs")" || pcrlock_status=$?

	if [ "$pcrlock_status" -ne 0 ]; then
		warn "Error creating the systemd-pcrlock prediction!\n$(cat "$tmpfile")"
		return 1
	fi

	# Predict again if some component was masked, as the previous
	# prediction was done with it in place
	if mask_unmatched_components; then
		pcrlock_status=0
		output="$(predict_pcrs "$pcrs")" || pcrlock_status=$?

		if [ "$pcrlock_status" -ne 0 ]; then
			warn "Error creating the systemd-pcrlock prediction!\n$(cat "$tmpfile")"
			return 1
		fi
	fi

	# An empty prediction means that no PCR survived the event log
	# alignment.  A policy can still be created from it, but it
	# will not bind the NVIndex to any PCR, silently dropping the
	# protection that the user asked for
	[ "$output" != "{}" ] || {
		warn "No PCR from $pcrs can be predicted, the event log cannot be aligned!\n$(cat "$tmpfile")"
		return 1
	}

	# PCRs that participate in the policy
	local pcrs_policy
	pcrs_policy="$(jq -r '[.sha256[] | select(.values | length <= 8) | .pcr] | join(",")' <<<"$output")"

	# PCRs dropped because they have more than 8 values
	local pcrs_policyor
	pcrs_policyor="$(jq -r '[.sha256[] | select(.values | length > 8) | .pcr] | join(",")' <<<"$output")"

	# The lists are comma separated but do not end with a comma, so
	# `read -d ','` would always drop the last PCR
	local i
	local -a list
	local -A pcrs_policy_table
	IFS=',' read -r -a list <<<"$pcrs_policy"
	for i in "${list[@]}"; do
		pcrs_policy_table[$i]=1
	done

	local -A pcrs_policy_missing_table
	IFS=',' read -r -a list <<<"$pcrs"
	for i in "${list[@]}"; do
		if [ -z "${pcrs_policy_table[$i]}" ]; then
			pcrs_policy_missing_table[$i]=1
		fi
	done

	local pcrs_policy_missing
	pcrs_policy_missing="$(IFS=","; echo "${!pcrs_policy_missing_table[*]}")"
	# `systemd-pcrlock` can report that a PCR matches the event log
	# and still predict a set of values that does not contain the
	# current one, for example when a component is replayed in the
	# wrong order.  The resulting policy is valid, but the device
	# will not be unsealed after a reboot that changes nothing, so
	# it is better to report it
	local pcr_current
	for i in "${!pcrs_policy_table[@]}"; do
		[ -e "/sys/class/tpm/tpm0/pcr-sha256/$i" ] || continue
		read -r pcr_current < "/sys/class/tpm/tpm0/pcr-sha256/$i"
		jq -e --argjson pcr "$i" --arg value "${pcr_current,,}" \
			'[.sha256[] | select(.pcr == $pcr) | .values[]] | index($value)' \
			<<<"$output" > /dev/null || \
			warn "The prediction for PCR $i does not contain the current value"
	done

	[ -z "$pcrs_policy_missing" ] || info "PCRs $pcrs_policy_missing dropped from the policy (component mapping)"
	[ -z "$pcrs_policyor" ] || info "PCRs $pcrs_policyor dropped from the policy (PolicyOR)"
	[ "$pcrs" = "$pcrs_policy" ] || info "Final PCRs list ($pcrs -> $pcrs_policy)"

	echo "$pcrs_policy"
}

generate_tpm2_predictions_pcrlock()
{
	local pcrs="$FDE_SEAL_PCR_LIST"

	info "Generating TPM2 predictions"

	# 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
	# Generates 620-secureboot-authority when the verb cannot
	pcrlock_secureboot_sbatlevel

	# Uses / by default, but firmware measures GPT of the disk a
	# boot application was loaded from, which is effectively the
	# disk where our ESP is located.
	shift_component 600-gpt
	pcrlock lock-gpt "$esp_root"

	# Measure the boot loader.  Combinations:
	#   - Removable media with shim: BOOTX64.EFI, grub.efi
	#   - Removable media w/out shim: BOOTX64.EFI
	#   - Installed system with shim: shim.efi, grub.efi
	#   - Installed system w/out shim and grub2-bls: grub.efi
	#   - Installed system w/out shim and systemd-boot: systemd-bootx64.efi
	local shim_path bootloader_path
	if is_shim_installed; then
		if [ -n "$arg_portable" ]; then
			shim_path="${esp_root}/EFI/BOOT/BOOT${firmware_arch^^}.EFI"
			bootloader_path="${esp_root}/EFI/BOOT/grub.efi"
		else
			shim_path="${esp_root}${esp_dst}/shim.efi"
			bootloader_path="${esp_root}${esp_dst}/grub.efi"
		fi
	else
		local bootloader_filename
		if is_sdboot; then
			bootloader_filename="systemd-boot${firmware_arch,,}.efi"
		elif is_grub2_bls; then
			bootloader_filename="grubbls.efi"
		fi
		if [ -n "$arg_portable" ]; then
			bootloader_path="${esp_root}/EFI/BOOT/BOOT${firmware_arch^^}.EFI"
		else
			bootloader_path="${esp_root}${esp_dst}/${bootloader_filename}"
		fi
	fi

	if [ -n "$shim_path" ]; then
		# 630-shim-efi-application is not part of the pcrlock
		# standards
		# TODO: move to shim-pcrlock.rpm
		shift_component 630-shim-efi-application
		pcrlock \
			lock-pe \
			--pcrlock=/var/lib/pcrlock.d/630-shim-efi-application.pcrlock.d/generated.pcrlock \
			"${shim_path}"
	fi

	# 640-boot-loader-efi-application is not part of the
	# pcrlock standards
	# This is measuring the grub / systemd-boot EFI binary
	# TODO: move to systemd-boot-pcrlock.rpm / grub2-bls.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 \
		"${bootloader_path}"

	if is_sdboot; then
		pcrlock_sdboot
	elif is_grub2_bls; then
		pcrlock_grub2_bls
	fi

	# The copy in the ESP is imported by `dracut-pcr-signature`,
	# and can be missing after a new ESP installation.  Both copies
	# are identical, so a missing one can be restored from the
	# other, and by itself it does not require a new policy.
	#
	# Restoring the local copy also matters because it stores the
	# PIN that `systemd-pcrlock` needs to update the NVIndex.
	# Without it a new policy cannot replace the current one
	if [ -e /var/lib/systemd/pcrlock.json ] && [ ! -e "${esp_root}${esp_dst}/pcrlock.json" ]; then
		dbg "Restoring the pcrlock policy in the ESP"
		cp /var/lib/systemd/pcrlock.json "${esp_root}${esp_dst}" || \
			warn "Cannot copy the pcrlock policy into the ESP"
	elif [ ! -e /var/lib/systemd/pcrlock.json ] && [ -e "${esp_root}${esp_dst}/pcrlock.json" ]; then
		dbg "Restoring the pcrlock policy from the ESP"
		cp "${esp_root}${esp_dst}/pcrlock.json" /var/lib/systemd/ || \
			warn "Cannot restore the pcrlock policy from the ESP"
	fi

	# Decide if the current policy can be reused before running
	# any expensive operation (`predict` and `make-policy`), and
	# before asking for any PIN.
	#
	# The hashes that can participate in the policy are compared
	# with the ones that were used to build the current policy, and
	# not with the ones present before regenerating the components.
	# The components are also regenerated at boot by
	# `sdbootutil-update-predictions.service`, so a change in the
	# firmware is already absorbed by `/var/lib/pcrlock.d` before
	# the user can call this command.  Comparing with the state of
	# this run would make such a change invisible forever, and only
	# wiping `/var/lib/pcrlock.d` would recover.
	#
	# Duplicated hashes do not add new values, so the list is
	# deduplicated
	local post_hashes
	post_hashes="$(get_predicted_hashes)"

	if [ -n "$arg_force" ]; then
		dbg "A new policy is forced (--force)"
	elif [ ! -e /var/lib/systemd/pcrlock.json ]; then
		# There is no policy to reuse, independently of how
		# the components compare
		dbg "Missing /var/lib/systemd/pcrlock.json"
	elif [ ! -e /var/lib/sdbootutil/pcrlock-hashes ]; then
		# The policy was created by a version that did not
		# track the hashes, so there is nothing to compare
		# with.  `make-policy` will detect that the prediction
		# is unchanged and will keep the NVIndex untouched
		dbg "Missing /var/lib/sdbootutil/pcrlock-hashes"
	elif ! echo "$post_hashes" | cmp -s - /var/lib/sdbootutil/pcrlock-hashes; then
		dbg "The components changed since the current policy was created"
	else
		info "Policy is up to date"
		return 0
	fi

	local final_pcrs
	final_pcrs="$(get_final_pcrs "$pcrs")" || return 1

	# 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
		# When enrolling, we should ask two times
		if [ -n "$arg_method" ]; then
			info "The recovery PIN is used when the TPM2 policy needs to be updated"
			ask_new_password "recovery PIN" pin
		else
			ask_password "Recovery PIN" pin
		fi
		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

	# Rewrite the NV index even if systemd-pcrlock considers the
	# prediction unchanged
	[ -z "$arg_force" ] || extra+=("--force")

	# `systemd-pcrlock` skips the update when the new prediction is
	# identical to the one stored in the current policy, and it does
	# so before authenticating against the TPM2.  The recovery PIN is
	# then never used, and an incorrect one is silently accepted.
	# The policy file is rewritten only when the NVIndex is updated,
	# so it can be used to tell both cases apart
	local policy_digest=
	[ ! -e /var/lib/systemd/pcrlock.json ] || \
		policy_digest="$(sha256sum < /var/lib/systemd/pcrlock.json)"

	# Like in `get_final_pcrs`, the status cannot be read in the next
	# line: `set -e` would end the script here, without printing the
	# reason that `systemd-pcrlock` left in $tmpfile
	local output
	local pcrlock_status=0
	output="$(PIN="$pin" pcrlock --pcr="$final_pcrs" "${extra[@]}" make-policy 2>"$tmpfile")" || pcrlock_status=$?

	# `systemd-pcrlock` explains in stderr what it did, but the
	# messages are discarded when the command succeeds
	dbg_cat "$tmpfile"

	if [ "$pcrlock_status" -ne 0 ]; then
		warn "Error creating the systemd-pcrlock policy!\n$(cat "$tmpfile")"
		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

	[ -e /var/lib/systemd/pcrlock.json ] || {
		warn "Missing /var/lib/systemd/pcrlock.json after the policy creation"
		return 1
	}

	# Track the hashes used for this policy, so a later run can
	# detect that the components diverged from it
	echo "$post_hashes" > /var/lib/sdbootutil/pcrlock-hashes

	# Publish the assets in the ESP, so can be imported by
	# dracut-pcr-signature
	cp /var/lib/systemd/pcrlock.json "${esp_root}${esp_dst}" || \
		warn "Cannot copy the pcrlock policy into the ESP"

	# With --force the NVIndex is always rewritten, and the policy
	# file can still be identical: the serializations of the NVIndex,
	# the SRK and the sealed PIN are reused from the old policy, so
	# only the prediction can make it different
	if [ -n "$arg_force" ] || \
	    [ "$policy_digest" != "$(sha256sum < /var/lib/systemd/pcrlock.json)" ]; then
		echo "NVIndex policy created"
	else
		info "The prediction did not change, so the NVIndex was not updated"
		# Only the TPM2 can tell if the recovery PIN is
		# correct, and it is asked to do it when the NVIndex is
		# written.  Without this message a wrong PIN looks
		# accepted, as the command succeeds
		[ -z "$pin" ] || \
			warn "The recovery PIN was not needed, so it was not validated. Use --force to rewrite the NVIndex with it"
	fi
}

get_device_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 "$PW" ]; then
		pw="$PW"
	elif [ -n "$KEY" ]; then
		pw="$KEY"
	elif [ -n "$keyid_ce" ]; then
		pw="$(keyctl pipe "$keyid_ce")"
	elif [ -n "$keyid" ]; then
		pw="$(keyctl pipe "$keyid")"
	elif [ -n "$arg_measure_pcr" ] || [ -n "$arg_ask_key_pin_or_pw" ]; then
		# Ask the password only as a last resort, and only if
		# --measure-pcr is set
		ask_password "Password for $dev" pw
	fi

	if [ -n "$pw" ]; then
		keyctl_add_with_timeout "cryptenroll" "$pw"
	fi

	echo "$pw"
}

get_volume_key()
{
	local dev="$1"
	local pw out
	pw="$(get_device_password "$dev")"
	out="$(cryptsetup luksDump --batch-mode --dump-master-key "$dev" 2> /dev/null<<<"$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 &> /dev/null || true
		keyctl reap &> /dev/null || true

		# Try one more time
		pw="$(get_device_password "$dev")"
		# The error is signaled by an empty volume key
		out="$(cryptsetup luksDump --batch-mode --dump-master-key "$dev" 2> /dev/null<<<"$pw")" || return 0
	}
	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

	command -v "${dgst}sum" &> /dev/null || { warn "${dgst}sum not found"; return 1; }
	hex_to_binary "$pcr$val" > "$tmpdir/pcr"
	read -r digest _ < <("${dgst}sum" "$tmpdir/pcr")
	echo "$digest"
}

# Create the key pair that signs the PCR 15 prediction, if it is not
# already there.  Returns 0 only when a new key pair was created: the
# public key is copied into the initrd by dracut (module-setup.sh), so
# any initrd generated before this point cannot validate PCR 15
create_measure_pcr_keys()
{
	local private="/var/lib/sdbootutil/measure-pcr-private.pem"
	local public="/var/lib/sdbootutil/measure-pcr-public.pem"

	[ ! -f "$private" ] || [ ! -f "$public" ] || return 1

	if [ ! -f "$private" ]; then
		info "Creating the key pair that signs the PCR 15 prediction"
		# A public key that comes from a different private key
		# cannot validate anything
		rm -f "$public"
		openssl genrsa -out "$private" 4096
	fi
	# 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
}

generate_tpm2_predictions_pcr_15()
{
	local devs=()
	local msgs=()
	local vks=()
	local name dev opts extra 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).  Also get
		# the mount point so we can identify /etc or /var.
		#
		# According to https://systemd.io/MOUNT_REQUIREMENTS/
		# /etc and swap are mounted in initrd, and /var is
		# mounted after initrd, but in MicroOS (via
		# microos-tools) is adding /var to be mounted in the
		# initrd stage too because of selinux
		# (98selinux-microos).
		extra="$(lsblk --noheadings -o FSTYPE,MOUNTPOINT "$dev")"
		if [[ "$extra" != *"swap"* ]] && [[ "$extra" != *"/etc"* ]] && { [[ "$extra" != *"/var"* ]] || ! is_transactional; }; 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

	# The key pair is created during the enrollment, but not when
	# `--no-measure-pcr` is used.  If PCR 15 is enabled later in
	# /etc/crypttab there is nothing to sign the prediction with,
	# and an unsigned prediction also halts the boot
	local private="/var/lib/sdbootutil/measure-pcr-private.pem"
	if create_measure_pcr_keys; then
		warn "The public key is only added to the initrd by 'sdbootutil mkinitrd'"
	fi

	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]}" | /usr/libexec/sdbootutil/uhmac "${dgsts[i]}" <(echo "${vks[j]}"))"
			# `extend_pcr` reports why it failed, but with
			# `set -e` the script would end here, leaving
			# an incomplete and unsigned prediction behind.
			# Drop only the affected hash algorithm
			pcr15="$(extend_pcr "${dgsts[i]}" "$pcr15" "$hmac")" || continue 2
			dbg "${msgs[j]} (${dgsts[i]}): $hmac"
		done
		echo "$pcr15" >> "/var/lib/sdbootutil/measure-pcr-prediction"
	done

	# An unsigned prediction is worse than no prediction at all, as
	# the ESP can still contain the signature of a previous one
	openssl dgst -sha256 \
		-sign "$private" \
		-out /var/lib/sdbootutil/measure-pcr-prediction.sha256 \
		/var/lib/sdbootutil/measure-pcr-prediction || {
		rm -f /var/lib/sdbootutil/measure-pcr-prediction
		rm -f /var/lib/sdbootutil/measure-pcr-prediction.sha256
		rm -f "${esp_root}${esp_dst}/measure-pcr-prediction"
		rm -f "${esp_root}${esp_dst}/measure-pcr-prediction.sha256"
		err "The PCR 15 prediction cannot be signed with $private"
	}

	# 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 "${esp_root}${esp_dst}"
	[ ! -e /var/lib/sdbootutil/measure-pcr-prediction.sha256 ] || \
		cp /var/lib/sdbootutil/measure-pcr-prediction.sha256 "${esp_root}${esp_dst}"
}

updated_crypttab()
{
	local crypttab_sha1
	grep -q "tpm2-measure-pcr=yes" /etc/crypttab || return 0
	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"

	info "Updating predictions"

	[ -z "$TRANSACTIONAL_UPDATE" ] || [ -n "$arg_force" ] || {
		warn "Inside transactional-update. Updating predictions must be done outside the transaction"
		return 0
	}

	[ -z "${DEVICETREE_SOURCE:-}" ] || ! is_sdboot || {
		warn "TPM2 predictions with systemd-boot devicetree entries are not validated. Use --disable-predictions if this affects boot."
	}

	! is_pcr_oracle || {
		warn "System enrolled with pcr-oracle. Re-enroll to use systemd-pcrlock"
		return 0
	}

	# is_pcr_oracle() is false once the pcr-oracle package is
	# gone, but the LUKS2 token can still require a signed PCR
	# policy that nothing signs anymore, in which case the pcrlock
	# predictions are updated but the TPM2 unlock keeps falling
	# back to the password.  Only a re-enrollment fixes that.
	local dev
	if detect_tracked_devices; then
		for dev in "${tracked_devices[@]}"; do
			! has_signed_pcr_policy "$dev" || \
				warn "$dev is enrolled with a signed PCR policy. Re-enroll to use systemd-pcrlock"
		done
	fi

	generate_tpm2_predictions_pcrlock

	# 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)
	#
	# For initial enrollments the LUKS2 key will be in the
	# keyring, so no password will be asked.  Internally the
	# function that ask for the volume key password will check
	# arg_measure_pcr (--measure-pcr)
	local missing_pcr_15=
	updated_crypttab || {
		generate_tpm2_predictions_pcr_15
		# shellcheck disable=SC2015
		if ! updated_crypttab || [ ! -f "/var/lib/sdbootutil/measure-pcr-prediction" ]; then
			warn "File measure-pcr-prediction should be updated"
			warn "Call sdbootutil update-predictions --measure-pcr"
			missing_pcr_15=1
		fi
	}

	# The user can remove measure-pcr-prediction file from the
	# ESP, but generate_tpm2_predictions_pcr_15 will not be called
	[ -f "${esp_root}${esp_dst}/measure-prediction" ] || {
		[ ! -e /var/lib/sdbootutil/measure-pcr-prediction ] || \
			cp /var/lib/sdbootutil/measure-pcr-prediction "${esp_root}${esp_dst}"
		[ ! -e /var/lib/sdbootutil/measure-pcr-prediction.sha256 ] || \
			cp /var/lib/sdbootutil/measure-pcr-prediction.sha256 "${esp_root}${esp_dst}"
	}

	# /etc/crypttab measures PCR 15 but the prediction does not
	# match it anymore, so `measure-pcr-validator` will halt the
	# next boot.  Report it to the caller of "update-predictions",
	# that can be a script that is about to do something
	# irreversible, like removing the key that is needed to
	# recompute the prediction.  When the predictions are a side
	# effect of another command (a kernel or a bootloader update)
	# the warning is enough, as failing there would also abort the
	# RPM transaction that triggered it
	[ -z "$missing_pcr_15" ] || [ -z "$explicit_predictions" ] || return 1
}

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
		if [ -e /etc/crypttab ]; then
			grep -E -q "(${dev}|${uuid})" /etc/crypttab || continue
			grep -E -q "${dev}[[:space:]].*x-sdbootutil.ignore" /etc/crypttab && continue
			grep -E -q "${uuid}[[:space:]].*x-sdbootutil.ignore" /etc/crypttab && continue
		fi
		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")
}

has_signed_pcr_policy()
{
	# A "tpm2_pubkey" field in the token means that a shard of the
	# sealed key is locked to a signed PCR policy, and that
	# unlocking the device also requires a tpm2-pcr-signature.json
	local dev="${1:?}"
	cryptsetup luksDump --dump-json-metadata "$dev" 2> /dev/null | \
		jq -e 'any(.tokens[]?; .type == "systemd-tpm2" and has("tpm2_pubkey"))' &> /dev/null
}

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

is_same_device()
{
	local dev1="$1"
	local dev2="$2"

	# If the device name is UUID=, convert as a real device name
	[[ "$dev1" = "UUID="* ]] && dev1="$(blkid --uuid "${dev1#"UUID="}")"
	[[ "$dev2" = "UUID="* ]] && dev2="$(blkid --uuid "${dev2#"UUID="}")"

	dev1="$(readlink -f "$dev1")"
	dev2="$(readlink -f "$dev2")"

	local id1 id2
	id1="$(stat -c "%t:%T" "$dev1")"
	id2="$(stat -c "%t:%T" "$dev2")"

	[ "$id1" = "$id2" ]
}

add_crypttab_option()
{
	local dev="$1"
	local option="$2"

	dbg "Adding \"$option\" for \"$dev\" 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 is_same_device "$dev" "$device" && [[ "$opts" != *"x-sdbootutil.ignore"* ]] && [[ "$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\" for \"$dev\" to /etc/crypttab"
	dbg_cat "/etc/crypttab"
}

remove_crypttab_option()
{
	local dev="$1"
	local option="$2"

	dbg "Removing \"$option\" for \"$dev\" 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 is_same_device "$dev" "$device" && [[ "$opts" != *"x-sdbootutil.ignore"* ]] && [[ "$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\" for \"$dev\" to /etc/crypttab"
	dbg_cat "/etc/crypttab"
}

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_tpm2()
{
	local dev="$1"
	local tpm2_pin="$2"
	local extra_args=()

	if [ -z "$tpm2_pin" ]; then
		info "Enrolling with TPM2: $dev"
	else
		info "Enrolling with TPM2+PIN: $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

	# Make sure that the LUKS2 password is in cryptenroll.  If
	# not, systemd-cryptenroll will ask one
	get_device_password &> /dev/null

	# Unless an empty --tpm2-public-key is passed,
	# systemd-cryptenroll loads "tpm2-pcr-public-key.pem" from the
	# configuration directories and adds a signed PCR policy as a
	# second shard of the sealed key, next to the pcrlock one.
	local pcr_oracle_key
	if pcr_oracle_key="$(pcr_oracle_public_key)"; then
		warn "Ignoring $pcr_oracle_key for the enrollment. Remove it, and the private key, if pcr-oracle is not used anymore"
	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 \
		 --tpm2-public-key= \
		 "${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

	# Not all PCR predictions match the current system state and
	# will be excluded from the policy. Trigger an update on the
	# next boot which should include them
	systemctl --quiet enable sdbootutil-update-predictions.service || true
}

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")
			enroll_tpm2 "$dev" "$pin_or_pw"
			;;

		"fido2")
			enroll_fido2 "$dev"
			;;

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

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

		*)
			local msg
			if [ -n "$arg_method" ]; then
				msg="Unexpected parameter for --method=: $arg_method"
			else
				msg="Missing value for parameter --method="
			fi
			err "$msg\nValid methods: tpm2,tpm2+pin,fido2,password,recovery-key"
			;;
	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[*]}"

	# Prepare /etc/crypttab and update initrd if required
	case "$arg_method" in
		"tpm2"|"tpm2+pin")
			have_tpm2 || err "No TPM2 found"
			have_pcrlock || err "No systemd-pcrlock found"
			! in_lockout || err "The TPM2 is in lockout. Use 'tpm2_dictionarylockout -c [ -p passwd ]' to continue"
			for dev in "${tracked_devices[@]}"; do
				add_crypttab_option "$dev" 'tpm2-device=auto'
				[ -n "$arg_no_measure_pcr" ] || add_crypttab_option "$dev" 'tpm2-measure-pcr=yes'
			done
			;;

		"fido2")
			have_fido2 || err "No FIDO2 key found"
			for dev in "${tracked_devices[@]}"; do
				add_crypttab_option "$dev" 'fido2-device=auto'
			done
			;;
	esac

	# For predicting PCR 15 we need to sign a file.  Create the
	# public and private key if missing, before the initrd is
	# regenerated below, as the public key is installed inside it
	if { [ "$arg_method" = "tpm2" ] || [ "$arg_method" = "tpm2+pin" ]; } && [ -z "$arg_no_measure_pcr" ]; then
		create_measure_pcr_keys || :
	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"
	fi

	if [ "$arg_method" = "tpm2" ] || [ "$arg_method" = "tpm2+pin" ]; then
		# During the initial enrollment it is expected that
		# for systemd-pcrlock the recovery PIN will be
		# extracted from the %u keyring "sdbootutil[-pin]"
		# entry
		#
		# This is done even when /etc/crypttab did not change,
		# as the enrollment points to an NV index that can be
		# missing (for example after a `tpm2_clear`) or to a
		# policy that does not match the current system
		generate_tpm2_predictions
		# Avoid the call of generate_tpm2_predictions at the
		# end of the script
		update_predictions=
	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 "$PIN" ]; then
			pin_or_pw="$PIN"
		elif [ -n "$keyid" ]; then
			pin_or_pw="$(keyctl pipe "$keyid")"
		else
			ask_new_password "TPM2 PIN" pin_or_pw
		fi
	elif [ "$arg_method" = "password" ]; then
		keyid="$(keyctl id %user:sdbootutil 2> /dev/null)" || true
		if [ -n "$PW" ]; then
			pin_or_pw="$PW"
		elif [ -n "$keyid" ]; then
			pin_or_pw="$(keyctl pipe "$keyid")"
		else
			ask_new_password "password" pin_or_pw
		fi
	fi

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

unenroll_all_pcrlock()
{
	systemctl --quiet disable sdbootutil-update-predictions.service || true
	pcrlock remove-policy &> /dev/null || true
	rm -fr /var/lib/pcrlock.d/* || true
	rm -f /var/lib/systemd/pcrlock.json
	rm -f "${esp_root}${esp_dst}/pcrlock.json"
	rm -f /var/lib/sdbootutil/pcrlock-hashes
	rm -f /var/lib/sdbootutil/crypttab.sha1
	rm -f /var/lib/sdbootutil/measure-pcr-private.pem
	rm -f /var/lib/sdbootutil/measure-pcr-public.pem
	rm -f /var/lib/sdbootutil/measure-pcr-prediction
	rm -f /var/lib/sdbootutil/measure-pcr-prediction.sha256
	rm -f "${esp_root}${esp_dst}/measure-pcr-prediction"
	rm -f "${esp_root}${esp_dst}/measure-pcr-prediction.sha256"
}

unenroll_all_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 "${esp_root}${esp_dst}/tpm2-pcr-public-key.pem"
	rm -f "${esp_root}${esp_dst}/tpm2-pcr-signature.json"
	rm -f /var/lib/sdbootutil/measure-pcr-prediction
	rm -f /var/lib/sdbootutil/measure-pcr-prediction.sha256
	rm -f "${esp_root}${esp_dst}/measure-pcr-prediction"
	rm -f "${esp_root}${esp_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"
			;;

		*)
			local msg
			if [ -n "$arg_method" ]; then
				msg="Unexpected parameter for --method=: $arg_method"
			else
				msg="Missing value for parameter --method="
			fi

			local methods=()
			local line
			while read -r line; do
				[[ "$line" =~ [[:digit:]]+[[:space:]] ]] || continue
				IFS=" " read -r -a s <<<"$line"
				methods+=("${s[1]}")
			done < <(systemd-cryptenroll "$dev")
			if [ "${#methods[@]}" -eq 0 ]; then
				err "$msg\nThe device $dev has no enrollment"
			else
				local methods
				methods="$(IFS=','; echo "${methods[*]}")"
				err "$msg\nMethods present in $dev: $methods"
			fi
			;;
	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"
			for dev in "${tracked_devices[@]}"; do
				remove_crypttab_option "$dev" 'tpm2-device=auto'
				remove_crypttab_option "$dev" 'tpm2-measure-pcr=yes'
			done
			;;

		"fido2")
			have_fido2 || err "No FIDO2 key found"
			for dev in "${tracked_devices[@]}"; do
				remove_crypttab_option "$dev" 'fido2-device=auto'
			done
			;;
	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

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

	# We can have a partial unenroll, so we drop the policy and
	# the assets only if there is no device left with a "tpm2"
	# slot.  Multiple devices shares the same NVIndex.
	if [ "$arg_method" = "tpm2" ] || [ "$arg_method" = "tpm2+pin" ]; then
		local unenroll_all=1
		# Reset the detected devices
		tracked_devices=()
		detect_tracked_devices
		for dev in "${tracked_devices[@]}"; do
			if have_slot "$dev" "tpm2"; then
				unenroll_all=0
				break
			fi
		done
		if [ "$unenroll_all" = 1 ]; then
			unenroll_all_pcrlock
			unenroll_all_pcr_oracle
		fi
	fi
}

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')"
	esp_root="$(bootctl --print-esp-path)"
	boot_root="$(bootctl --print-boot-path)"
}

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 0

	declare -gA arch_image_map=(
		[x64]="vmlinuz"
		[aa64]="Image"
		[riscv64]="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]=""
		[print-loader-path]=""
		[enroll]=""
		[unenroll]=""
		[update-predictions]=""
		[bootloader]=""
	)
}

define_options() {
	declare -gA options_with_arg=(
		[help]=""
		[verbose]=""
		[start-trace-code]=""
		[stop-trace-code]=""
		[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]=""
		[secure-boot]=""
		[removable]=""
		[only-default]=""
		[default-snapshot]=""
		[ask-key]=""
		[ask-pin]=""
		[ask-pw]=""
		[method]="_method"
		[no-measure-pcr]=""
		[measure-pcr]=""
		[pcr]="_none"
		[devices]="_devices"
		[rootfs]="_rootfs"
		[rootfs-data]="_rootfs_data"
		[esp-free-space]="_esp_free_space"
		[devicetree-source]="_file"
		[force]=""
		[disable-predictions]=""
	)
	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 ;;
		--start-trace-code) shift ;;
		--stop-trace-code) 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 ;;
		--secure-boot) arg_secure_boot=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 ;;
		--no-measure-pcr) arg_no_measure_pcr=1; shift ;;
		--measure-pcr) arg_measure_pcr=1; shift ;;
		--pcr) arg_pcr="$2"; shift 2 ;;
		--devices) IFS=',' read -r -a tracked_devices <<<"$2"; shift 2 ;;
		--rootfs) arg_rootfs="$2"; shift 2 ;;
		--rootfs-data) arg_rootfs_data="$2"; shift 2 ;;
		--esp-free-space) arg_esp_free_space="$2"; shift 2 ;;
		--devicetree-source) arg_devicetree_source="$2"; shift 2 ;;
		--force) arg_force=1; shift ;;
		--disable-predictions) arg_disable_predictions=1; shift ;;
		--) 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

# The variables documented in the help (PW, KEY, PIN) are read from
# the environment, so they must be set before the command name.
# `getopt` moves them to the end of the argument list, where they are
# indistinguishable from an optional [SNAPSHOT] and silently ignored,
# so a command like
#
#   sdbootutil update-predictions PW=secret --measure-pcr
#
# drops the password instead of using it
for variable_arg in "$@"; do
	[[ "$variable_arg" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]] || continue
	err "\"$variable_arg\" is not a command nor an option. Set the variable before the command name: ${variable_arg%%=*}=... ${0##*/} ..."
done

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

root_subvol=""
subvol_prefix=""
root_subvol_err=
is_btrfs_snapshots=
if [ "$(stat -f -c %T /)" = "btrfs" ] && [ -d /.snapshots ]; then
	is_btrfs_snapshots=1
	# `btrfs` lives in /usr/sbin, that is not present in every
	# PATH.  Without this check the only symptom is an empty
	# `root_subvol`, reported below as a generic "Can't determine
	# root subvolume" that hides the real cause
	command -v btrfs > /dev/null || \
		err "btrfs command not found. Install btrfsprogs or add /usr/sbin to the PATH"
	# Keep the error of the command, to report it together with
	# the failed detection below
	root_subvol=$(btrfs subvol show / 2> "$tmpdir/btrfs.err" | head -1)
	root_subvol_err="$(cat "$tmpdir/btrfs.err")" || :
	rm -f "$tmpdir/btrfs.err"
	subvol_prefix="${root_subvol%/.snapshots/*}"
	# The presence of "/.snapshots" is not enough: the system boots
	# from a snapshot only when the root subvolume is under
	# "<prefix>/.snapshots/<N>/snapshot", which is also the only
	# shape that "subvol_prefix" and "running_snapshot" can parse.
	# A plain "@" root with snapper installed (no rollback layout)
	# is a regular system, and treating it as a snapshot one builds
	# paths like "@/.snapshots/@/snapshot" that do not exist
	[[ "$root_subvol" =~ ^.+/\.snapshots/[0-9]+/snapshot$ ]] && have_snapshots=1
elif [ -e /.snapshots/1/info.xml ]; then
	# We can detect when we are in a image building situation
	# (mkosi) when we are not in btrfs, but there is a snapshots
	# directory partially available.  The `subvol_prefix` variable
	# is assumed what is used in openSUSE
	in_buildroot=1
	subvol_prefix="@"
fi

root_snapshot=""
running_snapshot=""
if [ -n "$have_snapshots" ]; then
	running_snapshot="${root_subvol#"${subvol_prefix}"/.snapshots/}"
	running_snapshot="${running_snapshot%/snapshot}"
	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="$running_snapshot"
	fi
	applied_snapshot="$(detect_applied_snapshot)"
elif [ -n "$in_buildroot" ]; then
	root_snapshot="1"
fi

if [ -n "$arg_esp_path" ] && [ "$esp_root" != "$arg_esp_path" ]; then
	err "Mismatch of ESP path"
fi
[ -n "$arg_arch" ] && firmware_arch="$arg_arch"

[ -n "$esp_root" ] || err "No ESP detected. Legacy system?"
[ -n "$root_subvol" ] || [ -z "$is_btrfs_snapshots" ] || \
	err "Can't determine root subvolume${root_subvol_err:+: $root_subvol_err}"
[ -n "$firmware_arch" ] || err "Can't determine firmware arch"
set_image_name

mountpoint -q "$esp_root" || warn "$esp_root is not a valid mountpoint"

dbg_var "root_snapshot"
dbg_var "running_snapshot"
dbg_var "applied_snapshot"
dbg_var "esp_root"
dbg_var "boot_root"

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

if is_sdboot; then
	esp_dst="/EFI/systemd"
elif is_grub2_bls; then
	set_os_release "${root_snapshot}"
	# shellcheck disable=SC2154
	read -r -a name <<<"${os_release_NAME,,}"
	esp_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

# Removable media layout is described in
# https://github.com/rhboot/shim/blob/main/README.fallback
if [ -n "$arg_portable" ]; then
	if [ -d "${esp_root}${esp_dst}" ]; then
		err "Bootloader is already installed permanently"
	fi
	esp_dst="/EFI/BOOT"
fi

# When we are doing an operation different from installation, the
# esp_dst should be already present.  We can use it to identify a
# removable installation
if [ "$1" != "install" ] && [ ! -d "${esp_root}${esp_dst}" ]; then
	info "Removable installation detected"
	esp_dst="/EFI/BOOT"
	arg_portable=1
fi

dbg_var "esp_dst"

if [ "$SECURE_BOOT" = "yes" ] || is_shim_installed; then
	arg_secure_boot=1
fi

if [ "$UPDATE_NVRAM" = "no" ]; then
	if has_efivars; then
		warn 'The system has EFI variables but UPDATE_NVRAM is set to "no"'
	fi
	arg_no_variables=1
fi

# If there is a config file, load it.  If not, but the bootloader was
# installed, create a default one (using the CLI arguments).  If not,
# use some default values.
#
# When loading a config file with missing values, the load code will
# use default values.
if is_config_file; then
	load_config_file
elif is_installed; then
	create_default_config_file
else
	set_default_config_values
fi

# 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)
		assert_snapshot "$2"
		install_bootloader "${2:-$root_snapshot}"
		install_extra_entries "${2:-$root_snapshot}"
		create_default_config_file ;;
	needs-update)
		bootloader_needs_update "${2:-$root_snapshot}" ;;
	update)
		assert_snapshot "$2"
		bootloader_update "${2:-$root_snapshot}" ;;
	force-update)
		if is_installed; then
			assert_snapshot "$2"
			install_bootloader "${2:-$root_snapshot}"
		else
			:
		fi ;;
	bootloader)
		bootloader_name "${2:-$root_snapshot}" ;;
	add-kernel)
		assert_snapshot "$3"
		install_kernel "${3:-$root_snapshot}" "$2" ;;
	add-all-kernels)
		assert_snapshot "$2"
		install_all_kernels "${2:-$root_snapshot}" ;;
	mkinitrd)
		assert_snapshot "$2"
		arg_no_reuse_initrd=1
		install_all_kernels "${2:-$root_snapshot}" ;;
	remove-kernel)
		assert_snapshot "$3"
		remove_kernel "${3:-$root_snapshot}" "$2" ;;
	remove-all-kernels)
		assert_snapshot "$2"
		remove_all_kernels "${2:-$root_snapshot}" ;;
	cleanup)
		cleanup_entries "${2:-}" ;;
	set-default-snapshot)
		assert_snapshot "$2"
		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)
		assert_snapshot "$3"
		update_entry "${3:-$root_snapshot}" "$2" ;;
	update-all-entries)
		assert_snapshot "$2"
		update_all_entries "${2:-$root_snapshot}" ;;
	is-bootable)
		is_bootable "${2:-$root_snapshot}" ;;
	set-default)
		assert_snapshot "$2"
		set_default_entry "$2" ;;
	get-default)
		get_default_entry "$2" ;;
	set-timeout)
		set_timeout "$2" ;;
	get-timeout)
		get_timeout "$2" ;;
	print-loader-path)
		print_loader_path ;;
	enroll)
		enroll ;;
	unenroll)
		unenroll ;;
	update-predictions)
		update_predictions=1
		explicit_predictions=1 ;;
	*)
		helpandquit ;;
esac

[ -z "$update_predictions" ] || [ -n "$arg_disable_predictions" ] || generate_tpm2_predictions
