#!/bin/sh
# SPDX-License-Identifier: MIT

# disable noisy shellcheck warnings
# shellcheck disable=SC2039,SC1090,SC2165,SC2167,SC2064,SC2015
# shellcheck disable=SC2247,SC2119,SC1090,SC2120,SC3043,SC3013
# shellcheck disable=SC2016 # $ in single quotes on purpose

if command -v gettext >/dev/null; then
	_gettext() { TEXTDOMAINDIR="$TEXTDOMAINDIR" TEXTDOMAIN=mkswu gettext -- "$@"; }
else
	_gettext() { printf -- "%s" "$@"; }
fi

printmsg() {
	local level="$1"
	shift
	[ "$VERBOSE" -lt "$level" ] && return

	if [ "$#" = "0" ] || [ -z "$1" ]; then
		echo
		return
	fi

	local fmt="$1"
	shift
	# shellcheck disable=SC2059 # fmt is a build time constant
	printf -- "$(_gettext "$fmt")\n" "$@"
}

error() {
	if [ "$#" = "0" ] || [ -z "$1" ]; then
		error "Error called without format string!"
		exit 1
	fi

	local fmt="$1"
	shift
	# shellcheck disable=SC2059 # fmt is a build time constant
	printf -- "ERROR: $(_gettext "$fmt")\n" "$@" >&2
	exit 1
}

warning() {
	printmsg 1 "$@" >&2
}

info() {
	printmsg 2 "$@"
}

trace() {
	printmsg 3 "$@" >&2
}

debug() {
	printmsg 4 "$@" >&2
}

# dash doesn't handle read -s
# and even if it did we wouldn't have gettext wrapping
# so provide our own helper
prompt() {
	local var="$1"
	local prompt_fmt="$2"
	shift 2

	if [ -n "$PASS" ] && tty -s; then
		trap 'stty echo; echo; error "Interrupted by user"' INT QUIT
		trap 'stty echo' EXIT
		stty -echo
	fi

	# shellcheck disable=SC2059 # fmt is a build time constant
	printf -- "$(_gettext "$prompt_fmt") " "$@"
	read -r "${var?}"

	if [ -n "$PASS" ] && tty -s; then
		stty echo
		trap "" EXIT INT QUIT
		echo
	fi
}

prompt_yesno() {
	local default="$1"
	shift 1
	local yesno

	while true; do
		prompt yesno "$@"
		[ -n "$yesno" ] || yesno="$default"
		case "$yesno" in
		[Yy]|[Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|1)
			return 0;;
		[Nn]|[Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|0)
			return 1;;
		esac
	done
}

usage() {
	info "Usage: %s [opts] desc [desc...]" "$0"
	info
	info "Options:"
	info "  -c, --config <conf>     path to config (default ~/mkswu/mkswu.conf)"
	info "  -o, --out <out.swu>     path to output file (default from first desc's name)"
	info "  --init                  walk through initial key and first image generation"
	info "  --import                import current directory's config and keys into config dir"
	info "  --genkey                toggle key generation mode (see below for suboptions)"
	info "  --show [--raw] <in.swu> print details about input swu"
	info "  --update-version [--version-base <base>] <desc> [<desc>...]"
	info "                          update version in desc file"
	info "                          if base is given, restart from base or fail if that would"
	info "                          lower the version"
	info "  --version-cmp <base_version> <version> [<version...>]"
	info "                          compare versions given with base version"
	info "  --version               print version and exit"
	info "  desc                    image description file(s), if multiple are given"
	info "                          then the generated image will merge all the contents"
	info
	info "desc file syntax:"
	info "  descriptions are imperative declarations building an image, the following"
	info "  commands available (see README for details):"
	info "  - swdesc_boot <bootfile>"
	info "  - swdesc_tar <tar_file> [--dest <dest>]"
	info "  - swdesc_files [--basedir <basedir>] [--dest <dest>] <files>"
	info "  - swdesc_command [--stdout-info] '<cmd>'"
	info "  - swdesc_script [--stdout-info] <script>"
	info "  - swdesc_exec [--stdout-info] <file> '<cmd>' (file is \$1 in command)"
	info "  - swdesc_embed_container <image_archive>"
	info "  - swdesc_usb_container <image_archive>"
	info "  - swdesc_pull_container <image_url>"
	info
	info "In most cases --version <component> <version> should be set,"
	info "<component> must be extra_os.* in order to update rootfs"
	info
	info "Key generation options:"
	info "  --cn          common name for key (mandatory for signing key)"
	info "  --plain       generate signing key without encryption"
	info "  --aes         generate aes key instead of default rsa key pair"
}

write_line() {
	local line
	for line; do
		[ -z "$line" ] && continue
		printf "%*s%s\n" "$((0${line:+1}?indent:0))" "" "$line"
	done
}

reindent() {
	local padding file
	padding=$(printf "%*s" "${indent:-0}" "")

	for file; do
		[ -e "$file" ] || continue
		sed -e "s/^/$padding/" "$file"
	done
}


link() {
	local src="$1"
	local dest="$2"
	local existing

	track_used "$src"

	src=$(readlink -e "$src") || error "Cannot find source file: %s" "$1"

	if [ -h "$dest" ]; then
		existing=$(readlink "$dest")
		[ "$src" = "$existing" ] && return
		rm -f "$dest" || error "Could not remove previous link at %s" "$dest"
	elif [ -e "$dest" ]; then
		check_validity "$dest" "$src" && return
	fi

	# files with hardlinks will mess up the order within the cpio,
	# and thus change the order in which components are installed
	# (e.g. rootfs after post script...)
	# workaround by copying file (reflinks are ok) instead if required
	if [ "$(stat -L -c %h "$src")" != 1 ]; then
		cp --reflink=auto -a "$src" "$dest" || error "Could not copy %s to %s" "$src" "$dest"
	else
		ln -s "$(readlink -e "$src")" "$dest" || error "Could not link %s to %s" "$dest" "$src"
	fi
}

gen_iv() {
	openssl rand -hex 16
}

encrypt_file() {
	local src="$1"
	local dest="$2"
	local iv="$3"

	openssl enc -aes-256-cbc -in "$src" -out "$dest" \
			-K "$ENCRYPT_KEY" -iv "$iv" \
		|| error "failed to encrypt %s" "$src"
}

decrypt_file() {
	local src="$1"
	local dest="${src#enc.}"
	[ "$dest" != "$src" ] || dest="${src%.enc}"
	[ "$dest" != "$src" ] || error "'%s' must start with 'enc.' or end in '.enc'" "$src"
	local iv="${2-}"

	if [ -z "$iv" ]; then
		local ivs="${src%/*}/sw-description-ivs" tmp file="${src##*/}"
		[ -e "$ivs" ] || error "ivs file %s does not exist" "$ivs"

		while read -r iv tmp; do
			[ "$tmp" != "$file" ] || break
		done < "$ivs"
		[ "$tmp" = "$file" ] || error "%s not found in %s" "$file" "$ivs"
	fi
	setup_encryption
	openssl enc -aes-256-cbc -d -in "$src" -out "$dest" \
			-K "$ENCRYPT_KEY" -iv "$iv" \
		|| error "failed to decrypt %s" "$src"

}

setup_encryption() {
	[ -z "$ENCRYPT_KEYFILE" ] && return
	[ -e "$ENCRYPT_KEYFILE" ] \
		|| error "AES encryption key %s was set but not found.\nPlease create it with genkey.sh --aes" \
			"$ENCRYPT_KEYFILE"
	ENCRYPT_KEY=$(cat "$ENCRYPT_KEYFILE")
	# XXX if sw-description gets encrypted, its iv is here
	ENCRYPT_KEY="${ENCRYPT_KEY% *}"
}

check_validity() {
	# Note that since this creates/updates validity file we must
	# not recreate file_out with an incomplete result

	local file_out="$1"
	shift
	local file_src stat=""

	track_used "$file_out.validity"

	# check validity with mtime/size for all source files
	for file_src; do
		stat="${stat:+$stat
}$(stat -L -c %n-%.Y-%s "$file_src")" \
			|| error "Source file %s for %s does not exist" "$file_src" "$file_out"
	done

	[ "$(cat "$file_out.validity" 2>/dev/null)" = "$stat"  ] \
		&& [ -e "$file_out" ] && return

	rm -f "$file_out"
	echo "$stat" > "$file_out.validity"
	return 1
}

check_version_higher() {
	local version="$1"
	local max err

	# handle only x.y.z.t or x.y.z-t
	printf %s "$version" | grep -qE '^[0-9]+(\.[0-9]+)?(\.[0-9]+)?(\.[0-9]*|-[A-Za-z0-9.]+(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?)?$' \
		|| error "Version %s must be x.y.z.t (numbers < 65536 only) or x.y.z-t (x-z numbers only)" "$version"
	# ... and check for max values
	if [ "${version%-*}" = "${version}" ]; then
		# only dots, "old style version" valid for 16 bits, but now overflow
		# falls back to semver which is signed int but only for 3 elements
		if printf %s "${version}" | grep -qE '\..*\..*\.'; then
			max=65535
		else
			max=2147483647
		fi
		# base_os should be x.y.z-t format to avoid surprises
		# with semver prerelease field filtering
		if [ "$component" = "base_os" ] \
		    && [ -z "$WARNING_ACK_BASEOS_PRERELEASE" ]; then
			warning "base_os version %s was not in x[.y[.z]]-t format" "$version"
			warning "Please note that %s > %s-at.1, so if installing atmark-provided updates" \
				"$version" "$version"
			warning "later then ensure upgrades stay possible."
			warning "This warning can be disabled by setting '%s' in '%s'" \
				"WARNING_ACK_BASEOS_PRERELEASE=1" "$CONFIG"
			WARNING_ACK_BASEOS_PRERELEASE=1
		fi
	else
		# semver, signed int
		max=2147483647
	fi
	# strip everything after + (ignored in semvers)
	version=${version%+*}
	err=$(printf %s "$version" | tr '.-' '\n' | awk '
		/^[0-9]+$/ && $1 > '$max' {
			print "1 " $1;
			exit
		}
		/[0-9][a-zA-Z]|[a-zA-Z][0-9]/ {
			print "2 " $1;
			exit
		}')
	case "$err" in
	1\ *) error "version check failed for %s: %s must be <= %s" "$version" "${err#* }" "$max";;
	2\ *) error "version check failed for %s: %s must not mix alpha and digits, e.g.:\n\t1.2.3-rc.4\n\t1.2.3.4\n\t1.2.3-4" \
		"$version" "${err#* }";;
	esac
}

check_version_different() {
	local version="$1"
	local max err

	# different still goes through integer parsing, but is more relaxed
	# as different ordering does not matter, and pure lexicographical
	# is also allowed... Who said painful ?...
	if printf %s "$version" | grep -qE '^[0-9]+(\.[0-9]+)?(\.[0-9]+)?(\.[0-9]*|-[A-Za-z0-9.-]+)?$'; then
		# same max check as above without mixed alnum check
		local max=2147483647
		[ "${version%-*}" = "${version}" ] \
			&& printf %s "${version}" | grep -qE '\..*\..*\.' \
			&& max=65535
		err=$(printf %s "$version" | tr '.-' '\n' | awk '
			/^[0-9]+$/ && $1 > '$max' {
				print "1 " $1;
				exit
			}')
		case "$err" in
		1\ *) error "version check failed for %s: %s must be <= %s" "$version" "${err#* }" "$max";;
		esac
	else
		# versions with 5+ components, semvers with 4+ leading digits,
		# and the '+' part of semvers are completly ignored and will be
		# considered identical so should be refused.
		# Anything else should be string-compared and is ok.
		if printf %s "$version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]*(\.[0-9]+|-[A-Za-z0-9.-]+)?$'; then
			error "version %s has too many digit components, please use somthing else" \
				"$version"
		fi
		if printf %s "$version" | grep -qE '^[0-9]+(\.[0-9]+)*([+-][A-Za-z0-9.+-]+)?$'; then
			error "metadata (+ part) in %s while valid semver are ignored by swupdate, please use something else" \
				"$version"
		fi
	fi
}

normalize_version() {
	# modifies $version in place
	local semver_prerelease="${version#*-}"
	if [ "$semver_prerelease" = "$version" ]; then
		semver_prerelease=""
	else
		version="${version%%-*}"
	fi

	# the version part is only allowed dots, and we can just
	# strip any leading 0 and trailing .0s
	version="$(echo "$version" \
			| sed -e 's/\<0\+\([0-9]\)/\1/g' -e 's/\(\.0\)\+$//')"
	if [ -n "$semver_prerelease" ]; then
		# semver should simplify leading zero if and only if it's a pure
		# number enclosed in dots; e.g. 01.01 -> 1.1 but 01-01 -> 01-01
		# as it is considered as a string...
		# This needs looping on a label because we eat the next dot and
		# sed does not have lookahead
		semver_prerelease="$(echo "$semver_prerelease" \
			| sed -e ':repeat;
				s/\(^\|\.\)0\+\([0-9]\+\(\.\|$\)\)/\1\2/g;
				t repeat' )"
		version="$version-$semver_prerelease"
	fi
}

check_version_valid() {
	[ -n "$MKSWU_TEST_ALLOW_BOGUS_VERSION" ] && return

	case "$component" in
	*" "*) error "component must not contain spaces (%s)" "$component";;
	extraos.*)
		warning "Warning: component '%s' starts with extraos, did you mean extra_os ?" \
			"$component"
		;;
	esac
	case "$version" in
	*" "*) error "version must not contain spaces (%s = %s)" "$component" "$version"
	esac

	case "$install_if" in
	higher)
		check_version_higher "$version"
		;;
	different)
		check_version_different "$version"
		;;
	*) error "install_if must be higher or different";;
	esac

	# check version is normalized
	local orig_version="$version"
	normalize_version
	if [ "$version" != "$orig_version" ]; then
		# send to stderr because stdout is sent to sw-description here
		info "%s version %s has been simplified to %s" \
			"$component" "$orig_version" "$version" >&2
	fi
}

compress() {
	local file_src="$1"
	local file_out="$2"

	track_used "$file_src"
	check_validity "$file_out" "$file_src" && return

	ZSTD_CLEVEL="${ZSTD_CLEVEL:-10}" \
	zstd -qf -o "$file_out.tmp" < "$file_src" \
		|| error "failed to compress %s" "$file_src"

	mv "$file_out.tmp" "$file_out" \
		|| error "Could not rename %s" "$file_out"
}

append_file() {
	printf "%s\n" "$FILES_FIRST" "$FILES" \
			| grep -q -x "$file" \
		&& return

	# we want to install anything marked base_os first,
	# because if base_os is written anything like swdesc_command
	# cannot run until baseos has been extracted
	if [ -n "$MKSWU_install_first" ] || [ "$component" = "base_os" ]; then
		FILES_FIRST="$FILES_FIRST
$file"
	else
		FILES="$FILES
$file"
	fi
}

