# Copyright (c) 2017 The NetBSD Foundation, Inc.
# All rights reserved.
#
# This code is derived from software contributed to The NetBSD Foundation
# by Johnny C. Lam.
#
# 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 NETBSD FOUNDATION, INC. 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 FOUNDATION 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.
#
# NAME
#	directories.subr -- directory management for packages
#
# SYNOPSIS
#	task_directories [-s] add | remove | perms
#	task_directories check-add | check-remove | check-perms
#
# DESCRIPTION
#	The task_directories function supports six actions: "add", "remove",
#	"perms", "check-add", "check-remove", and "check-perms".
#
#	The available options are as follows:
#
#	-s	Silent; don't write to standard output.
#
#	The task_directories function reads standard input line by line and
#	looks for lines of the form:
#
#	    # DIR: <directory> <flags> [<mode> [<user> [<group>]]]
#
#	If directory path is relative, then it is assumed to be relative to
#	${PKG_PREFIX}.
#
#	The third field in each line is set of flags with the following
#	meaning:
#
#	    f	ignore ${PKG_CONFIG} value
#	    m	directory is created
#	    o	directory is owned by the package
#	    .	placeholder flag to ensure the field is non-empty
#
#	The "add" action creates the directory and sets the permissions on
#	the directory if given.  A reference count for the directory will
#	be added for the package.
#
#	The "remove" action removes the directory.  A reference count for
#	the directory will be removed for the package.
#
#	The "perms" action sets the mode and permissions on the directory
#	if they are given.
#
#	The "check-add" action will check whether the directory exists or
#	otherwise writes a message to standard output noting the missing
#	directory.
#
#	The "check-remove" action will check whether the directory has
#	been removed or otherwise writes a message to standard output
#	noting the directory still exist.
#
#	The "check-perms" action will check whether the directory has the
#	correct permissions or otherwise writes a message to standard
#	output noting the directory has incorrect permissions.
#
# RETURN VALUES
#	The "add", "remove", and "perms" actions return 0 if they are
#	successful for all directories, and >0 if an error occurs.
#
#	The "check-add", "check-remove", and "check-perms" actions return
#	>0 if they write informative messages, and return 0 otherwise.
#
# ENVIRONMENT
#	The following variables are used if they are set:
#
#	PKGNAME
#		The name of the package.
#
#	PKG_CONFIG
#		If ${PKG_CONFIG} is a "truthy" value, then the "add" and
#		"remove" actions are allowed to make changes to the
#		filesystem as needed.
#
#	PKG_CONFIG_PERMS
#		If ${PKG_CONFIG_PERMS} is a "truthy" value, then the
#		"perms" action is allowed to make changes to the
#		filesystem as needed.
#
#	PKG_DESTDIR
#		A "destdir" prefix that is prepended to all filesystem
#		paths.  The default value is the empty string.
#
#	PKG_PREFIX
#		The installation prefix of the package.  The default is
#		"/usr/pkg".
#
#	RMDIR	The name or path to the rmdir(1) utility.
#
#	TASK_MSG
#		String prepended to all normal message written to
#		standard output.
#

__task_directories__="yes"

task_load echo
task_load makedir
task_load permissions
task_load refcount
task_load truthy
task_load valid_options

