#!/bin/sh
#
# revdep - Reverse dependency checker for broken packages
#
# This script checks for broken library linkages and identifies packages
# that need to be rebuilt due to missing shared library dependencies.
#
# Parallel mode: when called as 'revdep --check-file <path>' the script
# acts as a single-file worker and writes 'pkg|file|lib' lines to stdout.
# xargs -P uses this to fan out work without a temporary helper script.

# ==> Worker mode (invoked by xargs -P)
if [ "$1" = "--check-file" ]; then
	[ -z "$2" ] && exit 0
	line="$2"
	case "$(file -bi "$line")" in
		*application/x-sharedlib*|*application/x-executable*|*application/x-pie-executable*)
			ldd_out=$(ldd "$line" 2>/dev/null)
			case "$ldd_out" in
				*"not found"*)
					LIB_NAME=$(printf '%s\n' "$ldd_out" | awk '/not found/ && !seen[$1]++{print $1}')

					# Filter excluded libs (passed via environment)
					filtered=""
					for l in $LIB_NAME; do
						case " $EXCLUDED_LIBS " in
							*" $l "*) ;;
							*) filtered="$filtered $l" ;;
						esac
					done
					[ -z "$filtered" ] && exit 0

					# Identify owning package
					pkg_path=$(grep -Fxl "${line#/}" "$PKGDB_DIR"/* 2>/dev/null | head -n 1)
					[ -z "$pkg_path" ] && exit 0
					pkg_name=${pkg_path##*/}

					# Check NEEDED entries to confirm the library is truly required
					REQ_LIB=$(objdump -p "$line" 2>/dev/null | awk '/NEEDED/{printf "%s ", $2}')

					for i in $filtered; do
						# PRINTALL=1 (-a): emit every ldd hit regardless of NEEDED
						# Default:         only emit libs confirmed in NEEDED
						if [ "$PRINTALL" = 1 ]; then
							printf '%s|%s|%s\n' "$pkg_name" "$line" "$i"
						else
							case " $REQ_LIB" in *" $i "*) printf '%s|%s|%s\n' "$pkg_name" "$line" "$i" ;; esac
						fi
					done
					;;
			esac
			;;
	esac
	exit 0
fi
# ==> End worker mode

interrupted() {
	cleanup
	printf '\n'
	exit 1
}

cleanup() {
	rm -f "$FILE_LIST" "$TMP_OUT"
}

help() {
	cat << 'EOF'
Usage:
  revdep [option] [arg]
  
Options:
  -a, --all                         print all affected files
  -r, --rebuild                     rebuild & reinstall broken package
  -p, --package <pkg>               check for certain package
  -f, --no-filter                   skip filter (exclude) dirs, files and libraries
  -e, --exclude <pkg1 pkg2 pkgN>    exclude package when rebuild (use with -r/--rebuild)
  -y, --yes                         dont ask user confirmation to rebuild package (use with -r/--rebuild)
  -h, --help                        print this help message

EOF
}

confirm() {
	printf '%s (Y/n) ' "$1"
	read -r response
	case "$response" in
		[Nn][Oo]|[Nn]) printf '%s\n' "$2"; exit 2 ;;
		*) : ;;
	esac
}

parse_opts() {
	while [ "$1" ]; do
		case $1 in
			-[!-]?*)
				_rest=${1#-}; shift
				# Expand combined flags left-to-right. Non-arg flags are collected in $_flags.
				# When an arg-taking flag (e, p) is found, it is injected last so it naturally 
				# sees the original positional arguments.
				_flags=""
				while [ -n "$_rest" ]; do
					_chr="${_rest%"${_rest#?}"}"
					_rest=${_rest#?}
					case "$_chr" in
						e|p)
							set -- ${_flags} "-$_chr" "$@"
							_rest=""
							;;
						*)
							_flags="${_flags} -${_chr}"
							;;
					esac
				done
				[ -n "$_flags" ] && set -- ${_flags} "$@"
				continue ;;
		esac
		case $1 in
			-a|--all)       PRINTALL=1 ;;
			-r|--rebuild)   REBUILD=1 ;;
			-y|--yes)       NOCONFIRM=1 ;;
			-f|--no-filter) NO_FILTER=1 ;;
			-e|--exclude)	while [ "$2" ]; do
						case $2 in
						-*) break;;
						 *) [ -z "$expkg" ] && expkg="$2" || expkg="$expkg $2";;
						esac
						shift
					done
					;;
			-p|--package)
				[ -z "$2" ] && { printf 'Option -p requires a package name\n' >&2; exit 1; }
				PKG=$2; shift ;;
			-h|--help)      help; exit 0 ;;
			*)              printf 'Invalid option (%s)\n' "$1"; exit 1 ;;
		esac
		shift
	done
}

