##/bin/bash
# SPDX-License-Identifier: GPL-2.0
# Copyright (c) 2007 Silicon Graphics, Inc.  All Rights Reserved.
#
# common functions for excersizing hole punches with extent size hints etc.

_spawn_test_file() {
	echo "# spawning test file with $*"
	local blksize=$1
	local file_size=`expr $2 \* $blksize`
	local extent_size_hint=`expr $3 \* $blksize`
	local test_file=$4
	local reserve_space=$5

	if [ $extent_size_hint -ne 0 ]; then
		echo "+ setting extent size hint to $extent_size_hint"
		$XFS_IO_PROG -f \
		-c "extsize $extent_size_hint" \
		$test_file
	fi
	# print extent size hint for $test_file
	$XFS_IO_PROG -f \
	-c "extsize" \
	$test_file

	if [ "$reserve_space" == "noresv" ]; then
		echo "+ not using resvsp at file creation"
		$XFS_IO_PROG -f \
		-c "truncate $file_size" \
		$test_file
	else
		$XFS_IO_PROG -f \
		-c "truncate $file_size" \
		-c "resvsp 0 $file_size" \
		$test_file
	fi
}

_do_write() {
	echo "# writing with $*"
	local blksize=$1
	local write_offset=`expr $2 \* $blksize`
	local write_size=`expr $3 \* $blksize`
	local test_file=$4

	$XFS_IO_PROG -f \
	-c "pwrite $write_offset $write_size" \
	$test_file >/dev/null
}

_do_bmap() {
	echo "# showing file state $*"
	local test_file=$1

	$XFS_IO_PROG -f \
	-c "bmap -vvp" \
	$test_file
}

_coalesce_extents()
{
	block_size=$1

	[[ -z $block_size ]] && block_size=512

	awk -v block_size="$block_size" -F: '
	{
		range = $2;
		type = $3;

		split(range, bounds, "[\\[ \\.\\]]");
		low = bounds[3];
		high = bounds[5];

		if (type != prev_type) {
			if (prev_type != "")
				printf("%u]:%s\n", (low * 512 / block_size) - 1,
					prev_type);
			printf("%u: [%u..", out_count++,
				(low * 512) / block_size);
			prev_type = type;
		}
	}
	END {
		if (prev_type != "")
			printf("%u]:%s\n", ((high + 1) * 512 / block_size) - 1,
				prev_type);
	}'
}

_filter_fiemap()
{
	block_size=$1

	$AWK_PROG '
		$3 ~ /hole/ {
			print $1, $2, $3;
			next;
		}
		$5 ~ /0x[[:xdigit:]]*8[[:xdigit:]][[:xdigit:]]/ {
			print $1, $2, "unwritten";
			next;
		}
		$5 ~ /0x[[:xdigit:]]+/ {
			print $1, $2, "data";
		}' |
	_coalesce_extents $block_size
}

_filter_fiemap_flags()
{
	local include_encoded_flag=0

	# Unless a first argument is passed and with a value of 1, the fiemap
	# encoded flag is filtered out.
	# This is because tests that use this filter's output in their golden
	# output may get the encoded flag set or not depending on the filesystem
	# and its configuration. For example, running btrfs with "-o compress"
	# (or compress-force) set in MOUNT_OPTIONS, then extents that get
	# compressed are reported with the encoded flag, otherwise that flag is
	# not reported. Like this the fs configuration does not cause a mismatch
	# with the golden output, and tests that exercise specific configurations
	# can explicitly ask for the encoded flag to be printed.
	if [ ! -z "$1" ] && [ $1 -eq 1 ]; then
		include_encoded_flag=1
	fi

	local awk_script='
		$3 ~ /hole/ {
			print $1, $2, $3;
			next;
		}
		$5 ~ /0x[[:xdigit:]]*8[[:xdigit:]][[:xdigit:]]/ {
			print $1, $2, "unwritten";
			next;
		}
		$5 ~ /0x[[:xdigit:]]+/ {
			flags = strtonum($5);
			flag_str = "none";
			set = 0;

			if (and(flags, 0x2000)) {
				flag_str = "shared";
				set = 1;
			}'

	if [ $include_encoded_flag -eq 1 ]; then
		awk_script=$awk_script'
			if (and(flags, 0x8)) {
				if (set) {
					flag_str = flag_str"|";
				} else {
					flag_str = "";
				}
				flag_str = flag_str"encoded";
				set = 1;
			}'
	fi

	awk_script=$awk_script'
			if (and(flags, 0x1)) {
				if (set) {
					flag_str = flag_str"|";
				} else {
					flag_str = "";
				}
				flag_str = flag_str"last";
			}
			print $1, $2, flag_str
		}'

	$AWK_PROG -e "$awk_script" | _coalesce_extents
}

