# this file is sourced not run
#!/bin/bash

# fatdog-sync-mime-lib.sh  - Copyright (C) step 2022
# fatdog-sync-mime.sh      - Copyright (C) James Budiono 2016
# License: GNU GPL Version 3 or later

# ------------------------------------------------------------------------------
# Editing this file: please set your tabstop = 8 and indent lines with TABS.
# Read all at the end of this file.
# ------------------------------------------------------------------------------

(( ${BASH_VERSINFO[0]} == 4 && ${BASH_VERSINFO[1]} >= 3 || ${BASH_VERSINFO[0]} >= 5 )) || {
	echo >&2 "bash version >= 4.3 required"; return 1
}

# ---------------------------------------------------------------------------{{{
# This is where we initialize all important variables.
# option -p ==> mkdir some paths for fatdog-sync-mime-make-desktop-files.sh
# ------------------------------------------------------------------------------
# $1-work_tree $2-work_plan $3-source_regex
# ---------------------------------------------------------------------------}}}
set_vars() {
	local p
	ROOT=
	[ "$1" ] && ROOT="$(realpath "$1")"
	case "$2" in # core PLAN
		'all'|'') PLAN="mimetypes+mailcap+mimeapps" ;;
		*) PLAN="$2" ;;
	esac

	SN="${0##*/}"
	TMP="/tmp/${SN%.*}.$$"
	case "$TABLE_OUT" in
		''  ) TABLE_OUT='/dev/null'   ;;
		'-' ) TABLE_OUT='/dev/stdout' ;;
	esac
	SYSTEM_ONLY=${SYSTEM_ONLY:-0}
	USER_ONLY=${USER_ONLY:-0}
	WITH_AUDIT=${WITH_AUDIT:-0}

	XDG_DATA_DIRS="${XDG_DATA_DIRS:-/usr/share:/usr/local/share}"
	IFS=: read -a DATA_DIRS <<< "$XDG_DATA_DIRS"
	DATA_DIRS=( "${DATA_DIRS[@]/#/"$ROOT"}" )

	MIMETYPES="$ROOT/etc/mime.types"
	DEFAULTS_LIST='defaults.list'
	MIMEAPPS_LIST='mimeapps.list'
	MAILCAP="$ROOT/etc/mailcap"
	ROX_HANDLER="$ROOT/etc/xdg/rox.sourceforge.net"

	[[ -z "$HOME" || "$HOME" != *[\!/]* ]] && HOME='/root' # remap HOME=/
	LOCAL_GLOBS2="$ROOT${XDG_DATA_HOME:-$HOME/.local/share}/mime/globs2"
	LOCAL_MIMETYPES="$ROOT$HOME/.mime.types"
	LOCAL_MIMEAPPS_LIST="$ROOT${XDG_CONFIG_HOME:-$HOME/.config}/mimeapps.list"
	LOCAL_MAILCAP="$ROOT$HOME/.mailcap"
	LOCAL_DESKTOP_FILE_DIR="$ROOT${XDG_DATA_HOME:-$HOME/.local/share}/applications"
	LOCAL_ROX_HANDLER="$ROOT${XDG_CONFIG_HOME:-$HOME/.config}/rox.sourceforge.net"

	### For fatdog-sync-mime-make-desktop-files.sh
	# Create new .desktop files for matching authorized sources that
	# aren't already associated with a .desktop file; create an extra
	# mimeapps.list file alongside the new .desktop files
	###
if [[ "$SN" == *'make-desktop-files'* ]]; then
	case "$2" in
		'dry-run'|'desktopfiles'|'mimeapps') EXTRA_DESKTOP_PLAN="$2" ;;
		*) EXTRA_DESKTOP_PLAN="desktopfiles+mimeapps" ;;
	esac
	# Only create files for handlers that belong to these sources.
	EXTRA_DESKTOP_INCLUDE="${3:-"MIME-types+SendTo+URI+mailcap"}"
	# Locate new files in {EXTRA,LOCAL}_DESKTOP_FILE_DIR/EXTRA_DESKTOP_FOLDER
	EXTRA_DESKTOP_FILE_DIR="${DATA_DIRS[0]}/applications"
	EXTRA_DESKTOP_FOLDER="fatdog-sync-mime"
	# Additional mimeapps.list for the above
	EXTRA_DESKTOP_MIMEAPPS_LIST="${XDG_CURRENT_DESKTOP:-rox}" # [bib3.1]
	EXTRA_DESKTOP_MIMEAPPS_LIST="${EXTRA_DESKTOP_MIMEAPPS_LIST%%:*}-mimeapps.list"
	# Fixed 'Categories='
	EXTRA_DESKTOP_CATEGORY='X-fatdog-sync-mime'
	# Fallback 'Icon='
	EXTRA_DESKTOP_ICON="/usr/share/icons/fatdog32.png"
