#!/usr/bin/env bash

# NAME
# ----
# utrans: convert systemd units to generic unix equivalents
#
# AUTHORS
# -------
# [https://salsa.debian.org/kayg/systemd-unit-translator]
#
# K Gopal Krishna <mail@kayg.org> under the guidance of Benda Xu
# <heroxbd@gentoo.org>, Adam Borowski <kilobyte@angband.pl> and Mo Zhou
# <lumin@debian.org>.
#
# This project was a part of Google Summer of Code, 2020.
#
# [https://git.devuan.org/leepen/unit-translator]
#
# Mark Hindley <mark@hindley.org.uk>
#
# LICENSE
# -------
# BSD-2-Clause License
#
# Copyright (c) 2020, K Gopal Krishna <mail@kayg.org>. All rights reserved.
# Copyright (c) 2023-, Mark Hindley <mark@hindley.org.uk>. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# Safety options for bash
set -o errexit
set -o pipefail
set -o nounset
set -o noglob
set -o noclobber

# +++ Miscellaneous Functions +++

# read a boolean value and returns true or false
# usage: is_true val
# val		boolean value
is_true() { case "$1" in 1 | [Oo][Nn] | [Tt]* | [Yy]*) true ;; *) false ;; esac }


# print_usage():
#   accepted options: none
#   accepted arguments: none
#
# print how to use the translator
print_usage() {
	printf '%s\n' 'Unit Translator'
	printf '\n%s\n' '  Usage: utrans [-b <backend>] [-h] [-v] /path/to/unit/file /path/to/destination/directory'
	printf '\n%s\n' '  Supported options:'
	printf '%s\n' '    -b <backend>: load backend'
	printf '%s\n' '    -f <type>: force'
	printf '%s\n' '    -h: open manpage'
	printf '%s\n' '    -v: show version'
	exit 1
}

# print version
print_version() {
	printf '%s version %s\n' 'Unit Translator' 0.1
	exit
}

# find unit in load path
unit_path_find() {
	unit=$1
	IFS=':' read -ra systemd_unit_path <<< "${SYSTEMD_UNIT_PATH:-/usr/lib/systemd/system:/lib/systemd/system}"
	for dir in "${systemd_unit_path[@]}"; do
		if [[ -s "$dir/$unit" ]]; then
			echo "$dir/$unit" && return
		fi
	done
}

# handle or ignore special executable prefixes
handle_exec_prefixes() {
	exec_event=$1
	special_exec_prefix="^(\@|\-|\:|\+|\!|\!\!)(.*)"
	readarray -t _exec <<<"${exec_event}"
	for exec_line in "${_exec[@]}"; do
		[ "${exec_line}" ] || continue
		if [[ "${exec_line}" =~ ${special_exec_prefix} ]]; then
			exec_line="${BASH_REMATCH[2]}"
			if [[ "${BASH_REMATCH[1]}" == '-' ]]; then
				exec_line+=' || true'
			else
				echo "WARNING: ignoring special exec prefix ${BASH_REMATCH[1]}" >&2
			fi
		fi
		printf '%s\n' "${exec_line}"
	done
}

# print assignment directive
print_directive() {
	local d=$1
	local s=${2:-}
	if [[ -n "$s" ]]; then
		printf '%s="%s"\n' "${d}" "${s%% }" # Trim trailing whitespace
	fi
}

# print string, possibly multiline, optionally add prefix, skip any blank lines.
print_lines() {
	str=$1
	prfx=${2:-}
	while IFS= read -r line || [[ -n "$line" ]]; do
		[[ -z "$line" ]] || printf "%s%s\n" "${prfx}" "${line}"
	done <<< "$str"
}

# print shell-style function, if body not empty
print_sh_function() {
    	name=$1
	body=$2
	if [[ "$body" ]]; then
	    printf '%s\n' "${name}() {"
	    print_lines "${body}" '  '
	    printf '%s\n' "}"
	fi
}

# --- Miscellaneous Functions ---

# +++ Parsing-related Functions +++

# parse_section():
#   accepted options: none
#   accepted arguments: two
#     "${1}": the file to filter
#     "${2}": the section to filter
#
parse_section() {
	local file section key start value
	local -A var

	file="${1}"
	section="${2}"

	start=0
	while IFS= read -r line || [ -n "$line" ]; do
		if [[ "${start}" -eq 1 ]]; then
			if [[ "${line}" =~ ^\[.*\] ]]; then
				break
			elif [[ "${line}" =~ ^# ]]; then
				continue
			elif [[ "${line}" =~ .*=.* ]]; then
				while [[ "${line}" =~ \\$ ]]; do
					read -r continuation
					line="${line::-1} $continuation"
				done
				key="${line%%=*}"
				value="${line#*=}"

				if [[ -n "${var["${key}"]:-}" ]]; then
					var["${key}"]+=$'\n'
				fi
				var["${key}"]+="${value}"
			fi
		fi

		if [[ "${line}" == "[$section]" ]]; then
			start=1
		fi
	done <"${file}"
	if [[ -n "${var[*]}" ]]; then
		for k in "${!var[@]}"; do
			printf '[%s]=%q\n' "$k" "${var[$k]}"
		done
	fi
}

# --- Parsing-related Functions ---

# +++ Service-related Functions +++

# generate origin and sha256 header
gen_origin() {
	printf '# Generated by %s from:\n#  %s\n' "$0" "$(sha256sum "${systemd_unit}")"
	if [[ "${socket_service_unit:-}" ]] ; then
	    printf '#  %s\n' "$(sha256sum "${socket_service_unit}")"
	elif [[ -n "${systemd_unit##*.service}" ]] ; then
	    printf '#  %s\n' "$(sha256sum "${systemd_unit%.*}.service")"
	fi
	printf '\n'
}

# Add dependencies, avoid duplicates and excess whitespace.
add_depends() {
	key=$1
	shift
	for add in "$@"; do
		for d in ${depends[$key]:-}; do
		    [[ "$d" == "$add" ]] && continue 2 # Already present
		done
		[[ -z "${depends[$key]:-}" ]] || depends[$key]+=' '
		depends[$key]+="$add"
	done
}

# Translate dependencies to insserv style names. Backends can process further,
# as required.
map_known_dependencies() {

	if [[ -n "${service[BusName]:-}" ]]; then
		service[Type]=dbus
	fi

	case "${service[Type]:-}" in
		simple) ;;
		exec) ;;
		oneshot) ;;
		idle) ;;
		dbus)
			add_depends Requires dbus
			add_depends After dbus
			;;
		notify) ;;
		forking) ;;
	esac

	case "${unit[RequiresMountsFor]:-}" in
	    	/run*)
			add_depends Requires \$local_fs
			add_depends After \$local_fs
			;;
	    	/*)
			add_depends Requires \$remote_fs
			add_depends After \$remote_fs
			;;
	esac

	# Handle display-manager.service
	if [[ "${install[Alias]:-}" == 'display-manager.service' ]]; then
	    if grep -q "${service[ExecStart]}" /etc/X11/default-display-manager ; then
		install[Alias]=\$x-display-manager
	    else
		echo "WARNING: skipping non-default display manager" >&2
		exit 1
	    fi
	fi
	# Handle runtime directories
	for dir in Runtime State Cache Logs Configuration; do
	    if [[ -n "${service[${dir}Directory]:-}" ]]; then
		add_depends Requires \$remote_fs
		add_depends After \$remote_fs
		case "$dir" in
		    Runtime) dirbase='/run' ;;
		    State) dirbase='/var/lib' ;;
		    Cache) dirbase='/var/lib' ;;
		    Logs) dirbase='/var/log' ;;
		    Configuration) dirbase='/etc' ;;
		    *) ! echo 'ERROR: unknown runtime directory' >&2 ;;
		esac
		service[ExecStartPre]="mkdir -p ${service[${dir}DirectoryMode]:+-m ${service[${dir}DirectoryMode]} }${dirbase}/${service[${dir}Directory]}"$'\n'"${service[ExecStartPre]:-}"
		service[Environment]+=$'\n'"${dir@U}_DIRECTORY=${dirbase}/${service[${dir}Directory]}"
		is_true "${service[RuntimeDirectoryPreserve]:-no}" ||
		    service[ExecStopPost]+=$'\n'"rm -r ${dirbase}/${service[${dir}Directory]}"
	    fi
	done
	for key in After Before Requires Wants; do
		read -ra dependencies <<<"${unit[${key}]:-}"
		for dependency in "${dependencies[@]}"; do
			case "${dependency}" in
				*%*.*)
					echo "WARNING: dependencies with specifiers not supported" >&2
					;;
				network*.target | systemd-networkd.service)
					add_depends "${key}" \$network
					;;
				local-fs-pre.target)
					add_depends "${key}" mountkernfs
					;;
				time-sync.target)
					add_depends "${key}" \$time
					;;
				systemd-modules-load.service)
					add_depends "${key}" kmod
					;;
				local-fs.target)
					add_depends "${key}" \$local_fs
					;;
				systemd-sysctl.service)
					add_depends "${key}" procps
					;;
				nss-lookup.target)
					add_depends "${key}" \$named \$network
					;;
				rpcbind.target)
					add_depends "${key}" \$portmap
					;;
				remote-fs.target)
					add_depends "${key}" \$remote_fs
					;;
				syslog.socket)
					# systemd specific, ignore
					unit[${key}]=${unit[$key]/syslog.socket/}
					;;
				display-manager.service|graphical.target)
					add_depends "${key}" \$x-display-manager
					;;
				*.target)
					# TODO
					;;
				*.mount)
					#TODO
					;;
				*)
					[[ "${dependency%.*}" == "${systemd_unit_file%.*}" ]] && continue
					add_depends "${key}" "${dependency%.*}"
					;;
			esac
		done
	done
}

# --- Service-related Functions ---

# +++ Socket-related Functions +++

map_known_socket_types() {
	for listen_type in ListenStream ListenDatagram ListenSequentialPacket; do
		for listen_type_format in ${socket[${listen_type}]:-}; do
			if [[ "${listen_type_format:-}" =~ [1-9][0-9]{0,4} ]]; then
				if is_true "${socket[Accept]:-}"; then
					use_socket_activate=false
				fi
			elif [[ "${listen_type_format:-}" =~ ^/.* ]]; then
				use_socket_activate=true
			fi
		done
	done
}

# --- Socket-related Functions ---

translate() {
	systemd_unit="${1}"
	systemd_unit_file="$(basename "${systemd_unit}")"
	dest_dir="${2}"

	# parse key-value pairs from the [Unit] section
	declare -A unit="( $(parse_section "${systemd_unit}" Unit) )"

	# and the [Install] section
	declare -A install="( $(parse_section "${systemd_unit}" Install) )"

	# variable to pass calculated dependencies to backend
	declare -A depends

	# if the file provided is of type .service...
	if [[ "${systemd_unit}" == *.service ]]; then
		# Use a name-matched socket instead
		if  [[ -f "${systemd_unit%.service}.socket" ]]; then
			echo "Using ${systemd_unit%.service}.socket" >&2
			translate "${systemd_unit%.service}.socket" "$dest_dir"
			exit
		fi
		# parse key-value pairs from the [Service] section
		declare -A service="( $(parse_section "${systemd_unit}" Service) )"

		# map known dependencies
		map_known_dependencies

		for required in ${unit[Requires]:-} ; do
			case "$required" in
				*.socket|*.service)
				    echo "$systemd_unit_file Requires ${required}" >&2
				    service_required_unit=$(unit_path_find "${required}")
				    if [ "${service_required_unit}" ]; then
					echo "Found ${service_required_unit}" >&2
					# Process in subshell
					( set +o noclobber # may already have been processed, allow clobber
					  translate "${service_required_unit}" "$dest_dir" )
					# continue
				    else
					echo "Failed to find required unit (${required})." >&2
					exit 1
				    fi
				    ;;
			esac
		done

		export_service "${systemd_unit}" "${dest_dir}"

	elif [[ "${systemd_unit}" == *.socket ]]; then
		# parse key-value pairs from the [Socket] section
		declare -A socket="( $(parse_section "${systemd_unit}" Socket) )"
		if is_true "${socket[Accept]:-}"; then
			socket_service_unit=$(unit_path_find "${socket[Service]:-${systemd_unit_file%%.socket}@.service}")
		else
			socket_service_unit=$(unit_path_find "${socket[Service]:-${systemd_unit_file%%.socket}.service}")
		fi
		if [[ -z "${socket_service_unit}" ]]; then
			echo "Failed to find ${socket[Service]:-${systemd_unit_file%%.socket}@.service}" >&2
			exit 1
		fi
		# parse key-value pairs from the [Service] section of the accompanying service file
		declare -A service="( $(parse_section "${socket_service_unit}" Service) )"

		# parse key-value pairs from the [Unit] section of the accompanying service file
		declare -A unit="( $(parse_section "${socket_service_unit}" Unit) )"

			# and append the [Install] section
			declare -A install+="( $(parse_section "${socket_service_unit}" Install) )"

		# map known dependencies
		map_known_dependencies

		# determine the type of listener and the tool to be used
		map_known_socket_types

		# if the flag has been set to false...
		if [[ -n "${use_socket_activate:-}" && "${use_socket_activate}" != "true" ]]; then
			export_inetd "${systemd_unit}" "${dest_dir}"
		else
			export_service "${systemd_unit}" "${dest_dir}"
		fi
	elif [[ "${systemd_unit}" == *.timer ]]; then
		# parse key-value pairs from the [Timer] section
		declare -A timer="( $(parse_section "${systemd_unit}" Timer) )"

		# parse key-value pairs from the [Service] section from the accompanying service file
		declare -A service="( $(parse_section "${systemd_unit%%.timer}.service" Service) )"

		export_timer "${systemd_unit}" "${dest_dir}"
	fi
}

main() {
	while getopts "b:f:hv" o; do
	    case "${o}" in
		b)
		    backends+=" ${OPTARG}"
		    ;;
		f)
		    case "$OPTARG" in
			overwrite) set +o noclobber;;
			*) echo "ERROR: invalid argument to -f"; exit 1 ;;
		    esac
		    ;;
		h)
		    man utrans
		    ;;
		v)
		    print_version
		    ;;
		*)
		    print_usage
		    ;;
	    esac
	done
	shift $((OPTIND-1))
	[ $# -lt 2 ] && print_usage

	if [[ ! -s ${1} ]]; then
		echo "WARNING: ignoring masked unit ${1}" >&2
	else
		# Source default backends
		. "${UTRANS_DATA_DIR:-/usr/share/utrans}/backends/openrc"
		. "${UTRANS_DATA_DIR:-/usr/share/utrans}/backends/xinetd"
		. "${UTRANS_DATA_DIR:-/usr/share/utrans}/backends/cron"

		for backend in ${backends:-}; do
		    # shellcheck source=./backends/inetd
		    . "${UTRANS_DATA_DIR:-/usr/share/utrans}/backends/${backend}"
		done

		translate "$@"
	fi
}

main "${@}"