# Filters fiemap output to only print the
# file offset column and whether or not
# it is an extent or a hole
_filter_hole_fiemap()
{
	$AWK_PROG '
		$3 ~ /hole/ {
			print $1, $2, $3;
			next;
		}
		$5 ~ /0x[[:xdigit:]]+/ {
			print $1, $2, "extent";
		}' |
	_coalesce_extents
}

# Column 7 for datadev files and column 5 for rtdev files, To prevent the
# 5th column in datadev files from being potentially matched incorrectly,
# we need to match Column 7 for datadev files first, because the rtdev
# file only has 5 columns in the `bmap -vp` output.
#     10000 Unwritten preallocated extent
#     01000 Doesn't begin on stripe unit
#     00100 Doesn't end   on stripe unit
#     00010 Doesn't begin on stripe width
#     00001 Doesn't end   on stripe width
_filter_bmap()
{
	awk '
		$3 ~ /hole/ {
			print $1, $2, $3;
			next;
		}
		$7 ~ /1[01][01][01][01]/ {
			print $1, $2, "unwritten";
			next;
		}
		$7 ~ /0[01][01][01][01]/ {
			print $1, $2, "data"
			next;
		}
		$5 ~ /1[01][01][01][01]/ {
			print $1, $2, "unwritten";
			next;
		}
		$5 ~ /0[01][01][01][01]/ {
			print $1, $2, "data"
		}' |
	_coalesce_extents
}