rebuild() {
	# Preserve topological order from scratch deplist, keeping only broken packages.
	for allpkg in $(scratch deplist $ALLPKG | awk '!seen[$2]++ {print $2}'); do
		case " $ALLPKG " in *" $allpkg "*) order="${order:+$order }$allpkg" ;; esac
	done

	# Remove excluded packages from rebuild order
	if [ -n "$expkg" ] && [ -n "$order" ]; then
		filtered_order=""
		for p in $order; do
			case " $expkg " in
				*" $p "*) ;;
				*) filtered_order="${filtered_order:+$filtered_order }$p" ;;
			esac
		done
		order="$filtered_order"
	fi

	if [ -n "$order" ]; then
		if [ ! "$NOCONFIRM" ]; then
			printf '\nPackage will be rebuild & reinstall by this order:\n'
			printf ' %s\n\n' "$order"
			confirm "Continue rebuild & reinstall broken packages?" "Operation cancelled."
		fi
		for p in $order; do
			scratch install -fr "$p" || { cleanup; exit 1; }
		done
	fi
}

rev_exclude() {
	# Read both config sources once; classify each non-comment line:
	#   d: = directory (ends with /)
	#   f: = file      (starts with /)
	#   l: = library   (contains .so)
	_raw=$(
		{ cat /etc/revdep.conf 2>/dev/null
		  cat /etc/revdep.d/*.conf 2>/dev/null
		} | awk '
			/^[[:space:]]*$/ || /^#/ { next }
			/\/$/ { print "d:" $0; next }
			/^\// { print "f:" $0; next }
			/\.so/ { print "l:" $0 }
		'
	)

	for _entry in $_raw; do
		_val="${_entry#?:}"
		case "$_entry" in
			d:*)
				_val="${_val%/}"
				[ -d "$_val" ] || continue
				case " $_DIRS " in *" $_val "*) ;; *)
					_DIRS="$_DIRS $_val"
					ged="$ged -e ^$_val"
					EXCLUDED_DIRS="$EXCLUDED_DIRS -path $_val -prune -o "
				;; esac
				;;
			f:*)
				[ -f "$_val" ] || continue
				case " $_FILES " in *" $_val "*) ;; *)
					_FILES="$_FILES $_val"
					gef="$gef -e ^$_val\$"
					EXCLUDED_FILES="$EXCLUDED_FILES ! -path $_val "
				;; esac
				;;
			l:*)
				case " $EXCLUDED_LIBS " in
					*" $_val "*) ;;
					*) EXCLUDED_LIBS="${EXCLUDED_LIBS:+$EXCLUDED_LIBS }$_val" ;;
				esac
				;;
		esac
	done

	EXCLUDE_DIRS=$(printf '%s\n' $_DIRS | sort -u)
	EXCLUDE_FILES=$(printf '%s\n' $_FILES | sort -u)
}

check_pythonmodules() {
	command -v python3 >/dev/null 2>&1 || return
	pylibpath=$(python3 -c "import site; print(site.getsitepackages()[0])" 2>/dev/null)
	[ -z "$pylibpath" ] && return
	for i in /usr/lib/python3.*; do
		[ -d "$i" ] || continue
		[ "$i" = "$pylibpath" ] && continue
		brokenpkg="$brokenpkg $(scratch provide "$i/" | awk '{print $1}')"
	done
}

check_perlmodules() {
	command -v perl >/dev/null 2>&1 || return
	perlpath=$(perl -MConfig -e 'print $Config{sitearch}')
	[ -d "$perlpath" ] || return
	# CHANGED: replaced $(dirname "$perlpath") with ${perlpath%/*}
	for i in "${perlpath%/*}"/*; do
		[ "$perlpath" = "$i" ] && continue
		[ -d "$i" ] || continue
		brokenpkg="$brokenpkg $(scratch provide "$i/" | awk '{print $1}')"
	done
}

check_rubygem() {
	command -v gem >/dev/null 2>&1 || return
	gempath=$(gem env gemdir)
	# CHANGED: replaced $(dirname "$gempath") with ${gempath%/*}
	for i in "${gempath%/*}"/*; do
		[ "$gempath" = "$i" ] && continue
		brokenpkg="$brokenpkg $(scratch provide "$i/" | awk '{print $1}')"
	done
}

FILE_LIST=$(mktemp -t revdep.XXXXXXXXXX) || exit 1
TMP_OUT=$(mktemp -t revdep.out.XXXXXXXXXX) || exit 1

# Trap signals for cleanup
trap "interrupted" 1 2 3 15

# Get absolute path of the script with parameter expansion and command -v for PATH lookup
case "$0" in
    /*) SELF="$0" ;;
    */*) SELF="$(cd "${0%/*}" && pwd)/${0##*/}" ;;
    *)  SELF=$(command -v "$0" 2>/dev/null) || SELF=$(which "$0" 2>/dev/null) || {
        printf "Cannot locate %s in PATH\n" "$0" >&2
        exit 1
    } ;;
esac

[ -x "$SELF" ] || {
    printf "Cannot execute %s\n" "$SELF" >&2
    exit 1
}

# Check for required command
command -v pkgadd >/dev/null 2>&1 || {
	printf "'pkgadd' not found in \$PATH!\n" >&2
	exit 1
}

# Package database directory
PKGDB_DIR="$(pkgadd --print-dbdir)"
SEARCH_DIRS="/bin /usr/bin /sbin /usr/sbin /lib /usr/lib /lib64 /usr/lib64 /usr/libexec"

parse_opts "$@"

# Check for root privileges when rebuilding
if [ "$(id -u)" != 0 ] && [ "$REBUILD" = 1 ]; then
	printf '%s need to run as root to rebuild & reinstall package\n' "${0##*/}" >&2
	help
	exit 1
fi

# Verify package is installed
if [ "$PKG" ] && [ ! -f "$PKGDB_DIR/$PKG" ]; then
	printf "ERROR: Package '%s' not installed\n" "$PKG" >&2
	cleanup
	exit 1
fi

# Get extra search directories from ld.so.conf
while read -r line; do
	case "$line" in /*) EXTRA_SEARCH_DIRS="$EXTRA_SEARCH_DIRS $line ";;	esac
done < /etc/ld.so.conf

# Process ld.so.conf.d directory
if [ -d /etc/ld.so.conf.d ]; then
	for dir in /etc/ld.so.conf.d/*.conf; do
		[ -f "$dir" ] || continue
		while read -r line; do
			case "$line" in /*) EXTRA_SEARCH_DIRS="$EXTRA_SEARCH_DIRS $line ";; esac
		done < "$dir"
	done
fi

# Apply exclusion filters
if [ "$NO_FILTER" != 1 ]; then
	rev_exclude
fi

# Combine search directories
TARGET_SEARCH_DIRS="$SEARCH_DIRS $EXTRA_SEARCH_DIRS"
SEARCH_DIRS=""

printf 'SEARCH DIRS:\n'
for d in $TARGET_SEARCH_DIRS; do
	if [ -d "$d" ]; then
		SEARCH_DIRS="$SEARCH_DIRS $d"
		printf ' %s\n' "$d"
	fi
done

printf '\nEXCLUDED DIRS:\n'
for dd in $EXCLUDE_DIRS; do
	printf ' %s\n' "$dd"
done

printf '\nEXCLUDED FILES:\n'
for ff in $EXCLUDE_FILES; do
	printf ' %s\n' "$ff"
done

printf '\nEXCLUDED LIBS:\n'
for ll in $EXCLUDED_LIBS; do
	printf ' %s\n' "$ll"
done
printf '\n'

# Build file list to check
if [ "$PKG" ]; then
	# Build grep patterns for target directories
	for D in $TARGET_SEARCH_DIRS; do
		gx="$gx -e ^$D"
	done
	gx="$gx -e '*\.so' -e '*\.so\.*'"

	# Set up filters
	if [ -n "$gef" ]; then
		filterfile="grep -v $gef"
	else
		filterfile=cat
	fi
	if [ -n "$ged" ]; then
		filterdir="grep -v $ged"
	else
		filterdir=cat
	fi

	printf "Find '%s' files... " "$PKG"
	tail -n +2 "$PKGDB_DIR/$PKG" | sed 's/^/\//' | grep $gx | $filterfile | $filterdir > "$FILE_LIST"
else
	printf 'Find all files... '
	# shellcheck disable=SC2086
	find $SEARCH_DIRS $EXCLUDED_DIRS $EXCLUDED_FILES -type f \( -perm /+u+x -o -name '*.so' -o -name '*.so.*' \) -print 2> /dev/null | sort -u > "$FILE_LIST"
fi

total=$(wc -l "$FILE_LIST" | awk '{print $1}')

printf '%s files found\n' "$total"

if [ "$total" -eq 0 ]; then
	printf 'No files to check.\n'
	cleanup
	exit 0
fi

printf 'Checking for broken linkage...\n'

if [ "$PKG" ]; then
	# Single-package mode: sequential, with progress indicator
	count=0
	while read -r line; do
		count=$((count + 1))
		unset NEW_LIB_NAME
		libname=${line##*/}
		printf ' %d%% %s\033[0K\r' "$((100*count/total))" "$libname"

		case "$(file -bi "$line")" in
			*application/x-sharedlib* | *application/x-executable* | *application/x-pie-executable*)
				ldd_out=$(ldd "$line" 2>/dev/null)

				case "$ldd_out" in
				*"not found"*)
					LIB_NAME=$(printf '%s\n' "$ldd_out" | awk '/not found/ && !seen[$1]++{print $1}')

					# filter excluded libraries
					for l in $LIB_NAME; do
						case " $EXCLUDED_LIBS " in
							*" $l "*) ;;
							*) NEW_LIB_NAME="$NEW_LIB_NAME $l" ;;
						esac
					done

					[ "$NEW_LIB_NAME" ] || continue
					LIB_NAME=$NEW_LIB_NAME

					# Use the known package name directly
					PKG_NAME="$PKG"

					case " $expkg " in *" $PKG_NAME "*) continue ;;	esac

					# Get required libraries
					REQ_LIB=$(objdump -p "$line" 2>/dev/null | awk '/NEEDED/{printf "%s ", $2}')

					for i in $LIB_NAME; do
						[ "$PRINTALL" = 1 ] && printf ' %s -> %s (requires %s)\n' "$PKG_NAME" "$line" "$i"

						case " $REQ_LIB" in
						*" $i "*)
							[ "$PRINTALL" = 1 ] || printf ' %s -> %s (requires %s)\n' "$PKG_NAME" "$line" "$i"

							case " $ALLPKG " in
								*" $PKG_NAME "*) ;;
								*) ALLPKG="$ALLPKG $PKG_NAME" ;;
							esac
							;;
						esac
					done
					;;
				esac
				;;
		esac
	done < "$FILE_LIST"
	printf '\033[0K'
