#!/bin/bash
# SPDX-License-Identifier: GPL-3.0+
# Copyright (C) 2018 Western Digital Corporation or its affiliates.
#
# Tests for Zone Block Device.

. common/rc
. common/null_blk
. common/dm

#
# Test requirement check functions
#

group_requires() {
	_have_root && _have_program blkzone && _have_program dd &&
		_have_kernel_option BLK_DEV_ZONED && _have_null_blk &&
		_have_module_param null_blk zoned
}

group_device_requires() {
	if ! _test_dev_is_zoned; then
		SKIP_REASONS+=("${TEST_DEV} is not a zoned block device")
		return
	fi
}

_fallback_null_blk_zoned() {
	if ! _configure_null_blk nullb1 zone_size=4 size=1024 zoned=1 \
			power=1; then
		return 1
	fi
	echo /dev/nullb1
}

#
# Zone types and conditions
#
export ZONE_TYPE_CONVENTIONAL=1
export ZONE_TYPE_SEQ_WRITE_REQUIRED=2
export ZONE_TYPE_SEQ_WRITE_PREFERRED=3

export ZONE_COND_EMPTY=1
export ZONE_COND_IMPLICIT_OPEN=2
export ZONE_COND_CLOSED=4
export ZONE_COND_READ_ONLY=13
export ZONE_COND_FULL=14
export ZONE_COND_OFFLINE=15

export ZONE_TYPE_ARRAY=(
	[1]="CONVENTIONAL"
	[2]="SEQ_WRITE_REQUIRED"
	[3]="SEQ_WRITE_PREFERRED"
)

export ZONE_COND_ARRAY=(
	[0]="NOT_WP"
	[1]="EMPTY"
	[2]="IMPLICIT_OPEN"
	[3]="EXPLICIT_OPEN"
	[4]="CLOSE"
	[13]="READ_ONLY"
	[14]="FULL"
	[15]="OFFLINE"
)

# sysfs variable array indices
export SV_CAPACITY=0
export SV_CHUNK_SECTORS=1
export SV_PHYS_BLK_SIZE=2
export SV_PHYS_BLK_SECTORS=3
export SV_NR_ZONES=4

#
# Helper functions
#

# Obtain zone related sysfs variables and keep in a global array until put
# function call.
_get_sysfs_variable() {
	unset SYSFS_VARS
	local _dir=${TEST_DEV_SYSFS}
	if _test_dev_is_partition; then
		SYSFS_VARS[SV_CAPACITY]=$(<"${TEST_DEV_PART_SYSFS}"/size)
	else
		SYSFS_VARS[SV_CAPACITY]=$(<"${_dir}"/size)
	fi
	SYSFS_VARS[SV_CHUNK_SECTORS]=$(<"${_dir}"/queue/chunk_sectors)
	SYSFS_VARS[SV_PHYS_BLK_SIZE]=$(<"${_dir}"/queue/physical_block_size)
	SYSFS_VARS[SV_PHYS_BLK_SECTORS]=$((SYSFS_VARS[SV_PHYS_BLK_SIZE] / 512))

	# If the nr_zones sysfs attribute exists, get its value. Otherwise,
	# calculate its value based on the total capacity and zone size, taking
	# into account that the last zone can be smaller than other zones.
	if [[ -e "${_dir}"/queue/nr_zones ]] && ! _test_dev_is_partition; then
		SYSFS_VARS[SV_NR_ZONES]=$(<"${_dir}"/queue/nr_zones)
	else
		SYSFS_VARS[SV_NR_ZONES]=$(( (SYSFS_VARS[SV_CAPACITY] - 1) \
				/ SYSFS_VARS[SV_CHUNK_SECTORS] + 1 ))
	fi
}

_put_sysfs_variable() {
	unset SYSFS_VARS
}