write_entry_stdout() {
	local type="$1"
	local file_src="$2"
	local file="${file_src##*/}"
	local file_out="$OUTDIR/$file"
	local compress="$compress"
	local install_if="$install_if"
	shift 2
	local sha256="" iv="" indent=0 validity file_size
	local sha256_chunked=""
	local LF="
"
	local show_desc="${cmd_description%"$LF"*}"

	trace "Processing $show_desc ($file)"
	[ -e "$file_src" ] || error "Missing source file: %s" "$file_src"

	if [ -n "$compress" ]; then
		# Check if already compressed
		case "$file" in
		*.tar.*)
			if [ "$compress" = force ]; then
				# Force decompression through swupdate.
				# Only gzip and zstd are supported
				case "$file" in
				*.gz) compress=zlib;;
				*.zst) compress=zstd;;
				*) compress="";;
				esac
			else
				# archive handle will handle it
				compress=""
			fi
			;;
		*.apk)
			# already compressed
			compress=""
			;;
		*.zst)
			compress=zstd
			;;
		*)
			# do not compress files < 128 bytes
			if [ "$(stat -L -c "%s" "$file_src")" -lt 128 ]; then
				compress=""
			else
				compress=zstd
				file="zst.$file"
				file_out="$OUTDIR/$file"
				compress "$file_src" "$file_out"
				file_src="$file_out"
			fi
			;;
		esac
	fi

	if [ -n "$ENCRYPT_KEY" ] && [ -s "$file_src" ]; then
		file="enc.$file"
		file_out="$OUTDIR/$file"
		# if we already had encrypted this file we must reuse the same iv
		if [ -e "$OUTDIR/sw-description-ivs" ]; then
			local tmp
			while read -r iv tmp; do
				[ "$tmp" != "$file" ] || break
			done < "$OUTDIR/sw-description-ivs"
			[ "$tmp" = "$file" ] || iv=""
		fi
		if [ -z "$iv" ]; then
			iv=$(gen_iv)
			# also write it out for scripts
			printf "%s\n" "$iv $file" >> "$OUTDIR/sw-description-ivs"
			track_used "$OUTDIR/sw-description-ivs"
			encrypt_file "$file_src" "$file_out" "$iv"
		fi
		[ -n "$iv" ] || error "Could not generate an iv to encrypt %s" "$file_src"
		file_src="$file_out"

	fi

	append_file "$file"

	if [ "$file_src" != "$file_out" ]; then
		link "$file_src" "$file_out"
	fi

	track_used "$file_out" "$file_out.sha256sum"

	validity="$(stat -L -c %.Y-%s "$file_out")" \
		|| error "Could not stat %s" "$file_out"
	file_size="${validity##*-}"

	if [ -e "$file_out.sha256sum" ]; then
		sha256=$(cat "$file_out.sha256sum")
		# check validity with mtime + size
		[ "${sha256#* }" = "$validity" ] \
			|| sha256=""
		sha256="${sha256% *}"
	fi

	if [ -z "$sha256" ]; then
		sha256=$(sha256sum < "$file_out") \
			|| error "Checksumming %s failed" "$file_out"
		sha256=${sha256%% *}
		printf "%s\n" "$sha256 $validity" \
			> "$file_out.sha256sum" \
			|| error "Could not write %s" "$file_out.sha256sum"
	fi


	# exec stores an intermediate copy and does not have the problem,
	# skip to avoid growing sw-description on large embedded containers
	if [ "$type" != "exec" ]; then
		sha256_chunked="$file_out.sha256sum_chunked"
		track_used "$sha256_chunked"
		# check validity with mtime + size
		if ! [ -e "$sha256_chunked" ] \
		    || [ "$(head -n 1 "$sha256_chunked")" != "$validity" ]; then
			local nlines=1 wc # validity header
			{
				echo "$validity"
				if [ -s "$file_out" ]; then
					split -b $((1024*512)) \
							--filter='sha256sum -' \
							"$file_out" \
						| sed -e 's/^/"/' -e 's/\s*-$/",/'
				else
					# empty file still needs one line.. or does it?
					nlines=$((nlines+1))
					echo "\"$sha256\","
				fi
			} > "$sha256_chunked" \
				|| error "Could not write %s" "$sha256_chunked"
			# sanity check number of lines
			nlines=$((nlines + (file_size+1024*512-1) / 1024/512))
			wc="$(wc -l "$sha256_chunked")"
			wc="${wc%% *}"
			[ "$wc" = "$nlines" ] \
				|| error "Unexpected number of chunked sha256s for %s (real %s / expected %s)" \
					"$sha256_chunked" "$wc" "$nlines"
		fi
	fi

	write_line "{"
	indent=$((indent+2))
	write_line "filename = \"$file\";"
	# special handling for 'extra_os.': this is the no-version variant of extra-os,
	# so we just unset component here.
	[ "$component" = "extra_os." ] && component=""
	MKSWU_version_last_set=""
	if [ -n "$component" ] && [ -z "$version" ]; then
		error "item was set without version:\n%s\nPlease set a version (e.g. global swdesc_option version=...)" \
				"$show_desc"
	fi
	if [ -n "$version" ] && [ -z "$component" ]; then
		error "version %s was set without associated component:\n%s" \
			"$version" "$show_desc"
	fi
	if [ -n "$component" ]; then
		[ -n "$install_if" ] || install_if=higher
		check_version_valid
		write_line "name = \"$component\";" \
			   "version = \"$version\";" \
			   "install-if-${install_if} = true;"

		# remember version for scripts
		printf "%s\n" "$component $version $install_if ${board:-*}" >> "$OUTDIR/sw-description-versions"
	fi
	if [ -n "$main_version" ]; then
		[ -n "$component" ] && [ -n "$version" ] \
			|| error "%s requires --version to be set" "--main-version"
		write_line "# MAIN_COMPONENT $component" \
			   "# MAIN_VERSION $version"
	fi
	[ -n "$compress" ] && write_line "compressed = \"$compress\";"
	[ -n "$iv" ] && write_line "encrypted = true;" "ivt = \"$iv\";"
	[ -n "$MKSWU_TEST_NOT_DIRECTLY" ] \
		|| write_line "installed-directly = true;"
	write_line "sha256 = \"$sha256\";"
	if [ -n "$sha256_chunked" ]; then
		write_line 'chunked_sha256 = ('
		tail -n +2 "$sha256_chunked" \
			| indent=$((indent+2)) reindent /dev/stdin
		write_line ');'
	else
		write_line "size = $file_size;"
	fi
	local line properties_written=""
	if [ "$show_desc" != "$cmd_description" ]; then
		write_line "# mkswu_orig_cmd ${cmd_description##*"$LF"}"
	fi
	if [ -n "$MKSWU_hide_from_show" ]; then
		write_line "# hide_from_show"
	fi
	write_line "type = \"$type\";"
	for line; do
		write_line "$line"
		case "$line" in
		"properties: {")
			write_line "  description: \"$show_desc\";"
			properties_written=1
			;;
		"properties: {*")
			error "properties opening tag should be on its own line";;
		esac
	done
	[ -n "$properties_written" ] \
		|| write_line "properties: {" \
			"  description: \"$show_desc\";" "};"

	if [ -n "$desc_extra_text" ]; then
		write_line "$desc_extra_text"
	fi

	indent=$((indent-2))
	write_line "},"
}

write_entry() {
	local outfile="$OUTDIR/sw-description-$1${board:+-$board}"
	shift

	# Running init here allows .desc files to override key elements
	# before the first swdesc_* statement
	if [ -n "$FIRST_SWDESC_INIT" ]; then
		FIRST_SWDESC_INIT=""
		setup_encryption
		embedded_preinstall_script
	fi

	write_entry_stdout "$@" >> "$outfile"
}

parse_swdesc() {
	# first argument tells us what to parse for
	local CMD="$1"
	local ARG
	shift

	# build command faithfully
	if [ -z "$cmd_description" ]; then
		cmd_description="swdesc_$CMD"
		for ARG; do
			cmd_description="${cmd_description:+$cmd_description }$(shell_quote "$ARG")"
		done
		cmd_description="$(conf_quote "$cmd_description")"
	fi

	local SKIP=0
	local stdout_info=""
	for ARG; do
		# skip previously used argument
		# using a for loop means we can't shift ahead
		if [ "$SKIP" -gt 0 ]; then
			SKIP=$((SKIP-1))
			continue
		fi
		if [ "$SKIP" -lt 0 ]; then
			shift
			set -- "$@" "$ARG"
			continue
		fi

		shift
		# split --switch=value
		if [ "${ARG#--*=}" != "$ARG" ]; then
			set -- "${ARG#--*=}" "$@"
			ARG="${ARG%%=*}"
		fi
		case "$ARG" in
		"-b"|"--board")
			[ $# -lt 1 ] && error "%s requires an argument" "$ARG"
			board="$1"
			SKIP=1
			;;
		"-v"|"--version")
			[ $# -lt 2 ] && error "%s requires <component> <version> arguments" "$ARG"
			component="$1"
			version="$2"
			SKIP=2
			;;
		"--version-ignore")
			component=""
			;;
		"--version-force")
			MKSWU_FORCE_VERSION=1
			case "$component" in
			extra_os.*) component=extra_os.;;
			*) component="";;
			esac
			version=""
			;;
		"--base-os")
			component=base_os
			;;
		"--extra-os")
			case "$component" in
			extra_os.*) ;;
			*) component="extra_os.$component";;
			esac
			;;
		"--main-version")
			main_version=1
			;;
		"--install-if")
			install_if="$1"
			case "$install_if" in
			higher|different) ;;
			*) error "--install-if must be higher or different";;
			esac
			SKIP=1
			;;
		"--preserve-attributes")
			[ "$CMD" = "tar" ] || [ "$CMD" = "files" ] \
				|| error "%s only allowed for %s" "$ARG" "swdesc_files/swdesc_tar"
			preserve_attributes=1
			;;
		"-d"|"--dest")
			[ $# -lt 1 ] && error "%s requires an argument" "$ARG"
			[ "$CMD" = "tar" ] || [ "$CMD" = "files" ] \
				|| error "%s only allowed for %s" "$ARG" "swdesc_files/swdesc_tar"
			dest="$1"
			SKIP=1
			;;
		"--basedir")
			[ $# -lt 1 ] && error "%s requires an argument" "$ARG"
			[ "$CMD" = "files" ] \
				|| error "%s only allowed for %s" "$ARG" "swdesc_files"
			basedir="$1"
			SKIP=1
			;;
		"--description")
			[ $# -lt 1 ] && error "%s requires an argument" "$ARG"
			# keep original description after a new line:
			# these are not possible in config-escaped strings
			local LF="
"
			cmd_description="$(conf_quote "$1")$LF${cmd_description##*"$LF"}"
			SKIP=1
			;;
		"--container")
			[ $# -lt 1 ] && error "%s requires an argument" "$ARG"
			case "$CMD" in
			exec|command|script) ;;
			*) error "%s only allowed for %s" "$ARG" "swdesc_exec/swdesc_command/swdesc_script";;
			esac
			container="$1"
			SKIP=1
			;;
		"--tag")
			[ $# -lt 1 ] && error "%s requires an argument" "$ARG"
			[ "$CMD" = "pull_container" ] \
				|| error "%s only allowed for %s" "$ARG" "swdesc_pull_container"
			container_tag="$1"
			SKIP=1
			;;
		"--stdout-info")
			case "$CMD" in
			exec|exec_nochroot|command|command_nochroot|script|script_nochroot) ;;
			*) error "%s only allowed for %s" "$ARG" "swdesc_exec/swdesc_command/swdesc_script";;
			esac
			stdout_info=1
			;;
		--)
			# stop parsing
			SKIP=-1
			;;
		"-"*)
			error "%s is not a known %s argument" "$ARG" "swdesc_$CMD"
			;;
		*)
			# restore argument last to keep it around
			set -- "$@" "$ARG"
			;;
		esac
		[ "$SKIP" -gt 0 ] && shift "$SKIP"
	done

	# checks for "special components"
	if [ -z "$MKSWU_NO_SPECIAL_VERSION_CHECK" ]; then
		# boot versions can only be used in appropriate boot commands
		case "$CMD" in
		boot|boot_enc)
			[ "$component" = "boot" ] \
				|| error "%s: Version component for swdesc_%s must be set to %s" \
					"$cmd_description" "$CMD" boot
			;;
		boot_linux)
			[ "$component" = "boot_linux" ] \
				|| error "%s: Version component for swdesc_%s must be set to %s" \
					"$cmd_description" boot_linux boot_linux
			;;
		*)
			[ "$component" != "boot" ] \
				|| error "%s: Version component '%s' is reserved for swdesc_%s" \
					"$cmd_description" boot boot
			[ "$component" != "boot_linux" ] \
				|| error "%s: Version component '%s' is reserved for swdesc_%s" \
					"$cmd_description" boot_linux boot_linux
			;;
		esac
		# base_os cannot be updated after running a command
		case "$component" in
		base_os)
			# can only be set for swdesc_tar
			case "$CMD,$MKSWU_component_baseos_seen" in
			tar,) MKSWU_component_baseos_seen="archive";;
			tar,done)
				warning "Warning: 'base_os' has been used multiple times."
				warning "post-baseos extraction (e.g. swupdate_preserve_files POST step) will run after"
				warning "the first occurence so the result might be unexpected."
				MKSWU_component_baseos_seen="warned"
				;;
			tar,archive|tar,warned) ;;
			tar,extra_os*)
				if [ -z "$WARNING_ACK_BASEOS_ORDER" ]; then
					# re-ordering done in append_file with FILES_FIRST
					warning "Warning: 'base_os' update listed after '%s'" "$MKSWU_component_baseos_seen"
					warning "The 'base_os' archive will be extracted first to ensure installation"
					warning "succeeds, please check this does not impact file extraction order."
					warning "This warning can be disabled by setting '%s' in '%s'" \
						"WARNING_ACK_BASEOS_ORDER=1" "$CONFIG"
					WARNING_ACK_BASEOS_ORDER=1
				fi
				MKSWU_component_baseos_seen="archive"
				;;
			*) error "%s: Version component '%s' is reserved for swdesc_%s" \
				"$cmd_description" base_os tar;;
			esac
			;;
		boot|boot_enc)
			# these don't interfere with base_os update
			;;
		extra_os.*)
			# warn that order changed if a later base_os update comes
			if [ -z "$MKSWU_component_baseos_seen" ]; then
				MKSWU_component_baseos_seen="$component"
			fi
			;;
		*)
			# not touching base-os so not impacted by order change
			;;
		esac
	fi

	case "$CMD" in
	boot)
		[ $# -eq 0 ] && [ -n "$boot_file" ] && return
		[ $# -eq 1 ] || error "swdesc_boot requires an argument\nUsage: swdesc_boot [options] boot_file"
		boot_file="$1"
		;;
	boot_enc)
		[ $# -eq 0 ] && [ -n "$boot_file" ] && [ -n "$dek_offsets_file" ] && return
		[ $# -eq 2 ] || error "swdesc_boot_enc requires two arguments\nUsage: swdesc_boot_enc [options] boot_enc_file dek_offets_file"
		boot_file="$1"
		dek_offsets_file="$2"
		;;
	boot_linux)
		[ $# -eq 0 ] && [ -n "$image" ] && return
		[ $# -eq 1 ] || error "swdesc_boot_linux requires an argument\nUsage: swdesc_boot_linux [options] boot_linux_file"
		image="$1"
		;;
	tar)
		[ $# -eq 0 ] && [ -n "$file" ] && return
		[ $# -eq 1 ] || error "swdesc_tar requires an argument\nUsage: swdesc_tar [options] file.tar"
		file="$1"
		sbom_input "$file"
		;;
	files)
		[ $# -eq 0 ] && [ -n "$file" ] && [ -n "$tarfiles_src" ] && return
		[ $# -ge 1 ] || error "swdesc_files requires arguments\nUsage: swdesc_files [options] file [files...]"
		tarfiles_src="$(printf "%s\n" "$@")"
		;;
	command*)
		[ $# -eq 0 ] && [ -n "$cmd" ] && return
		[ $# -ge 1 ] || error "swdesc_command requires arguments\nUsage: swdesc_command [options] cmd [cmd..]"
		cmd=""
		for ARG; do
			cmd="${cmd:+$cmd && }{ $ARG; }"
		done
		cmd="sh -c $(shell_quote "$cmd") ${stdout_info:+">&\${SWUPDATE_INFO_FD:-1} "}--"
		;;
	script*)
		if [ $# -eq 0 ] && [ -n "$file" ]; then
			[ -n "$cmd" ] || cmd='sh $1'
			return
		fi
		[ $# -ge 1 ] || error "swdesc_script requires arguments\nUsage: swdesc_script [options] script [args]"
		file="$1"
		shift
		cmd='sh $1'
		for ARG; do
			cmd="${cmd:+$cmd }$(shell_quote "$ARG")"
		done
		cmd="sh -c $(shell_quote "$cmd") ${stdout_info:+">&\${SWUPDATE_INFO_FD:-1} "}--"
		;;
	exec*)
		[ $# -eq 0 ] && [ -n "$cmd" ] && [ -n "$file" ] && return
		[ $# -ge 2 ] || error "swdesc_%s requires at least two arguments\nUsage: swdesc_%s [options] file command" "$CMD" "$CMD"
		file="$1"
		shift
		cmd=""
		for ARG; do
			cmd="${cmd:+$cmd && }{ $ARG; }"
		done
		cmd="sh -c $(shell_quote "$cmd") ${stdout_info:+">&\${SWUPDATE_INFO_FD:-1} "}--"
		;;
	*container)
		[ $# -eq 0 ] && [ -n "$image" ] && return
		[ $# -eq 1 ] || error "%s requires an argument\nUsage: %s [options] image" "swdesc_$CMD" "swdesc_$CMD"
		image="$1"
		sbom_input "$image"
		;;
	sbom_source_file)
		source="$1"
		MKSWU_SBOM_SOURCE_FILE="${MKSWU_SBOM_SOURCE_FILE:+$MKSWU_SBOM_SOURCE_FILE }-f $(shell_quote "$(realpath -e "$source")")" \
			|| error "%s does not exist" "${opt#sbom_source_file=}"
		;;
	*)
		error "Unhandled command %s" "$CMD"
		;;
	esac
}

check_enc_boot() {
	local key="$1"

	[ -n "$MKSWU_NO_ARCH_CHECK" ] && return

	if [ -z "$MKSWU_encrypted_boot" ]; then
		# plain boot, we should find the keys
		grep -q "$key" "$boot_file" \
			|| error "%s did not contain '%s'\nThis check can be disabled by setting MKSWU_NO_ARCH_CHECK=1" \
				"$boot_file" "$key"
	else
		# .. but not if encrypted
		! grep -q "$key" "$boot_file" \
			|| error "%s contained '%s', is it encrypted?\nThis check can be disabled by setting MKSWU_NO_ARCH_CHECK=1" \
				"$boot_file" "$key"
	fi
}

check_pad_boot() {
	local header
	[ -e "$boot_file" ] || error "%s does not exist" "$boot_file"

	# common to all images
	check_enc_boot u-boot

	header=$(xxd -l 4 -p "$boot_file")
	case "$header" in
	d1002041)
		# imx8mp boot image, ok
		check_enc_boot aarch64
		return
		;;
	00200287)
		# imx8ulp boot image, ok
		check_enc_boot aarch64
		return
		;;
	d1002040)
		# unpadded imx6ull/imx7d, requires prepending 1kb padding
		local dest="$OUTDIR/${boot_file##*/}"
		dd if="$boot_file" of="$dest" bs=1M status=none \
				seek=1024 oflag=seek_bytes \
			|| error "Could not copy %s to %s" "$boot_file" "$dest"
		boot_file="$dest"
		check_enc_boot armv7
		return
		;;
	esac
	header=$(xxd -l 4 -p -s 1024 "$boot_file")
	case "$header" in
	d1002040)
		# padded imx6ull/imx7d, ok
		check_enc_boot armv7
		return
		;;
	esac
	error "Unrecognized boot image format for %s" "$boot_file"
}

swdesc_boot() {
	eval "$MKSWU_declare_swdesc_cmd_vars_stanza"
	local boot_file=""
	component=boot

	parse_swdesc boot "$@"

	check_pad_boot
	if [ -z "$version" ]; then
		version=$(grep -m1 -aoE '20[0-9]{2}.[0-1][0-9]-[0-9a-zA-Z.-]*' "$boot_file") \
			|| error "Could not guess boot version in %s" "$boot_file"
		# fix up version to allow install-if=higher:
		# - 2020.04 -> 2020.4
		normalize_version
		# - at4 -> at.4
		version=$(echo "$version" | sed -e 's/at\([0-9]\)/at.\1/')
		# - 2020.04-at.4-2-gabc1234-4-gdef5678 -> 2020.04-at.4.2.4+gabc1234-gdef5678
		version=$(echo "$version" | awk -F - '{
				printf("%s-%s", $1, $2)
				for (i=3; i<=NF; i++) {
					if ($i ~ /^([0-9]+|[a-zA-Z]+)$/) {
						# print only alpha or only num immediately with a .
						printf(".%s", $i)
					} else {
						# otherwise keep for later with +
						build[i]=$i
					}
				}
				sep="+"
				for (i=3; i<=NF; i++) {
					if (! build[i]) continue;
					printf("%s%s", sep, build[i])
					sep="-"
				}
				printf "\n"
			}')
	fi

	write_entry images raw "$boot_file" \
		"device = \"/dev/swupdate_bootdev\";"
}

swdesc_boot_enc() {
	eval "$MKSWU_declare_swdesc_cmd_vars_stanza"
	local boot_file="" dek_offsets_file="" offsets
	local MKSWU_encrypted_boot=1
	component=boot

	parse_swdesc boot_enc "$@"

	[ -n "$version" ] \
		|| error "Version must be set for swdesc_boot_encrypted"

	offsets=$(cat "$dek_offsets_file")
	case "$offsets" in
	"dek_spl_offset 0x"*" dek_fit_offset 0x"*)
		# G4/"old" offset format
		offsets="fw_setenv $offsets"
		;;
	*)
		error "%s did not have expected content, is it a .dek_offsets file?" \
			"$dek_offsets_file"
	esac
	[ -n "$offsets" ] \
		|| error "dek offset file %s was not readable or empty" "$dek_offsets_file"

	eval "$MKSWU_export_swdesc_cmd_vars_stanza"
	MKSWU_hide_from_show=1 \
		swdesc_command_nochroot --description "Setting up encryption offsets" \
			"$offsets"

	swdesc_boot "$boot_file"
}


swdesc_boot_linux() {
	eval "$MKSWU_declare_swdesc_cmd_vars_stanza"
	local image=""
	component=boot_linux

	parse_swdesc boot_linux "$@"

	[ -e "$image" ] || error "%s does not exist" "$image"
	[ -n "$version" ] \
		|| error "Version must be set for swdesc_boot_linux"
	# XXX no longer true for new systems, but keep checking for now,
	# should be plenty anyway.
	[ "$(stat -L -c %s "$image")" -lt "$((26 * 1024 * 1024))" ] \
		|| error "swdesc_boot_linux image must be at most 26MB big"

	case "$(xxd -l 4 -p "$image")" in
	d00dfeed)
		# FIT image, OK for imx8mp
		# we unfortunately cannot check if it's encrypted, but since
		# it's linux the worst that can happen is a rollback...
		;;
	00c00287)
		# signed imx8ulp image
		;;
	00080387)
		# encrypted imx8ulp image
		error "Encrypted linux image not yet supported"
		;;
	*)
		error "Unrecognized linux kernel format for %s" "$image"
		;;
	esac

	eval "$MKSWU_export_swdesc_cmd_vars_stanza"
	swdesc_exec_nochroot "$image" \
		'${TMPDIR:-/var/tmp}/scripts/install_boot_linux $1'
}

swdesc_tar() {
	eval "$MKSWU_declare_swdesc_cmd_vars_stanza"
	local target="${MKSWU_DEBUG_TARGET-/target}"

	parse_swdesc tar "$@"

	case "$component" in
	base_os)
		if [ "${dest:-/}" != "/" ]; then
			error "base_os upgrade must go to / (was: %s)" "$dest"
		fi
		dest="/"
		;;
	extra_os*)
		dest="${dest:-/}"
		if [ "${dest#/}" = "$dest" ]; then
			error "OS update must have an absolute dest (was: %s)" "$dest"
		fi
		;;
	*)
		dest="${dest:-/var/app/rollback/volumes}"
		case "$dest/" in
		..*|*/../*)
			error ".. is not allowed in destination path for volume update"
			;;
		/tmp/*|/var/tmp/*|/var/app/rollback/volumes/*|/var/app/volumes/*)
			# ok
			;;
		/*)
			[ -n "$target" ] \
				&& error "OS is only writable for base/extra_os updates and dest (%s) is not within volumes. Use --extra-os." "$dest"
			;;
		*)
			dest="/var/app/rollback/volumes/$dest"
			;;
		esac
	esac

	# it doesn't make sense to not set preserve_attributes
	# for base_os updates: fix it
	if [ "$component" = "base_os" ] \
	    && [ -z "$preserve_attributes" ]; then
		info "Info: automatically setting --preserve-attributes for base_os update" >&2
		preserve_attributes=1
	fi
	write_entry images archive "$file" \
		"path = \"$target$dest\";" \
		"properties: {" "  create-destination = \"true\";" "};" \
		"${preserve_attributes:+preserve-attributes = true;}"

	# re-expose variables to preserve version etc
	eval "$MKSWU_export_swdesc_cmd_vars_stanza"
	if [ "$MKSWU_component_baseos_seen" = archive ]; then
		MKSWU_component_baseos_seen="done"
		MKSWU_hide_from_show=1 \
			swdesc_command_nochroot --description "base_os post fixups" \
				'${TMPDIR:-/var/tmp}/scripts/post_rootfs_baseos.sh'
	fi
}

set_file_from_content() {
	local content="$*" extension=""
	# some commands like apk require extension to be kept to work
	case "$file" in
	*.*)
		extension="${file##*.}"
		[ "${#extension}" -gt 5 ] && extension=""
		;;
	esac

	file="$(printf %s "$content" | tr -c '[:alnum:]' '_')"
	if [ "${#file}" -gt 40 ]; then
		file="$(printf %s "$file" | head -c 20)..$(printf %s "$file" | tail -c 20)"
	fi
	file="${file}_$(printf %s "$content" | sha1sum | cut -d' ' -f1)"
	file="$OUTDIR/${file}${extension:+.$extension}"
}

create_archive() {
	# updates '$file' with new filename
	# basedir, tarfiles_src must be set
	local tarfile_raw tarfile

	set --
	: > "$OUTDIR/tar.validity.tmp"
	for tarfile_raw in $tarfiles_src; do
		if [ -z "$basedir" ]; then
			[ -d "$tarfile_raw" ] \
				&& basedir="$tarfile_raw" \
				|| basedir=$(dirname "$tarfile_raw")
		fi
		tarfile=$(realpath -e -s --relative-to="$basedir" "$tarfile_raw") \
			|| error "%s does not exist" "$tarfile_raw"
		[ "${tarfile#../}" = "$tarfile" ] \
			|| error "%s is not inside %s" "$tarfile_raw" "$basedir"

		find "$tarfile_raw" -exec stat -c "%n-%.Y-%s" {} + \
				| sort >> "$OUTDIR/tar.validity.tmp" \
			|| error "Could not create tar for %s" "$tarfile_raw"
		set -- "$@" "$tarfile"
		# tarfile is relative, renormalize it...
		[ "$tarfile" = "." ]  && tarfile=""
		track_used "$basedir/$tarfile"
		sbom_input "$basedir/$tarfile"
	done

	# $dest can be unset, just fix it early for base_os/extra_os
	if [ -z "$dest" ]; then
		case "$component" in
		base_os|extra_os.*) dest=/;;
		esac
	fi

	if [ -z "$file" ]; then
		set_file_from_content "$basedir" "$dest" "$@"
		file="$file.tar"
	elif [ "${file#/}" = "$file" ]; then
		file="$OUTDIR/$file"
	fi

	track_used "$file" "$file.validity"

	if ! [ -e "$file" ] || ! cmp -s "$file.validity" "$OUTDIR/tar.validity.tmp"; then
		local repro=""
		# swupdate 'archive' without preserve_attributes will ignore
		# user/time: strip them to make archives more reproducible
		[ -z "$preserve_attributes" ] && repro="1"
		# need to split '${repro:+}'s because IFS here is \n...
		tar -cf "$file" --sort=name \
				${repro:+--format=ustar} ${repro:+--numeric-owner} \
				${repro:+--owner=0} ${repro:+--group=0} \
				-C "$basedir" "$@" \
			|| error "Could not create tar for %s" "$file"
		mv "$OUTDIR/tar.validity.tmp" "$file.validity" \
			|| error "Could not rename %s" "$file.validity"
	else
		rm "$OUTDIR/tar.validity.tmp"
	fi

	# check for /var/app/volumes in files we're packing
	local varapp_prefix=/var/app/volumes
	# check if $dest contains /var/app/volumes
	varapp_prefix="${varapp_prefix#"$dest"}"
	if [ "$varapp_prefix" != /var/app/volumes ]; then
		varapp_prefix="${varapp_prefix#/}"
		# validity file is off by basedir
		if grep -qE "^$basedir/$varapp_prefix" "$file.validity"; then
			desc_extra_text='# tar has /var/app/volumes'
		fi
	fi
}

swdesc_files() {
	eval "$MKSWU_declare_swdesc_cmd_vars_stanza"
	local basedir="" tarfiles_src=""
	local IFS="
"
	parse_swdesc files "$@"

	create_archive

	eval "$MKSWU_export_swdesc_cmd_vars_stanza"
	swdesc_tar
}


shell_quote() {
	case "$1" in
	# do not escape strings that only contain the following sh-safe chars
	*[!a-zA-Z0-9=_.,:/-]*)
		# sh-compliant quote function from http://www.etalabs.net/sh_tricks.html
		printf %s\\n "$1" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/'/"
		;;
	"")
		printf %s\\n "''"
		;;
	*)
		printf %s\\n "$1"
		;;
	esac
}

conf_quote() {
	# Double backslashes, escape double-quotes, replace newlines by \n
	# (the last operation requires reading all input into patternspace first)
	printf %s "$1" | sed  ':a;$!N;$!ba;s/\\/\\\\/g;s/"/\\"/g;s/\n/\\n/g'
}

swdesc_exec_nochroot() {
	local original_file
	eval "$MKSWU_declare_swdesc_cmd_vars_stanza"

	parse_swdesc exec_nochroot "$@"
	original_file="${file##*/}"

	[ -f "$file" ] \
		|| error "%s does not exist or is not a regular file" "$file"
	[ ! -s "$file" ] || [ "$cmd" != "${cmd#*\$1}" ] \
		|| error "Using %s with a non-empty file, but not referring to it with \$1" \
			"swdesc_exec_nochroot"

	# allow multiple execs with same file if command changes
	# don't rename if no arguments for easier debugging
	if [ -n "$cmd" ] && [ "$cmd" != 'sh $1' ]; then
		local orig_file="$file"
		set_file_from_content "$file" "$cmd"
		link "$orig_file" "$file"
	fi

	write_entry files exec "$file" \
		"properties: {" \
		"  cmd: \"$(conf_quote "$cmd")\"" \
		"  filename: \"$(conf_quote "$original_file")\"" \
		"}"
}

