#!/bin/bash
#
# creating/updating linbofs with linbo password, ssh keys and firmware,
# has to be invoked during linuxmuster-setup, package upgrade or
# linbo password change in /etc/rsyncd.secrets.
#
# thomas@linuxmuster.net
# GPL V3
# 20260801
#

# read linuxmuster environment
source /usr/share/linuxmuster/helperfunctions.sh || exit 1

if [ ! -s "$SETUPINI" ]; then
	echo "linuxmuster.net is not yet set up, aborting!"
	exit 0
fi

# check & set lockfile
locker=/tmp/.update-linbofs.lock
if [ -e "$locker" ]; then
	echo "Caution! Probably there is another update-linbofs process running!"
	echo "If this is not the case you can safely remove the lockfile $locker"
	echo "and give update-linbofs another try."
	echo "update-linbofs is locked! Exiting!"
	exit 1
fi
touch $locker || exit 1
chmod 400 "$locker"


# globals
FW_SYSTEM="/lib/firmware"
FW_CACHE="$LINBOCACHEDIR/firmware"
FW_LIST_LOCAL="$FW_CACHE/.fw_list"
FW_LIST_REMOTE="WHENCE"
FW_MANIFEST="$FW_CACHE/.fw_manifest"
FW_URL="https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git/plain"
# one-time index of $FW_SYSTEM, built lazily by build_fw_index()
FW_INDEXED=""
FW_FILE_INDEX=""
FW_DIR_INDEX=""


# get kernel image, modules and version
# first set custom kernel, if provided
if [ -s "$LINBOSYSDIR/custom_kernel" ]; then
	# read custom kernel config if provided
	source "$LINBOSYSDIR/custom_kernel"
	# handle old kernel types for backward compatibility
	case "$KERNELPATH" in
		stable|longterm|legacy) ;;
		*)
			# custom kernel stuff (see /etc/linuxmuster/linbo/custom_kernel.ex for examples)
			if [ -s "$KERNELPATH" -a -d "$MODULESPATH" ]; then
				KVERS="$(basename "$MODULESPATH")"
				KTYPE="custom"
			else
				echo "Custom kernel configuration is not valid! Using default kernel instead!"
			fi
			;;
	esac
fi
# otherwise set default kernel
if [ "$KTYPE" != "custom" ]; then
	KERNELPATH="$LINBOVARDIR/linbo64"
	MODULESPATH="$LINBOVARDIR/modules.tar.xz"
	KVERS="$(cat "$LINBOVARDIR/kversion")"
	KTYPE="default"
fi


# clean tmpdir and exit with error
bailout() {
	echo "$1"
	[ -n "$locker" -a -e "$locker" ] && rm -f "$locker"
	exit 1
}


exec_hooks() {
	case "$1" in
		pre|post) ;;
		*) return ;;
	esac
    local hookdir="$HOOKSDIR/update-linbofs.$1.d"
	[ -d "$hookdir" ] || mkdir -p "$hookdir"
    local hook_files=$(find "$hookdir" -xtype f -executable)
	[ -z "$hook_files" ] && return
	local file
    for file in $hook_files; do
        if [ -x "$file" ]; then
            echo "Executing $1 hookfile $file"
            "$file"
        fi
    done
}


