#! /bin/bash
# SPDX-License-Identifier: GPL-2.0

. "$(dirname "${0}")/../lib.sh"

readonly KSFT_PASS=0
readonly KSFT_FAIL=1
readonly KSFT_SKIP=4

# shellcheck disable=SC2155 # declare and assign separately
readonly KSFT_TEST="${MPTCP_LIB_KSFT_TEST:-$(basename "${0}" .sh)}"

# These variables are used in some selftests, read-only
declare -rx MPTCP_LIB_EVENT_CREATED=1           # MPTCP_EVENT_CREATED
declare -rx MPTCP_LIB_EVENT_ESTABLISHED=2       # MPTCP_EVENT_ESTABLISHED
declare -rx MPTCP_LIB_EVENT_CLOSED=3            # MPTCP_EVENT_CLOSED
declare -rx MPTCP_LIB_EVENT_ANNOUNCED=6         # MPTCP_EVENT_ANNOUNCED
declare -rx MPTCP_LIB_EVENT_REMOVED=7           # MPTCP_EVENT_REMOVED
declare -rx MPTCP_LIB_EVENT_SUB_ESTABLISHED=10  # MPTCP_EVENT_SUB_ESTABLISHED
declare -rx MPTCP_LIB_EVENT_SUB_CLOSED=11       # MPTCP_EVENT_SUB_CLOSED
declare -rx MPTCP_LIB_EVENT_SUB_PRIORITY=13     # MPTCP_EVENT_SUB_PRIORITY
declare -rx MPTCP_LIB_EVENT_LISTENER_CREATED=15 # MPTCP_EVENT_LISTENER_CREATED
declare -rx MPTCP_LIB_EVENT_LISTENER_CLOSED=16  # MPTCP_EVENT_LISTENER_CLOSED

declare -rx MPTCP_LIB_AF_INET=2
declare -rx MPTCP_LIB_AF_INET6=10

MPTCP_LIB_SUBTESTS=()
MPTCP_LIB_SUBTESTS_DUPLICATED=0
MPTCP_LIB_SUBTEST_FLAKY=0
MPTCP_LIB_SUBTESTS_LAST_TS_NS=
MPTCP_LIB_TEST_COUNTER=0
MPTCP_LIB_TEST_FORMAT="%02u %-50s"
MPTCP_LIB_IP_MPTCP=0

# only if supported (or forced) and not disabled, see no-color.org
if { [ -t 1 ] || [ "${SELFTESTS_MPTCP_LIB_COLOR_FORCE:-}" = "1" ]; } &&
   [ "${NO_COLOR:-}" != "1" ]; then
	readonly MPTCP_LIB_COLOR_RED="\E[1;31m"
	readonly MPTCP_LIB_COLOR_GREEN="\E[1;32m"
	readonly MPTCP_LIB_COLOR_YELLOW="\E[1;33m"
	readonly MPTCP_LIB_COLOR_BLUE="\E[1;34m"
	readonly MPTCP_LIB_COLOR_RESET="\E[0m"
else
	readonly MPTCP_LIB_COLOR_RED=
	readonly MPTCP_LIB_COLOR_GREEN=
	readonly MPTCP_LIB_COLOR_YELLOW=
	readonly MPTCP_LIB_COLOR_BLUE=
	readonly MPTCP_LIB_COLOR_RESET=
fi

# SELFTESTS_MPTCP_LIB_OVERRIDE_FLAKY env var can be set not to ignore errors
# from subtests marked as flaky
mptcp_lib_override_flaky() {
	[ "${SELFTESTS_MPTCP_LIB_OVERRIDE_FLAKY:-}" = 1 ]
}

mptcp_lib_subtest_is_flaky() {
	[ "${MPTCP_LIB_SUBTEST_FLAKY}" = 1 ] && ! mptcp_lib_override_flaky
}

# $1: color, $2: text
mptcp_lib_print_color() {
	echo -e "${MPTCP_LIB_START_PRINT:-}${*}${MPTCP_LIB_COLOR_RESET}"
}

mptcp_lib_print_ok() {
	mptcp_lib_print_color "${MPTCP_LIB_COLOR_GREEN}${*}"
}

mptcp_lib_print_warn() {
	mptcp_lib_print_color "${MPTCP_LIB_COLOR_YELLOW}${*}"
}

mptcp_lib_print_info() {
	mptcp_lib_print_color "${MPTCP_LIB_COLOR_BLUE}${*}"
}

mptcp_lib_print_err() {
	mptcp_lib_print_color "${MPTCP_LIB_COLOR_RED}${*}"
}

# shellcheck disable=SC2120 # parameters are optional
mptcp_lib_pr_ok() {
	mptcp_lib_print_ok "[ OK ]${1:+ ${*}}"
}

mptcp_lib_pr_skip() {
	mptcp_lib_print_warn "[SKIP]${1:+ ${*}}"
}

mptcp_lib_pr_fail() {
	local title cmt

	if mptcp_lib_subtest_is_flaky; then
		title="IGNO"
		cmt=" (flaky)"
	else
		title="FAIL"
	fi

	mptcp_lib_print_err "[${title}]${cmt}${1:+ ${*}}"
}

mptcp_lib_pr_info() {
	mptcp_lib_print_info "INFO: ${*}"
}

# $1-2: listener/connector ns ; $3 port ; $4-5 listener/connector stat file
mptcp_lib_pr_err_stats() {
	local lns="${1}"
	local cns="${2}"
	local port="${3}"
	local lstat="${4}"
	local cstat="${5}"

	echo -en "${MPTCP_LIB_COLOR_RED}"
	{
		printf "\nnetns %s (listener) socket stat for %d:\n" "${lns}" "${port}"
		ip netns exec "${lns}" ss -Menitam -o "sport = :${port}"
		cat "${lstat}"

		printf "\nnetns %s (connector) socket stat for %d:\n" "${cns}" "${port}"
		ip netns exec "${cns}" ss -Menitam -o "dport = :${port}"
		[ "${lstat}" != "${cstat}" ] && cat "${cstat}"
	} 1>&2
	echo -en "${MPTCP_LIB_COLOR_RESET}"
}

# SELFTESTS_MPTCP_LIB_EXPECT_ALL_FEATURES env var can be set when validating all
# features using the last version of the kernel and the selftests to make sure
# a test is not being skipped by mistake.
mptcp_lib_expect_all_features() {
	[ "${SELFTESTS_MPTCP_LIB_EXPECT_ALL_FEATURES:-}" = "1" ]
}

# $1: msg
mptcp_lib_fail_if_expected_feature() {
	if mptcp_lib_expect_all_features; then
		echo "ERROR: missing feature: ${*}"
		exit ${KSFT_FAIL}
	fi

	return 1
}

# $1: file
mptcp_lib_has_file() {
	local f="${1}"

	if [ -f "${f}" ]; then
		return 0
	fi

	mptcp_lib_fail_if_expected_feature "${f} file not found"
}

mptcp_lib_check_mptcp() {
	if ! mptcp_lib_has_file "/proc/sys/net/mptcp/enabled"; then
		mptcp_lib_pr_skip "MPTCP support is not available"
		exit ${KSFT_SKIP}
	fi
}

mptcp_lib_check_kallsyms() {
	if ! mptcp_lib_has_file "/proc/kallsyms"; then
		mptcp_lib_pr_skip "CONFIG_KALLSYMS is missing"
		exit ${KSFT_SKIP}
	fi
}

# Internal: use mptcp_lib_kallsyms_has() instead
__mptcp_lib_kallsyms_has() {
	local sym="${1}"

	mptcp_lib_check_kallsyms

	grep -q " ${sym}" /proc/kallsyms
}

# $1: part of a symbol to look at, add '$' at the end for full name
mptcp_lib_kallsyms_has() {
	local sym="${1}"

	if __mptcp_lib_kallsyms_has "${sym}"; then
		return 0
	fi

	mptcp_lib_fail_if_expected_feature "${sym} symbol not found"
}

# $1: part of a symbol to look at, add '$' at the end for full name
mptcp_lib_kallsyms_doesnt_have() {
	local sym="${1}"

	if ! __mptcp_lib_kallsyms_has "${sym}"; then
		return 0
	fi

	mptcp_lib_fail_if_expected_feature "${sym} symbol has been found"
}

# !!!AVOID USING THIS!!!
# Features might not land in the expected version and features can be backported
#
# $1: kernel version, e.g. 6.3
mptcp_lib_kversion_ge() {
	local exp_maj="${1%.*}"
	local exp_min="${1#*.}"
	local v maj min

	# If the kernel has backported features, set this env var to 1:
	if [ "${SELFTESTS_MPTCP_LIB_NO_KVERSION_CHECK:-}" = "1" ]; then
		return 0
	fi

	v=$(uname -r | cut -d'.' -f1,2)
	maj=${v%.*}
	min=${v#*.}

	if   [ "${maj}" -gt "${exp_maj}" ] ||
	   { [ "${maj}" -eq "${exp_maj}" ] && [ "${min}" -ge "${exp_min}" ]; }; then
		return 0
	fi

	mptcp_lib_fail_if_expected_feature "kernel version ${1} lower than ${v}"
}

mptcp_lib_subtests_last_ts_reset() {
	MPTCP_LIB_SUBTESTS_LAST_TS_NS="$(date +%s%N)"
}
mptcp_lib_subtests_last_ts_reset

__mptcp_lib_result_check_duplicated() {
	local subtest

	for subtest in "${MPTCP_LIB_SUBTESTS[@]}"; do
		if [[ "${subtest}" == *" - ${KSFT_TEST}: ${*%% #*}" ]]; then
			MPTCP_LIB_SUBTESTS_DUPLICATED=1
			mptcp_lib_print_err "Duplicated entry: ${*}"
			break
		fi
	done
}

__mptcp_lib_result_add() {
	local result="${1}"
	local time="time="
	local ts_prev_ns
	shift

	local id=$((${#MPTCP_LIB_SUBTESTS[@]} + 1))

	__mptcp_lib_result_check_duplicated "${*}"

	# not to add two '#'
	[[ "${*}" != *"#"* ]] && time="# ${time}"

	ts_prev_ns="${MPTCP_LIB_SUBTESTS_LAST_TS_NS}"
	mptcp_lib_subtests_last_ts_reset
	time+="$(((MPTCP_LIB_SUBTESTS_LAST_TS_NS - ts_prev_ns) / 1000000))ms"

	MPTCP_LIB_SUBTESTS+=("${result} ${id} - ${KSFT_TEST}: ${*} ${time}")
}

# $1: test name
mptcp_lib_result_pass() {
	__mptcp_lib_result_add "ok" "${1}"
}

# $1: test name
mptcp_lib_result_fail() {
	if mptcp_lib_subtest_is_flaky; then
		# It might sound better to use 'not ok # TODO' or 'ok # SKIP',
		# but some CIs don't understand 'TODO' and treat SKIP as errors.
		__mptcp_lib_result_add "ok" "${1} # IGNORE Flaky"
	else
		__mptcp_lib_result_add "not ok" "${1}"
	fi
}

# $1: test name
mptcp_lib_result_skip() {
	__mptcp_lib_result_add "ok" "${1} # SKIP"
}

# $1: result code ; $2: test name
mptcp_lib_result_code() {
	local ret="${1}"
	local name="${2}"

	case "${ret}" in
		"${KSFT_PASS}")
			mptcp_lib_result_pass "${name}"
			;;
		"${KSFT_FAIL}")
			mptcp_lib_result_fail "${name}"
			;;
		"${KSFT_SKIP}")
			mptcp_lib_result_skip "${name}"
			;;
		*)
			echo "ERROR: wrong result code: ${ret}"
			exit ${KSFT_FAIL}
			;;
	esac
}

mptcp_lib_result_print_all_tap() {
	local subtest

	if [ ${#MPTCP_LIB_SUBTESTS[@]} -eq 0 ] ||
	   [ "${SELFTESTS_MPTCP_LIB_NO_TAP:-}" = "1" ]; then
		return
	fi

	printf "\nTAP version 13\n"
	printf "1..%d\n" "${#MPTCP_LIB_SUBTESTS[@]}"

	for subtest in "${MPTCP_LIB_SUBTESTS[@]}"; do
		printf "%s\n" "${subtest}"
	done

	if [ "${MPTCP_LIB_SUBTESTS_DUPLICATED}" = 1 ] &&
	   mptcp_lib_expect_all_features; then
		mptcp_lib_print_err "Duplicated test entries"
		exit ${KSFT_FAIL}
	fi
}

# get the value of keyword $1 in the line marked by keyword $2
mptcp_lib_get_info_value() {
	grep "${2}" 2>/dev/null |
		sed -n 's/.*\('"${1}"':\)\([0-9a-f:.]*\).*$/\2/p;q'
		# the ';q' at the end limits to the first matched entry.
}

# $1: info name ; $2: evts_ns ; [$3: event type; [$4: addr]]
mptcp_lib_evts_get_info() {
	grep "${4:-}" "${2}" 2>/dev/null |
		mptcp_lib_get_info_value "${1}" "^type:${3:-1},"
}

# $1: PID
mptcp_lib_kill_wait() {
	[ "${1}" -eq 0 ] && return 0

	kill -SIGUSR1 "${1}" > /dev/null 2>&1
	kill "${1}" > /dev/null 2>&1
	wait "${1}" 2>/dev/null
}

# $1: PID
mptcp_lib_pid_list_children() {
	local curr="${1}"
	# evoke 'ps' only once
	local pids="${2:-"$(ps o pid,ppid)"}"

	echo "${curr}"

	local pid
	for pid in $(echo "${pids}" | awk "\$2 == ${curr} { print \$1 }"); do
		mptcp_lib_pid_list_children "${pid}" "${pids}"
	done
}

# $1: PID
mptcp_lib_kill_group_wait() {
	# Some users might not have procps-ng: cannot use "kill -- -PID"
	mptcp_lib_pid_list_children "${1}" | xargs -r kill &>/dev/null
	wait "${1}" 2>/dev/null
}

# $1: IP address
mptcp_lib_is_v6() {
	[ -z "${1##*:*}" ]
}

# $1: ns, $2: MIB counter
mptcp_lib_get_counter() {
	local ns="${1}"
	local counter="${2}"
	local count

	count=$(ip netns exec "${ns}" nstat -asz "${counter}" |
		awk 'NR==1 {next} {print $2}')
	if [ -z "${count}" ]; then
		mptcp_lib_fail_if_expected_feature "${counter} counter"
		return 1
	fi

	echo "${count}"
}

mptcp_lib_make_file() {
	local name="${1}"
	local bs="${2}"
	local size="${3}"

	dd if=/dev/urandom of="${name}" bs="${bs}" count="${size}" 2> /dev/null
	echo -e "\nMPTCP_TEST_FILE_END_MARKER" >> "${name}"
}

# $1: file
mptcp_lib_print_file_err() {
	ls -l "${1}" 1>&2
	echo "Trailing bytes are: "
	tail -c 32 "${1}" | od -x | head -n2
}

# $1: input file ; $2: output file ; $3: what kind of file
mptcp_lib_check_transfer() {
	local in="${1}"
	local out="${2}"
	local what="${3}"

	if ! cmp "$in" "$out" > /dev/null 2>&1; then
		mptcp_lib_pr_fail "$what does not match (in, out):"
		mptcp_lib_print_file_err "$in"
		mptcp_lib_print_file_err "$out"

		return 1
	fi

	return 0
}

# $1: ns, $2: port
mptcp_lib_wait_local_port_listen() {
	wait_local_port_listen "${@}" "tcp"
}

# $1: error file, $2: cmd, $3: expected msg, [$4: expected error]
mptcp_lib_check_output() {
	local err="${1}"
	local cmd="${2}"
	local expected="${3}"
	local exp_error="${4:-0}"
	local cmd_ret=0
	local out

	out=$(${cmd} 2>"${err}") || cmd_ret=1

	if [ "${cmd_ret}" != "${exp_error}" ]; then
		mptcp_lib_pr_fail "unexpected returned code for '${cmd}', info:"
		if [ "${exp_error}" = 0 ]; then
			cat "${err}"
		else
			echo "${out}"
		fi
		return 2
	elif [ "${out}" = "${expected}" ]; then
		return 0
	else
		mptcp_lib_pr_fail "expected '${expected}' got '${out}'"
		return 1
	fi
}

mptcp_lib_check_tools() {
	local tool

	for tool in "${@}"; do
		case "${tool}" in
		"ip")
			if ! ip -Version &> /dev/null; then
				mptcp_lib_pr_skip "Could not run test without ip tool"
				exit ${KSFT_SKIP}
			fi
			;;
		"tc")
			if ! tc -help &> /dev/null; then
				mptcp_lib_pr_skip "Could not run test without tc tool"
				exit ${KSFT_SKIP}
			fi
			;;
		"ss")
			if ! ss -h | grep -q MPTCP; then
				mptcp_lib_pr_skip "ss tool does not support MPTCP"
				exit ${KSFT_SKIP}
			fi
			;;
		"iptables"* | "ip6tables"*)
			if ! "${tool}" -V &> /dev/null; then
				mptcp_lib_pr_skip "Could not run all tests without ${tool}"
				exit ${KSFT_SKIP}
			fi
			;;
		*)
			mptcp_lib_pr_fail "Internal error: unsupported tool: ${tool}"
			exit ${KSFT_FAIL}
			;;
		esac
	done
}

mptcp_lib_ns_init() {
	if ! setup_ns "${@}"; then
		mptcp_lib_pr_fail "Failed to setup namespaces ${*}"
		exit ${KSFT_FAIL}
	fi

	local netns
	for netns in "${@}"; do
		ip netns exec "${!netns}" sysctl -q net.mptcp.enabled=1
	done
}

mptcp_lib_ns_exit() {
	cleanup_ns "${@}"

	local netns
	for netns in "${@}"; do
		rm -f /tmp/"${netns}".{nstat,out}
	done
}

mptcp_lib_events() {
	local ns="${1}"
	local evts="${2}"
	declare -n pid="${3}"

	:>"${evts}"

	mptcp_lib_kill_wait "${pid:-0}"
	ip netns exec "${ns}" ./pm_nl_ctl events >> "${evts}" 2>&1 &
	pid=$!
}

mptcp_lib_print_title() {
	: "${MPTCP_LIB_TEST_COUNTER:?}"
	: "${MPTCP_LIB_TEST_FORMAT:?}"

	# shellcheck disable=SC2059 # the format is in a variable
	printf "${MPTCP_LIB_TEST_FORMAT}" "$((++MPTCP_LIB_TEST_COUNTER))" "${*}"
}

# $1: var name ; $2: prev ret
mptcp_lib_check_expected_one() {
	local var="${1}"
	local exp="e_${var}"
	local prev_ret="${2}"

	if [ "${!var}" = "${!exp}" ]; then
		return 0
	fi

	if [ "${prev_ret}" = "0" ]; then
		mptcp_lib_pr_fail
	fi

	mptcp_lib_print_err "Expected value for '${var}': '${!exp}', got '${!var}'."
	return 1
}

# $@: all var names to check
mptcp_lib_check_expected() {
	local rc=0
	local var

	for var in "${@}"; do
		mptcp_lib_check_expected_one "${var}" "${rc}" || rc=1
	done

	return "${rc}"
}

# shellcheck disable=SC2034 # Some variables are used below but indirectly
mptcp_lib_verify_listener_events() {
	local evt=${1}
	local e_type=${2}
	local e_family=${3}
	local e_saddr=${4}
	local e_sport=${5}
	local type
	local family
	local saddr
	local sport
	local rc=0

	type=$(mptcp_lib_evts_get_info type "${evt}" "${e_type}")
	family=$(mptcp_lib_evts_get_info family "${evt}" "${e_type}")
	if [ "${family}" ] && [ "${family}" = "${AF_INET6}" ]; then
		saddr=$(mptcp_lib_evts_get_info saddr6 "${evt}" "${e_type}")
	else
		saddr=$(mptcp_lib_evts_get_info saddr4 "${evt}" "${e_type}")
	fi
	sport=$(mptcp_lib_evts_get_info sport "${evt}" "${e_type}")

	mptcp_lib_check_expected "type" "family" "saddr" "sport" || rc="${?}"
	return "${rc}"
}

mptcp_lib_set_ip_mptcp() {
	MPTCP_LIB_IP_MPTCP=1
}

mptcp_lib_is_ip_mptcp() {
	[ "${MPTCP_LIB_IP_MPTCP}" = "1" ]
}

# format: <id>,<ip>,<flags>,<dev>
mptcp_lib_pm_nl_format_endpoints() {
	local entry id ip flags dev port

	for entry in "${@}"; do
		IFS=, read -r id ip flags dev port <<< "${entry}"
		if mptcp_lib_is_ip_mptcp; then
			echo -n "${ip}"
			[ -n "${port}" ] && echo -n " port ${port}"
			echo -n " id ${id}"
			[ -n "${flags}" ] && echo -n " ${flags}"
			[ -n "${dev}" ] && echo -n " dev ${dev}"
			echo " " # always a space at the end
		else
			echo -n "id ${id}"
			echo -n " flags ${flags//" "/","}"
			[ -n "${dev}" ] && echo -n " dev ${dev}"
			echo -n " ${ip}"
			[ -n "${port}" ] && echo -n " ${port}"
			echo ""
		fi
	done
}

mptcp_lib_pm_nl_get_endpoint() {
	local ns=${1}
	local id=${2}

	if mptcp_lib_is_ip_mptcp; then
		ip -n "${ns}" mptcp endpoint show id "${id}"
	else
		ip netns exec "${ns}" ./pm_nl_ctl get "${id}"
	fi
}

mptcp_lib_pm_nl_set_limits() {
	local ns=${1}
	local addrs=${2}
	local subflows=${3}

	if mptcp_lib_is_ip_mptcp; then
		ip -n "${ns}" mptcp limits set add_addr_accepted "${addrs}" subflows "${subflows}"
	else
		ip netns exec "${ns}" ./pm_nl_ctl limits "${addrs}" "${subflows}"
	fi
}

mptcp_lib_pm_nl_add_endpoint() {
	local ns=${1}
	local addr=${2}
	local flags dev id port
	local nr=2

	local p
	for p in "${@}"; do
		case "${p}" in
		"flags" | "dev" | "id" | "port")
			eval "${p}"=\$"${nr}"
			;;
		esac

		nr=$((nr + 1))
	done

	if mptcp_lib_is_ip_mptcp; then
		# shellcheck disable=SC2086 # blanks in flags, no double quote
		ip -n "${ns}" mptcp endpoint add "${addr}" ${flags//","/" "} \
			${dev:+dev "${dev}"} ${id:+id "${id}"} ${port:+port "${port}"}
	else
		ip netns exec "${ns}" ./pm_nl_ctl add "${addr}" ${flags:+flags "${flags}"} \
			${dev:+dev "${dev}"} ${id:+id "${id}"} ${port:+port "${port}"}
	fi
}

mptcp_lib_pm_nl_del_endpoint() {
	local ns=${1}
	local id=${2}
	local addr=${3}

	if mptcp_lib_is_ip_mptcp; then
		[ "${id}" -ne 0 ] && addr=''
		ip -n "${ns}" mptcp endpoint delete id "${id}" ${addr:+"${addr}"}
	else
		ip netns exec "${ns}" ./pm_nl_ctl del "${id}" "${addr}"
	fi
}

mptcp_lib_pm_nl_flush_endpoint() {
	local ns=${1}

	if mptcp_lib_is_ip_mptcp; then
		ip -n "${ns}" mptcp endpoint flush
	else
		ip netns exec "${ns}" ./pm_nl_ctl flush
	fi
}

mptcp_lib_pm_nl_show_endpoints() {
	local ns=${1}

	if mptcp_lib_is_ip_mptcp; then
		ip -n "${ns}" mptcp endpoint show
	else
		ip netns exec "${ns}" ./pm_nl_ctl dump
	fi
}

mptcp_lib_pm_nl_change_endpoint() {
	local ns=${1}
	local id=${2}
	local flags=${3}

	if mptcp_lib_is_ip_mptcp; then
		# shellcheck disable=SC2086 # blanks in flags, no double quote
		ip -n "${ns}" mptcp endpoint change id "${id}" ${flags//","/" "}
	else
		ip netns exec "${ns}" ./pm_nl_ctl set id "${id}" flags "${flags}"
	fi
}