swdesc_exec() {
	eval "$MKSWU_declare_swdesc_cmd_vars_stanza"
	local container="$container"
	local original_file

	parse_swdesc exec "$@"
	original_file="${file##*/}"

	[ -f "$file" ] \
		|| error "%s does not exist or is not a regular file" "$file"
	[ ! -s "$file" ] || [ "$cmd" != "${cmd#*\$1}" ] \
		|| error "Using %s with a non-empty file, but not referring to it with \$1" \
			"swdesc_exec"

	local chroot_cmd="run --net=host --rm -v \${TMPDIR:-/var/tmp}:\${TMPDIR:-/var/tmp}"
	case "$container,$component" in
	,base_os|,extra_os*)
		# run in target "chroot", writable OS
		chroot_cmd="podman $chroot_cmd --rootfs /target"
		;;
	*,base_os|*,extra_os*)
		# run in container, writable OS
		# use podman_target to use newly installed images
		chroot_cmd='${TMPDIR:-/var/tmp}/scripts/podman_target '"$chroot_cmd"
		chroot_cmd="$chroot_cmd --root /target/var/lib/containers"
		chroot_cmd="$chroot_cmd -v /target:/target $container"
		;;
	,*)
		# run in target "chroot", read-only OS, RW volumes
		chroot_cmd="podman $chroot_cmd --read-only -v /target/tmp:/tmp"
		chroot_cmd="$chroot_cmd -v /target/var/app/volumes:/var/app/volumes"
		chroot_cmd="$chroot_cmd -v /target/var/app/rollback/volumes:/var/app/rollback/volumes"
		chroot_cmd="$chroot_cmd --rootfs /target"
		;;
	*)
		# run in container, read-only OS, RW volumes
		chroot_cmd='${TMPDIR:-/var/tmp}/scripts/podman_target '"$chroot_cmd"
		chroot_cmd="$chroot_cmd -v /target/var/app/volumes:/target/var/app/volumes"
		chroot_cmd="$chroot_cmd -v /target/var/app/rollback/volumes:/target/var/app/rollback/volumes"
		chroot_cmd="$chroot_cmd -v /target:/target:ro $container"
		;;
	esac
	chroot_cmd="$chroot_cmd $cmd "

	# allow multiple execs with same file if command changes
	# don't rename if no arguments for easier debugging
	if [ -n "$cmd" ] && [ "$cmd" != 'sh $1' ]; then
		local orig_file="$file"
		set_file_from_content "$file" "$cmd"
		link "$orig_file" "$file"
	fi

	write_entry files exec "$file" \
		"properties: {" \
		"  cmd: \"$(conf_quote "$chroot_cmd")\"" \
		"  filename: \"$(conf_quote "$original_file")\"" \
		"}"
}

swdesc_command() {
	eval "$MKSWU_declare_swdesc_cmd_vars_stanza"

	parse_swdesc command "$@"

	set_file_from_content "$cmd"
	[ -e "$file" ] || : > "$file"

	eval "$MKSWU_export_swdesc_cmd_vars_stanza"
	swdesc_exec
}

swdesc_command_nochroot() {
	eval "$MKSWU_declare_swdesc_cmd_vars_stanza"

	parse_swdesc command_nochroot "$@"

	set_file_from_content "$cmd"
	[ -e "$file" ] || : > "$file"

	eval "$MKSWU_export_swdesc_cmd_vars_stanza"
	swdesc_exec_nochroot
}

swdesc_script() {
	eval "$MKSWU_declare_swdesc_cmd_vars_stanza"

	parse_swdesc script "$@"

	if grep -q /var/app/volumes "$file"; then
		local desc_extra_text='# script has /var/app/volumes'
	fi

	eval "$MKSWU_export_swdesc_cmd_vars_stanza"
	swdesc_exec
}

swdesc_script_nochroot() {
	eval "$MKSWU_declare_swdesc_cmd_vars_stanza"

	parse_swdesc script_nochroot "$@"

	eval "$MKSWU_export_swdesc_cmd_vars_stanza"
	swdesc_exec_nochroot
}

container_check_archive() {
	local manifest

	[ -f "$image" ] \
		|| error "%s does not exist or is not a regular file" "$image"

	if ! manifest=$(tar -Oxf "$image" manifest.json); then
		warning "Warning: Container image %s was not in docker-archive format, install might not work" "$image"
		return
	fi

	# skip check if no jq
	command -v jq >/dev/null || return 0
	if [ -z "$(printf "%s\n" "$manifest" | jq -r  '.[] | .RepoTags[]')" ]; then
		warning "Warning: Container image %s did not contain any tag, image will not be installed unless selected by id" "$image"
	fi
}

swdesc_embed_container() {
	eval "$MKSWU_declare_swdesc_cmd_vars_stanza"
	local image=""

	parse_swdesc embed_container "$@"
	container_check_archive

	eval "$MKSWU_export_swdesc_cmd_vars_stanza"
	swdesc_exec_nochroot "$image" '${TMPDIR:-/var/tmp}/scripts/podman_target load $1'
}

swdesc_pull_container() {
	eval "$MKSWU_declare_swdesc_cmd_vars_stanza"
	local image="" container_tag="" pull_cmd

	parse_swdesc pull_container "$@"

	pull_cmd='${TMPDIR:-/var/tmp}/scripts/podman_target pull '
	pull_cmd="$pull_cmd ${container_tag:+"--tag \"$container_tag\" "} \"$image\""

	eval "$MKSWU_export_swdesc_cmd_vars_stanza"
	swdesc_command_nochroot "$pull_cmd"
}

swdesc_usb_container() {
	eval "$MKSWU_declare_swdesc_cmd_vars_stanza"
	local image=""

	parse_swdesc usb_container "$@"
	container_check_archive

	local image_usb="${image##*/}"
	if [ "${image_usb%.tar.*}" != "$image_usb" ]; then
		info "Warning: podman does not handle compressed container images without an extra uncompressed copy"
		info "you might want to keep the archive as simple .tar"
	fi
	link "$image" "$OUTDIR/$image_usb"
	sign "$image_usb"
	COPY_USB="${COPY_USB:+$COPY_USB }$(shell_quote "$(realpath "$image")")"
	COPY_USB="$COPY_USB $(shell_quote "$(realpath "$OUTDIR/$image_usb.sig")")"

	eval "$MKSWU_export_swdesc_cmd_vars_stanza"
	swdesc_command_nochroot '${TMPDIR:-/var/tmp}/scripts/podman_target --pubkey /etc/swupdate.pem load '"$image_usb"
}

swdesc_sbom_source_file() {
	eval "$MKSWU_declare_swdesc_cmd_vars_stanza"
	parse_swdesc sbom_source_file "$@"
}

append_certificates() {
	# USER_PUBKEYS is comma-separated unlike other tar sources
	local IFS="," cert user_certs=""
	local source="$file"
	file="$OUTDIR/scripts_extras.tar"

	# we handle certificates separately for user certs and
	# atmark updates.
	# We will add here:
	# - atmark certificates if any to certs_atmark/
	# - if UPDATE_CERTS is set any certs that was in PUBKEY
	# (so by now USER_PUBKEYS)... except if the cert is the known
	# onetime public keys that we don't want to add.
	# We need such a gating flag to avoid breaking users that
	# already added their own keys with another mechanism.

	case "$UPDATE_CERTS" in
	[Yy]|[Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|1)
		for cert in $USER_PUBKEYS; do
			[ -e "$cert" ] || error "Required file not found: %s" \
				"$(realpath -m "$cert")"
			# compare cert's pubkey with known onetime-public key
			if [ "$(openssl x509 -noout -in "$cert" -pubkey \
					| sed -e '/-----/d' | tr -d '\n')" \
			    != "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEYTN7NghmISesYQ1dnby5YkocLAe2/EJ8OTXkx/xGhBVlJ57eGOovtPORd/JMkA6lWI0N/pD5p6eUGcwrQvRtsw==" ]; then
				user_certs="$cert${user_certs:+,$user_certs}"
			fi
		done
		;;
	[Nn]|[Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|0|"") ;;
	*) error "Invalid value for %s: %s" UPDATE_CERTS "$UPDATE_CERTS";;
	esac

	if [ -z "$user_certs$ATMARK_CERTS" ]; then
		# no key to add
		link "$source" "$file"
		return
	fi

	track_used "$file"
	# shellcheck disable=SC2086 ## expand certs on purpose
	check_validity "$file" "$source" $ATMARK_CERTS $user_certs && return

	cp -a --reflink=auto "$source" "$file.tmp" \
		|| error "Could not copy %s to %s" "$source" "$file"

	for cert in $user_certs; do
		# prefix ./ if no other / in filename...
		[ "${cert%/*}" != "$cert" ] || cert="./$cert"
		tar -C "${cert%/*}" --append \
			--transform 's:^:certs_user/:' \
			--format=ustar --numeric-owner --owner=0 --group=0 \
			-f "$file.tmp" "${cert##*/}" \
			|| error "Could not append %s to %s" "$cert" "$file.tmp"
	done
	for cert in $ATMARK_CERTS; do
		[ "${cert%/*}" != "$cert" ] || cert="./$cert"
		tar -C "${cert%/*}" --append \
			--transform 's:^:certs_atmark/:' \
			--format=ustar --numeric-owner --owner=0 --group=0 \
			-f "$file.tmp" "${cert##*/}" \
			|| error "Could not append %s to %s" "$cert" "$file.tmp"
	done
	mv "$file.tmp" "$file" \
		|| error "Could not rename %s" "$file"
}

create_prescript() {
	# prefile is a self-extracting tar archive
	check_validity "$file" "$PRE_SCRIPT" "$tar" && return

	cat "$PRE_SCRIPT" "$tar" > "$file.tmp" \
		&& mv "$file.tmp" "$file" \
		|| error "Could not create pre script"
}

embedded_preinstall_script() {
	eval "$MKSWU_declare_swdesc_cmd_vars_stanza"
	local component="" version="" install_if=""
	local cmd_description="pre_script"
	local MKSWU_install_first=1
	local EMBEDDED_SCRIPTS_DIR="$SCRIPT_DIR/scripts"
	local PRE_SCRIPT="$SCRIPT_DIR/scripts_pre.sh"
	# avoid duplicating extra text if it was on first entry
	local desc_extra_text=""

	local file="$OUTDIR/scripts.tar" basedir=""
	local tarfiles_src="$EMBEDDED_SCRIPTS_DIR"
	create_archive

	append_certificates

	local tar="$file"
	file="$OUTDIR/scripts_pre.sh"
	create_prescript

	eval "$MKSWU_export_swdesc_cmd_vars_stanza"
	swdesc_script_nochroot
}

embedded_postinstall_script() {
	eval "$MKSWU_declare_swdesc_cmd_vars_stanza"
	local component="" version="" install_if=""
	local cmd_description="post_script"
	local file="$SCRIPT_DIR/scripts_post.sh"

	eval "$MKSWU_export_swdesc_cmd_vars_stanza"
	swdesc_script_nochroot
}

swdesc_option() {
	local opt
	for opt; do
		case "$opt" in
		FORCE_VERSION) MKSWU_component=""; MKSWU_FORCE_VERSION=1;;
		POST_ACTION=*) MKSWU_POST_ACTION=${opt#POST_ACTION=};;
		NOTIFY_STARTING_CMD=*) MKSWU_NOTIFY_STARTING_CMD="${opt#NOTIFY_STARTING_CMD=}";;
		NOTIFY_FAIL_CMD=*) MKSWU_NOTIFY_FAIL_CMD="${opt#NOTIFY_FAIL_CMD=}";;
		NOTIFY_SUCCESS_CMD=*) MKSWU_NOTIFY_SUCCESS_CMD="${opt#NOTIFY_SUCCESS_CMD=}";;
		ROOTFS_FSTYPE=*) MKSWU_ROOTFS_FSTYPE="${opt#ROOTFS_FSTYPE=}";;
		BOOT_SIZE=*) warning "BOOT_SIZE is no longer used and has been ignored";;
		ENCRYPT_KEYFILE=*)
			[ -n "$FIRST_SWDESC_INIT" ] \
				|| error "%s must be set before the first swdesc_* action" \
					"ENCRYPT_KEYFILE"
			ENCRYPT_KEYFILE="${opt#ENCRYPT_KEYFILE=}"
			if [ -n "$ENCRYPT_KEYFILE" ]; then
				ENCRYPT_KEYFILE="$(realpath -e "$ENCRYPT_KEYFILE")" \
					|| error "%s does not exist" "${opt#ENCRYPT_KEYFILE=}"
			fi
			;;
		PUBKEY=*)
			# signing is normally done at the end, but swdesc_container_usb
			# also uses it
			[ -n "$FIRST_SWDESC_INIT" ] \
				|| error "%s must be set before the first swdesc_* action" \
					"PUBKEY"
			PUBKEY="$(realpath -e "${opt#PUBKEY=}")" \
				|| error "%s does not exist" "${opt#PUBKEY=}"
			;;
		UPDATE_CERTS=*) UPDATE_CERTS="${opt#UPDATE_CERTS=}";;
		PRIVKEY=*)
			[ -n "$FIRST_SWDESC_INIT" ] \
				|| error "%s must be set before the first swdesc_* action" \
					"PRIVKEY"
			PRIVKEY="$(realpath -e "${opt#PRIVKEY=}")" \
				|| error "%s does not exist" "${opt#PRIVKEY=}"
			;;
		PRIVKEY_PASS=*) PRIVKEY_PASS="${opt#PRIVKEY_PASS=}";;
		DESCRIPTION=*) DESCRIPTION="${opt#DESCRIPTION=}";;
		component=*)
			MKSWU_version_last_set="$*"
			MKSWU_component="${opt#component=}";;
		version=*)
			MKSWU_version_last_set="$*"
			MKSWU_version="${opt#version=}";;
		install_if=*)
			MKSWU_version_last_set="$*"
			MKSWU_install_if="${opt#install_if=}";;
		until=*)
			MKSWU_until="$(date -d "${opt#until=}" +%s)" \
				|| error "swdesc_option until=%s was not in a format 'date -d' understands" \
					"${opt#until=}"
			;;
		BUILD_SBOM=*)
			MKSWU_BUILD_SBOM="${opt#BUILD_SBOM=}";;
		sbom_config_yaml=*)
			MKSWU_SBOM_CONFIG_YAML="$(realpath -e "${opt#sbom_config_yaml=}")" \
				|| error "%s does not exist" "${opt#sbom_config_yaml=}"
			;;
		PUBLIC)
			[ -n "$FIRST_SWDESC_INIT" ] \
				|| error "%s must be set before the first swdesc_* action" \
					"PUBLIC"
			info "Building SWU with public-onetime certificate"
			PUBKEY="$SCRIPT_DIR/swupdate-onetime-public.pem"
			PRIVKEY="$SCRIPT_DIR/swupdate-onetime-public.key"
			UPDATE_CERTS=""
			ENCRYPT_KEYFILE=""
			# note public certificate is removed if users certs are
			# present regardless of ALLOW_PUBLIC_CERT
			swdesc_option ALLOW_PUBLIC_CERT ALLOW_EMPTY_LOGIN
			;;
		NO_PRESERVE_FILE)
			warning "NO_PRESERVE_FILE is not officially supported and might stop working in the future."
			warning "Please consider updating swupdate_preserve_files or removing specific files"
			warning "through explicit swdesc_command instead."
			MKSWU_SWDESC_OPTS="$MKSWU_SWDESC_OPTS