# provide system locale in linbofs
copy_locale() {
	[ -z "$LANG" -a -s "/etc/locale.conf" ] && source "/etc/locale.conf"
	[ -z "$LANG" -a -s "/etc/default/locale" ] && source "/etc/default/locale"
	[ -z "$LANG" ] && LANG="C.UTF-8"
	cmap="${LANG#*.}"
	echo "Copy locale $LANG ..."
	mkdir -p usr/lib/locale
	mkdir -p usr/share/locale
	mkdir -p usr/share/i18n/locales
	mkdir -p usr/share/i18n/charmaps
	cp -r "/usr/share/locale/${LANG%_*}" usr/share/locale
	cp -r "/usr/share/i18n/locales/${LANG%.*}" usr/share/i18n/locales
	cp /usr/share/i18n/SUPPORTED usr/share/i18n
	cp /usr/share/i18n/locales/i18n* usr/share/i18n/locales
	cp /usr/share/i18n/locales/iso14651_t1* usr/share/i18n/locales
	cp /usr/share/i18n/locales/translit_* usr/share/i18n/locales
	cp "/usr/share/i18n/charmaps/$cmap.gz" usr/share/i18n/charmaps
	rsync -a /usr/lib/locale/ usr/lib/locale/ || bailout "Failed to copy /usr/lib/locale!"
	#cp /usr/sbin/locale-gen usr/sbin
	#cp /usr/bin/localedef usr/bin
	#cp /usr/bin/locale usr/bin
	cp /etc/locale* etc
	cp /etc/vconsole.conf etc
	localtime="$(realpath /etc/localtime)"
	[ -e "$localtime" ] && cp "$localtime" etc/localtime
	#chroot ./ /usr/sbin/locale-gen --lang "$LANG"
	#rm -f usr/sbin/locale-gen usr/bin/localedef
	# create keyboard map for console
	busybox dumpkmap > etc/console.kmap || bailout "Failed to create console keymap!"
}


# download firmware filelist from kernel.org to cache
download_fwlist() {
	mkdir -p "$FW_CACHE"
	local tmp_file="${FW_LIST_LOCAL}.tmp"
	local RC=0
	# download to temporary file, if successful, move in place
	if wget -q "$FW_URL/$FW_LIST_REMOTE" -O "$tmp_file"; then
		mv "$tmp_file" "$FW_LIST_LOCAL" || RC=1
	# remove failed download
	else
		rm -f "$FW_LIST_LOCAL"*
		touch "$FW_LIST_LOCAL"
		RC=1
	fi
	if [ $RC = 0 ]; then
		echo "Firmware list successfully downloaded."
	else
		echo "Download of firmware list failed!"
	fi
}


# echo "<link path>\t<link target>" for every WHENCE "Link:" entry that
# resolves (relative to the link's own directory) to the given File: path,
# e.g. for "intel/iwlwifi/iwlwifi-8265-36.ucode" this finds
# "Link: iwlwifi-8265-36.ucode -> intel/iwlwifi/iwlwifi-8265-36.ucode"
find_fw_links() {
	local target_item="$1"
	local link_path link_target link_dir resolved
	while read -r _ link_path _ link_target; do
		[ -z "$link_path" ] && continue
		link_dir="$(dirname "$link_path")"
		[ "$link_dir" = "." ] && link_dir=""
		# normalize lexically only (no filesystem access) under a fixed
		# anchor, so a "../" in the link target can't escape the comparison
		resolved="$(realpath -sm "/__ANCHOR__/${link_dir:+$link_dir/}$link_target")"
		resolved="${resolved#/__ANCHOR__/}"
		[ "$resolved" = "$target_item" ] && printf '%s\t%s\n' "$link_path" "$link_target"
	done < <(grep '^Link:' "$FW_LIST_LOCAL")
}


# download firmware from kernel.org to cache; also recreates any compat
# symlink WHENCE documents for the downloaded file (see find_fw_links()),
# matching how copy_fw() handles firmware that is symlinked under $FW_SYSTEM
download_fw() {
	local fw_name="$1"
	# get remote firmware path(s) from downloaded firmware filelist
	local remote_files="$(grep ^File: "$FW_LIST_LOCAL" | grep -w "$fw_name" | awk '{print $2}')"
	[ -z "$remote_files" ] && return 1
	# download to temporary file, if successful, move in place
	local tmp_file
	local local_file
	local fw_dir
	local item
	local link_path
	local link_target
	local RC=0
	for item in $remote_files; do
		fw_dir="$(dirname "$item")"
		[ "$fw_dir" != "." ] && mkdir -p "$FW_CACHE/$fw_dir"
		local_file="$FW_CACHE/$item"
		tmp_file="${local_file}.tmp"
		if wget -q "$FW_URL/$item" -O "$tmp_file"; then
			mv "$tmp_file" "$local_file"
			# compress downloaded firmware file
			if zstd -qf --adapt --rm "$local_file"; then
				echo "${item}.zst"
				while IFS=$'\t' read -r link_path link_target; do
					mkdir -p "$FW_CACHE/$(dirname "$link_path")"
					ln -sf "${link_target}.zst" "$FW_CACHE/${link_path}.zst"
					echo "${link_path}.zst"
				done < <(find_fw_links "$item")
			else
				RC=1
			fi
		else
			rm -f "$tmp_file"
			RC=1
		fi
	done
	return $RC
}


# parse linbo logs for missing firmware and print found firmware names
parse_firmware_logs(){
	# return if no log file exists
	[ -z "$(find $LINBOLOGDIR -name \*_linbo.log)" ] && return
	# the kernel's firmware loader emits this generic message for every failed
	# firmware request, regardless of driver or bus type (pci, usb, ...), so
	# extract the firmware name directly instead of correlating via pci id
	# (which misses non-"00:xx.x" buses and breaks on truncated log lines)
	grep -ho "Direct firmware load for .* failed" $LINBOLOGDIR/*_linbo.log |\
	sed 's|Direct firmware load for ||;s| failed$||' |\
	sort -u |\
	xargs
}


# build a one-time index of $FW_SYSTEM so copy_fw() doesn't have to walk the
# whole tree again for every firmware name
build_fw_index() {
	[ -n "$FW_INDEXED" ] && return
	FW_FILE_INDEX="$(find "$FW_SYSTEM" -type f,l -printf '%f\t%p\n')"
	FW_DIR_INDEX="$(find "$FW_SYSTEM" -mindepth 1 -type d -printf '%f\t%p\n')"
	FW_INDEXED=1
}


# look up a basename in a "name<TAB>path" index built by build_fw_index()
lookup_fw_index() {
	awk -F'\t' -v name="$2" '$1 == name { print $2; exit }' <<< "$1"
}


# copy local firmware to cache; if the resolved file is itself a symlink
# (e.g. iwlwifi-*.ucode files, which live in a subdirectory of $FW_SYSTEM
# but are symlinked at its top level, exactly where the kernel's firmware
# loader looks for them), reproduce both the symlink and the file it
# points to, each at their original path relative to $FW_SYSTEM
copy_fw() {
	local fw_name="$(basename "$1")"
	local fw_zst="${fw_name}.zst"
	# many firmware files live in a subdirectory of $FW_SYSTEM but are
	# additionally symlinked directly at its top level, which is where the
	# kernel's firmware loader actually looks for them; check that exact
	# spot first so we always resolve to it deterministically instead of a
	# basename search below matching the subdirectory copy instead.
	# Use -f/-L, not -e: fw_name can also be a bare directory name (e.g.
	# "rtl_nic" in firmware.ex, meaning "pack the whole directory") and
	# -e would match that too, short-circuiting past the directory-copy
	# fallback below and into a plain (non-recursive) cp on a directory.
	local fw_src="$FW_SYSTEM/$fw_name"
	[ -f "$fw_src" -o -L "$fw_src" ] || fw_src="$FW_SYSTEM/$fw_zst"
	if [ ! -f "$fw_src" -a ! -L "$fw_src" ]; then
		build_fw_index
		# get firmware file from local system
		fw_src="$(lookup_fw_index "$FW_FILE_INDEX" "$fw_name")"
		[ -n "$fw_src" ] || fw_src="$(lookup_fw_index "$FW_FILE_INDEX" "$fw_zst")"
	fi
	if [ -n "$fw_src" ] && [ -f "$fw_src" -o -L "$fw_src" ]; then
		local fw_dir="$(dirname "$fw_src")"
		local fw_target="${fw_dir/$FW_SYSTEM/$FW_CACHE}"
		mkdir -p "$fw_target"
		if [ -L "$fw_src" ]; then
			# preserve the symlink itself ...
			cp -P "$fw_src" "$fw_target" || return 1
			# ... and additionally place the file it points to at its own path;
			# resolve only this symlink (readlink), not the full path, since
			# realpath/readlink -f would also resolve unrelated symlinks along
			# the way (e.g. the /lib -> /usr/lib usrmerge link) and thereby
			# escape the $FW_SYSTEM prefix this function relies on throughout
			local link_target fw_real fw_real_dir fw_real_target
			link_target="$(readlink "$fw_src")"
			case "$link_target" in
				/*) fw_real="$(realpath -sm "$link_target")" ;;
				*) fw_real="$(realpath -sm "$fw_dir/$link_target")" ;;
			esac
			[ -e "$fw_real" ] || return 1
			fw_real_dir="$(dirname "$fw_real")"
			fw_real_target="${fw_real_dir/$FW_SYSTEM/$FW_CACHE}"
			mkdir -p "$fw_real_target"
			cp "$fw_real" "$fw_real_target" || return 1
			printf '%s\n%s\n' "${fw_src#$FW_SYSTEM/}" "${fw_real#$FW_SYSTEM/}"
		else
			cp "$fw_src" "$fw_target" || return 1
			echo "${fw_src#$FW_SYSTEM/}"
		fi
		return
	fi
	# get firmware directory from local system
	build_fw_index
	fw_src="$(lookup_fw_index "$FW_DIR_INDEX" "$fw_name")"
	[ -n "$fw_src" ] || return 1
	cp -r "$fw_src" "$FW_CACHE" || return 1
	echo "$(basename "$fw_src")"
}


# look up a firmware name already downloaded to $FW_CACHE in a previous run,
# so it doesn't get re-fetched from kernel.org on every invocation
cached_fw() {
	local fw_name="$(basename "$1")"
	local fw_path
	fw_path="$(find "$FW_CACHE" -type f -name "${fw_name}.zst" 2>/dev/null | head -n1)"
	[ -n "$fw_path" ] || fw_path="$(find "$FW_CACHE" -type f -name "$fw_name" 2>/dev/null | head -n1)"
	[ -n "$fw_path" ] || return 1
	echo "${fw_path#$FW_CACHE/}"
}


# provide firmare defined in LINBOSYSDIR/firmware or found in LINBOLOGDIR/*_linbo.log
provide_firmware() {
	local fw_conf="$LINBOSYSDIR/firmware"
	local fw_names
	# extract firmware filenames from config, if it exists and contains lines
	if [ -s "$fw_conf" ]; then
		[ -n "$(grep -v ^# "$fw_conf")" ] && \
			fw_names="$(basename -a $(grep -v ^# "$fw_conf") | sed 's|.zst||' | xargs)"
	fi
	# extract firmware filenames from logs
	fw_names="$(echo "$fw_names $(parse_firmware_logs | xargs -n1 | sort -u)" | xargs)"
	# return if empty
	[ -z "$fw_names" ] && return

	mkdir -p "$FW_CACHE"

	echo "Collecting firmware to cache:"
	: > "$FW_MANIFEST"
	# use local copy of regulatory.db
	if rsync "$FW_SYSTEM"/regulatory.db* "$FW_CACHE"; then
		echo " - regulatory.db (local)"
		find "$FW_CACHE" -maxdepth 1 -name 'regulatory.db*' -printf '%f\n' >> "$FW_MANIFEST"
	fi
	# copy firmware to cache (local & remote)
	local fw_name
	local paths
	local fwlist_attempted
	for fw_name in $fw_names; do
		# regulatory.db is already copied to cache
		stringinstring regulatory "$fw_name" && continue
		# copy firmware locally available
		if paths="$(copy_fw "$fw_name")"; then
			echo " - "$fw_name" (local)"
			printf '%s\n' "$paths" >> "$FW_MANIFEST"
			continue
		fi
		# reuse firmware already downloaded to cache in a previous run
		if paths="$(cached_fw "$fw_name")"; then
			echo " - "$fw_name" (cached)"
			printf '%s\n' "$paths" >> "$FW_MANIFEST"
			continue
		fi
		# download firmware list from kernel.org, but only once and only
		# if firmware is actually missing locally and not cached
		if [ -z "$fwlist_attempted" ]; then
			fwlist_attempted=1
			download_fwlist
		fi
		# download firmware not locally available
		if [ -s "$FW_LIST_LOCAL" ] && paths="$(download_fw "$fw_name")"; then
			echo " - "$fw_name" (remote)"
			printf '%s\n' "$paths" >> "$FW_MANIFEST"
		fi
	done

	echo "Copying all cached firmware to linbofs:"
	local fw_target="${FW_SYSTEM/\/}"
	rsync -rlv --files-from="$FW_MANIFEST" "$FW_CACHE/" "$fw_target/"
	rm -f "$FW_MANIFEST"
}


create_linbofs() {
	local linbofs="linbofs64"
	local linbofs_template="$LINBOVARDIR/${linbofs}.tar.xz"
	local linbofs_cache="$LINBOCACHEDIR/$linbofs"
	local linbofs_xz="$LINBODIR/${linbofs}"
	local linbofs_md5="${linbofs_xz}.md5"
	local conf
	local i

	rm -f "$linbofs_md5"
	rm -rf "$linbofs_cache"

	# begin to process linbofs64
	echo "Creating new linbo filesystem ..."

	# sync linbofs filesystem to cache dir
	cd "$LINBOCACHEDIR" || bailout "Failed to change to $LINBOCACHEDIR!"
	tar -xf "$linbofs_template" || bailout "Failed to unpack $linbofs_template!"
	cd "$linbofs_cache" || bailout "Failed to change to $linbofs_cache!"

	# provide modules
	echo "Using $KTYPE kernel version $KVERS ..."
	mkdir -p lib/modules
	case "$KTYPE" in
		default)
			echo "Extracting modules ..."
			tar xf "$MODULESPATH" || bailout "Failed to extract modules!"
			;;
		*)
			echo "Copying modules ..."
			cp -r "$MODULESPATH" lib/modules || bailout "Failed to copy modules!"
			echo "Generating modules.dep and map files ..."
			depmod -a -b . "$KVERS" || bailout "Failed to generate module dependencies!"
			;;
	esac

	# set ownership of cache dir to root
	chown root:root -R "$linbofs_cache"

	# store linbo password hash
	echo -n "$linbo_pwhash" > etc/linbo_pwhash
	echo -n "$linbo_salt" > etc/linbo_salt
	chmod 600 etc/linbo_*

	# provide dropbear ssh host key
	mkdir -p etc/dropbear
	cp "$LINBOSYSDIR"/dropbear_*_host_key etc/dropbear || bailout "Failed to copy dropbear host keys!"
	mkdir -p etc/ssh
	cp "$LINBOSYSDIR"/ssh_host_*_key* etc/ssh || bailout "Failed to copy ssh host keys!"
	mkdir -p .ssh
	cat /root/.ssh/id_*.pub > .ssh/authorized_keys || bailout "Failed to write authorized_keys!"
	# supplemental authorized_keys
	[ -s /root/.ssh/authorized_keys ] && cat /root/.ssh/authorized_keys >> .ssh/authorized_keys
	mkdir -p var/log
	touch var/log/lastlog
	# check and repair permissions
	for i in .ssh .ssh/authorized_keys; do
		perms="$(LANG=C stat "$i" | grep ^Access | grep Uid: | awk -F\( '{ print $2 }' | awk -F\/ '{ print $1 }')"
		if [ "${perms:1:3}" = "666" -o "${perms:1:3}" = "777" ]; then
			echo "WARNING! $i has bogus permissions!"
			sleep 3
			echo "Repairing for now but check your filesystem!"
			[ -d "$i" ] && chmod 755 "$i"
			[ -f "$i" ] && chmod 644 "$i"
		fi
	done

	# copy linbo-torrent defaults
	cp -f /etc/default/linbo-torrent etc/default || bailout "Failed to copy linbo-torrent defaults!"

	# copy default start.conf
	cp -f "$LINBODIR"/start.conf . || bailout "Failed to copy start.conf!"

	# copy efi pxe devicenames
	cp "$LINBOSHAREDIR/efipxe" usr/share/linbo || bailout "Failed to copy efipxe!"

	# locale
	copy_locale

	# firmware
	provide_firmware

	# provide wpa_supplicant config
	conf="$LINBOSYSDIR/wpa_supplicant.conf"
	if [ -s "$conf" ]; then
		echo "Copying $(basename "$conf") ..."
		cp "$conf" etc || bailout "Failed to copy $(basename "$conf")!"
	fi

	# provide additional inittab entries
	conf="$LINBOSYSDIR/inittab"
	if [ -s "$conf" ]; then
		echo "Adding custom $(basename "$conf") entries ..."
		echo "# custom entries" >> "etc/$(basename "$conf")" || bailout "Failed to add custom $(basename "$conf") entries!"
		grep -v ^# "$conf" | grep -v '^$' >> "etc/$(basename "$conf")"
	fi

	# execute pre hook scripts
	exec_hooks pre

	# pack linbofs64
	echo "Creating linbofs archive (may take a while) ..."
	local linbofs_size
	linbofs_size=$(du -sb . | awk '{print $1}')
	set -o pipefail
	find . -print | cpio --quiet -o -H newc | pv -f -s "$linbofs_size" | xz -e --check=none -z -f -T 0 --block-size=4MiB -c > "$linbofs_xz" ; RC="$?"
	set +o pipefail
  	[ $RC -ne 0 ] && bailout "failed!"

	# create md5sum file
	md5sum "$linbofs_xz"  | awk '{ print $1 }' > "$linbofs_md5"

	# link to old filename
	#linbofs_lz="${linbofs_xz}.lz"
	#rm -f "${linbofs_lz}"*
	#ln -s "$(basename $linbofs_xz)" "$linbofs_lz"
	#ln -s "$(basename $linbofs_md5)" "${linbofs_lz}.md5"

	# copy kernel image
	echo "Copying $KTYPE kernel version $KVERS ..."
	cp "$KERNELPATH" "$LINBODIR/linbo64"
	md5sum "$LINBODIR/linbo64" | awk '{ print $1 }' > "$LINBODIR/linbo64.md5"

	echo "Ok!"
}


# grep linbo rsync password to sync it with linbo account
[ ! -s /etc/rsyncd.secrets ] && bailout "/etc/rsyncd.secrets not found!"
linbo_passwd="$(grep ^linbo /etc/rsyncd.secrets | awk -F\: '{ print $2 }')"
if [ -z "$linbo_passwd" ]; then
  bailout "Cannot read linbo password from /etc/rsyncd.secrets!"
fi
# hash of linbo password goes into linbofs
echo -n "Hashing linbo password ... "
linbo_salt="$(tr -dc 'A-Za-z0-9!"#$%&'\''()*+,-./:;<=>?@[\]^_`{|}~' </dev/urandom | head -c 32  ; echo)"
linbo_pwhash="$(echo "$linbo_passwd" | LANG=C argon2 "$linbo_salt" -t 1000 | grep ^Hash | awk '{print $2}')"
if [ -n "$linbo_salt" -a -n "$linbo_pwhash" ]; then
	echo "Success!"
else
	echo "Failed!"
	exit 1
fi

create_linbofs

# create iso files
"$LINBOSHAREDIR"/make-linbo-iso.sh

# execute post hook scripts
exec_hooks post

rm -f "$locker"
