#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-only
#
# This file is part of the distrobox project:
#    https://github.com/89luca89/distrobox
#
# Copyright (C) 2021 distrobox contributors
#
# distrobox is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3
# as published by the Free Software Foundation.
#
# distrobox is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with distrobox; if not, see <http://www.gnu.org/licenses/>.

# POSIX
# Expected env variables:
#	HOME
#	USER
#	SHELL

trap '[ "$?" -ne 0 ] && printf "Error: An error occurred\n"' EXIT

# Defaults
init=0
init_hook=""
verbose=0
version="1.2.13"
# Print usage to stdout.
# Arguments:
#   None
# Outputs:
#   print usage with examples.
show_help() {
	cat <<EOF
distrobox version: ${version}

Usage:

	distrobox-init --name ${USER} --user $(id -ru) --group $(id -rg) --home ${HOME}

Options:

	--name/-n:		user name
	--user/-u:		uid of the user
	--group/-g:		gid of the user
	--home/-d:		path/to/home of the user
	--help/-h:		show this message
	--init/-I:		whether to use or not init
	--verbose/-v:		show more verbosity
	--version/-V:		show version
	--:			end arguments execute the rest as command to execute during init
EOF
}

# Parse arguments
while :; do
	case $1 in
	-h | --help)
		# Call a "show_help" function to display a synopsis, then exit.
		show_help
		exit 0
		;;
	-v | --verbose)
		shift
		verbose=1
		;;
	-V | --version)
		printf "distrobox: %s\n" "${version}"
		exit 0
		;;
	-n | --name)
		if [ -n "$2" ]; then
			container_user_name="$2"
			shift
			shift
		fi
		;;
	-i | --init)
		if [ -n "$2" ]; then
			init="$2"
			shift
			shift
		fi
		;;
	-d | --home)
		if [ -n "$2" ]; then
			container_user_home="$2"
			shift
			shift
		fi
		;;
	-u | --user)
		if [ -n "$2" ]; then
			container_user_uid="$2"
			shift
			shift
		fi
		;;
	-g | --group)
		if [ -n "$2" ]; then
			container_user_gid="$2"
			shift
			shift
		fi
		;;
	--)
		shift
		init_hook=$*
		break
		;;
	*) # Default case: If no more options then break out of the loop.
		break ;;
	esac
done

set -o errexit
set -o nounset
# set verbosity
if [ "${verbose}" -ne 0 ]; then
	set -o xtrace
fi

# Check we're running inside a container and not on the host
if [ ! -f /run/.containerenv ] && [ ! -f /.dockerenv ]; then
	printf >&2 "You must run %s inside a container!\n" " $(basename "$0")"
	printf >&2 "distrobox-init should only be used as an entrypoint for a distrobox!\n\n"
	printf >&2 "This is not intended to be used manually, but instead used by distrobox-enter\n"
	printf >&2 "to set up the container's entrypoint.\n"
	exit 126
fi

# Ensure the foundamental variables are set and not empty, we will not proceed if
# they are not all set.
[ -z "${container_user_gid}" ] && printf "Error: Invalid arguments, missing user gud\n" && exit 2
[ -z "${container_user_home}" ] && printf "Error: Invalid argument, missing user home\n" && exit 2
[ -z "${container_user_name}" ] && printf "Error: Invalid arguments, missing username\n" && exit 2
[ -z "${container_user_uid}" ] && printf "Error: Invalid arguments, missing user uid\n" && exit 2

# Bind mount or error.
# Arguments:
#   source_dir
#	target_dir
#	mount_flags -> optional
# Outputs:
#   No output if all ok
#	Error if not
mount_bind() (
	source_dir="$1"
	target_dir="$2"
	mount_flags="$3"

	# if source dir doesn't exist, just exit normally
	! [ -d "${source_dir}" ] && ! [ -f "${source_dir}" ] && return 0

	# if the source_dir exists, then create the target_dir
	if [ -d "${source_dir}" ]; then
		# exit if not successful
		if ! mkdir -p "${target_dir}"; then
			printf "Warning: cannot create mount target directory: %s\n" "${target_dir}"
			return 1
		fi
	# if instead it's a file, create it with touch
	elif [ -f "${source_dir}" ]; then
		# exit if not successful
		if ! touch "${target_dir}"; then
			printf "Warning: cannot create mount target file: %s\n" "${target_dir}"
			return 1
		fi
	fi

	# Add mountflags if needed, if no are specificed, use rslave as default.
	if [ "${mount_flags}" = "" ]; then
		mount_flags="rslave"
	fi
	# bind mount source_dir to target_dir, return error if not successful
	if ! mount --rbind -o "${mount_flags}" "${source_dir}" "${target_dir}"; then
		printf "Warning: failed to bind mount %s to %s\n" "${source_dir}" "${target_dir}"
		return 1
	fi

	return 0
)