$opt";;
		CONTAINER_CLEAR|NO_PRESERVE_FILES|\
		ENCRYPT_ROOTFS|ENCRYPT_USERFS|\
		SKIP_APP_SUBVOL_SYNC|ALLOW_PUBLIC_CERT|ALLOW_EMPTY_LOGIN)
			MKSWU_SWDESC_OPTS="$MKSWU_SWDESC_OPTS
$opt";;
		*) error "Unknown option for swdesc_option: %s" "$opt";;
		esac
	done
}

write_sw_desc() {
	local indent=4
	local file line section board=""
	local board_hwcompat board_normalize
	local IFS="
"

	track_used "$OUTDIR/sw-description"

	[ -n "$DESCRIPTION" ] || error "DESCRIPTION must be set"
	cat <<EOF
software = {
  version = "$MKSWU_VERSION";
  description = "$DESCRIPTION";
EOF

	# handle boards files first
	for file in "$OUTDIR/sw-description-"*-*; do
		[ -e "$file" ] || break
		board="${file#*sw-description-*-}"
		[ "$board" = "-" ] && continue
		track_used "$file"
		[ -e "$OUTDIR/sw-description-done-$board" ] && continue
		touch "$OUTDIR/sw-description-done-$board"
		board_normalize=$(printf %s "$board" | tr -c '[:alnum:]' '_')
		board_hwcompat=$(eval "printf %s \"\$HW_COMPAT_$board_normalize"\")
		[ -n "$board_hwcompat" ] || board_hwcompat="$HW_COMPAT"
		[ -n "$board_hwcompat" ] || error "HW_COMPAT or HW_COMPAT_%s must be set" "$board_normalize"
		indent=2 write_line "$board = {"
		indent=4 write_line "hardware-compatibility = [ \"$board_hwcompat\" ];"
		for file in "$OUTDIR/sw-description-"*"-$board"; do
			[ -s "$file" ] || continue
			# also include common section if any
			# XXX in case of duplicate here we should favor
			# current board's file and ignore second one instead
			# but it would be better with an explicit "board=none" kind
			# of syntax. The problem would be when to consider these
			# elements versions for sw-versions merging script, as we'd
			# need to check if a board matched first, so leave for later.
			check_duplicate_files "$file" "$OUTDIR/sw-description-$section"
			section=${file##*sw-description-}
			section=${section%%-*}
			indent=4 write_line "$section: ("
			indent=6 reindent "$file" "$OUTDIR/sw-description-$section"
			indent=4 write_line ");"
		done
		indent=2 write_line "};"
	done

	# only set global hardware-compatibility if no board specific ones found
	if [ -z "$board" ]; then
		[ -n "$HW_COMPAT" ] || error "HW_COMPAT must be set"
		echo "  hardware-compatibility = [ \"$HW_COMPAT\" ];"
	fi

	for file in "$OUTDIR/sw-description-"*; do
		board="${file##*sw-description-}"
		section="${board%-*}"
		[ "$section" = "$board" ] && board="" || board="${board#*-}"
	done

	# main sections for all boards
	for section in images files scripts; do
		file="$OUTDIR/sw-description-$section"
		[ -e "$file" ] || continue
		track_used "$file"
		check_duplicate_files "$file"
		indent=2 write_line "" "$section: ("
		indent=4 reindent "$file"
		indent=2 write_line ");"
	done

	# Store highest versions in special comments
	if [ -e "$OUTDIR/sw-description-versions" ]; then
		track_used "$OUTDIR/sw-description-versions"
		sort -u -k 1,1 -k 4,4 -k 1 < "$OUTDIR/sw-description-versions" \
				| sort -u -k 1,1 -k 4,4 -c \
			|| error "above component used multiple times with different versions or install-if mode"
		sort -u -k 1,1 -k 4,4 -k 1 < "$OUTDIR/sw-description-versions" \
				| sed -e 's/^/  #VERSION /'
	fi
	MKSWU_SWDESC_OPTS="${MKSWU_SWDESC_OPTS:+$MKSWU_SWDESC_OPTS
}${MKSWU_FORCE_VERSION:+FORCE_VERSION}"
	local option
	for option in $MKSWU_SWDESC_OPTS; do
		echo "  # MKSWU_$option 1"
	done
	[ -n "${MKSWU_NOTIFY_STARTING_CMD+1}" ] \
		&& echo "$MKSWU_NOTIFY_STARTING_CMD" \
			| sed -e 's/^/  # MKSWU_NOTIFY_STARTING_CMD /'
	[ -n "${MKSWU_NOTIFY_FAIL_CMD+1}" ] \
		&& echo "$MKSWU_NOTIFY_FAIL_CMD" \
			| sed -e 's/^/  # MKSWU_NOTIFY_FAIL_CMD /'
	[ -n "${MKSWU_NOTIFY_SUCCESS_CMD+1}" ] \
		&& echo "$MKSWU_NOTIFY_SUCCESS_CMD" \
			| sed -e 's/^/  # MKSWU_NOTIFY_SUCCESS_CMD /'
	case "$MKSWU_ROOTFS_FSTYPE" in
	ext4|btrfs) echo "# MKSWU_ROOTFS_FSTYPE $MKSWU_ROOTFS_FSTYPE";;
	"") ;;
	*) error "invalid ROOTFS_FSTYPE \"%s\", must be empty, ext4 or btrfs" \
		"$MKSWU_ROOTFS_FSTYPE";;
	esac
	[ -n "$MKSWU_until" ] \
		&& echo "  # MKSWU_UNTIL $(date +%s) $MKSWU_until"
	case "$MKSWU_POST_ACTION" in
	poweroff|wait|container) echo " # MKSWU_POST_ACTION $MKSWU_POST_ACTION";;
	""|reboot) ;;
	*) error "invalid POST_ACTION \"%s\", must be empty, poweroff or wait" \
		"$MKSWU_POST_ACTION";;
	esac

	# and also add extra debug comments
	for line in $DEBUG_SWDESC; do
		indent=2 write_line "$line"
	done

	indent=0 write_line "};"
}

check_common_mistakes() {
	local swdesc="$OUTDIR/$1"

	# grep for common patterns of easy mistakes that would fail installing
	! grep -qF '$6$salt$hash' "$swdesc" \
		|| error "Please set user passwords (usermod command in .desc)"
}

check_duplicate_files() {
	local file duplicates

	duplicates=$(for file; do
		[ -e "$file" ] || continue
		cat "$file"
	done | awk -F\" '
		/filename = / {
			if (seen[$2]) {
				print $2
			}
			seen[$2] = 1;
		}
	')

	[ -z "$duplicates" ] || error "Duplicate files detected in sw-description: duplicate swdesc_* commands?\n%s" "$duplicates"
}

sign() {
	local file="$OUTDIR/$1"

	track_used "$file.sig"

	check_validity "$file.sig" "$file" "$PUBKEY" "$PRIVKEY" && return
	[ -n "$PRIVKEY" ] || error "PRIVKEY must be set"
	[ -n "$PUBKEY" ] || error "PUBKEY must be set"
	[ -r "$PRIVKEY" ] || error "Cannot read PRIVKEY: %s" "$PRIVKEY"
	[ -r "$PUBKEY" ] || error "Cannot read PUBKEY: %s" "$PUBKEY"

	# allow some retries for password failures
	local retries=0
	while ! openssl cms -sign -in "$file" -out "$file.sig.tmp" \
			-signer "$PUBKEY" -inkey "$PRIVKEY" \
			-outform DER -nosmimecap -binary \
			${PRIVKEY_PASS:+-passin "$PRIVKEY_PASS"}; do
		retries=$((retries + 1))
		if [ -n "$PRIVKEY_PASS" ] || [ "$retries" -ge 3 ]; then
			error "Could not sign %s" "$file"
		fi
		info "File signature failed, was the password correct?"
		info "Retrying %s more time(s)." "$((3 - retries))"
	done

	mv "$file.sig.tmp" "$file.sig" \
		|| error "Could not rename %s" "$file.sig"
}

verify() {
	# note: this is only a helper for debug
	local file="$1"
	openssl cms -verify -inform DER -in "$file.sig" -content "$file" \
		-nosmimecap -binary -CAfile "$PUBKEY" > /dev/null \
		|| error "Signature verification failed for $file"
	echo "$file: OK"
}

mkcpio() {
	local CPIO_FILES

	FILES="${FILES_FIRST}${FILES}"

	check_common_mistakes sw-description
	sign sw-description
	(
		IFS="
"
		cd "$OUTDIR" || error "Could not enter %s" "$OUTDIR"
		# GNU gpio has a bug with checksums for files bigger than 2GB
		# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=962188
		# fall back to newc in this case, we also compute sha256 for
		# every file so this isn't a big loss
		format=crc
		# shellcheck disable=SC2086 # we want split by newline
		if ! stat -L -c %s $FILES | awk '$1 > 2147483648 { exit 1; }'; then
			format=newc
		fi
		printf %s "$FILES" | cpio -o -H "$format" -L --quiet
	) > "$OUT" || error "Could not create SWU (cpio archive)"

	CPIO_FILES=$(cpio -t --quiet < "$OUT")
	[ "$CPIO_FILES" = "$FILES" ] \
		|| error "cpio does not contain files we requested (in the order we requested): check %s" "$OUT"
}

track_used() {
	local file

	for file; do
		# only track files inside outdir
		[ "${file#"$OUTDIR"}" = "$file" ] && continue

		if [ -d "$file" ]; then
			find "$file" -type f -print0
		else
			printf "%s\0" "$file"
		fi >> "$OUTDIR/used_files"
	done
}

sbom_input() {
	local file
	for file; do
		if [ -e "$file" ]; then
			MKSWU_SBOM_INPUT="${MKSWU_SBOM_INPUT:+$MKSWU_SBOM_INPUT }-f $(shell_quote "$(realpath "$file")")"
		else
			MKSWU_SBOM_INPUT="${MKSWU_SBOM_INPUT:+$MKSWU_SBOM_INPUT }-f $(shell_quote "$file")"
		fi
	done
}

cleanup_outdir() {
	local file

	sort -z < "$OUTDIR/used_files" > "$OUTDIR/used_files.sorted"
	find "$OUTDIR" -not -type d -print0 | sort -z \
		| join -z -t '\0' -v 1 - "$OUTDIR/used_files.sorted" \
		| xargs -0 -r rm -f

	# also remove any empty dir
	# busybox find does not have find -empty, but we have
	# GNU coreutils so we can use rm --ignore-fail-on-non-empty
	find "$OUTDIR" -depth -mindepth 1 -type d \
		-exec rmdir --ignore-fail-on-non-empty {} +
}

update_mkswu_conf() {
	local confbase="${CONFIG##*/}"
	local NEW_CONFIG="$CONFIG"

	if [ "$confbase" = mkimage.conf ]; then
		NEW_CONFIG="$(dirname "$CONFIG")/mkswu.conf"
		[ -e "$NEW_CONFIG" ] && error "Trying to convert from mkimage.conf to mkswu.conf, but mkswu.conf already exists!"
	fi

	[ -e "$CONFIG_DIR" ] || mkdir -vp "$CONFIG_DIR"

	# subshell to not source multiple versions of same file
	(
		set +e
		sha=$(sha256sum "$SCRIPT_DIR/mkswu.conf.defaults")
		sha=${sha%% *}
		if [ -e "$CONFIG" ]; then
			local DEFAULTS_MKSWU_CONF_SHA256
			DEFAULTS_MKSWU_CONF_SHA256=$(sed -ne 's/DEFAULTS_\(MKIMAGE\|MKSWU\)_CONF_SHA256="\(.*\)"/\2/p' "$CONFIG")
			# config exist + no sha: don't update
			[ -z "$DEFAULTS_MKSWU_CONF_SHA256" ] && exit
			# sha didn't change: don't update
			[ "$DEFAULTS_MKSWU_CONF_SHA256" = "$sha" ] && exit

			# keep old version
			cp "$CONFIG" "$CONFIG.autosave-$(date +%Y%m%d)" \
				|| error "Could not update config %s" "$CONFIG"

			# update hash, trim comments/empty lines past auto section comment
			# and update obsolete header if still present
			sed -e "s/^DEFAULTS_\(MKSWU\|MKIMAGE\)_CONF_SHA256=.*/DEFAULTS_MKSWU_CONF_SHA256=\"$sha\"/" \
			    -e '/^## auto section/p' -e '/^## auto section/,$ {/^#\|^$/ d}' \
			    -e 's/^# defaults section: if you remove this include you must keep this file up/# defaults section: used to keep auto section comments below up to date/' \
			    -e 's/^# to date with mkimage.conf\(.defaults\)\? changes!/# if you remove it the file will not be edited again./' \
			    -e '/^\. .*mkimage.conf.defaults/d' \
			    "$CONFIG" > "$NEW_CONFIG.new" \
				|| error "Could not update config %s" "$CONFIG"
		else
			cat > "$NEW_CONFIG.new" <<EOF \
				|| error "Could not update config %s" "$CONFIG"
# defaults section: used to keep auto section comments below up to date
# if you remove it the file will not be edited again.
DEFAULTS_MKSWU_CONF_SHA256="$sha"

## user section: this won't be touched

## auto section: you can make changes here but comments will be lost
EOF
		fi

		sed -e 's/^[^#]/#&/' "$SCRIPT_DIR/mkswu.conf.defaults" >> "$NEW_CONFIG.new" \
			&& mv "$NEW_CONFIG.new" "$NEW_CONFIG" \
			|| error "Could not update config %s" "$CONFIG"

		info "Updated config file %s" "$NEW_CONFIG" >&2
	) || exit

	# if renamed, remove old config after all is done
	if [ "$CONFIG" != "$NEW_CONFIG" ] && [ -e "$NEW_CONFIG" ]; then
		rm -f "$CONFIG"
		CONFIG="$NEW_CONFIG"
	fi
}

mkswu_import() {
	# import standalone config file and its keys to $HOME/swu
	local NEW_CONFIG_DIR="${MKSWU_IMPORT_CONFIG_DIR:-$HOME/mkswu}"
	local NEW_CONFIG="$NEW_CONFIG_DIR/mkswu.conf"
	CONFIG_DIR="$(realpath "$(dirname "$CONFIG")")" \
		|| error "Could not resolve %s directory name" "$CONFIG"

	[ $# -gt 0 ] && error "--%s had extra arguments?" "$(mode_to_opt "$MODE")"
	if [ -e "$NEW_CONFIG" ]; then
		warning "Config %s already exists, skipping import" "$NEW_CONFIG"
		return
	fi

	info "Importing config %s and associated keys to %s" "$CONFIG" "$NEW_CONFIG_DIR"

	mkdir -vp "$NEW_CONFIG_DIR"
	for file in "$PRIVKEY" "$PUBKEY" "$ENCRYPT_KEYFILE"; do
		[ -z "$file" ] && continue
		[ -e "$file" ] \
			|| error "Key file %s could not be found, try setting absolute path in config file and reimport" "${file##*/}"

		cp -v "$file" "$NEW_CONFIG_DIR/" \
			|| error "Could not copy %s to %s" "$file" "$NEW_CONFIG_DIR/"
	done

	sed -e "s@^PRIVKEY=.*@PRIVKEY=\"\$CONFIG_DIR/$(basename "$PRIVKEY")\"@" \
	    -e "s@^PUBKEY=.*@PUBKEY=\"\$CONFIG_DIR/$(basename "$PUBKEY")\"@" \
	    -e "s@^ENCRYPT_KEYFILE=.*@ENCRYPT_KEYFILE=\"\$CONFIG_DIR/$(basename "$ENCRYPT_KEYFILE")\"@" \
	    "$CONFIG" > "$NEW_CONFIG" \
		|| error "Could not update config %s" "$NEW_CONFIG"

	info "Imported config %s to %s" "$CONFIG" "$NEW_CONFIG"
	info "You can know check mkswu works with new config and remove the old directory"
}

absolutize_file_paths() {
	[ "${PRIVKEY#/}" != "$PRIVKEY" ] || PRIVKEY=$(realpath "$PRIVKEY")
	[ "${PUBKEY#/}" != "$PUBKEY" ] || PUBKEY=$(realpath "$PUBKEY")
	[ -z "$ENCRYPT_KEYFILE" ] \
		|| [ "${ENCRYPT_KEYFILE#/}" != "$ENCRYPT_KEYFILE" ] \
		|| ENCRYPT_KEYFILE=$(realpath "$ENCRYPT_KEYFILE")
}

mkswu_genkey_aes() {
	local oldumask

	[ $# -gt 0 ] && error "--%s had extra arguments?" "$(mode_to_opt "$MODE")"
	if [ -z "$ENCRYPT_KEYFILE" ]; then
		info "Info: using default aes key path"
		ENCRYPT_KEYFILE="$CONFIG_DIR/swupdate.aes-key"
		printf "%s\n" '' '# Default encryption key path (set by genkey.sh)' \
			'ENCRYPT_KEYFILE="$CONFIG_DIR/swupdate.aes-key"' >> "$CONFIG" \
			|| error "Could not update %s in %s" "ENCRYPT_KEYFILE" "$CONFIG"
	fi
	while [ -s "$ENCRYPT_KEYFILE" ]; do
		if [ -n "$NOPROMPT" ]; then
			warning "%s already exists, skipping" "$ENCRYPT_KEYFILE"
			return
		fi
		prompt_yesno y "%s already exists, generate new key? [Y/n]" "$ENCRYPT_KEYFILE" \
			|| return
		# increment index, update config at the end
		local idx
		ENCRYPT_KEYFILE="${ENCRYPT_KEYFILE%.aes-key}"
		idx="${ENCRYPT_KEYFILE##*-}"
		case "$idx" in
		""|*[a-zA-Z/]*) idx=1;;
		*)
			ENCRYPT_KEYFILE="${ENCRYPT_KEYFILE%-"$idx"}"
			;;
		esac
		idx=$((idx+1))
		ENCRYPT_KEYFILE="${ENCRYPT_KEYFILE}-$idx.aes-key"
		extrakey=1
	done

	oldumask=$(umask)
	umask 0377
	ENCRYPT_KEY="$(openssl rand -hex 32)" || error "Generating random number failed"
	printf "%s\n" "$ENCRYPT_KEY $(openssl rand -hex 16)" > "$ENCRYPT_KEYFILE"
	umask "$oldumask"

	info "Created encryption keyfile %s" "$ENCRYPT_KEYFILE"

	if [ -n "$extrakey" ]; then
		info "Info: Adding %s to config" "$ENCRYPT_KEYFILE"
		NEWKEY="${ENCRYPT_KEYFILE#"$CONFIG_DIR"}"
		if [ "$NEWKEY" != "$ENCRYPT_KEYFILE" ]; then
			NEWKEY="\$CONFIG_DIR/${NEWKEY#/}"
		fi
		printf "%s\n" '' '# extra encryption key. Remove the old one and use new' \
				'# ENCRYPT_KEYFILE after having installed an update with this first' \
				"NEW_ENCRYPT_KEYFILE=\"$NEWKEY\"" >> "$CONFIG" \
			|| error "Could not update %s in %s" "NEW_ENCRYPT_KEYFILE" "$CONFIG"
	fi

	info "You must also enable aes encryption with initial_setup.desc or equivalent"
}

mkswu_genkey_sign() {
	local oldumask extrakey=""
	local CURVE="${GENKEY_CURVE:-secp256k1}"
	local DAYS="${GENKEY_DAYS:-$((5*365))}"
	local NEWCERT

	[ $# -gt 0 ] && error "--%s had extra arguments?" "$(mode_to_opt "$MODE")"
	[ -n "$PRIVKEY" ] || error "PRIVKEY is not set in config file"
	[ -n "$PUBKEY" ] || error "PUBKEY is not set in config file"
	[ -z "$NOPROMPT" ] || [ -n "$GENKEY_CN" ] \
		|| error "%s must be set if using %s" "--cn" "--noprompt"

	while [ -s "$PRIVKEY" ] && [ -s "$PUBKEY" ]; do
		if [ -n "$NOPROMPT" ]; then
			warning "%s already exists, skipping" "$PRIVKEY"
			return
		fi
		prompt_yesno y "%s already exists, generate new key pair? [Y/n]" "$PRIVKEY" \
			|| return
		# increment index, update config at the end
		local idx
		PRIVKEY="${PRIVKEY%.key}"
		PUBKEY="${PUBKEY%.pem}"
		idx="${PRIVKEY##*-}"
		case "$idx" in
		""|*[a-zA-Z/]*) idx=1;;
		*)
			PRIVKEY="${PRIVKEY%-"$idx"}"
			PUBKEY="${PUBKEY%-"$idx"}"
			;;
		esac
		idx=$((idx+1))
		PRIVKEY="${PRIVKEY}-$idx.key"
		PUBKEY="${PUBKEY}-$idx.pem"
		extrakey=1
	done

	while [ -z "$GENKEY_CN" ]; do
		prompt GENKEY_CN "Enter certificate common name:"
	done

	info "Creating signing key %s and its public counterpart %s" "$PRIVKEY" "${PUBKEY##*/}"

	openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:"$CURVE" \
		-keyout "$PRIVKEY.tmp" -out "$PUBKEY.tmp" -subj "/O=SWUpdate/CN=$GENKEY_CN" \
		${GENKEY_PLAIN:+-nodes} ${PRIVKEY_PASS:+-passout $PRIVKEY_PASS} \
		-days "$DAYS" || error "Generating certificate/key pair failed"

	mv "$PRIVKEY.tmp" "$PRIVKEY" \
		|| error "Could not rename %s" "$PRIVKEY"
	sed -e "1i# ${PUBKEY##*/}: $GENKEY_CN" "$PUBKEY.tmp" > "$PUBKEY" \
		&& rm -f "$PUBKEY.tmp" \
		|| error "Could not rename %s" "$PUBKEY"

	if [ -n "$extrakey" ]; then
		info "Info: Adding %s to config" "$PUBKEY"
		NEWCERT="${PUBKEY#"$CONFIG_DIR"}"
		if [ "$NEWCERT" != "$PUBKEY" ]; then
			NEWCERT="\$CONFIG_DIR/${NEWCERT#/}"
		fi
		printf "%s\n" '' '# extra swupdate certificate. Remove the old one and use new' \
				'# PRIVKEY after having installed an update with this first' \
				"PUBKEY=\"\$PUBKEY,$NEWCERT\"" \
				'# remove "NEW_" to use' \
				"NEW_PRIVKEY=\"${NEWCERT%.pem}.key\"" >> "$CONFIG" \
			|| error "Could not update %s in %s" "NEW_PRIVKEY" "$CONFIG"
		case "$UPDATE_CERTS" in
		[Nn]|[Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|0|"")
			printf "%s\n" "# This controls if we should update certificates on device, and can be" \
					"# removed once all devices have been updated to only allow new certificate" \
					"UPDATE_CERTS=yes" >> "$CONFIG" \
				|| error "Could not update %s in %s" "UPDATE_CERTS" "$CONFIG"
			;;
		esac
	fi

	info "%s will be copied over to /etc/swupdate.pem when installing newly generated swu" "$PUBKEY"
	info "You will then be able to remove the previous key by editing %s" "$CONFIG"
}