fi

	for (( i = ${#DATA_DIRS[*]} - 1; i >= 0; i-- )); do
		[ -e "${DATA_DIRS["$i"]}" ] || unset DATA_DIRS["$i"]
	done

	if ((!${#DATA_DIRS[*]})); then
		warn "UNRECOVERABLE: no DATA_DIRS found under '%s'" "${ROOT:-/}"
		exit 1
	fi

	### defaultprograms
	# Get a relative order over the set of defaultprograms; in the pairs
	# below define a partial order - left item precedes right item.  Item
	# is both the binary name _and_ the desktop file name without extension.
	# Fatdog's 'default-browser' is included deliberately even through this
	# script never treats it as a defaultprogram - because it isn't.
	declare -g -A DEFAULTPROGRAM_ORDER
	get_programs_relative_order DEFAULTPROGRAM_ORDER \
		"${DATA_DIRS[0]}/applications/default" \
		"$ROOT/usr/local/bin/default" "\
defaulttexteditor defaultbrowser
defaultbrowser default-browser
defaultspreadsheet defaultbrowser
defaultwordprocessor defaultbrowser"
}

# ---------------------------------------------------------------------------{{{
# Return a partial order over a set of programs and their desktop files.[A]
# Use the associative array to lookup relative sort order between pairs of
# programs or pairs of their desktop files.[B]
# ------------------------------------------------------------------------------
# $1-varname_order_map - reference to an associative array that returns the
# partial order. Array keys are integers (`index`, 0 mean "precedes"), file
# names without extension and full pathnames. Array values are the same array
# keys so as to form an overall map that can be looked up by index or by file
# name or by full pathname. Visualize with the help of [D].  Rankings are
# repeated with and without a leading plus sign '+'. When an array key starts
# with '+' its element's value is a file name without extension otherwise it's
# a full pathname, therefore wrap the key like `$(($key))` in conditional
# expressions and quote correctly when indexing, e.g., `item="${ary["$key"]}"`.
# $ROOT, if any, is chopped from full pathname keys and values.
# $2-desktop_file_stem - pathname + name prefix that generates the set of
#    desktop files.  It's globbed as "$desktop_file_stem"*'.desktop'.
#    It is recommended to root the stem under $ROOT.
# $3-bin_file_stem - pathname + name prefix that generates the set of
#    binary files.  It's globbed as "$bin_file_stem"*.
#    It is recommended to root the stem under $ROOT.
# $4-input_pairs - multi-line string (no empty lines); each line holds a pair
#    of file names without extension but including the stem's file name part.
#    White space separates the pair's items. The left item will get a lower
#    output index not necessarily contiguous to the right item's index.
# See also tsort(1), DEFAULTPROGRAM_ORDER.
# ------------------------------------------------------------------------------
# [A] To simplify usage a single array is used to hold both partially ordered sets.  This works well as long as as the application knows to look up one set at the time, which this script does.
# ------------------------------------------------------------------------------
# [B] See actual usage in update_mimeapps_list (for desktop file paths) and
# update_mailcap (for program paths). The functions that are directly involved
# with the output order map are add_to_sorted_entry and expand_sorted_entry.
# ------------------------------------------------------------------------------
# This mechanism could be extended to other sets. Here an input set is hardcoded
# to be the programs that match '/usr/local/bin/default'*; another input set
# is the desktop files that match '/usr/share/applications/default'*'.desktop'.
# ---------------------------------------------------------------------------}}}
get_programs_relative_order () {
	local -n varname_order_map="$1"; varname_order_map=()
	local desktop_file_stem="$2" bin_file_stem="$3" input_pairs="$4" p
	local -i index=0 ong
	local -a _a _b

	### get paths of default programs and their .desktop files

	shopt -q nullglob; ((ong=!$?)) || shopt -s nullglob # enable
	_a=( "$desktop_file_stem"*'.desktop' )
	_b=( "$bin_file_stem"* )
	((ong)) || shopt -u nullglob # restore initial state

	### express paths as partially ordered pairs

	# normalize each path to basename no extension
	_a=( "${_a[@]##*/}" )       ; _b=( "${_b[@]##*/}" )
	_a=( "${_a[@]%.*}" )        ; _b=( "${_b[@]%.*}" )
	# transform to partial order pairs <[ab]'_'path, 'last'>
	_a=( "${_a[@]/#/"a_"}" )    ; _b=( "${_b[@]/#/"b_"}" )
	_a=( "${_a[@]/%/" last"}" ) ; _b=( "${_b[@]/%/" last"}" )

	### add input pairs

	while read -r p; do
		_a+=( "a_${p% *} a_${p#* }" )
		_b+=( "b_${p% *} b_${p#* }" )
	done <<< "$input_pairs"
	# one pair per line
	_a=( "${_a[@]/#/$'\n'}" )  ; _b=( "${_b[@]/#/$'\n'}" )

	### set output array from topological sort

	while IFS= read -r p; do
		case "$p" in
			'a_'* ) p="${desktop_file_stem%/*}/${p#??}.desktop" ;;
			'b_'* ) p="${bin_file_stem%/*}/${p#??}" ;;
			'last') break ;;
		esac
		varname_order_map["$index"]="${p#$ROOT}"
		varname_order_map["${p#$ROOT}"]="$index" # $p absolute path
		varname_order_map["+$index"]="${p##*/}"  # '+' marks basename
		varname_order_map["${p##*/}"]="+$index"
		: $((index++))

	done < <(tsort <<< "${_a[@]}${_b[@]}")

	#declare -p DEFAULTPROGRAM_ORDER | sed 's/[[]/\n&/g' | sort >&2 # [D]
}

# ---------------------------------------------------------------------------{{{
# out: $REALPATH
# GNU multi-argument `realpath -e` otherwise busybox `realpath`, which implies
# `-e`.  Crucially we use realpath in lieu of readlink because realpath is
# vocal - on stderr - about missing targets.
# ---------------------------------------------------------------------------}}}
find_best_realpath() {
	type realpath > /dev/null && REALPATH="realpath -e"
	! $REALPATH / > /dev/null 2>&1 && REALPATH="busybox realpath"
}

# ---------------------------------------------------------------------------{{{
# This function outputs both the realpath _and_ the first symlink target of
# input pathnames. It was used to find the default<X> that symlinks to
# /usr/local/bin/defaultprogram but get_real_or_default() took its place.
# ------------------------------------------------------------------------------
# Input: null-separated ('\0') list of pathnames
# ------------------------------------------------------------------------------
# Output: TABLE@2 `rp` `l1`
# `rp` $REALPATH of input pathname
# `l1` target of input pathname if pathname is a symlink otherwise pathname
# Errors:
# if the final target is missing then `rp` starts with 'realpath: '.
# if the first target is missing then `l1` starts with 'stat: '.
# ------------------------------------------------------------------------------
# This function is better used as a process because it redirects fds 3 and 4.
# ---------------------------------------------------------------------------}}}
tbl2_realpath_link1 () {
	set -- '' '' "$TMP-$FUNCNAME"{3,4}
	[ -e "$3" -a -e "$4" ] || mkfifo "$3" "$4"
	# paste the output of the process substitutions below 'tee'
	while IFS= read -r -u 3 rp; do
		IFS= read -r -u 4 l1
		# keep only the link target from 'stat'
		l1="${l1#*"' -> "}"
		l1="${l1#"'"}"
		l1="${l1%"'"}"

		printf "%s\037%s\n" "$rp" "$l1"
	done 3< "$3" 4<> "$4" &
	tee \
		>(xargs -0 $REALPATH > "$3" 2>&1) |
			xargs -0 stat -c '%N' > "$4" 2>&1
	wait
	rm -f "$3" "$4"
}

# ---------------------------------------------------------------------------{{{
# See tbl2_realpath_link1 for background information.
# Print either the realpath or the default<X> program's path of input pathnames.
# ------------------------------------------------------------------------------
# Input: null-separated ('\0') list of pathnames
# ------------------------------------------------------------------------------
# Output: TABLE@2 `rp` `l1`
# `rp` $REALPATH or default<X>'s path of input pathname where
#      '/usr/local/bin/default'<X> -> 'defaultprogram'
# Errors:
# if the final target is missing then `rp` starts with 'realpath: '.
# ------------------------------------------------------------------------------
# [A] LIMITATION: default<X> can only be determined for a two-hop link chain:
# input_pathname -> /usr/local/bin/default<X> -> defaultprogram.
# This limitation could be removed by using this gist but it seems overkill
# https://gist.github.com/step-/b0c3a34eb29e42940be8f6da07b1065f
# ------------------------------------------------------------------------------
# This function is better used as a process because it redirects fds 3 and 4.
# ---------------------------------------------------------------------------}}}
get_real_or_default () {
	set -- '' '' "$TMP-$FUNCNAME"{3,4}
	[ -e "$3" -a -e "$4" ] || mkfifo "$3" "$4"

	# merge the output of the process substitutions below 'tee'
	while IFS= read -r -u 3 rp; do
		IFS= read -r -u 4 l1

		# keep only the link target from 'stat' [A]
		if [[ "$rp" == *'defaultprogram' ]]; then
			l1="${l1#*"' -> "}"
			l1="${l1#"'"}"
			rp="${l1%"'"}"
		fi
		printf "%s\n" "$rp"
	done 3< "$3" 4<> "$4" &
	tee \
		>(xargs -0 $REALPATH > "$3" 2>&1) |
			xargs -0 stat -c '%N' > "$4" 2>&1
	wait
}

# ---------------------------------------------------------------------------{{{
# Output: a TABLE@8 row in which cell 1 is a \034-separated record of the call
# positional parameters, and the remaining columns are empty.
# ---------------------------------------------------------------------------}}}
tbl8_new_section() { # $@
	printf '###\034%s' "$1"; shift
	(($#)) && printf '\034%s' "$@"
	printf '\037\037\037\037\037\037\037\n'
	# TABLE@8
}

warn() { # $1-message | $1-printf_format $2@-arguments
	local m="$1"; shift
	cprint 31 44 "$SN: WARNING: $m" "$@" >&2
}

hint() { # $1-message | $1-printf_format $2@-arguments
	local m="$1"; shift
	cprint 31 44 "hint: $m" "$@" >&2
}

cprint() { # [fg_color] [bg_color] $1-message_or_printf_format [$2@-printf_arguments]
	local fg bg m
	case "$1" in [0-9]|[0-9][0-9]) fg="$1"; shift ;; esac
	case "$1" in [0-9]|[0-9][0-9]) bg="$1"; shift ;; esac
	[ $# -gt 1 ] && printf -v m "$@" || m="$1"
	if [ -n "$bg" ]; then
		printf "\033[0;${fg}m\033[${bg}m%s\033[0m\n" "$m"
	else
		printf "\033[${fg:-32}m%s\033[0m\n" "$m"
	fi
}

# ---------------------------------------------------------------------------{{{
# This function rewrites file mime.types[bib4] by merging <mime-type,file-ext>
# pairs read from $1-mime_types and shared-mime's $globs2. When a conflict
# occurs, globs2's pair prevails because we deem shared-mime the authoritative
# source.
# ------------------------------------------------------------------------------
# $1-mime_types $2-globs2 $3-sort_order ('asc'|'desc')
# ------------------------------------------------------------------------------
# Output: none
# ---------------------------------------------------------------------------}}}
# Sets bash option 'pipefail'
# ---------------------------------------------------------------------------}}}
update_mime_types() {
# [A] $globs2 is a sorted desc TABLE@3(sep=':') `priority` `mt` `ext`
# [A] `ext`, a file extension, can be a glob: we cajole globs .* and *. globs,
# and drop other globs.
#     [A1] e.g. `10:text/x-readme:readme*`.
#     [A2] e.g. `10:text/x-makefile:makefile.*`.
#     [A3] e.g. other globs: $mime_types doesn't support globs.
# `priority` is an integer >= 0 (0 for symbol __NO_GLOBS__, which corresponds
# to XML tag <glob-deleteall/>)
# [B] $mime_types is a variable-length record `mime-type` [ `ext`...]: we
# reshape it to long format as a TABLE@3(sep=':') where <1, `mt`(i), `ext`(i,j)>
# such that `mt`(i) for each `ext`(i,j) including empty `ext`.
# [C] merge [A],[B] in glob2 format; [A] wins conflicts because it precedes [B].

	local mime_types="${1:?}" globs2="${2:?}" pri mt ext exts
	local -A e p
	[[ "$PLAN" == *'mimetypes'* ]] || return
	cprint "Updating $mime_types" >&2
	((WITH_AUDIT)) && [ -e "$mime_types" ] && wc "$mime_types" >&2

	set pipefail
	{
	if [ -e  "$globs2" ]; then # [A]
		while IFS=':' read pri mt ext || [ -n "$pri" ]; do # [A]
			[[ "$pri" == '#'* ]] && continue
			ext="${ext/'*.'}";
			[[ "$ext" == *[!.]'*' ]] && ext="${ext%'*'}" # [A1]
			ext="${ext%'.*'}" # [A2]
			[[ "$ext" == *[][?*]* ]] && continue # [A3]
			echo "$pri:$mt:$ext"
		done < "$globs2"
	fi
	if [ -e "$mime_types" ]; then # [B]
		while read mt exts || [ -n "$mt" ]; do
			[[ -n "$mt" && "$mt" != '#'* ]] &&
				printf "1:${mt//%/%%}:%s\n" $exts
		done < "$mime_types"
	fi
	} | {
		while IFS=':' read pri mt ext; do # [C]
			if [ -z "${p["$mt:$ext"]}" ]; then
				p["$mt:$ext"]="$pri"
				e["$pri:$mt"]+="${ext:+ }$ext"
			fi
		done

		for ext in ${!e[*]}; do
			printf "%s%s\n" "$ext" "${e["$ext"]}"
		done |
			# _stable_ sort by `priority` desc
			sort -t ':' -s -nr -k1,1 | cut -d ':' -f2-
	} > "$TMP-$FUNCNAME"
	if [ $? -eq 0 -a -s "$TMP-$FUNCNAME" ]; then
		mv "$TMP-$FUNCNAME" "$mime_types" &&
		cprint 42 30 "Updated $mime_types" >&2
		((WITH_AUDIT)) && wc "$mime_types" >&2
	fi
}

### Rox-specific functions START

# ---------------------------------------------------------------------------{{{
# This function visits mime-related subfolders of $1 and outputs a combined
# TABLE@8 by calling tbl8_rox_sub_table for each visited folder.  The combined
# TABLE@8 must include section separators for further Fatdog mime-processing
# tools.  The folders to look into, if existing are authorized mimetype
# definition sources {'MIME-types','SendTo','URI'}.
# ------------------------------------------------------------------------------
# [A] Inject hardcoded mimetype-handler associations for which no physical
# entry exists in Rox's 'MIME-types folder'. Don't abuse. Pass associations
# via environment variable HARDCODED; separate associations with \n'. Format:
#   LABEL':'MIME-TYPE':'HANDLER [\n]
# Example 'rox:inode_directory:/usr/bin/rox -d'    MIND '_' instead of '/'
# NOTE: before a recent xdg-open can use this handler a .desktop file must be
# created. Without the .desktop file only mailcap gets the hardcoded handler.
# ------------------------------------------------------------------------------
# $1-rox_config_dir
# ------------------------------------------------------------------------------
# Output: see tbl8_rox_sub_table
# ---------------------------------------------------------------------------}}}
tbl8_rox_dir_table() {
	local rox_config_dir="$1" fo
	local -r from='mime association from'
	local p h mime key src
	local -a hc
	local -i i

for fo in 'MIME-types' 'URI' 'SendTo'; do
	if [ -e "$rox_config_dir/$fo" ]; then
		tbl8_new_section "$from" "$rox_config_dir/$fo"
		tbl8_rox_sub_table "$rox_config_dir/$fo"
	fi

	# [A] hardcoded handlers
	if [ "$fo" = 'MIME-types' -a -n "$HARDCODED" ]; then
		tbl8_new_section "$from" "hardcoded $rox_config_dir/$fo"
		IFS=$'\n' read -a hc <<< "$HARDCODED"
		# forge a tbl8_rox_sub_table row: fo mt hp hn rp an na ex
		for (( i = 0; i < ${#hc[*]}; i++ )); do
			IFS=':' read p mime h <<< "${hc["$i"]}"
			printf "%s\037%s\037%s\037%s\037%s\037%s\037%s\037%s\n" \
				"$fo" "${mime/_/\/}" "$fo/$mime" "$p" "$h" '' '' ''
		done
	fi
done
}

# ---------------------------------------------------------------------------{{{
# This BASH 4.3 function recursively prints non-directory leaves including the
# 'AppRun' file of a Rox AppDir without all the rest.[A]
# 'SendTo/'{'.all','.group'}'/' is pruned to discard Jun7's extensions 2b) 2c).
# ------------------------------------------------------------------------------
# $1-look_in_path must end with {'MIME-types','SendTo','URI'}.
# ------------------------------------------------------------------------------
# Output: a TABLE@1 of PATHNAMEs:
# pathname: starts with {'MIME-types','SendTo','URI'}'/'.
# Do not assume the existence of a physical file for pathname.
# ------------------------------------------------------------------------------
# BLACK-LISTING SOME PROGRAMS
# To blacklist a specific program from mimecaps _and_ mimeapps.list, add the
# file or folder name of the program to the prune_name array in the `case`
# statement below.
# An alternative method is also possible. Pros: no need to modify this script
# because changes are only applied to .desktop files. Cons: the program can be
# black-listed from mimeapps.list only.  The method illustrated by example:
# replace `Exec=/usr/local/apps/UExtract/AppRun`   with
#         `Exec=/usr/local/apps/UExtract/./AppRun` in
# /usr/share/applications/UExtract.desktop. This method works because the
# modified path makes the UExtract.desktop "magically disapper" when
# update_mimeapps_list looks for an existing desktop file associated with a
# given mimetype.
# ---------------------------------------------------------------------------}}}
find_rox_handlers () {
	local look_in_dn="${1%/*}" bn="${1##*/}" p i start_with
	local -a apprun prune prune_name
	local -i ogs odg
	case "$bn" in
		'MIME-types' | 'URI' )
			start_with="$bn/[[:alnum:]]*"
			prune_name=() ;;
		'SendTo' )
			start_with="$bn/.[[:alnum:]]*"
			prune_name=(".all" ".group" ".DirIcon") ;;
		* ) return 1 ;;
	esac

	# assemble pruning expressions for find(1)
	for p in "${prune_name[@]}"; do
		prune+=('-name' "$p" '-prune' '-o')
	done

[ -d "$look_in_dn/$bn" ] && cd "$look_in_dn" || return 1
	shopt -q globstar; ((ogs=!$?)) || shopt -s globstar # enable, BASH >= 4.3
	shopt -q dotglob;  ((odg=!$?)) || shopt -s dotglob  # enable
	apprun=("$bn/"**"/AppRun")
	for i in ${!apprun[*]}; do
		p="${apprun[$i]%"/AppRun"}"
		if [[ "$p" == *"/**" ]]; then
			unset "apprun[$i]"; continue
		else
			prune+=('-path' "$p" '-prune' '-o') # [A]
		fi
	done
	((ogs)) || shopt -u globstar # restore initial state
	((odg)) || shopt -u dotglob  # restore initial state

	### print leaves while pruning AppDirs with find(1)
	command find "$bn" "${prune[@]}" ! -type d -path "$start_with" -print

	### AppDirs' output turn now; first prune specific names
	for i in ${!apprun[*]}; do
		for p in "${prune_name[@]}"; do
			if [[ "${apprun[$i]}" == *"/$p/"* ]]; then
				unset "apprun[$i]"; break
			fi
		done
	done
	# then print AppRun paths - 1001 of them in Fatdog-812!
	((${#apprun[*]})) && printf "%s\n" "${apprun[@]}"
cd - > /dev/null
}

# ---------------------------------------------------------------------------{{{
# This BASH 4.0 function recursively searches for Rox mimetypes under $1.  It
# outputs the mimetype and the relative and real paths of the mimetype handler,
# and further information when the handler is a Rox AppDir or a .desktop file.
# ------------------------------------------------------------------------------
# $1-look_in_path must end with a folder, either {'MIME-types','SendTo','URI'}.
# ------------------------------------------------------------------------------
# Errors: read [B] below.
# ------------------------------------------------------------------------------
# Output: TABLE@8 columns: fo mt hp hn rp an na ex
# fo: FOLDER is the basename of $1, either {'MIME-types','SendTo','URI'}.
# mt: MIMETYPE is PART1'/'PART2 - see [C] below.
# hp: HANDLER_PATH starts with FOLDER'/'.
# hn: HANDLER_NAME is the basename of HANDLER_PATH.
# rp: HANDLER_REAL_PATH is a canonical physical path obtained from realpath(1).
# an: APPNAME is a Rox AppDir's basename when HANDLER_PATH is an AppDir/AppRun.
# na: Value of the 'Name' key if HANDLER_REAL_PATH is a .desktop file.
# ex: Value of the 'Exec' key if HANDLER_REAL_PATH is a .desktop file.
# Trailing "Exec %variables" are deleted from ex if possible.
# ------------------------------------------------------------------------------
# [A] The regex below matches $1=folder, $2=mimetype, $3=next_path_component.
# The combined `if` condition excludes files like this:
#   - mimetype can't be multi-word e.g. 'Send to Trash'
#   - SendTo mimetype can't be followed by 'AppRun' e.g. 'PackIt/AppRun',
#     'UExtract/AppRun' (used to make AppDirs show up as menu entries for all
#     files - we discard it for the same reason we discard Jun7's '.all', that
#     is, there is no valid media type for "all").
# - the '.' in '/.'? applies to folder 'SendTo' only
# [B] A symlink to a directory needs a second lookup before it can be used,
# e.g. SendTo/.application_x-pdf -> .application_pdf, which is the root of a
# right-click menu tree, needs to be expanded into its individual menu entries
# (files). We do this using the '**' glob instead of a more costly `find`.
# [C] If HANDLER_REAL_PATH starts with "realpath: " then HANDLER_PATH is a
# dangling link. We output a warning and carry over both paths under the
# principle that the user is in charge of fixing configuration errors.
# However, we prefix the HANDLER_REAL_PATH with 'NA://' (Not Available) for
# easier recognition in pipelines involving this function's output.
# [D] Rox formats the mimetype as PART1 for "URI" and PART1["_"PART2] for
# 'MIME-type' and 'SendTo'.  We normalize output MIMETYPE as PART1'/'PART2.
# ---------------------------------------------------------------------------}}}
tbl8_rox_sub_table () {
	local -r look_in_dn="${1%/*}" bn="${1##*/}" # dirname basename
	local -a amt ahp a__ # a__ is an empty column helper
	local hp rp
	local -i ogs
	shopt -q globstar; ((ogs=!$?)) || shopt -s globstar # enable, BASH >= 4.3
	while IFS= read -r hp; do
		# [A]
		if [[ "$hp" =~ ^([^/]+)"/."?([^/*[:blank:]]+)("/"[^/]+|$) ]] &&
			[[ "${bn}${BASH_REMATCH[3]}" != 'SendTo/AppRun' ]]
		then
			rp="$look_in_dn/$hp"
			if [[ -L "$rp" && -d "$rp" ]]; then
				# [B]
				for rp in "$rp"/**; do
					if [[ -L "$rp" && -d "$rp" ]]; then
						warn "symlink to symlink to directory isn't supported (source '%s')" "$rp"
						((WITH_AUDIT)) && hint 'do without this symlink' # lame
					elif [ ! -d "$rp" ]; then
						ahp+=("${rp#"$look_in_dn/"}")
						amt+=("${BASH_REMATCH[2]}")
						a__+=('')
					fi
				done
			else
				ahp+=("$hp")
				amt+=("${BASH_REMATCH[2]}")
				a__+=('')
			fi
		# else cprint 31 "DISCARD($hp) bn($bn) 2(${BASH_REMATCH[2]}) 3(${BASH_REMATCH[3]})" >&2
		fi
	done < <(find_rox_handlers "$look_in_dn/$bn") # BASH 4.3
	((ogs)) || shopt -u globstar # restore initial state
	((${#ahp[*]})) || return

	# paste 8 columns together: fo mt hp hn rp an na ex
	paste -d $'\037' \
		<(printf '%s\n' "${a__[@]/#/"$bn"}") \
		<(printf '%s\n' "${amt[@]}") \
		<(printf '%s\n' "${ahp[@]}") \
		\
		<(tbl5_desktop_name_exec < <(paste -d $'\037' \
			<(printf '%s\n' "${ahp[@]##*/}") \
			<(printf '%s\0' "${ahp[@]/#/"$look_in_dn/"}" | get_real_or_default) \
			<(printf '%s\n' "${a__[@]}") \
			<(printf '%s\n' "${a__[@]}") \
			<(printf '%s\n' "${a__[@]}") )
			#          hn rp an na ex
		) |	# fo mt hp hn rp an na ex

		# transform rows:
		while IFS=$'\037' read -r fo mt hp hn rp an na ex; do
			# [C]
			if [[ "$rp" == 'realpath: '* && -n "$hp" ]]; then
				warn "$LINENO: $rp"
				rp="NA://$look_in_dn/$hp"
			fi
			# [D]
			if [[ "$mt" != *'_'* ]]; then
				[ "$fo" = 'URI' ] && mt="${hp/$fo/'x-scheme-handler'}" || mt+='/*'
			fi
			mt="${mt/_//}"

			[[ "$hp" == *'/AppRun' ]] && an="${hp%'/AppRun'}" && an="${an##*/}" || an=

			#echo "fo($fo) mt($mt) hp($hp) hn($hn) rp($rp) an($an) na($na) ex($ex)" >&2
			printf  "%s\037%s\037%s\037%s\037%s\037%s\037%s\037%s\n" \
				"$fo" "$mt" "$hp" "$hn" "$rp" "$an" "$na" "$ex"
		done
}

### Rox-specific functions END

### Mailcap-specific functions

# ---------------------------------------------------------------------------{{{
# This function reads the mailcap file $1 and outputs a TABLE@8 that is
# consistent with tbl8_rox_sub_table's output, and can be used by other
# Fatdog mime-processing tools.
# ------------------------------------------------------------------------------
# Input: $1-mailcap_file
# According to man(5) mailcap, a typical mailcap entry looks like this:
# text/plain; cat %s; copiousoutput
# ------------------------------------------------------------------------------
# [A] Since mime handler is a command in the sense of the system(3) call, its
# pathname is resolved similarly to tbl5_desktop_name_exec[D].
# ------------------------------------------------------------------------------
# Output: TABLE@8: fo mt hp hn rp an na ex
# fo 'mailcap'
# mt MIMETYPE
# hp basename of $mailcap_file - used by other Fatdog mime-processing tools
# hn `rp`'s basename - if non-empty then it enables other mime-processing tools
# rp mailcap handler's full pathname (can be empty if $1 is corrupted)
# an ''
# na ''
# ex ''
# See tbl8_rox_sub_table for further details.
# ---------------------------------------------------------------------------}}}
tbl8_mailcap_table () {
	local mailcap_file="$1" fo mt hp hn rp arg0 path0
	local -r from='mime association from'
	[ -e "$mailcap_file" ] || return 1

	tbl8_new_section "$from" "$mailcap_file"

	fo='mailcap' hp="${mailcap_file##*/}"
	while IFS=';' read -r mt arg0 || [ -n "$mt" ]; do
		[[ "$mt" =~ ^"#"|^[[:blank:]]*$ ]] && continue
		unset hn rp

		# [A]
		[[ "$arg0" =~ ^[[:blank:]]*([^[:blank:]]+) ]] || continue
		arg0="${BASH_REMATCH[1]}"
		if [[ "$arg0" != '/'* ]]; then
			# try to find arg0's absolute pathname
			if command -v "$arg0" > "$TMP-$FUNCNAME"; then
				read path0 < "$TMP-$FUNCNAME"    # 2-3x faster than =$()
				rp="$path0" hn="$arg0"
			else
				warn "can't confirm that binary '%s' exists (source '%s')" "$arg0" "$mailcap_file"
				((WITH_AUDIT)) && hint 'use absolute pathname for binary to bypass this test'
				rp="$arg0" hn="$arg0" # not a full path!
			fi
		else
			rp="$arg0" hn="${arg0##*/}"
		fi

		# always output a record even if warnings occurred
		printf  "%s\037%s\037%s\037%s\037%s\037%s\037%s\037%s\n" \
			"$fo" "$mt" "$hp" "$hn" "$rp" "" "" ""
	done < "$mailcap_file"
}

### Mailcap-specific functions END

# ---------------------------------------------------------------------------{{{
# This function reads a TABLE@5. If a row reveals a .desktop file then its Name
# and Exec values are copied to the row.
# ------------------------------------------------------------------------------
# Input: TABLE@5: hn rp an na ex
# ------------------------------------------------------------------------------
# Output: TABLE@5: hn rp an na ex
# hn: HANDLER_NAME (carry over stdin)
# rp: FILEPATH     (carry over stdin)
# an: don't care   (carry over stdin)
# na: Value of the 'Name' key[D] if `rp` is a .desktop file.
# ex: Value of the 'Exec' key[E] if `rp` is a .desktop file.
# Trailing "Exec %variables"[bib2.2] are deleted from `ex` if possible.
# ------------------------------------------------------------------------------
# [A] This case occurs so frequently in Fatdog that it's worth optimizing it out
# On a post v812 Fatdog64 ISO with input data coming from find_rox_handlers:
# (c0=rows entering test [A], c1=rows entering test [B]'s first condition, etc.)
#                         c0   c1   c2   c3    c1/c0   c3/c2
#   /etc  SendTo:       1261  262  259  254    20.78%  98.07%
#   /etc  MIME-types:    230   55   32    1    23.91%   0.03%
#   /etc  URI:             8    0    0    0        0%     NA
#   my ~  SendTo:         71   60   59   59    84.51% 100.00%
#   my ~  MIME-types:     24   11   10    4    45.83%  40.00%
#   my ~  URI:             0    0    0    0       NA      NA
# ------------------------------------------------------------------------------
# [B] Mime type 'application/x-desktop' is assessed with a subset of the rules
# found in /usr/share/mime/packages/freedesktop.org.xml @application/x-desktop.
# Simply globbing '*.desktop' isn't enough because the spec[bib2.1] allows file
# names not to end with ".desktop".  Motivating example:
#   ~/.config/rox.sourceforge.net/SendTo/.text_html ->
#     -> 'Localized Label' ->
#     -> /usr/share/applications/seamonkey-composer-html-editor.desktop
# ------------------------------------------------------------------------------
# [C] Chop exec %variables[bib2.2] from the command tail.  If the %variable is
# in the middle of the command we remit chopping to the user. (Loop used in
# lieu of regex because bash RE engine only provides greedy `*`).
# ------------------------------------------------------------------------------
# [D0] Input `na` could be empty if `rp` doesn't exist. If `rp` isn't a valid
# desktop file[A] or the 'Name' key can't be found then `na` is
# set to some value, typically `rp`'s basename.
# [D1] Update: This program no longer uses `na` but the column is still used by
# other Fatdog MIME tools. Now lines pertaining to [D0] are commented out.
# ------------------------------------------------------------------------------
# [E] Input `ex` is a command, in the sense of the system(3) call. Output `ex`
# is resolved to a full pathname so that update_mimeapps_list can use `mt:`ex`
# as a unique key. It may be impossible to resolve `ex`'s pathname if input
# `ex` isn't a valid command when this function runs.  Moreover, output `ex`
# could be empty if `rp` doesn't exist or it isn't a valid desktop file[A] or
# the 'Exec' key can't be found in `rp`.
# ---------------------------------------------------------------------------}}}
tbl5_desktop_name_exec () {
	local hn rp an na ex k v arg0 path0
	local len="[Desktop Entry]"; len=$((${#len} + 32))

while IFS=$'\037' read hn rp an na ex; do
	unset na ex
	# c0 [A] optimize out frequent case
	if [[ "$hn" != "AppRun" && "$rp" != *"/bin/"* ]] &&
		# c1 [B] if (mimetype = application/x-desktop)
		[ -f "$rp" ] && read -rN $len v < "$rp" && # physical file exists
		# c2
		[[ "$rp" == *'.desktop' || "$v" == *'[Desktop Entry]'* ]]
	then
		## c3 extract 'Name' and 'Exec'
		na='NA' # [D1]
		unset k v
		while IFS="=" read -r k v; do
			if   [[ -z "$ex" && "$k" == 'Exec' ]]; then ex="$v"
#[D0] 			elif [[ -z "$na" && "$k" == 'Name' ]]; then na="$v"
			fi
			[[ -n "$ex" && -n "$na" ]] && break
		done < "$rp"

		if [[ -z "$ex" || -z "$na" ]]; then
			warn "missing 'Name' or 'Exec' key in desktop file '%s'" "$rp"
#[D0] 			# fallback name [D0]
# 			if [ -z "$na" ]; then
# 				[[ "$rp" == *'/'* ]] && na="${rp##*/}" || na='invalid'
# 				na="${na%.*}"
# 			fi
		fi

		# [E]
		while case "$ex" in
			*[[:blank:]]  ) ex="${ex%?}" ;;
			*%[fFuU]      ) ex="${ex%??}" ;;
			*) false ;;
		esac; do :; done

		# [D]
		[[ "$ex" =~ ^[[:blank:]]*\"?([^[:blank:]]+)\"? ]] && arg0="${BASH_REMATCH[1]}" || arg0="$ex"
		if [[ "$arg0" != '/'* ]]; then
			# try to find ex's arg0's absolute pathname
			if command -v "$arg0" > "$TMP-$FUNCNAME"; then
				read path0 < "$TMP-$FUNCNAME"    # 2-3x faster than =$()
				ex="$path0${ex#$arg0}"
			else
				warn "can't confirm that binary '%s' exists (source '%s')" "$arg0" "$rp"
				((WITH_AUDIT)) && hint 'use absolute pathname for binary to bypass this test'
			fi
		fi
	fi

	# this function must output exactly a row for each input row
	# even if warnings or errors occurred
	printf  '%s\037%s\037%s\037%s\037%s\n' \
		"$hn" "$rp" "$an" "$na" "$ex"
done
}

# --------------------------------------------------------------------------{{{
# This function calls tbl5_desktop_name_exec then outputs a TABLE@8 that
# conforms to tbl8_rox_dir_table's output and describes the files located at
# level 1 under $@. Most of them are .desktop files.
# -----------------------------------------------------------------------------
# $1-tag $2@-desktop_dirs e.g. usr/share/applications, etc.
# -----------------------------------------------------------------------------
# Output: TABLE@8: fo mt hp hn rp an na ex
# fo: $1-tag - only used to tag the call
# mt: ''
# hp: ''
# hn: ''
# rp: a .desktop file pathname    *** ONLY VALID IFF `ex` ISN'T EMPTY ***
# an: ''
# na: Value of rp's 'Name' key
# ex: Value of rp's 'Exec' key    *** USE `ex` TO TEST FOR .desktop file ***
# -----------------------------------------------------------------------------
# [A] instead of using find[1] we use a shell `*` glob.  Globbing *'.desktop'
# isn't sufficient to find all .desktop files because the spec[bib2.1] allows
# file names not to end with '.desktop'.
# [1] `find -H $1 -maxdepth 1 -name '*.desktop'`
# --------------------------------------------------------------------------}}}
tbl8_desktop_dirs_table () {
	local tag="${1:?}"; shift
	local -i i ong
	local -a arp a__ # a__ is an empty column helper

	shopt -q nullglob; ((ong=!$?)) || shopt -s nullglob # enable

	# gather pathnames
	for p; do shift; [ -d "$p" ] || continue; arp+=("$p"/*); done # [A]
	for ((i = ${#arp[*]} - 1; i >= 0; i-- )); do
		[[ -f "${arp[$i]}" ]] && a__+=('') || unset "arp[$i]"
	done

	((ong)) || shopt -u nullglob # restore initial state

	((${#arp[*]})) || return

	# paste 8 columns together: fo mt hp hn rp an na ex
	paste -d $'\037' \
		<(printf '%s\n' "${a__[@]/#/"dde$tag"}") \
		<(printf '%s\n' "${a__[@]}") \
		<(printf '%s\n' "${a__[@]}") \
		<(printf "\037%s\037\037\037\n" "${arp[@]}" |
			tbl5_desktop_name_exec) # hn rp an na ex
}

# ---------------------------------------------------------------------------{{{
# This function pipes TABLE@8 taps into mailcap and mimeapps.list updaters
# (sinks).  Taps include the .desktop file directory[A] and all authorized
# sources (currently Rox and mailcap).
# ------------------------------------------------------------------------------
# $1-tag $2-rox_handler $3-mailcap_file $4-desktop_file_dir [$5-mimeapps_list_file]
# ------------------------------------------------------------------------------
# [A] In addition to tbl8_desktop_dirs_table's output, .desktop data also
# include some rows from tbl8_rox_dir_table's output, specifically from Rox
# handlers that link to .desktop files located anywhere in the file system.
# ------------------------------------------------------------------------------
# [B] IMPORTANT: This function does not wait for its descendants therefore it
# returns before update_mimeapps_list has finished!  This is done to allow more
# parallelism in the calling program. In many cases not waiting doesn't matter
# because update_mimeapps_list doesn't write to stdout.  However, it matters
# when a subsequent program needs to read mimeapps.list - indeed it's
# fatdog-sync-mime-update-desktop-database.sh's case.  Wait for all descendants
# of this function by wrapping it as discussed in [bib9].  For instance,
# fatdog-sync-mime-core.sh calls this function and others inside a waiting
# wrapper derived from [bib9]. Hence you can run *-update-desktop-database.sh
# immediately after *-core.sh being assured that the former will read
# up-to-date data from mimeapps.list.
# ------------------------------------------------------------------------------
# Output: TABLE@8 - see the documentation for the called taps.
# ---------------------------------------------------------------------------}}}
tbl8_update_mailcap_and_mimeapps_list() {
	local tag="${1:?}" rox_handler="${2:?}" mailcap_file="${3:?}" desktop_file_dir="${4:?}"
	local mimeapps_list_file="$desktop_file_dir/$MIMEAPPS_LIST" deprecated_mimeapps_list_file
	[ -n "$5" ] && deprecated_mimeapps_list_file="$mimeapps_list_file" mimeapps_list_file="$5"

	# TABLE@8: fo mt hp hn rp an na ex
	{
		tbl8_desktop_dirs_table "$tag" "$desktop_file_dir"
		{	# authorized sources
			tbl8_rox_dir_table "$rox_handler" # [A]
			tbl8_mailcap_table "$mailcap_file"
		} |
			tee >(update_mailcap "$mailcap_file")
	} | # [B]
		tee >(update_mimeapps_list \
			"$mimeapps_list_file" \
			"$deprecated_mimeapps_list_file")

	# TABLE@8: fo mt hp hn rp an na ex
}

# ---------------------------------------------------------------------------{{{
# Read a TABLE@8 from tbl8_rox_dir_table and overwrites mailcap file $1.
# ------------------------------------------------------------------------------
# $1-mailcap
# ------------------------------------------------------------------------------
# Output: none
# ------------------------------------------------------------------------------
# This function must be used as a process because it can `exec`.
# ------------------------------------------------------------------------------
# There can be multiple handlers capable of opening a given mimetype,
# some of which can be defaultprograms that must be given due priority.
# [C1] adds `rp` or its priority index to `mt`s entry
# [C2] expands `mt`s sorted handlers
# [C3] `mt`s first handler is output uncommented while subsequent handlers are
# printed as a cluster of comments after the first handler
# ---------------------------------------------------------------------------}}}
update_mailcap () {
	local mailcap="$1" fo mt rp ex __ e
	local -A entry
	local -a w

	# sink data if an update of mailcap wasn't requested
	[[ "$PLAN" == *"mailcap"* ]] || exec cat > /dev/null

	cprint "Updating $mailcap" >&2
	((WITH_AUDIT)) && [ -e "$mailcap" ] && wc "$mailcap" >&2

	while IFS=$'\037' read -r fo mt __ __ rp __ __ ex; do
		[[ -n "$ex" || "$fo" == '###'* ]] &&
			continue # .desktop entry or section header

		rp="${rp#"$ROOT"}"
		# [C1]
		add_to_sorted_entry entry "$mt" "$rp"
	done

	# create / update file
	if ((${#entry[*]})); then
		! [ -e "$mailcap" ] && mkdir -p "${mailcap%/*}"
		{
			for mt in ${!entry[*]}; do
				e="${entry["$mt"]}"

				# [C2]
				expand_sorted_entry e

				# [C3]
				# path -> \034'# 'path'; %s' (multiple)
				IFS=';' read -a w <<< "$e"
				w=( "${w[@]/#/$'\034'"# $mt; "}" )
				w=( "${w[@]/%/" %s"}" )
				printf -v e %s "${w[0]#???}" "${w[@]:1:${#w[@]}}"

				printf "%s\n" "$e"
			done | sort | tr '\034' '\n'
		} > "$mailcap"

		cprint 42 30 "Updated $mailcap" >&2
		((WITH_AUDIT)) && wc "$mailcap" >&2
	fi
}

# ---------------------------------------------------------------------------{{{
# This function reads TABLE@8 tables from tbl8_desktop_dirs_table and
# authorized sources and overwrites mimeapps.list file $1.
# ------------------------------------------------------------------------------
# $1-mimeapps_list file (overwrite) [$2-deprecated_mimeapps_list]
# ------------------------------------------------------------------------------
# Output: none
# ------------------------------------------------------------------------------
# This function must be used as a process because it can `exec`.
# ------------------------------------------------------------------------------
# [A] Only handlers for which a .desktop file's 'Exec' key is pre-existing are
# considered for output.  All input entries that arise from .desktop files are
# grouped at the start of the input stream to enable creating $ex2df upfront.
# ------------------------------------------------------------------------------
# [B] Since all the examples in [bib3] show a .desktop file _basename_, we
# record the basename instead of the full path `rp`. However, there can be cases
# in which the .desktop file isn't colocated with the mimeapps.list file, for
# instance, when a SendTo item links to a .desktop file in SendTo-resources/.
# For these cases we record the full path of the .desktop file.
# ------------------------------------------------------------------------------
# There can be multiple handlers capable of opening a given mimetype,
# some of which can be defaultprograms that must be given due priority.
# [C1] adds `rp` or its priority index to `mt`s entry
# [C2] expands `mt`s sorted handlers
# ------------------------------------------------------------------------------
# [D] New file locations[bib3.1] are enforced, and deprecated file locations are
# symlinked for compatibility with Fatdog64-812's xdg-utils 1.1.0.rc1 package.
# `XDG_UTILS_DEBUG_LEVEL=2 xdg-mime ...` shows search locations.
# ---------------------------------------------------------------------------}}}
update_mimeapps_list () {
	local mimeapps_list="${1:?}" fo mt rp ex __ df e
	local deprecated_mimeapps_list="$2"
	local -r dn="${1%/*}" # in $ROOT
	local -A entry ex2df

	# sink data if a mimeapps.list update wasn't planned
	[[ "$PLAN" == *"mimeapps"* ]] || exec cat > /dev/null

	cprint "Updating $mimeapps_list" >&2
	((WITH_AUDIT)) && [ -e "$mimeapps_list" ] && wc "$mimeapps_list" >&2

	# TABLE@8: fo mt hp hn rp an na ex
	while IFS=$'\037' read -r fo mt __ __ rp __ __ ex; do
		[[ "$fo" == '###'* ]] && # section header
			continue

		# [A]
		if [[ "$fo" == 'dde'* && -n "$ex" ]]; then
			ex2df["$ex"]="$rp"
			ex2df["${rp#$ROOT}"]="$rp"
			continue
		fi

		df="${ex2df["$rp"]}"
		# [B]
		df="${df#"$dn/"}" # try relative to $ROOT first
		df="${df#"${dn#"$ROOT"}/"}" # try relative to /
		[ -z "$df" ] && continue

		# [C1]
		add_to_sorted_entry entry "$mt" "$df"
	done

	# create / update file
	if ((${#entry[*]})); then
		! [ -e "$mimeapps_list" ] && mkdir -p "${mimeapps_list%/*}"
		{
			echo '[Default Applications]'
			for mt in ${!entry[*]}; do
				e="${entry["$mt"]}"

				# [C2]
				expand_sorted_entry e

				printf "%s=%s\n" "$mt" "$e"
			done | sort
		} > "$mimeapps_list"

		# symlink deprecated file locations [D]
		if [[ "$mimeapps_list" == *"$XDG_CONFIG_HOME"* ]]; then
			# symlink relative path if possible otherwise absolute
			ln -sf -r "$mimeapps_list" \
				"$deprecated_mimeapps_list" ||
				ln -sf "$mimeapps_list" \
				"$deprecated_mimeapps_list"
			ln -sf "${deprecated_mimeapps_list##*/}" \
				"${deprecated_mimeapps_list%/*}/$DEFAULTS_LIST"
		else
			ln -sf "${mimeapps_list##*/}" \
				"${mimeapps_list%/*}/$DEFAULTS_LIST"
		fi &&

			cprint 42 30 "Updated $mimeapps_list" >&2 &&
			((WITH_AUDIT)) && wc "$mimeapps_list" >&2
	fi
}

# ---------------------------------------------------------------------------{{{
# Insert a path into a mimetype entry prioritizing some paths over others.
# ------------------------------------------------------------------------------
# Mimetype Sorted Entry (MSE)
# MSE  ::= [HEAD ';'] PATH ';' [ PATH ';' ...]
# HEAD ::= ORDER_MAP_NAME ':' INDEX [ ':' INDEX ...]
# PATH ::= ABSOLUTE_PROGRAM_PATH | DESKTOP_FILE_NAME
# ------------------------------------------------------------------------------
# $1-varname_ary_entry - IN/OUT array of mimetype entries - only $2's is changed
# $2-mime_type
# $3-fp - absolute pathname to be inserted - it can be just a filename
#    chop $ROOT before passing $3
# [$4-filename_prefix] - file name prefix associated with order map $5
#    default 'default'
# [$5-order_map] - array name of the relative order map over the subset of
#    pathnames defined by $4; get_programs_relative_order creates the order map;
#    this name is also used to mark entries that include sorted items;
#    default 'DEFAULTPROGRAM_ORDER' (string - name of an associative array).
# ------------------------------------------------------------------------------
# [A1] There can be multiple handlers capable of opening a given mimetype. They
# are normally added to the mimetype entry in FIFO order.
# [A2] However, defaultprogram handlers (defaultbrowser, defaulttexteditor,
# etc.) are placed at the head of the entry and sorted according to
# DEFAULTPROGRAM_ORDER. Since a mimetype entry is built incrementally,
# defaultprograms are replaced with a token as they're encountered. The token
# expresses the defaultprogram's relative order in array DEFAULTPROGRAM_ORDER.
# ------------------------------------------------------------------------------
# Function expand_sorted_entry is called after this function to expand the
# tokens back into pathnames.
# This function allows general reuse by passing $4 != '' and $5 != ''.
# ---------------------------------------------------------------------------}}}
add_to_sorted_entry () {
	local -n varname_ary_entry="${1:?}" # IN/OUT
	local -r mt="${2:?}" fp="${3:?}"
	local -r fn_prefix="${4:-"default"}"
	local -r name="${5:-"DEFAULTPROGRAM_ORDER"}"
	local -n varname_order_map="$name"
	local k e="${varname_ary_entry["$mt"]}"
	local -a w
	local -i i

	: # "entry["$mt"]($e) fp($fp)"
	[[ ";$e" == *";$fp;"* ]] &&
		return # $fp already recorded in $mt's entry

	# [A1]
	# if both filename and filepath aren't elements of the
	# partially ordered set
	if [[ "$fp" != "$fn_prefix"* && "$fp" != *"/$fn_prefix"* ]]; then
		entry["$mt"]="$e$fp;"
	else
		# [A2] replace $fp with token $name':'$k[':'$k ...]
		# where $name is the name of the order map and
		# $k is the relative order index in the map
		k=${varname_order_map["$fp"]}
		[[ "$e" == *":$k"[:\;]* ]] && return # $k for $mt already recorded

		if [[ "$e" != "$name:"* ]]; then
			# map_name':'sentinel_value(-1)':'initial_index';'unordered_entries
			entry["$mt"]="$name:-1:$k;$e"
		else
			# find insertion point i
			IFS=':' read -a w <<< "${e%%;*}"
			for (( i = ${#w[*]} - 1; i > 0 && $((${w[$i]})) > $(($k)); i-- )); do
				: # "w[$i](${w[$i]}) > k($k)"
			done

			# insert $k in order
			w=( "${w[@]:1:i}" "$k" "${w[@]:i+1}" )

			# entry["$mt"] = implode($w)
			printf -v entry["$mt"] %s "$name" "${w[@]/#/":"}"
			: # "entry[$mt](${entry["$mt"]})"
		fi
	fi
}

# ---------------------------------------------------------------------------{{{
# Expand a Mimetype Sorted Entry (MSE).
# ------------------------------------------------------------------------------
# $1-varname_ary_entry - IN/OUT entry name reference
# [$2-order_map] - see order_map in add_to_sorted_entry
#    default 'DEFAULTPROGRAM_ORDER'
# ------------------------------------------------------------------------------
# Function add_to_sorted_entry is called before this function to insert the
# element to be expanded into pathnames.
# This function allows general reuse by passing $2 != ''.
# ---------------------------------------------------------------------------}}}
expand_sorted_entry () {
	local -n varname_entry="${1:?}" # IN/OUT
	local -r name="${2:-"DEFAULTPROGRAM_ORDER"}"
	local -n varname_order_map="$name"
	local _buf

	[[ "$varname_entry" == "$name:"* ]] || return # nothing to be expanded

	varname_entry="${varname_entry#"$name"}"
	while [[ "$varname_entry" =~ ':'('+'?[[:digit:]]+)(.*) ]]; do
		_buf+="${varname_order_map["${BASH_REMATCH[1]}"]};"
		varname_entry="${BASH_REMATCH[2]}"
	done
	varname_entry="${_buf%;}$varname_entry"
}

# ---------------------------------------------------------------------------{{{
# Merge mimeapps.list handlers into mimeinfo.cache. For any given mimetype that
# requires a full merge, mimeapps.list's handlers are prioritized higher
# because they derive from Fatdog's authorized mimetype sources (rox and
# mailcap). Moreover, since mimeapps.list sorts defaultprograms first, also
# mimeinfo.cache's merged entry gets defaultprograms in first position.
# File mimeinfo.cache is rewritten.
# ------------------------------------------------------------------------------
# Motivation
# According to [bib7], applications that read .desktop files to determine how
# to open a file can pick a random .desktop in case multiple .desktop files
# match the given mimetype.  Indeed, mimeinfo.cache seemingly lists an entry's
# handlers in random order.  However, while I couldn't find it in writing, it
# does make sense for any program to always pick the first handler of a
# multi-handler entry. For instance, when I clicked the homepage link in Meld's
# About dialog, Meld ran the first entry of the HTML handler in mimeinfo.cache,
# which was links.desktop; defaultbrowser.desktop was at the end of the line.
# ------------------------------------------------------------------------------
# $1-mimeinfo_cache $2-mimeapps.list
# ------------------------------------------------------------------------------
# Input: none
# ------------------------------------------------------------------------------
# Output: none
# ---------------------------------------------------------------------------}}}
rearrange_mimeinfo_cache () {
	local mimeinfo_cache="${1:?}" mimeapps_list="${2:?}" mt p
	local -a A
	local -i i nA
	local -A C L M seen

	cprint "Updating $mimeinfo_cache from $mimeapps_list" >&2

	[ -r "$mimeinfo_cache" ] && readarray -t A < "$mimeinfo_cache"
	nA=${#A[*]}
	((nA)) || return
	# C : mimeinfo.cache's desktop file list by mimetype
	for (( i = 1; i < nA; i++ )); do C["${A[$i]%%=*}"]="${A[$i]#*=}"; done

	[ -r "$mimeapps_list" ] && readarray -t A < "$mimeapps_list"
	nA=${#A[*]}
	((nA)) || return
	# L : mimapps.list's desktop file list by mimetype
	for (( i = 1; i < nA; i++ )); do L["${A[$i]%%=*}"]="${A[$i]#*=}"; done

	# M : union set of all mimetypes
	for mt in ${!L[*]} ${!C[*]}; do M["$mt"]=1; done

	printf '[MIME Cache]\n' > "$mimeinfo_cache"

	for mt in ${!M[*]}; do
		IFS=';' read -a A <<< "$mt;${L["$mt"]%;};${C["$mt"]#;}"
		nA=${#A[*]} seen=()
		printf '%s=' "$mt"
		for (( i = 1; i < nA; i++ )); do
			p="${A[$i]}"
			if [ -n "$p" ] && [ -z "${seen["$p"]}" ]; then
				printf '%s;' "$p"
				seen["$p"]=1
			fi
		done
		printf '\n'
	done | sort >> "$mimeinfo_cache"

	cprint 42 30 "Updated $mimeinfo_cache" >&2
}

# Notes {{{
################################################################################
#                                 USER UPGRADES                                #
################################################################################

# Fatdog-sync-mime scripts are normally installed with package "fatdog-scripts".
# Upgrade in a new savefile otherwise invalid entries will carry over from the
# previous version. Alternatively and without official support, manually clear
# invalid entries from your savefile then upgrade the scripts: 1) first install
# package "fatdog-scripts"; 2) replace /etc/mailcap inside the savefile with the
# equivalent file from package "mime-support"; 3) review entries in ~/.mailcap;
# 4) run fatdog-sync-mime-core.sh; fatdog-sync-mime-update-desktop-database.sh.
# These scripts update files mailcap, mimeapps.list and mimeinfo.cache in the
# system and user's home. Repeat step 4 when you install a new package or
# delete/change a package.

################################################################################
#                                  Compatibility                               #
################################################################################

# Fatdog64-812's package xdg-utils 1.1.0.rc1 has two issues: 1) deprecated file
# locations, see update_mimeapps_list's note [D] for a work-around; 2) xdg-open
# chops command options, e.g. it runs `rox` instead of `rox -d`. This can only
# be fixed by upgrading xdg-utils (I tested xdg-open 1.1.3 successfully).

# We strive to use bash as much as possible not to invoke sed for every little
# thing or awk for arrays.  Require bash 4.0 for associative arrays; bash
# 4.3's `shopt globstar` for a version of glob `**` that doesn't follow links.

###############################################################################
#                                    Scope                                    #
###############################################################################

### fatdog-sync-mime-core.sh:
# Sync all mime-handling managed by /usr/share/mime/, /etc/{mime.types,mailcap}
# /usr/share/applications/{defaults,mimeapps}.list, ROX Filer's MIME-types,
# URI and SendTo, and /etc/defaultprograms. Similarly for other XDG_DATA_DIR.
# Similarly for the corresponding user's configuration files:
# ~/.local/share/mime/, ~/{.mime.types,.mailcap}, ~/.config/mimeapps.list,
# ROX's ~/.config/rox.sourceforge.net/, and ~/.fatdog/defaultprograms.
#
# Defaultprograms as mimetype handlers are prioritized higher than other
# programs.  Defaultprogram files aren't changed in any way.
#
### fatdog-sync-mime-update-desktop-database.sh:
# Merge mimeapps.list into mimeinfo.cache. The resulting file is the union
# set of rox's, mailcap's and shared-mime's databases.
#
### fatdog-sync-mime-make-desktop-files.sh:
# Identify missing desktop files; create new desktop file skeletons - manual
# selection and editing are necessary before using these as system files.

### data:
# Output a data table that can be used to implement additional mime sync tools.
#
################################################################################
#                             Implementation Notes                             #
################################################################################

# James's notes 2016

# 1. MIME types: Between /etc/mime.types and shared-mime, I consider
#    shared-mime (/usr/shared/mime) authoritative. This is because
#    many file managers and widget libraries use it - Rox, and others.
#
#    So /etc/mime.types will be updated from /usr/share/mime/globs2.
#    However, sometimes /etc/mime.types has some MIME info that are not
#    not recorded by shared-mime; these usually show up as types without
#    any file extensions. We need to preserve this types too.
#
#    Note1: Rox is never a source for mime-types, it actually uses shared-mime.
#    Note2: "file" magic-file is not a source of MIME types. The magic-file
#           contains magic strings for detecting MIME type, but it in itself
#           does not provide the full list of MIME types.
#
# 2. MIME handlers: Between /etc/mailcap, desktop files, and ROX Filer
#    MIME handlers, I consider ROX Filer as the authoritative source.
#
#    /etc/mailcap can only specify one handler for one mime-type, so
#    it is out of question.
#
#    Desktop files is actually the "official" default list but for them
#    to work, there must be a "MimeType" element in the desktop file to
#    specify what kind of MIME types a desktop file can handle, but
#    unfortunately a lot of Fatdog desktop files are missing this element,
#    making it unreliable.
#
#    Another reason is because there is no easy way to add/remove handlers;
#    editing a desktop file doesn't cut it. Rox on the other hand already
#    provides this by its "Set Run Action" menu and "Customise Menu".
#
#    So we will "merge" /etc/mailcap from Rox mime handlers. We keep existing
#    entries in mailcap that aren't replaced with Rox mime handlers.
#    We could update MimeType element from desktop files with info from
#    Rox, but we avoid that (I don't want to update desktop files
#    unless it is absolutely necessary).
#
#    Note1: This decision to make Rox authoritative unfortunately also makes us
#    depend unnecessarily on Rox. It means that if you use other file managers,
#    you still have to use Rox too as the source of the MIME handlers.
#    To reduce this dependency, the function that obtains handlers from Rox
#    is isolated and returns a generic handler list; so it can be replaced
#    with others later.
#
#    Note2: We use mimeapps.list, but we symlink defaults.list to mimeapps.list
#    for maximum compatibility. [step: symlinks extended].
#
# 3. Default application handlers: Between /usr/share/applications/defaults.list,
#    mimeapps.list, /etc/mailcap and ROX filer MIME handler,
#    I consider ROX as the authoritative source.
#
#    Main reason is because Rox provides a GUI to handle that, while there is
#    no such thing for mimeapps.list. Also, mimeapps.list are already deprecated.
#
#    Aside from the benefit that Rox provides, as above, I want to note that
#    mimeapps.list are supposedly update-able from desktop files;
#    and as discussed above Fatdog desktop files are usually of the shortened form.
#
#    How does defaultprogram get into this picture? defaultprogram comes
#    as another indirection layer. This is how it works:
#
#    - without default program, if I want to associate .text and .c and .csv
#      files (text/plain, text/x-csrc, text/csv) to my favorite text editor
#      (say geany), I will have to associate it 3 times with Rox, one for each
#      MIME type. Sure if I use desktop files I can just edit geany.desktop and
#      specify MimeType=text/plain;text/x-csrc;text/csv but for this to take
#      effect it has to go to defaults.list; making sure that it wins over
#      all other programs that handles the same type. Tough stuff.
#      Now let's say I want to use vim instead of geany. What to do?
#      Repeat all of the above again.
#
#    - with defaultprogram, you just create a symlink defaulttexteditor
#      that points to defaultprogram, and then define DEF_TEXTEDITOR=geany
#      in its config file.
#      Then in Rox filer assign all the MIME types to defaulttexteditor
#      (or, if you're adventurous, create defaulttexteditor.desktop
#      and update its MimeType, and then re-update defaults.list)
#      The next time you want to use vim instead of geany, just edit
#      defaultprogram config file and change DEF_TEXTEDITOR=vim.
#      See also "step's notes 2022" about defaultprogram.
#

# -----------------------------------------------------------------------------
# step's notes 2022
#
# TABLE@8 and in general TABLE@n, is a table with record separator = \n
# and field separator = \037 (\x1F)[bib5].  Many functions in
# this program are implemented as filters that read and write such tables.
# The number following the at sign is the number of columns of the table.
# Sometimes, as a matter of convenience, we refer to a table with a different
# field separator still as a TABLE@ but indicate the separator like this, e.g.,
# TABLE@N(sep=':').  When we introduce a TABLE@ we generally also list out the
# names of its columns like this (many times without backticks):
#    TABLE@8 `fo` `mt` `hp` `hn` `rp` `an` `na` `ex`
# Currently TABLE@8 (N=8) is the standard format for most functions that pipe
# their output into other functions. By keeping an equal number of in/out
# columns we can create simple but powerful pipelines with these functions.
#
# Tip: to eye-ball the contents of a TABLE@n you can use sed like this:
#       tbl8_rox_sub_table | sed 's/\x1f/> </g'
# Eg: Which rows aren't AppDirs or .desktop files?
#   tbl8_rox_sub_table /etc/xdg/rox.sourceforge.net/SendTo |
#     sed 's/\x1f/> </g' | grep -vF -e '<AppRun>' -e '.desktop>'

# -----------------------------------------------------------------------------

# Rox MIME-types and Rox SendTos
#    Rox provides two distinct sources of MIME-type handler candidates: the
#    "MIME-types" flat folder and the "SendTo" folder tree, which reflects the
#    structure of the SendTo menu.  Folder "MIME-types" serves direct-click
#    default open actions, while "SendTo" serves opposite-click actions.

# Rox SendTo
#    Level-1 tree elements can be:
#    1a) MIME-type handler files named "."<content_type>"_"<subtype>
#        e.g. ".image_jpeg";
#    1b) folders named like in 1a) at the root of a possibly nested submenu
#        structure for the MIME-type;
#    2)  special folders:
#        a) "."<content_type> targeting at files with the given content type
#        regardless of the subtype, e.g. ".image" (all images);
#        b) ".all" targeting at all files (any MIME-type);
#        c) ".group" targeting at a Rox multi-object selection;
#    3)  other files targeting at all files (any MIME-type);
#    4)  other folders at the root of a possibly nested submenu structure
#        targeting at all files (any MIME-type).

# Jun7's Rox SendTo Extensions
#    2) and 4) are Jun7's extensions not found in the legacy Rox
#    implementation. Notably, 2a) extends the set of MIME-type handler
#    candidates with globs like "image/*", "text/*", etc. Whether such globs
#    can be used is application-dependent (Rox can!). In my tests
#    update-mime-database did not complaint about globbed mimetypes.

# Resolving the handler's command - AppDirs
#    Leaf nodes 1a) can be regular files or (multi-hop) links, in which case we
#    follow the link to determine the handler's path, and process it as either
#    a .desktop file or an executable file. The target handler path can also
#    resolve to a directory AppDir, which is completed with its AppRun script.
#    Leaf nodes in 1b), 2a) are MIME-type handler kinds 1a) or 3).
#    Finally, we discard some handlers:
#    - 2b), 2c) and 4)   for lack of MIME-type info
#    - *"usr/local/Trash", *"defaultterm" - clearly unable to handle types.

# Defaultprograms - /usr/local/bin/default[[:alnum:]].*
#    When defaultprograms are associated to SendTo items, it becomes possible
#    for multiple defaultprograms to be associated with the same handler. This
#    raises the issue of relative order between these programs. This is solved
#    with configuration variable DEFAULTPROGRAM_ORDER. Consequently, system
#    files mailcap, mimeapps.list and mimeinfo.cache are written in such
#    a way that the lowest-index defaultprogram is placed at the head of the
#    list of handlers for a given mimetype.

################################################################################
#                                 Known Issues                                 #
################################################################################

# Due to the deliberate use of realpath, get_real_or_default outputs a couple
# of handler pathnames I don't particularly like:
# `/usr/lib64/libreoffice/program/soffice` instead of `libreoffice` and
# `gimp-2.8` instead of `gimp`.

# Running this script in a chroot environment where /dev isn't visible will
# yield bash errors,typically "file not found" and "ambiguous redirection".
# Proper /dev/null and /dev/fd/ are needed - the latter for bash >() and <()
# process redirections. Work-around: consider using $ROOT instead of chroot.

################################################################################
#                                    Future                                    #
################################################################################

# When an app uses the shared mime database - specifically mimeapps.list - to
# open a file, e.g. engrampa's Open With menu, the app can't show rox mimetype
# handlers if there is no desktop file with an Exec= key that lists the rox
# handler's _realpath_. It would be worth adding a desktop file - even a
# cutdown one with the Exec= key and little more - for important applications.
# For instance, I added rox_send_to_libreoffice.desktop.
# Running fatdog-sync-mime-make-desktop-files.sh outputs skeletons of all the
# missing desktop files, many of which are probably not needed.

# Desktop file's 'MimeType' data are ignored by design. Should they be needed,
# tbl5_desktop_name_exec could be easily adapted to extract them like it used
# to extract the 'Name' key.

################################################################################
#                                  References                                  #
################################################################################

# [bib1] The freedesktop.org Shared MIME database provides a single way to store
#    static information about MIME types and rules for determining a type.
#  - https://www.freedesktop.org/wiki/Specifications/shared-mime-info-spec/
# [bib2] The freedesktop.org Desktop Entry specification allows applications to
#    announce which MIME types they support.
#  - http://www.freedesktop.org/wiki/Specifications/desktop-entry-spec/
# [bib2.1] https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.1.html#basic-format
# [bib2.2] https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.1.html#exec-variables
# [bib3] The freedesktop.org MIME Associations specification solves the remaining
#    issues: which application should open a file by default, how to let the
#    user change the default application, and how to let the user add or remove
#    associations between applications and mimetypes.
#  - https://freedesktop.org/wiki/Specifications/mime-apps-spec/
#  - https://wiki.archlinux.org/title/XDG_MIME_Applications#mimeapps.list
#  - https://wiki.archlinux.org/title/default_applications
# [bib3.1] File locations; the role of $XDG_CURRENT_DESKTOP
#  - https://specifications.freedesktop.org/mime-apps-spec/mime-apps-spec-1.0.1.html#file
# [bib4] /etc/mime.types defines system-wide mappings from filename extensions to mime-types
#  - https://wiki.debian.org/MIME/etc/mime.types
#  - https://en.wikipedia.org/wiki/Media_type#Mime.types
#  - It is compiled by hand using mostly information provided by IANA.
#  - not to be confused with CUPS's mime.types file
#  - nowhere does it say that mime.types must be sorted by mimetype
# [bib5] TABLE@n
#  - https://en.wikipedia.org/wiki/C0_and_C1_control_codes#Field_separators
# [bib6] File System Hierarchy System (FHS)
#  - https://refspecs.linuxfoundation.org/FHS_3.0/fhs-3.0.html
# [bib7] Role of mimeapps.list vs mimeinfo.cache
#  - https://askubuntu.com/questions/351123
# [bib8] OpenOffice / StarOffice / LibreOffice MIME types
#  - https://www.openoffice.org/framework/documentation/mimetypes/mimetypes.html
# [bib9] Waiting for processes and bash process substitutions
#  - https://unix.stackexchange.com/a/388530

# Misc
#  - ``Is an "image/*" mime type possible?''
#    https://superuser.com/questions/979135
#  - https://linux.die.net/man/4/mailcap
# ------
# }}}