# Extract shell name from the $SHELL environment variable
# If not present as package in the container, we want to install it.
shell_pkg="$(basename "${SHELL:-"bash"}")"
# Check if dependencies are met for the script to run.
if ! command -v find || ! command -v mount || ! command -v passwd ||
	! command -v sudo || ! command -v useradd || ! command -v usermod ||
	! ls /etc/profile.d/*vte* ||
	! command -v "${shell_pkg}"; then

	# Detect the available package manager
	# install minimal dependencies needed to bootstrap the container:
	#	the same shell that's on the host
	#	sudo, mount, find
	#	passwd, groupadd and useradd
	if command -v apk; then
		apk add \
			"${shell_pkg}" \
			findutils \
			ncurses \
			procps \
			shadow \
			sudo \
			util-linux \
			vte3

	elif command -v apt-get; then
		export DEBIAN_FRONTEND=noninteractive
		apt-get update
		apt-get install -y \
			"${shell_pkg}" \
			findutils \
			libnss-myhostname \
			libvte-common \
			ncurses-base \
			passwd \
			procps \
			sudo \
			util-linux

	elif command -v dnf; then
		dnf install -y \
			"${shell_pkg}" \
			findutils \
			ncurses-base \
			passwd \
			procps-ng \
			shadow-utils \
			sudo \
			util-linux \
			vte-profile

	elif command -v emerge; then
		# update repos
		emerge --sync
		emerge --ask=n --autounmask-continue \
			"${shell_pkg}" \
			findutils \
			ncurses \
			procps \
			shadow \
			sudo \
			util-linux \
			vte-profile

	elif command -v pacman; then
		pacman -Sy --noconfirm \
			"${shell_pkg}" \
			findutils \
			ncurses \
			procps-ng \
			shadow \
			sudo \
			util-linux \
			vte-common

	elif command -v slackpkg; then
		slackpkg update
		yes | slackpkg install -default_answer=yes -batch=yes \
			"${shell_pkg}" \
			libvte-2 \
			findutils \
			ncurses \
			procps \
			shadow \
			sudo \
			util-linux

	elif command -v swupd; then
		swupd bundle-add \
			shells \
			sudo \
			findutils \
			procps-ng

	elif command -v xbps-install; then
		xbps-install -Sy \
			"${shell_pkg}" \
			findutils \
			ncurses-base \
			procps-ng \
			shadow \
			sudo \
			util-linux

	elif command -v yum; then
		yum install -y \
			"${shell_pkg}" \
			findutils \
			ncurses-base \
			passwd \
			procps \
			shadow-utils \
			sudo \
			util-linux \
			vte-profile

	elif command -v zypper; then
		zypper install -y \
			"${shell_pkg}" \
			findutils \
			libvte-2* \
			ncurses \
			procps \
			shadow \
			sudo \
			util-linux

	else
		printf "Error: could not find a supported package manager.\n"
		printf "Error: could not set up base dependencies.\n"
		# Exit as command not found
		exit 127
	fi

fi
# We'll also bind mount in READ-ONLY useful directories from the host
HOST_MOUNTS_RO="
	/etc/machine-id
	/var/lib/flatpak
	/var/lib/systemd/coredump
	/var/log/journal"
for host_mount_ro in ${HOST_MOUNTS_RO}; do
	mount_bind /run/host"${host_mount_ro}" "${host_mount_ro}" ro
done

# We'll also bind mount READ-WRITE useful mountpoints to pass external drives and libvirt from
# the host to the container
HOST_MOUNTS="
	/media
	/mnt
	/run/libvirt
	/run/media
	/run/systemd/journal
	/run/udev
	/srv
	/var/lib/libvirt
	/var/mnt"
for host_mount in ${HOST_MOUNTS}; do
	if ! mount_bind /run/host"${host_mount}" "${host_mount}" rw; then
		printf "Warning: Cannot bind mount %s to /run/host%s\n" "${host_mount}" "${host_mount}"
	fi
done

# Find all the user's socket and mount them inside the container
# this will allow for continuity of functionality between host and container
#
# for example using `podman --remote` to control the host's podman from inside
# the container or accessing docker and libvirt sockets.
host_sockets="$(find /run/host/run -name 'user' -prune -o -name 'nscd' -prune -o -type s -print \
	2>/dev/null || :)"
for host_socket in ${host_sockets}; do
	container_socket="$(printf "%s" "${host_socket}" | sed 's|/run/host||g')"
	# Check if the socket already exists or the symlink already esists
	if [ ! -S "${container_socket}" ] && [ ! -L "${container_socket}" ]; then
		# link it.
		rm -f "${container_socket}"
		mkdir -p "$(dirname "${container_socket}")"
		if ! ln -s "${host_socket}" "${container_socket}"; then
			printf "Warning: Cannot link socket %s to %s\n" "${host_socket}" "${container_socket}"
		fi
	fi
done

# This fix is needed as on Selinux systems, the host's selinux sysfs directory
# will be mounted inside the rootless container.
#
# This works around this and allows the rootless container to work when selinux
# policies are installed inside it.
#
# Ref. Podman issue 4452:
#    https://github.com/containers/podman/issues/4452
if [ -d "/sys/fs/selinux" ]; then
	mkdir -p /usr/share/empty
	if ! mount_bind /usr/share/empty /sys/fs/selinux rw; then
		printf "Warning: Cannot bind mount %s to /run/host%s\n" "/usr/share/empty" " /sys/fs/selinux"
	fi
fi

# In case of an RPM distro, we can specify that our bind_mount directories
# are in fact net shares. This prevents conflicts during package installations.
if [ -d "/usr/lib/rpm/macros.d/" ]; then
	# Loop through all the environment vars
	# and export them to the container.
	set +o xtrace
	net_mounts=""
	for net_mount in \
		${HOST_MOUNTS_RO} ${HOST_MOUNTS} \
		'/dev' '/proc' '/sys' '/tmp' \
		'/etc/host.conf' '/etc/hosts' '/etc/resolv.conf' '/etc/localtime' \
		'/usr/share/zoneinfo'; do

		net_mounts="${net_mount}:${net_mounts}"

	done
	# re-enable logging if it was enabled previously.
	if [ "${verbose}" -ne 0 ]; then
		set -o xtrace
	fi
	net_mounts=${net_mounts%?}
	cat <<EOF >/usr/lib/rpm/macros.d/macros.distrobox
%_netsharedpath ${net_mounts}
EOF
# In case of an DEB distro, we can specify that our bind_mount directories
# have to be ignored. This prevents conflicts during package installations.
elif [ -d "/etc/dpkg/dpkg.cfg.d/" ]; then
	# Loop through all the environment vars
	# and export them to the container.
	set +o xtrace
	printf "" >/etc/dpkg/dpkg.cfg.d/00_distrobox
	for net_mount in ${HOST_MOUNTS_RO} ${HOST_MOUNTS}; do
		printf "path-exclude %s/*\n" "${net_mount}" >>/etc/dpkg/dpkg.cfg.d/00_distrobox
	done
	# re-enable logging if it was enabled previously.
	if [ "${verbose}" -ne 0 ]; then
		set -o xtrace
	fi

	# Also we put a hook to clear some critical paths that do not play well
	# with read only filesystems, like systemd.
	if [ -d "/etc/apt/apt.conf.d/" ]; then
		cat <<EOF >/etc/apt/apt.conf.d/00_distrobox
DPkg::Pre-Invoke {"if mountpoint /var/log/journal; then umount /var/log/journal; fi";};
DPkg::Post-Invoke {"if [ -e /run/host/var/log/journal ]; then mount --rbind -o ro /run/host/var/log/journal /var/log/journal; fi";};
EOF
	fi
fi

mkdir -p /etc/sudoers.d
# Do not check fqdn when doing sudo, it will not work anyways
if ! grep -q 'Defaults !fqdn' /etc/sudoers.d/sudoers; then
	printf "Defaults !fqdn\n" >>/etc/sudoers.d/sudoers
fi
# Ensure passwordless sudo is set up for user
if ! grep -q "${container_user_name} ALL = (root) NOPASSWD:ALL" /etc/sudoers.d/sudoers; then
	printf "%s ALL = (root) NOPASSWD:ALL\n" "${container_user_name}" >>/etc/sudoers.d/sudoers
fi

# If not existing, ensure we have a group for our user.
if ! grep -q "${container_user_name}" /etc/group; then
	groupadd --force --gid "${container_user_gid}" "${container_user_name}"
fi
# Let's add our user to the container if the user already exists, just go ahead.
if ! grep -q "${container_user_name}" /etc/passwd; then
	if ! useradd \
		--home-dir "${container_user_home}" \
		--no-create-home \
		--shell "${SHELL:-"/bin/bash"}" \
		--uid "${container_user_uid}" \
		--gid "${container_user_gid}" \
		"${container_user_name}"; then

		printf "Warning: there was a problem setting up the user\n"
	fi
fi

# We generate a random password to initialize the entry for the user and root.
temporary_password="$(cat /proc/sys/kernel/random/uuid)"
printf "%s\n%s\n" "${temporary_password}" "${temporary_password}" | passwd "${container_user_name}"
printf "%s\n%s\n" "${temporary_password}" "${temporary_password}" | passwd root
# Delete password for root and user
passwd --delete "${container_user_name}"
passwd --delete root

# If we do not have profile files in the home, we should copy the
# skeleton files, if present.
# Ensure we copy only if the dotfile is not already present.
if [ -d "/etc/skel" ]; then
	skel_files="$(find /etc/skel/ -type f || :)"
	for skel_file in ${skel_files}; do
		if [ ! -f "${container_user_home}/$(basename "${skel_file}")" ] &&
			[ ! -L "${container_user_home}/$(basename "${skel_file}")" ]; then

			cp "${skel_file}" "${container_user_home}"
			chown "${container_user_uid}":"${container_user_gid}" \
				"${container_user_home}/$(basename "${skel_file}")"

		fi
	done
fi

# Themes and icons integration works using a bind mount inside the container
# of the host's themes and icons directory. This ensures that the host's home will
# not be littered with files and directories and broken symlinks.
#
# bind mount distrobox directory for themes and icons
mkdir -p "${container_user_home}/.local/share/themes"
mkdir -p "${container_user_home}/.local/share/icons"
mkdir -p "${container_user_home}/.local/share/fonts"
# Fix permissions for home directories
chown "${container_user_uid}":"${container_user_gid}" "${container_user_home}/.local"
chown "${container_user_uid}":"${container_user_gid}" "${container_user_home}/.local/share"
chown "${container_user_uid}":"${container_user_gid}" "${container_user_home}/.local/share/themes"
chown "${container_user_uid}":"${container_user_gid}" "${container_user_home}/.local/share/icons"
chown "${container_user_uid}":"${container_user_gid}" "${container_user_home}/.local/share/fonts"
if ! mount_bind "/run/host/usr/share/themes" "${container_user_home}/.local/share/themes" rw; then
	printf "Warning: Cannot bind mount /run/host/usr/share/themes to %s/.local/share/themes\n" "${container_user_home}"
	printf "Warning: Themes integration with the host is disabled.\n"
fi
if ! mount_bind "/run/host/usr/share/icons" "${container_user_home}/.local/share/icons" rw; then
	printf "Warning: Cannot bind mount /run/host/usr/share/icons to %s/.local/share/icons\n" "${container_user_home}"
	printf "Warning: Icons integration with the host is disabled.\n"
fi
if ! mount_bind "/run/host/usr/share/fonts" "${container_user_home}/.local/share/fonts" rw; then
	printf "Warning: Cannot bind mount /run/host/usr/share/fonts to %s/.local/share/fonts\n" "${container_user_home}"
	printf "Warning: Fonts integration with the host is disabled.\n"
fi

# execute eventual init hooks if specified
# shellcheck disable=SC2086
eval ${init_hook}

printf "container_setup_done\n"

if [ "${init}" -eq 0 ]; then
	# Keepalive loop
	sleep infinity
else
	# some of this directories are needed by
	# the init system. If they're mounts, there might
	# be problems. Let's unmount them.
	HOST_MOUNTS_RO_INIT="
		/run/systemd/journal
		/var/lib/systemd/coredump
		/var/log/journal"
	for host_mount in ${HOST_MOUNTS_RO_INIT}; do
		umount "${host_mount}"
	done
	# Now we can launch init
	exec /sbin/init
fi