mkswu_internal() {
	[ $# -lt 1 ] && error "%s requires an argument" "--internal"
	local command="$1"
	shift
	case "$command" in
	get_var)
		[ $# -lt 1 ] && error "%s requires an argument" "--internal $command"
		local var val
		for var; do
			case "$var" in
			*[!0-9a-zA-Z_]*) error "Invalid variable name %s" "$var";;
			esac
			# check if set
			eval 'val=${'"$var"'+1}'
			[ -n "$val" ] || error "Variable %s was not set" "$var"
			eval 'val=$'"$var"
			printf "%s=%s\n" "$var" "$val"
		done
		;;
	sign)
		[ $# -lt 1 ] && error "%s requires an argument" "--internal $command"
		local file
		for file; do
			[ -e "$file" ] || error "%s does not exist" "$file"
			OUTDIR=$(dirname "$file")
			sign "$(basename "$file")"
		done
		;;

	verify)
		[ $# -lt 1 ] && error "%s requires an argument" "--internal $command"
		local file
		for file; do
			[ -e "$file" ] || error "%s does not exist" "$file"
			[ -e "$file.sig" ] || error "%s does not exist" "$file.sig"
			verify "$file"
		done
		;;
	encrypt)
		[ $# -lt 1 ] && error "%s requires an argument" "--internal $command"
		setup_encryption
		local file
		for file; do
			iv=$(gen_iv)
			encrypt_file "$file" "enc.$file" "$iv"
			echo "$file iv: $iv"
		done
		;;
	decrypt)
		[ $# -lt 2 ] && error "%s requires at least two arguments" "--internal $command"
		[ $(($# % 2)) = 0 ] || error "%s requires an even number of arguments" "--internal $command"
		local file iv

		while [ $# -ge 2 ]; do
			file="$1"
			iv="$2"
			shift 2
			decrypt_file "$file" "$iv"
		done
		;;
	mkcpio)
		[ $# -lt 1 ] && error "%s requires an argument" "--internal $command"
		OUT="$1"
		[ "${OUT%.swu}" != "$OUT" ] || error "%s must end with .swu" "$OUT"
		OUTDIR=$(dirname -- "$OUT")/.$(basename -- "$OUT" .swu)
		shift
		FILES_FIRST=""
		if [ $# -eq 0 ]; then
			[ -e "$OUT" ] || error "%s does not exist" "$OUT"
			FILES="$(cpio -t --quiet < "$OUT")"
			info "Packing:\n%s" "$FILES"
		else
			FILES=$(printf "%s\n" "$@")
		fi
		mkcpio
		info "Successfully generated %s" "$OUT"
		;;
	eval)
		eval "$*"
		;;
	*)
		error "Unrecognized internal command %s" "$command"
		;;
	esac
}

mkinit_genkey() {
	local GENKEY_CN
	local KEYPASS KEYPASS_CONFIRM

	[ -e "$PUBKEY" ] && [ -e "$PRIVKEY" ] && return

	while [ -z "$GENKEY_CN" ]; do
		prompt GENKEY_CN "Enter certificate common name:"
	done

	while true; do
		PASS=1 prompt KEYPASS "Enter private key password (4-1024 char)"
		if [ -z "$KEYPASS" ]; then
			info "Empty key password is not recommended, re-enter empty to confirm"
		elif [ "${#KEYPASS}" -lt 4 ] || [ "${#KEYPASS}" -gt 1024 ]; then
			info "Must be between 4 and 1024 characters long"
			continue
		fi
		PASS=1 prompt KEYPASS_CONFIRM "private key password (confirm):"
		if [ "$KEYPASS" != "$KEYPASS_CONFIRM" ]; then
			info "Passwords do not match"
			continue
		fi
		break
	done

	if [ -n "$KEYPASS" ]; then
		echo "$KEYPASS" | PRIVKEY_PASS="stdin" \
			VERBOSE=1 NOPROMPT=1 mkswu_genkey_sign
	else
		GENKEY_PLAIN=1 VERBOSE=1 NOPROMPT=1 mkswu_genkey_sign
	fi || exit 1

	# Also prompt for encryption
	[ -n "$ENCRYPT_KEYFILE" ] && [ -e "$ENCRYPT_KEYFILE" ] && return
	if prompt_yesno n "Use AES encryption? (N/y)"; then
		VERBOSE=1 mkswu_genkey_aes
		info "Generated %s" "$ENCRYPT_KEYFILE"
	fi
}

geninitdesc_hashpw() {
	local plain="$1"

	printf "%s" "$plain" | openssl passwd -6 -stdin
}

check_password_strength() {
	local password="$1"

	# sbin not in default debian PATH, add it temporarily for cracklib-check
	local PATH="$PATH:/usr/sbin"

	if command -v cracklib-check >/dev/null; then
		if ! echo "$password" | cracklib-check | grep -qE ': OK$'; then
			warning "password not strong enough:"
			# cracklib-check output is already localized; remove password
			# from output though...
			echo "$password" | cracklib-check | sed -e 's/.*: /    /'
			return 1
		fi
	fi

	# check password length
	local PASSWORD_MIN_LEN=8
	if [ "${#password}" -lt "$PASSWORD_MIN_LEN" ]; then
		warning "Password is too short, please set at least %s characters." "$PASSWORD_MIN_LEN"
		return 1
	fi
}

geninitdesc_promptpass() {
	local user="$1"
	local password
	local confirm

	while true; do
		if [ "$user" = "root" ]; then
			PASS=1 prompt password "%s user password:" "$user"
			if [ -z "$password" ]; then
				info "A root password is required"
				continue
			fi
		elif [ "$user" = "abos-web" ]; then
			PASS=1 prompt password "abos-web password (empty = service disabled):"
		else
			PASS=1 prompt password "%s user password (empty = locks account):" "$user"
		fi
		[ -z "$password" ] || check_password_strength "$password" || continue
		PASS=1 prompt confirm "%s password (confirm):" "$user"
		if [ "$password" != "$confirm" ]; then
			info "Passwords do not match"
			continue
		fi
		if [ -z "$password" ]; then
			password="-L"
		else
			password=$(echo "$password" | sed -e "s/\"/\"'\"'r/")
			password=$(geninitdesc_hashpw "$password")
			[ -n "$password" ] || error "Could not generate password"
			password="-p '\"'$password'\"'"
		fi
		break
	done
	PASSWD="$password"
}

geninitdesc_prompt_abosweb() {
	local PASSWD

	info "Please set the password to log into abos-web."
	geninitdesc_promptpass abos-web
	if [ "$PASSWD" != "-L" ]; then
		sed -i -e 's:setting passwords.*:&\
	"if ! id abos-web-admin >/dev/null 2>\&1; then\
		addgroup -S abos-web-admin 2>/dev/null;\
		adduser -S -D -H -h /var/empty -s /sbin/nologin -G abos-web-admin -g abos-web-admin abos-web-admin 2>/dev/null;\
	fi" \\\
	"usermod '"$PASSWD"' abos-web-admin" \\:' "$desc.tmp" \
			|| error "Could not update %s" "$desc.tmp"
	fi
}

geninitdesc_fixinitdesc() {
	local version max_version=4
	version=$(sed -ne 's/swdesc_option.*version=\([0-9]*\).*/\1/p' "$desc")
	if [ -z "$version" ]; then
		echo "initial_setup.desc is too old, regenerating it"
		echo "old initial_setup.desc is kept in %s" "$desc.old"
		mv "$desc" "$desc.old" || error "Could not rename %s" "$desc"
		return 1
	fi
	[ "$version" -ge "$max_version" ] && return

	trap "rm -f $desc.tmp" EXIT
	cp "$desc" "$desc.tmp" || error "Could not copy %s to %s" "$desc" "$desc.tmp"

	if [ "$version" -le 1 ]; then
		info "atmark password was incorrectly generated, regenerating it"
		info "if initial_setup was already installed please adjust password"
		info "with %s if necessary" "$SCRIPT_DIR/examples/reset_atmark_pass.desc"

		local PASSWD
		geninitdesc_promptpass atmark
		sed -i -e 's:\(^[ \t]*"usermod\).*atmark:\1 '"$PASSWD"' atmark:' \
				"$desc.tmp" \
			|| error "Could not update %s" "$desc.tmp"
	fi

	if [ "$version" -le 2 ]; then
		# read all to pattern space for newlines, there might be a
		# better way of doing this but it's "good enough"
		sed -i -e ':a;$!N;$!ba;s/\(\nswdesc_command\)\( \\\n[^\n]*usermod\)/\1 --description "setting passwords"\2/' \
				"$desc.tmp" \
			|| error "Could not update %s" "$desc.tmp"
	fi

	if [ "$version" -le 3 ]; then
		geninitdesc_prompt_abosweb
	fi

	sed -i -e "s/version=[0-9]*/version=$max_version/" \
			"$desc.tmp" \
		|| error "Could not update %s" "$desc.tmp"
	mv "$desc.tmp" "$desc" \
		|| error "Could not rename %s" "$desc"
	trap "" EXIT
}

mkinit_geninitdesc() {
	local KEEPATMARKPEM AUTOUPDATE
	local PASSWD ROOTPW ATMARKPW
	local desc="$CONFIG_DIR/initial_setup.desc"

	[ -e "$desc" ] && geninitdesc_fixinitdesc && return

	prompt_yesno y "Allow updates signed by Atmark Techno? (Y/n)" && KEEPATMARKPEM=1

	geninitdesc_promptpass "root"
	ROOTPW="$PASSWD"
	geninitdesc_promptpass "atmark"
	ATMARKPW="$PASSWD"

	# cleanup if we fail here
	trap "rm -f $desc.tmp" EXIT

	cp "$SCRIPT_DIR/examples/initial_setup.desc" "$desc.tmp" \
		|| error "Could not copy initial_setup.desc from example dir"
	if [ -z "$KEEPATMARKPEM" ]; then
		sed -i -e 's@^#\(.*> /etc/swupdate.pem\)@\1@' "$desc.tmp" \
			|| error "Could not update %s" "$desc.tmp"
	fi

	sed -i -e 's:\(^[ \t]*"usermod\).*atmark:\1 '"$ATMARKPW"' atmark:' \
			-e 's:\(^[ \t]*"usermod\).*root:\1 '"$ROOTPW"' root:' "$desc.tmp" \
		|| error "Could not update %s" "$desc.tmp"

	if [ -n "$KEEPATMARKPEM" ] && \
	    prompt_yesno n "Enable auto-updates (BaseOS / upgradable containers) from armadillo.atmark-techno.com servers? (N/y)"; then

		while true; do
			prompt AUTOUPDATE "Select update frequency ([weekly]/daily)"
			case "$AUTOUPDATE" in
			weekly|"")
				AUTOUPDATE="next week"; break;;
			daily)
				AUTOUPDATE="tomorrow"; break;;
			esac
		done
		cat >> "$desc.tmp" <<EOF \
			|| error "Could not update %s" "$desc.tmp"

# autoupdate
swdesc_command 'rc-update add swupdate-url default' \
	'echo -e "schedule=\"0 $AUTOUPDATE\"\nrdelay=21600" > /etc/conf.d/swupdate-url'
EOF
	fi
	geninitdesc_prompt_abosweb

	mv "$desc.tmp" "$desc" \
		|| error "Could not rename %s" "$desc"
	trap "" EXIT
}

mkinit_mkimageinitswu() {
	"$0" --config-dir "$CONFIG_DIR" --config "$CONFIG" \
			"$CONFIG_DIR/initial_setup.desc" \
		|| error "Could not generate initial setup swu"
	echo
	info "You can use \"%s\" as is or" "$CONFIG_DIR/initial_setup.swu"
	info "regenerate an image with extra modules using the following command:"
	info "  mkswu \"%s\" [other_desc_files]" \
		"$CONFIG_DIR/initial_setup.desc"
	info
	info "Note that once installed, you must preserve this directory as losing"
	info "key files means you will no longer be able to install new updates without"
	info "manually adjusting /etc/swupdate.pem on devices"
}


mkswu_init() {
	mkinit_genkey
	mkinit_geninitdesc
	mkinit_mkimageinitswu
}

update_one_version() {
	local desc="$1"
	local old_version version="$VERSION_BASE"

	if ! [ -e "$desc" ]; then
		warning "%s does not exist" "$desc"
		return 1
	fi

	old_version=$(sed -ne 's/^swdesc_option.*version=\([^ ]*\).*/\1/p' "$desc")
	if [ -z "$old_version" ]; then
		warning "Warning: Could not find current version in %s, not updating" "$desc"
		return 1
	fi
	trace "Found old version %s" "$old_version"

	case "$version,$old_version" in
	"$version,$version"-*)
		version="$version-$((${old_version##*-} + 1))"
		;;
	,*-*)
		# no version base, just increment last digit
		version="${old_version%-*}-$((${old_version##*-} + 1))"
		;;
	,*.*)
		# same with dot separation...
		version="${old_version%.*}.$((${old_version##*.} + 1))"
		;;
	,*)
		# same with no separator
		version="$((old_version + 1))"
		;;
	*)
		# old version isn't new version, check we're higher
		# and just use as is.
		if command -v version_higher >/dev/null \
		    && version_higher "$version-0" "$old_version"; then
			warning "Warning: Desc %s previous version %s is higher than base %s-0, refusing to update" \
				"$desc" "$old_version" "$version"
			return 1
		fi
		version="$version-0"
		;;
	esac

	mv "$desc" "$desc.old" \
		|| error "Could not rename %s" "$desc"
	if ! sed -e 's/\(swdesc_option.*version=\)\([^ ]*\)/\1'"$version"'/' \
			"$desc.old" > "$desc"; then
		mv "$desc.old" "$desc"
		error "Could not update %s" "$desc"
	fi

	info "Updated %s version from %s to %s" "$desc" "$old_version" "$version"
}

