#!/bin/bash
#
# /usr/lib/initscripts/arch-tmpfiles
#
# Control creation, deletion, and cleaning of volatile and temporary files
#

warninvalid() {
	printf "arch-tmpfiles: ignoring invalid entry on line %d of \`%s'\n" "$LINENUM" "$FILE"
	(( ++error ))
} >&2

checkparams() {
	local parmreq=$1; shift
	local path=$1 mode=$2 uid=$3 gid=$4

	# parmcount must be >= parmreq
	if (( $# < parmreq )); then
		return 1
	fi

	# mode must be valid octal and 3 or 4 digits
	if [[ $mode && ! $mode =~ ^[0-7]{3,4}$ ]]; then
		return 1
	fi

	# uid must be numeric or a valid user name
	if [[ $uid && $uid != +([[:digit:]]) ]] && ! getent passwd "$uid" >/dev/null; then
		return 1
	fi

	# gid must be numeric or a valid group name
	if [[ $gid && $gid != +([[:digit:]]) ]] && ! getent group "$gid" >/dev/null; then
		return 1
	fi

	return 0
}

_f() {
	# Create a file if it doesn't exist yet
	local path=$1 mode=$2 uid=$3 gid=$4

	(( CREATE )) || return 0

	if ! checkparams 4 "$@"; then
		warninvalid
		return
	fi

	if [[ ! -e $path ]]; then
		install -m"$mode" -o"$uid" -g"$gid" <(:) "$path"
	fi
}

_F() {
	# Create or truncate a file
	local path=$1 mode=$2 uid=$3 gid=$4

	(( CREATE )) || return 0

	if ! checkparams 4 "$@"; then
		warninvalid
		return
	fi

	install -m"$mode" -o"$uid" -g"$gid" <(:) "$path"
}

_d() {
	# Create a directory if it doesn't exist yet
	local path=$1 mode=$2 uid=$3 gid=$4

	(( CREATE )) || return 0

	if ! checkparams 4 "$@"; then
		warninvalid
		return
	fi

	if [[ ! -d "$path" ]]; then
		install -d -m"$mode" -o"$uid" -g"$gid" "$path"
	fi
}

_D() {
	# Create or empty a directory
	local path=$1 mode=$2 uid=$3 gid=$4

	(( CREATE )) || return 0

	if ! checkparams 4 "$@"; then
		warninvalid
		return
	fi

	if [[ -d $path ]]; then
		find "$path" -mindepth 1 -maxdepth 1 -xdev -print0 | xargs -r0 rm -rf
	fi
	install -d -m"$mode" -o"$uid" -g"$gid" "$path"
}

_p() {
	# Create a named pipe (FIFO) if it doesn't exist yet
	local path=$1 mode=$2 uid=$3 gid=$4

	(( CREATE )) || return 0

	if ! checkparams 4 "$@"; then
		warninvalid
		return
	fi

	if [[ ! -p "$path" ]]; then
		mkfifo -m$mode "$path"
		chown "$uid:$gid" "$path"
	fi
}

_x() {
	# Ignore a path during cleaning. Use this type to exclude paths from clean-up as
	# controlled with the Age parameter. Note that lines of this type do not
	# influence the effect of r or R lines. Lines of this type accept shell-style
	# globs in place of of normal path names.
	:
	# XXX: we don't implement this
}

_r() {
	# Remove a file or directory if it exists. This may not be used to remove
	# non-empty directories, use R for that. Lines of this type accept shell-style
	# globs in place of normal path names.
	local path
	local -a paths=($1)

	(( REMOVE )) || return 0

	if ! checkparams 1 "$@"; then
		warninvalid
		return
	fi

	for path in "${paths[@]}"; do
		if [[ -f $path ]]; then
			rm -f "$path"
		elif [[ -d $path ]]; then
			rmdir "$path"
		fi
	done
}

_R() {
	# Recursively remove a path and all its subdirectories (if it is a directory).
	# Lines of this type accept shell-style globs in place of normal path names.
	local path
	local -a paths=($1)

	(( REMOVE )) || return 0

	if ! checkparams 1 "$@"; then
		warninvalid
		return
	fi

	for path in "${paths[@]}"; do
		[[ -d $path ]] && rm -rf --one-file-system "$path"
	done
}

shopt -s nullglob

declare -i CREATE=0 REMOVE=0 CLEAN=0 error=0 LINENO=0
declare FILE=
declare -A fragments
declare -a tmpfiles_d=(
	/usr/lib/tmpfiles.d/*.conf
	/etc/tmpfiles.d/*.conf
	/run/tmpfiles.d/*.conf
)

while (( $# )); do
	case $1 in
		--create) CREATE=1 ;;
		--remove) REMOVE=1 ;;
	esac
	shift
done

if (( !(CREATE + REMOVE) )); then
	printf 'usage: %s [--create] [--remove]\n' "${0##*/}"
	exit 1
fi

# directories declared later in the tmpfiles_d array will override earlier
# directories, on a per file basis.
# Example: `/etc/tmpfiles.d/foo.conf' supersedes `/usr/lib/tmpfiles.d/foo.conf'.
for path in "${tmpfiles_d[@]}"; do
	[[ -f $path ]] && fragments[${path##*/}]=${path%/*}
done

# catch errors in functions so we can exit with something meaningful
set -E
trap '(( ++error ))' ERR

# loop through the gathered fragments, sorted globally by filename.
# `/run/tmpfiles/foo.conf' will always be read after `/etc/tmpfiles.d/bar.conf'
while read -d '' fragment; do
	LINENUM=0

	printf -v FILE '%s/%s' "${fragments[$fragment]}" "$fragment"

	### FILE FORMAT ###
	# XXX: We ignore the final 'Age' parameter
	# 0    1              2    3    4    5
	# Type Path           Mode UID  GID  Age
	# d    /run/user      0755 root root 10d

	# omit read's -r flag to honor escapes here, so that whitespace can be
	# escaped for paths. We will _not_ honor quoted paths.
	while read -a line; do
		(( ++LINENUM ))

		# skip over comments and empty lines
		if (( ! ${#line[*]} )) || [[ ${line[0]:0:1} = '#' ]]; then
			continue
		fi

		# whine about invalid entries
		if ! type -t _${line[0]} >/dev/null; then
			warninvalid
			continue
		fi

		# fall back on defaults when parameters are passed as '-'
		if [[ ${line[2]} = '-' ]]; then
			case ${line[0]} in
				p|f|F) line[2]=0644 ;;
				d|D) line[2]=0755 ;;
			esac
		fi
		[[ ${line[3]} = '-' ]] && line[3]=0
		[[ ${line[4]} = '-' ]] && line[4]=0

		_${line[0]} "${line[@]:1}"
	done <"$FILE"
done < <(printf '%s\0' "${!fragments[@]}" | sort -z)

exit $error

# vim: set ts=2 sw=2 noet:
