#!/bin/sh

# Extract kernel header paths from xlat files that contain
# "#Generated by maint/gen_xlat_defs.sh",
# "#Generated by maint/enum2xlat.sh", or "#From" annotations
#
# Copyright (c) 2025 The strace developers.
# All rights reserved.
#
# SPDX-License-Identifier: LGPL-2.1-or-later

# Usage: list-xlat-linux-headers.sh -d LINUX_SRC \
#	xlat_file1.in [xlat_file2.in] ...

set -eu

error()
{
	printf '%s: error: %s\n' "$0" "$*" >&2
}

warning()
{
	printf '%s: warning: %s\n' "$0" "$*" >&2
}

fatal()
{
	error "$@"
	exit 1
}

usage()
{
	rc=0
	if [ $# -gt 0 ]; then
		error "$@"
		exec >&2
		rc=1
	fi
	cat <<-EOF
		Usage: $0 -d LINUX_SRC [-t] [-c COMMIT_RANGE] \\
			xlat_file1.in [xlat_file2.in] ...

		-d LINUX_SRC    Path to Linux kernel source directory
				(mandatory)
		-t              Table output format: prints
				xlat_file<TAB>line_type<TAB>header_file
				for each header (default: header file only)
		-c COMMIT_RANGE Filter table to only include headers that
				changed between commits (requires -t)
				(e.g., COMMIT1..COMMIT2)

		The script reads xlat files specified as arguments,
		looks for lines starting with
		"#Generated by maint/gen_xlat_defs.sh",
		"#Generated by maint/enum2xlat.sh", or "#From",
		and prints the list of Linux kernel headers that
		would be used.
	EOF
	exit $rc
}

LINUX_SRC=
TABLE=
COMMIT_RANGE=

while [ $# -gt 0 ]; do
	case "$1" in
		-d)
			shift
			[ $# -gt 0 ] ||
				usage "-d requires an argument"
			LINUX_SRC="$1"
			;;
		-t)
			TABLE=1
			;;
		-c)
			shift
			[ $# -gt 0 ] ||
				usage "-c requires an argument"
			COMMIT_RANGE="$1"
			;;
		-h|--help)
			usage
			;;
		-*)
			usage "unknown option: $1"
			;;
		*)
			break
			;;
	esac
	shift
done

[ $# -gt 0 ] ||
	usage "at least one xlat file must be specified"
[ -n "$LINUX_SRC" ] ||
	usage "-d LINUX_SRC is mandatory"
[ -d "$LINUX_SRC" ] ||
	fatal "Linux source directory does not exist: $LINUX_SRC"
[ -d "$LINUX_SRC/include/uapi" ] ||
	fatal "$LINUX_SRC/include/uapi does not exist"

# Validate -c option
if [ -n "$COMMIT_RANGE" ]; then
	[ -n "$TABLE" ] ||
		fatal "-c option requires -t option"

	# Validate commit range format
	case "$COMMIT_RANGE" in
		*...*)
			fatal "Three-dot syntax (COMMIT1...COMMIT2) is not supported"
			;;
		*..*)
			# Valid: two-dot range syntax
			;;
		*)
			fatal "Single commit is not supported, use COMMIT1..COMMIT2"
			;;
	esac

	# Validate commit range endpoints
	COMMIT1="${COMMIT_RANGE%%..*}"
	COMMIT2="${COMMIT_RANGE#*..}"

	cd "$LINUX_SRC" || exit 1

	git rev-parse "$COMMIT1^{commit}" >/dev/null ||
		fatal "Commit $COMMIT1 not found in $LINUX_SRC"

	git rev-parse "$COMMIT2^{commit}" >/dev/null ||
		fatal "Commit $COMMIT2 not found in $LINUX_SRC"

	cd - > /dev/null || exit 1
fi

# Expand a pattern (glob or plain filename) and print matching header files
# Arguments:
#   $1: pattern to expand (may contain wildcards or be a plain filename)
expand_pattern()
{
	local pattern="$1"
	local header_path

	# Check if pattern contains wildcards
	case "$pattern" in
		*[*?]*)
			# Pattern has wildcards, skip if expansion failed
			for header_path in $pattern; do
				# Check if header_path is the literal glob pattern
				# (i.e., expansion failed)
				[ "$header_path" = "$pattern" ] && continue
				# Process valid match
				[ -f "$header_path" ] || continue
				echo "$header_path"
			done | sort
			;;
		*)
			# No wildcards, just check if file exists
			[ -f "$pattern" ] && echo "$pattern"
			;;
	esac
}

# Function to find common headers matching a glob pattern
find_common_headers()
{
	local glob="$1"
	local uapi_dir="include/uapi"

	expand_pattern "$uapi_dir/$glob"
}

# Function to find arch-specific headers
find_arch_headers()
{
	local arch_file="$1"

	expand_pattern "arch/*/include/uapi/$arch_file"
}

# Process a line generated by gen_xlat_defs.sh
process_gen_xlat_defs_line()
{
	local line="$1"
	local xlat_file="$2"
	local common_glob arch_file header_file

	# Extract the options from the line using sed
	# Format: #Generated by maint/gen_xlat_defs.sh \
	#	-f 'FORMAT' -p 'PATTERN' \
	#	-c 'COMMON_GLOB' -a 'ARCH_FILE'

	# Extract -c option (COMMON_DEFS_GLOB_PATTERN)
	common_glob=$(echo "$line" |
		sed -n "s/.*-c[[:space:]]*'\([^']*\)'.*/\1/p")
	if [ -n "$common_glob" ]; then
		# Expand and print common headers
		find_common_headers "$common_glob" |
		while IFS= read -r header_file; do
			if [ -n "$header_file" ]; then
				if [ -n "$TABLE" ]; then
					printf '%s\t%s\t%s\n' \
						"$xlat_file" "gxd" "$header_file"
				else
					echo "$header_file"
				fi
			fi
		done ||:
	fi

	# Extract -a option (ARCH_DEFS_FILE)
	arch_file=$(echo "$line" |
		sed -n "s/.*-a[[:space:]]*'\([^']*\)'.*/\1/p")
	if [ -n "$arch_file" ]; then
		# Find and print arch-specific headers
		find_arch_headers "$arch_file" |
		while IFS= read -r header_file; do
			if [ -n "$header_file" ]; then
				if [ -n "$TABLE" ]; then
					printf '%s\t%s\t%s\n' \
						"$xlat_file" "gxd" "$header_file"
				else
					echo "$header_file"
				fi
			fi
		done ||:
	fi
}

# Process a line generated by enum2xlat.sh
process_enum2xlat_line()
{
	local line="$1"
	local xlat_file="$2"
	local header_file header_path

	# Format: #Generated by maint/enum2xlat.sh from
	#	"enum ENUM_NAME" in
	#	bundled/linux/include/uapi/...; do not edit.
	# Extract file path from "in" clause
	header_file=$(echo "$line" |
		sed -n "s/.* in bundled\/linux\/\(.*\); do not edit\./\1/p")
	if [ -n "$header_file" ]; then
		# Check if file exists and print it
		# header_file already includes include/uapi/ prefix
		if [ -f "$header_file" ]; then
			if [ -n "$TABLE" ]; then
				printf '%s\t%s\t%s\n' \
					"$xlat_file" "enum" "$header_file"
			else
				echo "$header_file"
			fi
		fi
	fi
}

# Process a #From annotation line
process_from_line()
{
	local line="$1"
	local xlat_file="$2"
	local header_file

	# Format: #From <path>
	# Extract the path after "#From " (space is mandatory)
	header_file=$(echo "$line" | sed -n 's/^#From \(.*\)/\1/p')
	if [ -n "$header_file" ]; then
		# header_file is relative to current directory (LINUX_SRC)
		if [ -f "$header_file" ]; then
			if [ -n "$TABLE" ]; then
				printf '%s\t%s\t%s\n' \
					"$xlat_file" "from" "$header_file"
			else
				echo "$header_file"
			fi
		fi
	fi
}

# Process a single line from an xlat file
process_line()
{
	local line="$1"
	local file="$2"

	case "$line" in
		"#Generated by maint/gen_xlat_defs.sh "*)
			process_gen_xlat_defs_line "$line" "$file"
			;;
		"#Generated by maint/enum2xlat.sh "*)
			process_enum2xlat_line "$line" "$file"
			;;
		"#From "*)
			process_from_line "$line" "$file"
			;;
	esac
}

# Process files
process_file()
{
	local file="$1"
	local pattern

	[ -f "$file" ] || {
		warning "file not found: $file"
		return 1
	}

	# Process each generated-by or #From line
	# Space after ".sh" and after "From" is mandatory
	pattern='^#(Generated by maint/(gen_xlat_defs|enum2xlat)\.sh|From) '
	grep -E "$pattern" "$file" | {
		cd "$LINUX_SRC" &&
		while IFS= read -r line; do
			process_line "$line" "$file"
		done ||:
	}
}

# Process all input files and output sorted unique headers
process_all_files()
{
	for file; do
		process_file "$file" ||:
	done |
		sort -u
}

# Process input files, collecting all headers and deduplicating
if [ -z "$COMMIT_RANGE" ]; then
	# Normal processing
	process_all_files "$@"
else
	# Generate table and filter by changed headers
	table_file=
	changed_file=
	cleanup()
	{
		trap - EXIT HUP PIPE INT QUIT TERM
		[ -z "$table_file" ] || rm -f -- "$table_file"
		[ -z "$changed_file" ] || rm -f -- "$changed_file"
		exit "$@"
	}

	trap 'cleanup $?' EXIT
	trap 'cleanup 1' HUP PIPE INT QUIT TERM

	# Use temporary file to collect table output
	table_file=$(mktemp)
	process_all_files "$@" > "$table_file"

	# Filter by changed headers
	cd "$LINUX_SRC" || exit 1

	# Extract unique header files from column 3 and find which changed
	# Use temporary file instead of variable to avoid storing large output
	changed_file=$(mktemp)
	cut -f3 < "$table_file" | sort -u |
		xargs git diff --name-only "$COMMIT_RANGE" -- |
		sort -u > "$changed_file"

	cd - > /dev/null || exit 1

	# Join table with changed headers list
	sort -t $'\t' -k3 "$table_file" |
		join -t $'\t' -1 3 -2 1 -o 1.1,1.2,1.3 - "$changed_file"
fi