# Issue zone report command and keep reported information in global arrays
# until put function call.
_get_blkzone_report() {
	local target_dev=${1}
	local cap_idx wptr_idx conds_idx type_idx

	# Initialize arrays to store parsed blkzone reports.
	# Number of reported zones is set in REPORTED_COUNT.
	# The arrays have REPORTED_COUNT+1 elements with additional one at tail
	# to simplify loop operation.
	ZONE_STARTS=()
	ZONE_LENGTHS=()
	ZONE_CAPS=()
	ZONE_WPTRS=()
	ZONE_CONDS=()
	ZONE_TYPES=()
	NR_CONV_ZONES=0
	REPORTED_COUNT=0

	TMP_REPORT_FILE=${TMPDIR}/blkzone_report
	if ! blkzone report "${target_dev}" > "${TMP_REPORT_FILE}"; then
		echo "blkzone command failed"
		return 1
	fi

	cap_idx=3
	wptr_idx=5
	conds_idx=11
	type_idx=13
	if grep -qe "cap 0x" "${TMP_REPORT_FILE}"; then
		cap_idx=5
		wptr_idx=7
		conds_idx=13
		type_idx=15
	fi

	local _IFS=$IFS
	local -i loop=0
	IFS=$' ,:'
	while read -r -a _tokens
	do
		ZONE_STARTS+=($((_tokens[1])))
		ZONE_LENGTHS+=($((_tokens[3])))
		ZONE_CAPS+=($((_tokens[cap_idx])))
		ZONE_WPTRS+=($((_tokens[wptr_idx])))
		ZONE_CONDS+=($((${_tokens[conds_idx]%\(*})))
		ZONE_TYPES+=($((${_tokens[type_idx]%\(*})))
		if [[ ${ZONE_TYPES[-1]} -eq ${ZONE_TYPE_CONVENTIONAL} ]]; then
			(( NR_CONV_ZONES++ ))
		fi
		(( loop++ ))
	done < "${TMP_REPORT_FILE}"
	IFS="$_IFS"
	REPORTED_COUNT=${loop}

	if [[ ${REPORTED_COUNT} -eq 0 ]] ; then
		echo "blkzone report returned no zone"
		return 1
	fi

	# Set value to allow additional element access at array end
	local -i max_idx=$((REPORTED_COUNT - 1))
	ZONE_STARTS+=( $((ZONE_STARTS[max_idx] + ZONE_LENGTHS[max_idx])) )
	ZONE_LENGTHS+=( "${ZONE_LENGTHS[max_idx]}" )
	ZONE_CAPS+=( "${ZONE_CAPS[max_idx]}" )
	ZONE_WPTRS+=( "${ZONE_WPTRS[max_idx]}" )
	ZONE_CONDS+=( "${ZONE_CONDS[max_idx]}" )
	ZONE_TYPES+=( "${ZONE_TYPES[max_idx]}" )

	rm -f "${TMP_REPORT_FILE}"
}

_put_blkzone_report() {
	unset ZONE_STARTS
	unset ZONE_LENGTHS
	unset ZONE_CAPS
	unset ZONE_WPTRS
	unset ZONE_CONDS
	unset ZONE_TYPES
	unset REPORTED_COUNT
	unset NR_CONV_ZONES
}

# Issue reset zone command with zone count option.
# Call _get_blkzone_report() beforehand.
_reset_zones() {
	local target_dev=${1}
	local -i idx=${2}
	local -i count=${3}

	if ! blkzone reset -o "${ZONE_STARTS[idx]}" -c "${count}" \
	     "${target_dev}" >> "$FULL" 2>&1 ; then
		echo "blkzone reset command failed"
		return 1
	fi
}

_find_first_sequential_zone() {
	for ((idx =  NR_CONV_ZONES; idx < REPORTED_COUNT; idx++)); do
		if [[ ${ZONE_TYPES[idx]} -eq ${ZONE_TYPE_SEQ_WRITE_REQUIRED} ]];
		then
			echo "${idx}"
			return 0
		fi
	done
	echo "Sequential write required zone not found"

	return 1
}

_find_last_sequential_zone() {
	for ((idx = REPORTED_COUNT - 1; idx > 0; idx--)); do
		if ((ZONE_TYPES[idx] == ZONE_TYPE_SEQ_WRITE_REQUIRED)); then
			echo "${idx}"
			return 0
		fi
	done

	echo "-1"
	return 1
}

# Try to find a sequential required zone between given two zone indices
_find_sequential_zone_in_middle() {
	local -i s=${1}
	local -i e=${2}
	local -i idx
	local -i i=1

	if ((s < 0 || e >= REPORTED_COUNT || e <= s + 1)); then
		echo "Invalid arguments: ${s} ${e}"
		return 1
	fi

	idx=$(((s + e) / 2))

	while ((idx != s && idx != e)); do
		if ((ZONE_TYPES[idx] == ZONE_TYPE_SEQ_WRITE_REQUIRED)); then
			echo "${idx}"
			return 0
		fi
		if ((i%2 == 0)); then
			((idx += i))
		else
			((idx -= i))
		fi
		((i++))
	done

	echo "-1"
	return 1
}

# Search zones and find two contiguous sequential write required zones.
# Return index of the first zone of the found two zones.
# When the argument cap_eq_len is specified, find the two contiguous
# sequential write required zones with first zone that has zone capacity
# same as zone size.
# Call _get_blkzone_report() beforehand.
_find_two_contiguous_seq_zones() {
	local cap_eq_len="${1}"
	local -i type_seq=${ZONE_TYPE_SEQ_WRITE_REQUIRED}

	for ((idx = NR_CONV_ZONES; idx < REPORTED_COUNT; idx++)); do
		if [[ ${ZONE_TYPES[idx]} -eq ${type_seq} &&
		      ${ZONE_TYPES[idx+1]} -eq ${type_seq} ]]; then
			if [[ ${cap_eq_len} = "cap_eq_len" ]] &&
				   ((ZONE_CAPS[idx] != ZONE_LENGTHS[idx])); then
				continue
			fi
			echo "${idx}"
			return 0
		fi
	done

	return 1
}

_require_test_dev_is_logical() {
	if ! _test_dev_is_partition && ! _test_dev_is_dm; then
		SKIP_REASONS+=("$TEST_DEV is not a logical device")
		return 1
	fi
	return 0
}

_test_dev_has_dm_map() {
	local target_type=${1}
	local dm_name

	dm_name=$(<"${TEST_DEV_SYSFS}/dm/name")
	if ! dmsetup status "${dm_name}" | grep -qe "${target_type}"; then
		return 1
	fi
	if dmsetup status "${dm_name}" | grep -v "${target_type}"; then
		return 1
	fi
	return 0
}

# Given sector of TEST_DEV, return the device which contain the sector and
# corresponding sector of the container device.
_get_dev_container_and_sector() {
	local -i sector=${1}
	local cont_dev
	local -i offset
	local -a tbl_line
	local -i dev_idx=3 off_idx=4

	if _test_dev_is_partition; then
		offset=$(<"${TEST_DEV_PART_SYSFS}/start")
		cont_dev=$(_get_dev_path_by_id "$(<"${TEST_DEV_SYSFS}/dev")")
		echo "${cont_dev}" "$((offset + sector))"
		return 0
	fi

	if ! _test_dev_is_dm; then
		echo "${TEST_DEV} is not a logical device"
		return 1
	fi
	if ! _test_dev_has_dm_map linear &&
			! _test_dev_has_dm_map flakey &&
			! _test_dev_has_dm_map crypt; then
		echo -n "dm mapping test other than linear/flakey/crypt is"
		echo "not implemented"
		return 1
	fi

	if _test_dev_has_dm_map crypt; then
		dev_idx=6
		off_idx=7
	fi

	# Parse dm table lines for dm-linear, dm-flakey or dm-crypt target
	while read -r -a tbl_line; do
		local -i map_start=${tbl_line[0]}
		local -i map_end=$((tbl_line[0] + tbl_line[1]))

		if ((sector < map_start)) || (((map_end) <= sector)); then
			continue
		fi

		offset=${tbl_line[off_idx]}
		if ! cont_dev=$(_get_dev_path_by_id \
					"${tbl_line[dev_idx]}"); then
			echo -n "Cannot access to container device: "
			echo "${tbl_line[dev_idx]}"
			return 1
		fi

		echo "${cont_dev}" "$((offset + sector - map_start))"
		return 0

	done < <(dmsetup table "$(<"${TEST_DEV_SYSFS}/dm/name")")

	echo -n "Cannot find container device of ${TEST_DEV}"
	return 1
}