# test the different corner cases for zeroing a range:
#
#	1. into a hole
#	2. into allocated space
#	3. into unwritten space
#	4. hole -> data
#	5. hole -> unwritten
#	6. data -> hole
#	7. data -> unwritten
#	8. unwritten -> hole
#	9. unwritten -> data
#	10. hole -> data -> hole
#	11. data -> hole -> data
#	12. unwritten -> data -> unwritten
#	13. data -> unwritten -> data
#	14. data -> hole @ EOF
#	15. data -> hole @ 0
#	16. data -> cache cold ->hole
#	17. data -> hole in single block file
#
# Test file is removed, created and sync'd between tests.
#
# Use -k flag to keep the file between tests.  This will
# test the handling of pre-existing holes.
#
# Use the -d flag to not sync the file between tests.
# This will test the handling of delayed extents
#
# Use the -u flag to not run unwritten tests.
# This will eliminate some unnecessary information.
#
_test_generic_punch()
{

	remove_testfile=1
	sync_cmd="-c fsync"
	unwritten_tests=1
	OPTIND=1
	while getopts 'dku' OPTION
	do
		case $OPTION in
		k)	remove_testfile=
		;;
		d)	sync_cmd=
		;;
		u)	unwritten_tests=
		;;
		?)	echo Invalid flag
		_exit 1
		;;
		esac
	done
	shift $(($OPTIND - 1))

	alloc_cmd=$1
	punch_cmd=$2
	zero_cmd=$3	#if not testing zero just set to punch
	map_cmd=$4
	filter_cmd=$5
	testfile=$6

	# The punch hole tests needs multiple of the largest extent size being
	# tested, with multiple=16 it can test extent size upto 64k.
	multiple=16
	_4k="$((multiple * 4))k"
	_8k="$((multiple * 8))k"
	_12k="$((multiple * 12))k"
	_20k="$((multiple * 20))k"
	_require_congruent_file_oplen "$(dirname "$testfile")" $((multiple * 4096))

	# initial test state must be defined, otherwise the first test can fail
	# due ot stale file state left from previous tests.
	rm -f $testfile

	echo "	1. into a hole"
	$XFS_IO_PROG -f -c "truncate $_20k" \
		-c "$zero_cmd $_4k $_8k" \
		-c "$map_cmd -v" $testfile | $filter_cmd
	[ $? -ne 0 ] && _fatal
	_md5_checksum $testfile

	echo "	2. into allocated space"
	if [ "$remove_testfile" ]; then
		rm -f $testfile
	fi
	$XFS_IO_PROG -f -c "truncate $_20k" \
		-c "pwrite 0 $_20k" $sync_cmd \
		-c "$zero_cmd $_4k $_8k" \
		-c "$map_cmd -v" $testfile | $filter_cmd
	[ $? -ne 0 ] && _fatal
	_md5_checksum $testfile

	if [ "$unwritten_tests" ]; then
		echo "	3. into unwritten space"
		if [ "$remove_testfile" ]; then
			rm -f $testfile
		fi
		$XFS_IO_PROG -f -c "truncate $_20k" \
			-c "$alloc_cmd 0 $_20k" \
			-c "$zero_cmd $_4k $_8k" \
			-c "$map_cmd -v" $testfile | $filter_cmd
		[ $? -ne 0 ] && _fatal
		_md5_checksum $testfile
	fi

	echo "	4. hole -> data"
	if [ "$remove_testfile" ]; then
		rm -f $testfile
	fi
	$XFS_IO_PROG -f -c "truncate $_20k" \
		-c "pwrite $_8k $_8k" $sync_cmd \
		-c "$zero_cmd $_4k $_8k" \
		-c "$map_cmd -v" $testfile | $filter_cmd
	[ $? -ne 0 ] && _fatal
	_md5_checksum $testfile

	if [ "$unwritten_tests" ]; then
		echo "	5. hole -> unwritten"
		if [ "$remove_testfile" ]; then
			rm -f $testfile
		fi
		$XFS_IO_PROG -f -c "truncate $_20k" \
			-c "$alloc_cmd $_8k $_8k" \
			-c "$zero_cmd $_4k $_8k" \
			-c "$map_cmd -v" $testfile | $filter_cmd
		[ $? -ne 0 ] && _fatal
		_md5_checksum $testfile
	fi

	echo "	6. data -> hole"
	if [ "$remove_testfile" ]; then
		rm -f $testfile
	fi
	$XFS_IO_PROG -f -c "truncate $_20k" \
		-c "pwrite 0 $_8k" $sync_cmd \
		 -c "$zero_cmd $_4k $_8k" \
		-c "$map_cmd -v" $testfile | $filter_cmd
	[ $? -ne 0 ] && _fatal
	_md5_checksum $testfile

	if [ "$unwritten_tests" ]; then
		echo "	7. data -> unwritten"
		if [ "$remove_testfile" ]; then
			rm -f $testfile
		fi
		$XFS_IO_PROG -f -c "truncate $_20k" \
			-c "pwrite 0 $_8k" $sync_cmd \
			-c "$alloc_cmd $_8k $_8k" \
			-c "$zero_cmd $_4k $_8k" \
			-c "$map_cmd -v" $testfile | $filter_cmd
		[ $? -ne 0 ] && _fatal
		_md5_checksum $testfile

		echo "	8. unwritten -> hole"
		if [ "$remove_testfile" ]; then
			rm -f $testfile
		fi
		$XFS_IO_PROG -f -c "truncate $_20k" \
			-c "$alloc_cmd 0 $_8k" \
			-c "$zero_cmd $_4k $_8k" \
			-c "$map_cmd -v" $testfile | $filter_cmd
		[ $? -ne 0 ] && _fatal
		_md5_checksum $testfile

		echo "	9. unwritten -> data"
		if [ "$remove_testfile" ]; then
			rm -f $testfile
		fi
		$XFS_IO_PROG -f -c "truncate $_20k" \
			-c "$alloc_cmd 0 $_8k" \
			-c "pwrite $_8k $_8k" $sync_cmd \
			-c "$zero_cmd $_4k $_8k" \
			-c "$map_cmd -v" $testfile | $filter_cmd
		[ $? -ne 0 ] && _fatal
		_md5_checksum $testfile
	fi

	echo "	10. hole -> data -> hole"
	if [ "$remove_testfile" ]; then
		rm -f $testfile
	fi
	$XFS_IO_PROG -f -c "truncate $_20k" \
		-c "pwrite $_8k $_4k" $sync_cmd \
		-c "$zero_cmd $_4k $_12k" \
		-c "$map_cmd -v" $testfile | $filter_cmd
	[ $? -ne 0 ] && _fatal
	_md5_checksum $testfile

	echo "	11. data -> hole -> data"
	if [ "$remove_testfile" ]; then
		rm -f $testfile
	fi
	$XFS_IO_PROG -f -c "truncate $_20k" \
		-c "$alloc_cmd 0 $_20k" \
		-c "pwrite 0 $_8k" \
		-c "pwrite $_12k $_8k" $sync_cmd \
		-c "$punch_cmd $_8k $_4k" \
		-c "$zero_cmd $_4k $_12k" \
		-c "$map_cmd -v" $testfile | $filter_cmd
	[ $? -ne 0 ] && _fatal
	_md5_checksum $testfile

	if [ "$unwritten_tests" ]; then
		echo "	12. unwritten -> data -> unwritten"
		if [ "$remove_testfile" ]; then
			rm -f $testfile
		fi
		$XFS_IO_PROG -f -c "truncate $_20k" \
			-c "$alloc_cmd 0 $_20k" \
			-c "pwrite $_8k $_4k" $sync_cmd \
			-c "$zero_cmd $_4k $_12k" \
			-c "$map_cmd -v" $testfile | $filter_cmd
		[ $? -ne 0 ] && _fatal
		_md5_checksum $testfile

		echo "	13. data -> unwritten -> data"
		if [ "$remove_testfile" ]; then
			rm -f $testfile
		fi
		$XFS_IO_PROG -f -c "truncate $_20k" \
			-c "$alloc_cmd 0 $_20k" \
			-c "pwrite 0k $_4k" $sync_cmd \
			-c "pwrite $_12k $_8k" -c "fsync" \
			-c "$zero_cmd $_4k $_12k" \
			-c "$map_cmd -v" $testfile | $filter_cmd
		[ $? -ne 0 ] && _fatal
		_md5_checksum $testfile
	fi

	# Don't need to check EOF case for collapse range.
	# VFS layer return invalid error in this case,
	# So it is not a proper case for collapse range test of each local fs.
	if [ "$zero_cmd" != "fcollapse" ]; then
		echo "	14. data -> hole @ EOF"
		rm -f $testfile
		$XFS_IO_PROG -f -c "truncate $_20k" \
			-c "pwrite 0 $_20k" $sync_cmd \
			-c "$zero_cmd $_12k $_8k" \
			-c "$map_cmd -v" $testfile | $filter_cmd
		[ $? -ne 0 ] && _fatal
		_md5_checksum $testfile
	fi

	if [ "$zero_cmd" == "fcollapse" ]; then
		echo "	14. data -> hole @ 0"
	else
		echo "	15. data -> hole @ 0"
	fi

	if [ "$remove_testfile" ]; then
		rm -f $testfile
	fi
	$XFS_IO_PROG -f -c "truncate $_20k" \
		-c "pwrite 0 $_20k" $sync_cmd \
		-c "$zero_cmd 0 $_8k" \
		-c "$map_cmd -v" $testfile | $filter_cmd
	[ $? -ne 0 ] && _fatal
	_md5_checksum $testfile

	# If zero_cmd is fcollpase, don't check unaligned offsets
	if [ "$zero_cmd" == "fcollapse" ]; then
		return
	fi

	# If zero_cmd is finsert, don't check unaligned offsets
	if [ "$zero_cmd" == "finsert" ]; then
		return
	fi

	echo "	16. data -> cache cold ->hole"
	if [ "$remove_testfile" ]; then
		rm -f $testfile
		rm -f $testfile.2
	else
		cp $testfile $testfile.2
	fi
	$XFS_IO_PROG -f -c "truncate $_20k" \
		-c "pwrite $_8k $_12k" -c "fsync" $testfile.2 \
		> /dev/null
	$XFS_IO_PROG -f -c "truncate $_20k" \
		-c "pwrite 0 $_20k" $sync_cmd \
		-c "$zero_cmd 0k $_8k" \
		-c "fadvise -d" \
		-c "$map_cmd -v" $testfile | $filter_cmd
	diff $testfile $testfile.2
	[ $? -ne 0 ] && _fatal
	rm -f $testfile.2
	_md5_checksum $testfile

	# different file sizes mean we can't use md5sum to check the hole is
	# valid. Hence use hexdump to dump the contents and chop off the last
	# line of output that indicates the file size. We also have to fudge
	# the extent size as that will change with file size, too - that's what
	# the sed line noise does - it will always result in an output of [0..7]
	# so it matches 4k block size...
	echo "	17. data -> hole in single block file"
	if [ "$remove_testfile" ]; then
		rm -f $testfile
	fi
	block_size=`_get_file_block_size $TEST_DIR`
	$XFS_IO_PROG -f -c "truncate $block_size" \
		-c "pwrite 0 $block_size" $sync_cmd \
		-c "$zero_cmd 128 128" \
		-c "$map_cmd -v" $testfile | $filter_cmd | \
			 sed -e "s/\.\.[0-9]*\]/..7\]/"
	[ $? -ne 0 ] && _fatal
	od -x $testfile | head -n -1
}

