#!/usr/bin/env bash
. "${0%/*}"/../lib/common.bashlib || exit 1 #C#
init
depend grep portageq qlist sort stat
esanitize
include shellparse
usage <<-EOU
	Usage: ${0##*/} [-O <service-script>...] [<atom|image>]

	Reports common mistakes in OpenRC service scripts. If scripts are not
	given with the -O option, will look for scripts in the given installation
	image (i.e. \${PORTAGE_TMPDIR}/[...]/image/), or the currently installed
	system copy matching the given atom.

	Options:
	  -p, --no-perms     Disable file permissions QA check
	  -s, --no-ssd-args  Disable start_stop_daemon_args linter

	  -O, --openrc       Run checks on given scripts rather than the atom/image

	      --confdir=PATH Configuration dir to use instead of defaults
	                     (@confdir@ + ${XDG_CONFIG_HOME:-~/.config}/@package@)
	      --dumpconfig   Display config and exit (> ${0##*/}.conf)

	      --root=PATH    Set ROOT (command-line-only, default: '${ROOT}')
	      --eprefix=PATH Set EPREFIX (likewise, default: '${EPREFIX}')

	  -h, --help         Display usage information and exit
	      --version      Display version information and exit

	*Notes*
	This is currently basic with few checks, will be extended as needed and
	is a subject to breaking changes. Note that portage already checks for
	bashisms if have dev-util/checkbashisms.

	*Portage Integration*
	Can be integrated by using ${EROOT}/etc/portage/bashrc, either by using the
	example ${ROOT}@datadir@/bashrc or by manually adding:

	    source @datadir@/${0##*/}.bashrc

	    post_pkg_preinst() {
	        qa-openrc_post_pkg_preinst
	    }

	bashrc environment options (export/make.conf/package.env):
	  QA_OPENRC=y | =n         Enable or disable, can also use IWDT_ALL=y | =n
	  QA_OPENRC_CMD=${0##*/}  This script, needs to be changed if not in PATH
	  QA_OPENRC_ARGS=          Extra arguments to pass, see options above
	  QA_OPENRC_LOG=eqawarn    Portage output command, can also use IWDT_LOG=ewarn
	Note: eqawarn post-emerge log needs "qa" in make.conf's PORTAGE_ELOG_CLASSES
EOU
optauto args "${@}" <<-EOO
	p|!perms=bool:true
	s|!ssd-args=bool:true
	O|openrc=bool:false
EOO
set -- "${args[@]}"; unset args

if ${O[openrc]}; then
	(( ${#} >= 1 )) || die "no scripts specified, see \`${0##*/} --help\`"
else
	(( ${#} == 1 )) || die "need exactly one atom/image specified, see \`${0##*/} --help\`"
fi

# openrc-get_services <array> <image|atom>
#	Set <array> to list of OpenRC service files from <image|atom>.
openrc-get_services() {
	local -n outref=${1}

	local -a servicefiles
	if [[ ${2::1} != = && -d ${2} ]]; then
		local image=${2}
		# hopefully image itself does not have this dir
		[[ -d ${image}/image ]] && image+=/image

		set +f
		servicefiles=("${image}${EPREFIX}"/etc/init.d/*)
		set -f
	else
		# TODO: vdb/qlist stuff should be offloaded to a vdb.bashlib
		openrc-export_qvdb
		qlist -Cqe "${2}" | grep "^${EPREFIX}/etc/init.d/[^/]*$" | sort | map servicefiles
		[[ ${PIPESTATUS[*]} == '0 '[01]' 0 0' ]] || die "qlist failed for '${2}'"

		servicefiles=("${servicefiles[@]/#/${ROOT}}")
	fi

	local servicefile
	outref=()
	for servicefile in "${servicefiles[@]}"; do
		[[ -f ${servicefile} && ! -h ${servicefile} ]] &&
			openrc-is_service "${servicefile}" &&
			outref+=("${servicefile}")
	done
}

# openrc-check_permissions <file>
#	Although this check is useless for ebuilds that use doinitd/newinitd, it may
#	be useful if init scripts are installed by upstream build system.
openrc-check_permissions() {
	local permissions
	local expected_value=-rwxr-xr-x
	permissions=$(stat -Lc%A "${1}") || die "stat failed"
	if [[ ${permissions} != "${expected_value}" ]]; then
		msg "OPENRC: incorrect permissions for service '${1##*/}'"
		msg "    have: ${permissions}"
		msg "expected: ${expected_value}"
	fi
}

# openrc-check_ssd_args <file>
#	Finds flags in start_stop_daemon_args that can be replaced with declarative
#	config variables.
openrc-check_ssd_args() {
	local command_user svc=${1##*/}

	local -A si
	shellimport si "${1}" RC_SVCNAME="${svc}" || die "failed to import '${1}'"
	set -- ${si[start_stop_daemon_args]:-} #!SC2086

	while (( ${#} )); do
		flag=${1}
		shift

		# roughly consider anything until next option as the argument
		val=
		while (( ${#} )); do
			[[ ${1} != -* ]] || break
			val+=${val:+ }${1//[\"\'\\]/}
			shift
		done

		# order as in start-stop-daemon(8)
		case ${flag} in
			-x|--exec)
				openrc-ssd_msg "${svc}" "${flag}" "should be deleted" \
					'as it is included by default when command="" is declared';;
			-p|--pidfile)
				openrc-ssd_msg "${svc}" "${flag}" "pidfile=\"${val}\"";;
			-n|--name)
				openrc-ssd_msg "${svc}" "${flag}" "procname=\"${val}\"";;
			-u|--user|-c|--chuid)
				command_user=${val}
				openrc-ssd_msg "${svc}" "${flag}" "command_user=\"${val}\"";;
			-P|--progress)
				openrc-ssd_msg "${svc}" "${flag}" "command_progress=yes";;
			-d|--chdir)
				openrc-ssd_msg "${svc}" "${flag}" "directory=\"${val}\"";;
			-r|--chroot)
				openrc-ssd_msg "${svc}" "${flag}" "chroot=\"${val}\"";;
			-g|--group)
				openrc-ssd_msg "${svc}" "${flag}" \
					"command_user=\"${command_user:-<user>}:${val}\"";;
			-k|--umask)
				openrc-ssd_msg "${svc}" "${flag}" "umask=\"${val}\"";;
			-b|-m|--background|--make-pidfile)
				openrc-ssd_msg "${svc}" "${flag}" "command_background=yes";;
			-1|--stdout)
				openrc-ssd_msg "${svc}" "${flag}" "output_log=\"${val}\"";;
			-2|--stderr)
				openrc-ssd_msg "${svc}" "${flag}" "error_log=\"${val}\"";;
			-3|--sdtout-logger)
				openrc-ssd_msg "${svc}" "${flag}" "output_logger=\"${val}\"";;
			-4|--sdterr-logger)
				openrc-ssd_msg "${svc}" "${flag}" "error_logger=\"${val}\"";;
		esac
	done
}

# openrc-export_qvdb
#	Exports Q_VDB variable according to portageq for q* tools.
openrc-export_qvdb() {
	Q_VDB=$(portageq vdb_path) || die "portageq vdb_path failed"
	[[ -d ${Q_VDB} ]] \
		|| die "portageq returned '${Q_VDB}' as VDB path which does not appear usable"
	export Q_VDB=${Q_VDB#"${ROOT}"}
}

# openrc-is_service <file>
#	Return true if <file> has an openrc-run shebang.
openrc-is_service() {
	local -a shebang
	mapfile -tn1 shebang < "${1}" || die
	[[ ${shebang[0]:-} =~ ^'#!'[[:space:]]*(.+) ]] || return 1
	split shebang "${BASH_REMATCH[1]}" $' \t'

	if [[ ${shebang[0]} == */openrc-run ]]; then
		return 0
	elif [[ ${shebang[0]} == */env ]]; then
		local shebangelem
		for shebangelem in "${shebang[@]:1}"; do
			case ${shebangelem} in
				# first non-option nor name=value should be the command
				-*|*=*) continue;;
				openrc-run|*/openrc-run) return 0;;
			esac
			break
		done
	fi
	return 1
}

# openrc-ssd_msg <service> <flag> <replacement>
#	Print a message, adding header if necessary.
openrc-ssd_msg() {
	local svc=${1}
	local flag=${2}
	local modal text
	if (( ${#} == 3 )); then
		modal="should be replaced with"
		text=${3}
	elif (( ${#} == 4 )); then
		modal=${3}
		text=${4}
	fi

	if [[ ! ${SSD_HEADER_PRINTED} ]]; then
		msg "OPENRC: unnecessary usage of start_stop_daemon_args found:"
		SSD_HEADER_PRINTED=1
	fi

	msg "${svc}: ${flag} ${modal} ${text}"
}

# init globals
SSD_HEADER_PRINTED=
declare -a SERVICES=()

if ${O[openrc]}; then
	while (( ${#} )); do
		if openrc-is_service "${1}"; then
			SERVICES+=("${1}")
		else
			msg "${1}: skipped, does not appear to be an OpenRC service" >&2
		fi
		shift
	done
else
	openrc-get_services SERVICES "${1}"
fi

if ${O[perms]}; then
	for service in "${SERVICES[@]}"; do
		openrc-check_permissions "${service}"
	done
fi

if ${O[ssd-args]}; then
	for service in "${SERVICES[@]}"; do
		openrc-check_ssd_args "${service}"
	done
fi

:

# vim: ts=4