mkswu_version_cmp() {
	[ "$#" -ge 2 ] \
		|| error "Usage: mkswu --version-cmp <base_version> <version> [<version...>]"
	local base="$1"
	local rc=0
	shift

	# for version_higher
	[ -e "$SCRIPT_DIR/scripts/versions.sh" ] \
		|| error "Required file not found: %s" "$SCRIPT_DIR/scripts/versions.sh"
	. "$SCRIPT_DIR/scripts/versions.sh"

	check_version_higher "$base"

	for version in "$@"; do
		if ! ( check_version_higher "$version" ); then
			# invalid version
			[ "$rc" -lt 3 ] && rc=3
			continue
		fi
		if version_higher "$base" "$version"; then
			info "%s < %s" "$base" "$version"
		else
			if version_higher "$version" "$base"; then
				info "%s < %s" "$version" "$base"
			else
				info "%s = %s" "$base" "$version"
			fi
			# non-installed artefacts
			[ "$rc" -lt 2 ] && rc=2
		fi
	done
	return $rc
}

mkswu_update_version() {
	local desc
	local status=0

	# make sure version base is valid
	case "$VERSION_BASE" in
	*-*)
		error "--version-base %s must not include a dash" "$VERSION_BASE"
		;;
	*.*.*.*)
		error "--version-base %s must have at most 3 components (x[.y[.z]])" \
			"$VERSION_BASE"
		;;
	"")	;;
	*)
		check_version_higher "$VERSION_BASE"
		;;
	esac

	# optionally source versions.sh for version_higher.
	# skipped if not found
	if [ -e "$SCRIPT_DIR/scripts/versions.sh" ]; then
		. "$SCRIPT_DIR/scripts/versions.sh"
	fi

	for desc; do
		update_one_version "$desc" || status=$?
	done
	return "$status"
}

mkswu_show() {
	local swu desc sig order awk=""

	if command -v gawk >/dev/null; then
		awk="gawk"
	elif command -v awk > /dev/null; then
		# gensub isn't posix and mawk in particular doesn't like it
		echo "test" | awk ' {
				$0 = gensub(/(te)st/, "\\1", "g");
				if ($0 != "te") {
					exit(1)
				}
			}' 2>/dev/null \
			&& awk="awk"
	fi

	for swu; do
		[ "$swu" != "${swu%.swu}" ] \
			|| error "File does not end in .swu: %s" "$swu"

		printf "# %s\n\n" "$swu"

		desc=$(cpio -i --quiet --to-stdout sw-description < "$swu")
		[ -n "$desc" ] || error "Could not get swu sw-description content from %s" "$swu"

		# default to raw for old swu generated before command hints
		if ! printf "%s\n" "$desc" | grep -qE '# swdesc|description:'; then
			info "SWU was build with an old version of mkswu or was empty,"
			info "falling back to --raw"
			echo
			SHOW_RAW=1
		fi
		if [ -n "$SHOW_RAW" ]; then
			printf "%s\n" "$desc"
			continue
		fi

		[ -n "$awk" ] || error "mkswu --show requires a compatible awk (e.g. gawk)"

		sig=$(cpio -i --quiet --to-stdout sw-description.sig < "$swu" \
				| openssl asn1parse -inform DER \
				| "$awk" -F: -v "verbose=$VERBOSE" '
					$3 == "commonName" { in_cn=1 }
					in_cn=1 && /UTF8STRING/ { cn=$4 }
					in_cn=1 && /INTEGER/ { serial=$4 }
					{ in_cn=0 }
					END {
						if (!cn) { exit }
						printf("# signed by \"%s\"", cn)
						if (verbose > 2 && serial) { print ", serial:", serial }
					}')

		order=$(cpio -t --quiet < "$swu") \
			|| error "Could not get SWU file list from %s" "$swu"

		printf "%s\n" "$desc" | "$awk" -v "sig=$sig" -v "order_string=$order" '
			function dequote() {
				# undo "conf_quote"
				sub(/";$/, "")
				# only "unquote" after an odd number of backslashes...
				$0 = gensub(/(^|[^\\])((\\\\)*)\\n/, "\\1\\2\n", "g");
				$0 = gensub(/(^|[^\\])((\\\\)*)\\"/, "\\1\\2\"", "g");
				gsub(/\\\\/, "\\")
			}
			# pre mkswu-5.0
			/^ *# Built with mkswu/ {
				print "# Built with mkswu", $NF
				if (sig) print sig;
			}
			NR == 2 && /version = "/ {
				sub(/.*version = "/, "")
				dequote()
				print "# Built with mkswu", $1
				if (sig) print sig;
			}
			# pre mkswu-4.11
			/^ *# swdesc/ {
				sub(/ *# /, "")
				cmd_pending=$0
			}
			/^ *# mkswu_orig_cmd / {
				sub(/ *# mkswu_orig_cmd /, "")
				orig_cmd=1
				dequote()
				# record all occurences of commands if different (for --board)
				if (! cmd[filename]) {
					cmd[filename]=$0
				} else if (! (cmd[filename] ~ "(^|\n)"$0"($|\n)")) {
					cmd[filename]=cmd[filename] "\n" $0
				}
			}
			/^ *description: / && ! cmd[filename] {
				sub(/ *description: "/, "")
				dequote()
				if ($0 ~ /^pre_script$|^post_script$/) {
					# skip
				} else {
					cmd[filename]=$0
				}
			}
			/^ *filename =/ {
				gsub(/.*= "|";/, "")
				filename = $0
				if (cmd_pending) {
					cmd[filename]=cmd_pending;
					cmd_pending=""
				}
			}
			/^ *version =/ && filename {
				gsub(/.*= "|";/, "")
				version[filename]=$0
			}
			/^ *name =/ && filename {
				gsub(/.*= "|";/, "")
				component[filename]=$0
			}
			/^ *encrypted = true/ && filename {
				encrypted[filename]="1"
			}
			/^ *install-if-different/ && filename {
				install_if[filename]="different"
			}
			/^ *# hide_from_show/ && filename {
				cmd[filename] = ""
				filename=""
				cmd_pending=""
			}
			/^ *}/ && filename {
				filename=""
				cmd_pending=""
			}
			/^ *# MKSWU_/ {
				sub(/ *# MKSWU_/, "")
				sub(/ 1$/, "")
				sub(/ /, "=")
				print "swdesc_option " $0
			}
			END {
				split(order_string, order);
				for (key in order) {
					filename = order[key];
					if (!(cmd[filename])) {
						continue
					}
					print ""
					if (! orig_cmd) {
						# only add version/install-if found if we did not record the original command
						if (!(cmd[filename] ~ /--version/) && component[filename] && version[filename]) {
							sub(/ /, "  --version " component[filename] " " version[filename] " ", cmd[filename])
						}
						if (!(cmd[filename] ~ /--install-if/) && install_if[filename]) {
							sub(/ /, "  --install-if " install_if[filename] " ", cmd[filename])
						}
					}
					print cmd[filename]
					if (encrypted[filename]) {
						print "#  (encrypted)"
					}
				}
			}
			'
	done
}

mode_to_opt() {
	case "$1" in
	genkey*) echo "genkey";;
	update_version) echo "update-version";;
	*) echo "$1";;
	esac
}