_test_block_boundaries()
{

	remove_testfile=1
	sync_cmd="-c fsync"
	unwritten_tests=1
	OPTIND=1
	while getopts 'dk' OPTION
	do
		case $OPTION in
		k)	remove_testfile=
		;;
		d)	sync_cmd=
		;;
		?)	echo Invalid flag
		_exit 1
		;;
		esac
	done
	shift $(($OPTIND - 1))

	bs=$1
	zero_cmd=$2
	filter_cmd=$3
	testfile=$4

	# Block size plus 1
	bs_p1=$(($bs + 1))
	# Block size plus 2
	bs_p2=$(($bs + 2))

	# Block size minus 1
	bs_m1=$(($bs - 1))

	# Block size multiplied by 2
	bs_t2=$(($bs * 2))

	# Block size divided by 2
	bs_d2=$(($bs / 2))

	echo "zero 0, 1"
	$XFS_IO_PROG -f -t -c "pwrite -S 0x41 0 $bs" \
			   -c "pwrite -S 0x42 $bs $bs" \
			   -c "$zero_cmd 0 1" \
			   -c "pread -v 0 $bs_t2" \
			   $testfile | $filter_cmd

	echo "zero 0, $bs_m1"
	$XFS_IO_PROG -f -t -c "pwrite -S 0x41 0 $bs" \
			   -c "pwrite -S 0x42 $bs $bs" \
			   -c "$zero_cmd 0 $bs_m1" \
			   -c "pread -v 0 $bs_t2" \
			   $testfile | $filter_cmd

	echo "zero 0, $bs"
	$XFS_IO_PROG -f -t -c "pwrite -S 0x41 0 $bs" \
			   -c "pwrite -S 0x42 $bs $bs" \
			   -c "$zero_cmd 0 $bs" \
			   -c "pread -v 0 $bs_t2" \
			   $testfile | $filter_cmd

	echo "zero 0, $bs_p1"
	$XFS_IO_PROG -f -t -c "pwrite -S 0x41 0 $bs" \
			   -c "pwrite -S 0x42 $bs $bs" \
			   -c "$zero_cmd 0 $bs_p1" \
			   -c "pread -v 0 $bs_t2" \
			   $testfile | $filter_cmd

	echo "zero $bs_m1, $bs"
	$XFS_IO_PROG -f -t -c "pwrite -S 0x41 0 $bs" \
			   -c "pwrite -S 0x42 $bs $bs" \
			   -c "$zero_cmd $bs_m1 $bs" \
			   -c "pread -v 0 $bs_t2" \
			   $testfile | $filter_cmd

	echo "zero $bs_m1, $bs_p1"
	$XFS_IO_PROG -f -t -c "pwrite -S 0x41 0 $bs" \
			   -c "pwrite -S 0x42 $bs $bs" \
			   -c "$zero_cmd $bs_m1 $bs_p1" \
			   -c "pread -v 0 $bs_t2" \
			   $testfile | $filter_cmd

	echo "zero $bs_m1, $bs_p2"
	$XFS_IO_PROG -f -t -c "pwrite -S 0x41 0 $bs" \
			   -c "pwrite -S 0x42 $bs $bs" \
			   -c "$zero_cmd $bs_m1 $bs_p2" \
			   -c "pread -v 0 $bs_t2" \
			   $testfile | $filter_cmd


	echo "zero $bs, $bs"
	$XFS_IO_PROG -f -t -c "pwrite -S 0x41 0 $bs" \
			   -c "pwrite -S 0x42 $bs $bs" \
			   -c "$zero_cmd $bs $bs" \
			   -c "pread -v 0 $bs_t2" \
			   $testfile | $filter_cmd


	echo "zero $bs_d2 , $bs"
	$XFS_IO_PROG -f -t -c "pwrite -S 0x41 0 $bs" \
			   -c "pwrite -S 0x42 $bs $bs" \
			   -c "$zero_cmd $bs_d2 $bs" \
			   -c "pread -v 0 $bs_t2" \
			   $testfile | $filter_cmd
}