else
	# All-files mode: parallel via self-invocation, no temporary helper script
	NPROC=$(nproc 2>/dev/null) || NPROC=4
	export PKGDB_DIR EXCLUDED_LIBS PRINTALL
	xargs -P "$NPROC" -I {} "$SELF" --check-file {} < "$FILE_LIST" > "$TMP_OUT"

	# Collect and display results
	if [ -s "$TMP_OUT" ]; then
		while IFS='|' read -r pkg_name file lib; do
			case " $expkg " in
				*" $pkg_name "*) continue ;;
			esac
			printf ' %s -> %s (requires %s)\n' "$pkg_name" "$file" "$lib"
			case " $ALLPKG " in
				*" $pkg_name "*) ;;
				*) ALLPKG="$ALLPKG $pkg_name" ;;
			esac
		done < "$TMP_OUT"
	fi
fi

# Check for broken module packages
if [ -z "$PKG" ]; then
	printf '\nChecking for broken packages...\n'
	check_pythonmodules
	check_perlmodules
	check_rubygem
fi

# Consolidate broken packages
if [ "$brokenpkg" ]; then
	for i in $brokenpkg; do
		case " $expkg " in *" $i "*) continue ;; esac
		case " $ALLPKG " in
			*" $i "*) ;;
			*) ALLPKG="$ALLPKG $i" ;;
		esac
	done
fi

# Report and optionally rebuild
if [ "$ALLPKG" ]; then
	printf '\nBroken package(s):\n'
	LISTALLPKG=$(printf ' %s\n' $ALLPKG | awk '!seen[$0]++') # remove duplicates, preserve order
	printf '%s\n' "$LISTALLPKG"
	if [ "$REBUILD" = 1 ]; then rebuild; fi
else
	printf 'All packages are doing fine.\n'
fi

cleanup

exit 0
