#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright 2021 Helmut Grohne <helmut@subdivi.de>

# A "hashset" is a shell variable containing a sequence of elements separated
# and surrounded by hash (#) characters. None of the elements may contain a
# hash character. The character is thus chosen, because it initiates a comment
# in /etc/shells. All variables ending in _SHELLS in this file are hashsets.

set -e

# Check whether hashset $1 contains element $2.
hashset_contains() {
	case "$1" in
		*"#$2#"*) return 0 ;;
		*) return 1 ;;
	esac
}

log() {
	if [ "$VERBOSE" = 1 ]; then
		echo "$*"
	fi
}

ROOT=
VERBOSE=0
NOACT=0

while [ $# -gt 0 ]; do
	case "$1" in
		--help)
			cat <<EOF
usage: $0 [options]

 --no-act    Do not move the actual update into place
 --verbose   Be more verbose
 --root DIR  Operate on the given chroot, defaults to /
EOF
			exit 0
		;;
		--no-act)
			NOACT=1
		;;
		--root)
			shift
			if [ "$#" -lt 1 ]; then
				echo "missing argument to --root" 1>&2
				exit 1
			fi
			ROOT=$1
		;;
		--verbose)
			VERBOSE=1
		;;
		*)
			echo "unrecognized option $1" 1>&2
			exit 1
		;;
	esac
	shift
done

PKG_DIR="$ROOT/usr/share/debianutils/shells.d"
STATE_FILE="$ROOT/var/lib/shells.state"
TARGET_ETC_FILE="$ROOT/etc/shells"
SOURCE_ETC_FILE="$TARGET_ETC_FILE"
NEW_ETC_FILE="$TARGET_ETC_FILE.tmp"
NEW_STATE_FILE="$STATE_FILE.tmp"

if ! test -e "$SOURCE_ETC_FILE"; then
	SOURCE_ETC_FILE="$ROOT/usr/share/debianutils/shells"
fi

PKG_SHELLS='#'
LC_COLLATE=C.UTF-8  # glob in reproducible order
for f in "$PKG_DIR/"*; do
	[ "$f" = "$PKG_DIR/*" ] && break
	while IFS='#' read -r line _; do
		[ -n "$line" ] || continue
		PKG_SHELLS="$PKG_SHELLS$line#"
		realshell=$(dpkg-realpath --root "$ROOT" "$line")
		if [ "$line" != "$realshell" ]; then
			PKG_SHELLS="$PKG_SHELLS$realshell#"
		fi
	done < "$f"
done

STATE_SHELLS='#'
if [ -e "$STATE_FILE" ] ; then
	while IFS='#' read -r line _; do
		[ -n "$line" ] && STATE_SHELLS="$STATE_SHELLS$line#"
	done < "$STATE_FILE"
fi

cleanup() {
	rm -f "$NEW_ETC_FILE" "$NEW_STATE_FILE"
}
trap cleanup EXIT

: > "$NEW_ETC_FILE"
ETC_SHELLS='#'
while IFS= read -r line; do
	shell=${line%%#*}
	# copy all comment lines, packaged shells and local additions
	if [ -z "$shell" ] ||
			hashset_contains "$PKG_SHELLS" "$shell" ||
			! hashset_contains "$STATE_SHELLS" "$shell"; then
		echo "$line" >> "$NEW_ETC_FILE"
		ETC_SHELLS="$ETC_SHELLS$shell#"
	else
		log "removing shell $shell"
	fi
done < "$SOURCE_ETC_FILE"

: > "$NEW_STATE_FILE"
saved_IFS=$IFS
IFS='#'
set -f
# shellcheck disable=SC2086 # word splitting intended, globbing disabled
set -- ${PKG_SHELLS###}
set +f
IFS=$saved_IFS
for shell; do
	echo "$shell" >> "$NEW_STATE_FILE"
	# add shells that are neither already present nor locally removed
	if ! hashset_contains "$ETC_SHELLS" "$shell" &&
			! hashset_contains "$STATE_SHELLS" "$shell"; then
		echo "$shell" >> "$NEW_ETC_FILE"
		log "adding shell $shell"
	fi
done

if [ "$NOACT" = 0 ]; then
	if [ -e "$STATE_FILE" ]; then
		chmod --reference="${STATE_FILE}" "${NEW_STATE_FILE}" || chmod $(stat -c %a "${STATE_FILE}") "${NEW_STATE_FILE}"
		chown --reference="${STATE_FILE}" "${NEW_STATE_FILE}" || chown $(stat -c %U "${STATE_FILE}") "${NEW_STATE_FILE}"
	else
		chmod 0644 "$NEW_STATE_FILE"
	fi
	chmod --reference="${SOURCE_ETC_FILE}" "${NEW_ETC_FILE}" || chmod $(stat -c %a "${SOURCE_ETC_FILE}") "${NEW_ETC_FILE}"
	chown --reference="${SOURCE_ETC_FILE}" "${NEW_ETC_FILE}" || chown $(stat -c %U "${SOURCE_ETC_FILE}") "${NEW_ETC_FILE}"
	sync -d "$NEW_ETC_FILE" "$NEW_STATE_FILE"
	mv -Z "${NEW_ETC_FILE}" "${TARGET_ETC_FILE}" || mv "${NEW_ETC_FILE}" "${TARGET_ETC_FILE}"
	sync "$TARGET_ETC_FILE"
	sync "$(dirname "$TARGET_ETC_FILE")"
	mv "$NEW_STATE_FILE" "$STATE_FILE"
	sync "$STATE_FILE"
	sync "$(dirname "$STATE_FILE")"
	trap "" EXIT
fi