mkimage() {
	local SCRIPT_DIR
	SCRIPT_DIR="$(realpath -- "$0")" \
		&& SCRIPT_DIR="$(dirname -- "$SCRIPT_DIR")" \
		|| error "Could not get script dir"
	local OUT=""
	local OUTDIR=""
	local CONFIG_DIR
	local CONFIG
	local FILES_FIRST="sw-description
sw-description.sig"
	local FILES=""
	local FIRST_SWDESC_INIT=1
	local MKSWU_component_baseos_seen=""
	local MKSWU_hide_from_show=""
	local COPY_USB=""
	local MODE=""
	local VERBOSE=2
	local TEXTDOMAINDIR="$SCRIPT_DIR/locale"
	local MKSWU_VERSION=""

	# config file variables
	local PRIVKEY="" PUBKEY="" PRIVKEY_PASS=""
	local USER_PUBKEYS="" ATMARK_CERTS="" UPDATE_CERTS=""
	local ENCRYPT_KEYFILE="" ENCRYPT_KEY=""
	local HW_COMPAT="" DESCRIPTION=""

	# swdesc_option variables
	# Note we do not define e.g. MKSWU_NOTIFY_STARTING_CMD to allow
	# overriding the command with an empty command: in this case nothing
	# will be run by mkswu scripts
	local MKSWU_SWDESC_OPTS="${MKSWU_SWDESC_OPTS:-}"

	# mode options
	local GENKEY_PLAIN="" GENKEY_CN=""
	local VERSION_BASE=""
	local SHOW_RAW=""
	local NOPROMPT=""

	# default default values
	local compress=1
	local MKSWU_declare_swdesc_cmd_vars_stanza='
local component="$MKSWU_component" version="$MKSWU_version"
local board="$MKSWU_board" main_version="$MKSWU_main_version"
local install_if="$MKSWU_install_if" cmd_description="$MKSWU_cmd_description"
local file="$MKSWU_file" dest="$MKSWU_dest" cmd="$MKSWU_cmd"
local preserve_attributes="$MKSWU_preserve_attributes"
local container="$MKSWU_container"
local desc_extra_text="$MKSWU_desc_extra_text"
local MKSWU_component="" MKSWU_version="" MKSWU_board=""
local MKSWU_main_version="" MKSWU_install_if="" MKSWU_cmd_description=""
local MKSWU_file="" MKSWU_dest="" MKSWU_cmd=""
local MKSWU_preserve_attributes="" MKSWU_container=""
local MKSWU_desc_extra_text=""
'
	local MKSWU_export_swdesc_cmd_vars_stanza='
local MKSWU_component="$component" MKSWU_version="$version"
local MKSWU_board="$board" MKSWU_main_version="$main_version"
local MKSWU_install_if="$install_if" MKSWU_cmd_description="$cmd_description"
local MKSWU_file="$file" MKSWU_dest="$dest" MKSWU_cmd="$cmd"
local MKSWU_preserve_attributes="$preserve_attributes"
local MKSWU_container="$container"
local MKSWU_desc_extra_text="$desc_extra_text"
local component="" version="" board="" main_version=""
local install_if="" cmd_description="" file="" dest=""
local cmd="" preserve_attributes="" container=""
local desc_extra_text=""
# only used after having checked once: skip further checks
local MKSWU_NO_SPECIAL_VERSION_CHECK=1
'
	local main_cwd desc desc_file
	local MKSWU_version="" MKSWU_component="" MKSWU_install_if=""
	local MKSWU_install_first="" MKSWU_version_last_set=""
	local MKSWU_until=""

	# use make-sbom scan file
	local MKSWU_SBOM_CONFIG_YAML="/usr/share/make-sbom/config/config.yaml"
	local MKSWU_SBOM_INPUT=""
	local MKSWU_SBOM_SOURCE_FILE=""
	local MKSWU_BUILD_SBOM="$MKSWU_BUILD_SBOM"
	# (busybox realpath does not have --version and errors)
	realpath --version >/dev/null 2>&1 \
		|| error "Please install coreutils first."

	if [ "${SCRIPT_DIR%/usr/bin}" != "$SCRIPT_DIR" ]; then
		SCRIPT_DIR=${SCRIPT_DIR%/bin}/share/mkswu
		TEXTDOMAINDIR="${SCRIPT_DIR%/mkswu}/locale"
	fi

	for CONFIG_DIR in "$SCRIPT_DIR" "$HOME/mkswu"; do
		for CONFIG in mkswu.conf mkimage.conf; do
			if [ -e "$CONFIG_DIR/$CONFIG" ]; then
				CONFIG="$CONFIG_DIR/$CONFIG"
				break 2
			fi
		done
	done
	# not found if doesn't start with /
	# just checking for -e $CONFIG would find ./mkimage.conf...
	[ "${CONFIG#/}" != "$CONFIG" ] \
		|| CONFIG="$CONFIG_DIR/mkswu.conf"

	if [ -z "$MKSWU_VERSION" ]; then
		if [ -e "$SCRIPT_DIR/.git" ]; then
			MKSWU_VERSION="$(git --git-dir "$SCRIPT_DIR/.git" describe)"
		elif [ -e "$SCRIPT_DIR/.version" ]; then
			MKSWU_VERSION=$(cat "$SCRIPT_DIR/.version")
		else
			error "mkswu not installed and could not guess mkswu version from git"
		fi
	fi

	local ARG SKIP=0
	for ARG; do
		# skip previously used argument
		# using a for loop means we can't shift ahead
		if [ "$SKIP" -gt 0 ]; then
			SKIP=$((SKIP-1))
			continue
		fi
		if [ "$SKIP" -lt 0 ]; then
			shift
			set -- "$@" "$ARG"
			continue
		fi

		shift
		# split --switch=value
		if [ "${ARG#--*=}" != "$ARG" ]; then
			set -- "${ARG#--*=}" "$@"
			ARG="${ARG%%=*}"
		fi
		case "$ARG" in
		"-c"|"--config")
			[ $# -lt 1 ] && error "%s requires an argument" "$ARG"
			CONFIG="$(realpath -e "$1")" \
				|| error "%s does not exist" "$1"
			CONFIG_DIR="${CONFIG%/*}"
			[ -n "$CONFIG_DIR" ] || CONFIG_DIR=/
			SKIP=1
			;;
		"--config-dir")
			[ $# -lt 1 ] && error "%s requires an argument" "$ARG"
			CONFIG_DIR="$(realpath -m "$1")"
			CONFIG="$1/mkswu.conf"
			SKIP=1
			;;
		"-o"|"--out")
			[ $# -lt 1 ] && error "%s requires an argument" "$ARG"
			OUT="$1"
			[ "${OUT%.swu}" != "$OUT" ] || error "%s must end with .swu" "$OUT"
			SKIP=1
			;;
		"-vv"*)
			ARG=${ARG#-}
			while [ "${ARG#v}" != "$ARG" ]; do
				ARG="${ARG#v}"
				VERBOSE=$((VERBOSE+1))
			done
			[ -z "$ARG" ] || error "Only v can be repeated in -vvv..."
			;;
		"-v"|"--verbose")
			VERBOSE=$((VERBOSE+1))
			;;
		"-qq"*)
			ARG=${ARG#-}
			while [ "${ARG#q}" != "$ARG" ]; do
				ARG="${ARG#q}"
				VERBOSE=$((VERBOSE-1))
			done
			[ -z "$ARG" ] || error "Only q can be repeated in -qqq..."
			;;
		"-q"|"--quiet")
			VERBOSE=$((VERBOSE-1))
			;;
		"--mkconf")
			update_mkswu_conf
			exit 0
			;;
		"--init")
			[ -z "$MODE" ] || error "%s is incompatible with --%s" "$ARG" "$(mode_to_opt "$MODE")"
			MODE=init
			;;
		"--import")
			[ -z "$MODE" ] || error "%s is incompatible with --%s" "$ARG" "$(mode_to_opt "$MODE")"
			MODE=import
			# for import mode, also use config in current directory if
			# another one wasn't already found/set
			if ! [ -e "$CONFIG" ]; then
				[ -e "mkimage.conf" ] && CONFIG="$PWD/mkimage.conf"
				[ -e "mkswu.conf" ] && CONFIG="$PWD/mkswu.conf"
				CONFIG_DIR="$PWD"
			fi
			;;
		"--internal")
			[ -z "$MODE" ] || error "%s is incompatible with --%s" "$ARG" "$(mode_to_opt "$MODE")"
			MODE=internal
			;;
		"--version-cmp")
			[ -z "$MODE" ] || error "%s is incompatible with --%s" "$ARG" "$(mode_to_opt "$MODE")"
			MODE=version_cmp
			;;
		"--update-version")
			[ -z "$MODE" ] || error "%s is incompatible with --%s" "$ARG" "$(mode_to_opt "$MODE")"
			MODE=update_version
			;;
		"--version-base")
			[ "$MODE" = "update_version" ] || error "%s must be passed after %s" "$ARG" "--update-version"
			[ $# -lt 1 ] && error "%s requires an argument" "$ARG"
			VERSION_BASE="$1"
			SKIP=1
			;;
		"--show")
			[ -z "$MODE" ] || error "%s is incompatible with --%s" "$ARG" "$(mode_to_opt "$MODE")"
			MODE=show
			;;
		"--raw")
			[ "$MODE" = "show" ] || error "%s must be passed after %s" "$ARG" "--show"
			SHOW_RAW=1
			;;
		"--genkey")
			[ -z "$MODE" ] || error "%s is incompatible with --%s" "$ARG" "$(mode_to_opt "$MODE")"
			MODE=genkey_sign
			;;
		"--aes")
			[ "$MODE" = "genkey_sign" ] || error "%s must be passed after %s" "$ARG" "--genkey"
			MODE=genkey_aes
			;;
		"--plain")
			[ "$MODE" = "genkey_sign" ] || error "%s must be passed after %s" "$ARG" "--genkey"
			GENKEY_PLAIN=1
			;;
		"--cn")
			[ "$MODE" = "genkey_sign" ] || error "%s must be passed after %s" "$ARG" "--genkey"
			[ $# -lt 1 ] && error "%s requires an argument" "$ARG"
			GENKEY_CN="$1"
			SKIP=1
			;;
		"--noprompt")
			[ "$MODE" = "genkey_sign" ] || [ "$MODE" = "genkey_aes" ] \
				|| error "%s must be passed after %s" "$ARG" "--genkey"
			NOPROMPT=1
			;;
		"--version")
			info "mkswu version %s" "$MKSWU_VERSION"
			exit 0
			;;
		"--")
			# stop parsing
			SKIP=-1
			;;
		"-h"|"--help")
			usage
			exit 0
			;;
		"-")
			# single dash is stdin, treat it as normal file
			# (duplicate to handle unrecognized options)
			set -- "$@" "$ARG"
			;;
		"-"*)
			error "Unrecognized option %s" "$ARG"
			;;
		*)
			set -- "$@" "$ARG"
			;;
		esac
		[ "$SKIP" -gt 0 ] && shift "$SKIP"
	done

	. "$SCRIPT_DIR/mkswu.conf.defaults"
	[ -n "${MKSWU_CONFIG+1}" ] && CONFIG="$MKSWU_CONFIG"
	if [ "$MODE" = "import" ] && ! [ -e "$CONFIG" ]; then
		error "Could not find config file to import, specify it with --config"
	fi
	if [ -z "$MODE" ] && ! [ -e "$CONFIG" ]; then
		[ -e "mkimage.conf" ] && error "Please import current config with mkswu --import first"
		error "Config file not found, create one with mkswu --init"
	fi
	if [ -n "$CONFIG" ]; then
		trace "Loading config %s" "$CONFIG"
		update_mkswu_conf
		[ -e "$CONFIG" ] || error "%s does not exist" "$CONFIG"
		[ "${CONFIG#/}" = "$CONFIG" ] && CONFIG="./$CONFIG"
		. "$CONFIG"
	fi
	# Force config values from env
	[ -n "${MKSWU_PRIVKEY+1}" ] && PRIVKEY="$MKSWU_PRIVKEY"
	[ -n "${MKSWU_PUBKEY+1}" ] && PUBKEY="$MKSWU_PUBKEY"
	[ -n "${MKSWU_PRIVKEY_PASS+1}" ] && PRIVKEY_PASS="$MKSWU_PRIVKEY_PASS"
	[ -n "${MKSWU_ENCRYPT_KEYFILE+1}" ] && ENCRYPT_KEYFILE="$MKSWU_ENCRYPT_KEYFILE"
	[ -n "${MKSWU_HW_COMPAT+1}" ] && HW_COMPAT="$MKSWU_HW_COMPAT"
	[ -n "${MKSWU_DESCRIPTION+1}" ] && DESCRIPTION="$MKSWU_DESCRIPTION"

	# split PUBKEY into actually used certificate and extra ones
	USER_PUBKEYS="$PUBKEY"
	PUBKEY="${PUBKEY%%,*}"

	if [ -n "$MODE" ]; then
		[ -n "$OUT" ] && error "%s is incompatible with --%s" "out" "$(mode_to_opt "$MODE")"
		"mkswu_$MODE" "$@"
		exit
	fi

	if [ "$#" = "0" ]; then
		usage
		echo
		error "Must provide at least one desc file"
	fi

	# actual image building
	if [ -z "$OUT" ]; then
		[ "$1" = "-" ] && error "Cannot guess output name from stdin"
		# OUT defaults to first swu name if not set
		OUT="${1%.desc}.swu"
	fi
	OUTDIR=$(dirname -- "$OUT")/.$(basename -- "$OUT" .swu)
	mkdir -p "$OUTDIR"
	OUTDIR=$(realpath -- "$OUTDIR")
	rm -f "$OUTDIR/sw-description-"* "$OUTDIR/used_files"

	main_cwd="$PWD"
	absolutize_file_paths
	# build sw-desc fragments with set -e to catch user errors in desc files
	set -e
	for desc; do
		# shellcheck disable=SC2034 ## MKSWU_version is used, yes...
		MKSWU_version=""
		# shellcheck disable=SC2034 ## and this as well.
		MKSWU_install_if=""
		MKSWU_FORCE_VERSION="";

		if [ "$desc" = "-" ]; then
			MKSWU_component="stdin"
			desc_file="/proc/self/fd/0"
		else
			[ -e "$desc" ] || error "%s does not exist" "$desc"
			cd "$(dirname -- "$desc")" \
				|| error "cannot enter %s directory" "$desc"
			[ "${desc%.desc}" != "$desc" ] \
				|| info "Warning: %s does not end in .desc, wrong file?" "$desc"
			MKSWU_component="${desc%.desc}"
			MKSWU_component=${MKSWU_component##*/}
			desc_file="./${desc##*/}"
		fi
		. "$desc_file"

		# make key files path absolute after each iteration:
		# this is required if a desc file sets a key path
		absolutize_file_paths
		cd "$main_cwd" || error "Cannot return to %s we were in before" "$main_cwd"
		[ "$#" -gt 1 ] && info "Successfully included %s contents" "$desc"
	done
	set +e

	[ -z "$FIRST_SWDESC_INIT" ] \
		|| error "No command could be found in desc files"

	if [ -n "$MKSWU_version_last_set" ]; then
		warning "Warning: 'swdesc_option %s' was not used, did you" \
			"$MKSWU_version_last_set"
		warning "place it before the commands you want to run?"
	fi
	embedded_postinstall_script
	write_sw_desc > "$OUTDIR/sw-description"
	# ignore swdesc_exec "chroot" lines...
	if sed -e 's@-v /target/var/app/volumes:/var/app/volumes@@g' "$OUTDIR/sw-description" \
			| grep -qF "/var/app/volumes"; then
		warning "Warning: update is using /var/app/volumes"
		warning "It is not safe to modify /var/app/volumes while the system uses it,"
		warning "consider using /var/app/rollback/volumes instead"
	fi

	# XXX debian's libconfig is obsolete and does not allow
	# trailing commas at the end of lists (allowed from 1.7.0)
	# probably want to sed these out at some point for compatibility
	# (Note this is only required to run swupdate on debian,
	#  not for image generation)
	mkcpio

	# create make-sbom
	local MKSWU_RETVAL=0
	case "$MKSWU_BUILD_SBOM" in
		[Yy]|[Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|1)
			if ! command -v make_sbom.sh >/dev/null; then
				warning "make_sbom.sh command not found. Install python3-make-sbom package to create sbom."
				MKSWU_BUILD_SBOM=no
				MKSWU_RETVAL=1
			else
				MKSWU_BUILD_SBOM=yes
			fi
			;;
		[Aa][Uu][Tt][Oo])
			if command -v make_sbom.sh >/dev/null; then
				MKSWU_BUILD_SBOM=yes
			else
				trace "skipping sbom creation."
			fi
			;;
		[Nn]|[Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|0|"")
			trace "skipping sbom creation."
			;;
		*)
			error "Invalid BUILD_SBOM \"%s\", must be yes, no or auto."
	esac

	if [ "$MKSWU_BUILD_SBOM" = yes ]; then
		if ! eval make_sbom.sh -i "$(shell_quote "$OUT")" \
				-o "$(shell_quote "$OUT")" \
				-c "$(shell_quote "$MKSWU_SBOM_CONFIG_YAML")" \
				"$MKSWU_SBOM_INPUT" "$MKSWU_SBOM_SOURCE_FILE"; then
			warning "Could not create sbom."
			MKSWU_RETVAL=1
		fi
	fi

	if [ -n "$COPY_USB" ]; then
		info "You have sideloaded containers, copy all these files to USB drive:"
		info "%s" "$(shell_quote "$(realpath "$OUT")") $COPY_USB"
		info
	fi

	cleanup_outdir

	info "Successfully generated %s" "$OUT"
	return $MKSWU_RETVAL
}


# check if sourced: basename $0 should only be mkswu if run directly
[ "$(basename "$0")" != "mkswu" ] && return

mkimage "$@"