task_directories()
{
	: ${RMDIR:=rmdir}

	: ${PKG_PREFIX:=/usr/pkg}
	: ${PKGNAME:=${0##*/}}

	: ${PKG_CONFIG:=yes}
	: ${PKG_CONFIG_PERMS:=yes}
	: ${TASK_MSG:=""}

	local arg
	local echo="task_echo"
	local OPTIND=1
	while getopts ":s" arg "$@"; do
		case $arg in
		s)	echo=":" ;;
		*)	return 127 ;;
		esac
	done
	shift $(( ${OPTIND} - 1 ))
	[ $# -gt 0 ] || return 127

	local action="$1"; shift
        case $action in
	add|remove|perms|check-add|check-remove|check-perms)
		: "valid actions" ;;
	*)      return 0 ;;
	esac

	# Guard against ${PKG_PREFIX} == "/".
	local prefix
	case ${PKG_PREFIX}/ in
	//)	prefix= ;;
	*)	prefix=${PKG_PREFIX} ;;
	esac

	local pkg_config="yes"
	local pkg_config_perms="yes"
	task_is_truthy "${PKG_CONFIG}" || pkg_config=
	task_is_truthy "${PKG_CONFIG_PERMS}" || pkg_config_perms=

	local result line_result
	local msg
	local owned makedir removedir changes
	local refcount

	result=0
	local d_path
	local hash tag path flags mode user group
	while read hash tag path flags mode user group; do
		# Filter for "# DIR:".
		case $hash/$tag in
		"#/DIR:")
			: "use this line" ;;
		*)	continue ;;
		esac
		task_valid_options "$flags" "fmo." || continue

		# Canonicalize paths.
		case $path in
		"")	# skip lines without any required args
			continue ;;
		[!/]*)	path="$prefix/$path" ;;
		esac
		d_path="${PKG_DESTDIR}$path"

		msg=
		case $mode/$user/$group in
                //)	msg="$d_path" ;;
		[!/]*//)
			msg="$d_path (m=$mode)" ;;
		[!/]*/[!/]*/)
			msg="$d_path (m=$mode, o=$user)" ;;
		[!/]*/[!/]*/[!/]*)
			msg="$d_path (m=$mode, o=$user, g=$group)" ;;
		esac

		# Set owner values if the directory should be owned by this package.
		owned=
		dir_msg="directory"
		case $flags in
		*o*)	owned="yes"
			dir_msg="owned directory"
			if task_refcount prop_exists dirs "$path" owner; then
				# previous owner detected
				if task_refcount prop_match dirs "$path" owner; then
					: "same owner"
				else
					# conflicting owners; failure event
					$echo "${TASK_MSG}! directory owner conflict: $d_path"
					result=1
					continue
				fi
			fi ;;
		esac

		makedir=
		case $flags in
		*m*)	makedir="seen"
			case $flags in
			*f*)	# "f" always implies makedir.
				makedir="yes" ;;
			*)	# otherwise, makedir only if PKG_CONFIG is truthy.
				[ -z "$pkg_config" ] || makedir="yes" ;;
			esac ;;
		esac

		removedir=
		case $flags in
		*)	removedir="seen"
			case $flags in
			*f*)	# "f" always implies removedir.
				removedir="yes" ;;
			*)	# otherwise, removedir only if PKG_CONFIG is truthy.
				[ -z "$pkg_config" ] || removedir="yes" ;;
			esac ;;
		esac

		line_result=0
		changes=
		case $action in
		add)	refcount="yes"
			task_refcount exists dirs "$path" || refcount=
			if task_refcount add dirs "$path"; then
				if [ -n "$owned" ]; then
					# Directory is owned by this package.
					if task_refcount prop_put dirs "$path" owner; then
						: "directory owner property added"
					else
						line_result=1
					fi
				fi
				if [ $line_result -gt 0 ]; then
					: "skip directory creation"
				elif [ -d "$d_path" ]; then
					# Directory already exists.
					if [ -z "$refcount" ]; then
						task_refcount prop_put dirs "$path" preexist || line_result=1
					fi
					# Only warn if directory exists if it should be owned.
					if [ -n "$owned" ]; then
						$echo "${TASK_MSG}! $dir_msg already exists: $d_path"
					fi
				elif [ -z "$makedir" ]; then
					: "directory is never created"
				elif [ "$makedir" = "seen" ]; then
					$echo "${TASK_MSG}! $dir_msg creation skipped: $d_path"
				elif task_makedir "$d_path"; then
					# Defer writing output until permissions are set.
					#$echo "${TASK_MSG}> $dir_msg created: $d_path"
					changes="$changes create"
				else
					$echo "${TASK_MSG}! $dir_msg not created: $d_path"
					line_result=1
				fi
			else
				# add refcount failed; skip to next line
				$echo "${TASK_MSG}! refcount add failure: directories $path"
				result=1
				continue
			fi ;;
		check-add)
			if [ -d "$d_path" ]; then
				: "directory already exists"
			elif [ -n "$makedir" ]; then
				task_echo "!!! INFO: ${PKGNAME}: Create directory: $msg"
				line_result=1
			fi ;;
		esac
		if [ $line_result -eq 0 ]; then
			case $mode/$user/$group in
			//)	case " $changes " in
				*" create "*)
					$echo "${TASK_MSG}> $dir_msg created: $d_path" ;;
				*)	: "no permissions to set" ;;
				esac ;;
			*)	case $action in
				add|perms)
					task_refcount prop_put dirs "$path" permissions "$mode" "$user" "$group" || line_result=1
					if [ ! -d "$d_path" ]; then
						$echo "${TASK_MSG}! $dir_msg permissions not set on missing: $msg"
						if [ "$makedir" = "yes" ]; then
							# The directory should have been created; otherwise,
							# it's an error.
							#
							line_result=1
						fi
					elif [ "$action" = "perms" -a -z "$pkg_config_perms" ]; then
						# "perms" action, but PKG_CONFIG_PERMS is not trutyy.
						$echo "${TASK_MSG}! $dir_msg permissions skipped: $msg"
					elif task_set_permissions "$d_path" "$mode" "$user" "$group"; then
						case " $changes " in
						*" create "*)
							$echo "${TASK_MSG}> $dir_msg created: $msg" ;;
						*)	$echo "${TASK_MSG}> $dir_msg permissions set: $msg" ;;
						esac
					else
						case " $changes " in
						*" create "*)
							$echo "${TASK_MSG}> $dir_msg created: $d_path" ;;
						*)	: "no directory was created" ;;
						esac
						$echo "${TASK_MSG}! $dir_msg permissions not set: $msg"
						line_result=1
					fi ;;
				check-add|check-perms)
					if [ -d "$d_path" ] &&
					   task_check_permissions "$d_path" "$mode" "$user" "$group"; then
					   	: "directory has correct permissions"
					else
					   	task_echo "!!! INFO: ${PKGNAME}: Set directory permissions: $msg"
						line_result=1
					fi
				esac ;;
			esac
		fi
		if [ $line_result -eq 0 ]; then
			case $action in
			remove)	if task_refcount remove dirs "$path"; then
					if task_refcount exists dirs "$path"; then
						# refcount is not zero
						if [ -n "$owned" ]; then
							# owned directory has existing references; failure event.
							$echo "${TASK_MSG}! directory owned but references exist: $path"
							line_result=1
						fi
					else
						# no more references
						if task_refcount prop_exists dirs "$path" preexist; then
							: "directory is preexisting"
						elif [ ! -d "$d_path" ]; then
							: "directory already removed"
						elif [ "$removedir" = "yes" ]; then
							${RMDIR} -p "$d_path" 2>/dev/null
						fi
						# Only warn if the directory is owned by the package.
						if [ -n "$owned" ]; then
							if [ -d "$d_path" ]; then
								$echo "${TASK_MSG}! $dir_msg not removed: $d_path"
							else
								$echo "${TASK_MSG}> $dir_msg removed: $d_path"
							fi
						fi
						# delete the reference count
						task_refcount delete dirs "$path"
					fi
				else
					# remove refcount failed
					$echo "${TASK_MSG}! refcount remove failure: directories $path"
					line_result=1
				fi ;;
			check-remove)
				if task_refcount exists dirs "$path"; then
					: "refcount is not zero"
				elif [ ! -d "$d_path" ]; then
					: "directory already removed"
				elif [ -n "$removedir" -a -n "$owned" ]; then
					# Only warn if the directory is owned by the package.
					task_echo "!!! INFO: ${PKGNAME}: Remove $dir_msg: $d_path"
					line_result=1
				fi
			esac
		fi
		if [ $line_result -gt 0 ]; then
			# Undo changes if there was an error.
			case " $changes " in
			*" create "*)
				${RMDIR} -p "$d_path" 2>/dev/null ;;
			esac
		fi
		[ $line_result -eq 0 ] || result=1
	done
	return $result
}
