#! /bin/bash
# SPDX-License-Identifier: GPL-2.0
# Copyright (c) 2021 Oracle.  All Rights Reserved.
#
# FS QA Test No. 185
#
# Regression test for commits:
#
# c02f6529864a ("xfs: make xfs_rtalloc_query_range input parameters const")
# 9ab72f222774 ("xfs: fix off-by-one error when the last rt extent is in use")
# 7e1826e05ba6 ("xfs: make fsmap backend function key parameters const")
#
# These commits fix a bug in fsmap where the data device fsmap function would
# corrupt the high key passed to the rt fsmap function if the data device
# number is smaller than the rt device number and the data device itself is
# smaller than the rt device.
#
. ./common/preamble
_begin_fstest auto fsmap prealloc punch

_cleanup()
{
	cd /
	rm -r -f $tmp.*
	_scratch_unmount
	test -n "$ddloop" && _destroy_loop_device "$ddloop"
	test -n "$rtloop" && _destroy_loop_device "$rtloop"
	test -n "$ddfile" && rm -f "$ddfile"
	test -n "$rtfile" && rm -f "$rtfile"
	test -n "$old_use_external" && USE_EXTERNAL="$old_use_external"
}

_require_test
_require_loop
_require_xfs_io_command "falloc"
_require_xfs_io_command "fpunch"
_require_xfs_io_command "fsmap"

# Synthesize data and rt volumes that as needed to trigger the bug
ddfile=$TEST_DIR/data
rtfile=$TEST_DIR/rt
rm -f "$rtfile" "$ddfile"

ddsize="$((100 * 1048576))"
rtsize="$((200 * 1048576))"

$XFS_IO_PROG -f -c "truncate $ddsize" $ddfile
$XFS_IO_PROG -f -c "truncate $rtsize" $rtfile
ddloop="$(_create_loop_device $ddfile)"
rtloop="$(_create_loop_device $rtfile)"

test -z "$ddloop" && _fail "Could not create data loop device"
test -z "$rtloop" && _fail "Could not create rt loop device"

# dataloop must have a smaller device number than rtloop because fsmap sorts
# the output by device number
ddmajor=$(stat -c '%t' "$ddloop")
rtmajor=$(stat -c '%t' "$rtloop")
test $ddmajor -le $rtmajor || \
	_notrun "Data loopdev major $ddmajor larger than rt major $rtmajor"

ddminor=$(stat -c '%T' "$ddloop")
rtminor=$(stat -c '%T' "$rtloop")
test $ddmajor -le $rtmajor || \
	_notrun "Data loopdev minor $ddminor larger than rt minor $rtminor"

# Inject our custom-built devices as an rt-capable scratch device.
# We avoid touching "require_scratch" so that post-test fsck will not try to
# run on our synthesized scratch device.
old_use_external="$USE_EXTERNAL"
USE_EXTERNAL=yes
SCRATCH_RTDEV="$rtloop"
SCRATCH_DEV="$ddloop"

_scratch_mkfs >> $seqres.full
_try_scratch_mount >> $seqres.full || \
	_notrun "mount with injected rt device failed"

# Create a file that we'll use to seed fsmap entries for the rt device,
# and force the root directory to create only data device files
rtfile="$SCRATCH_MNT/rtfile"
$XFS_IO_PROG -R -f -c 'stat -v' $rtfile | grep -q 'fsxattr.*xflags.*realtime' || \
	_notrun "could not create realtime file"
$XFS_IO_PROG -c 'chattr -t' $SCRATCH_MNT
rtextsize="$(stat -c '%o' $rtfile)"

# Make sure the data device is smaller than the rt device by at least a few
# realtime extents.
ddbytes="$(stat -f -c '%S * %b' $SCRATCH_MNT | bc)"
rtbytes="$(stat -f -c '%S * %b' $rtfile | bc)"

test "$ddbytes" -lt "$((rtbytes + (10 * rtextsize) ))" || \
	echo "data device ($ddbytes) has more bytes than rt ($rtbytes)"

# Craft the layout of the sole realtime file in such a way that the only
# allocated space on the realtime device is at a physical disk address that is
# higher than the size of the data device.  For realtime files this is really
# easy because fallocate for the first rt file always starts allocating at
# physical offset zero.
rtfreebytes="$(stat -f -c '%S * %a' $rtfile | bc)"
alloc_rtx="$((rtfreebytes / rtextsize))"
$XFS_IO_PROG -c "falloc 0 $((alloc_rtx * rtextsize))" $rtfile

# log a bunch of geometry data to the full file for debugging
echo "rtbytes $rtbytes rtfreebytes $rtfreebytes rtextsize $rtextsize" >> $seqres.full
echo "allocrtx $alloc_rtx falloc $((alloc_rtx * rtextsize))" >> $seqres.full
$XFS_IO_PROG -c statfs $SCRATCH_MNT >> $seqres.full

total_rtx=$(_xfs_statfs_field $SCRATCH_MNT geom.rtextents)
expected_end="$(( (total_rtx * rtextsize - 1) / 512 ))"

# Print extent mapping of rtfile in format:
# file_offset file_end physical_offset physical_end
rtfile_exts() {
	$XFS_IO_PROG -c 'bmap -elp' $rtfile | \
		grep -E -v '(^/|EXT:|hole)' | \
		tr ':.[]' '    ' | \
		while read junk foff fend physoff physend junk; do
			echo "$foff $fend $physoff $physend"
		done
}

# Make sure that fallocate actually managed to map the entire rt device.  The
# realtime superblock may consume the first rtx, so we allow for that here.
# Allow for multiple contiguous mappings if the rtgroups are very small.
allowed_start=$((rtextsize / 512))
rtfile_exts | $AWK_PROG -v exp_start=$allowed_start -v exp_end=$expected_end '
BEGIN {
	start = -1;
	end = -1;
}
{
	if (end >= 0 && ($3 != end + 1))
		printf("%s: non-contiguous allocation should have started at %s\n", $0, end + 1);
	if (start < 0 || $3 < start)
		start = $3;
	if (end < 0 || $4 > end)
		end = $4;
}
END {
	if (start > exp_start)
		printf("%s: unexpected physical offset %d, wanted <= %d\n", $0, start, exp_start);
	if (end != exp_end)
		printf("%s: unexpected physical end %d, wanted %d\n", $0, end, exp_end);
}'

# Now punch out a range that is slightly larger than the size of the data
# device.
punch_bytes="$((ddsize + rtextsize))"
punch_rtx="$((punch_bytes / rtextsize))"
$XFS_IO_PROG -c "fpunch 0 $((punch_rtx * rtextsize))" $rtfile

expected_offset="$((punch_rtx * rtextsize / 512))"

echo "rtfile should have physical extent from $expected_offset to $expected_end sectors" >> $seqres.full

# Make sure that the realtime file now maps one large extent at the end of the
# realtime device.  Due to rtgroups boundary rules, there may be multiple
# contiguous mappings.
rtfile_exts | $AWK_PROG -v exp_start=$expected_offset -v exp_end=$expected_end '
BEGIN {
	start = -1;
	end = -1;
}
{
	if (end >= 0 && ($3 != end + 1))
		printf("%s: non-contiguous allocation should have started at %s\n", $0, end + 1);
	if (start < 0 || $3 < start)
		start = $3;
	if (end < 0 || $4 > end)
		end = $4;
}
END {
	if (start < exp_start)
		printf("%s: unexpected physical offset %d, wanted >= %d\n", $0, start, exp_start);
	if (end != exp_end)
		printf("%s: unexpected physical end %d, wanted %d\n", $0, end, exp_end);
}'

$XFS_IO_PROG -c 'bmap -elpv' $rtfile >> $seqres.full
$XFS_IO_PROG -c 'fsmap' $SCRATCH_MNT >> $seqres.full

fsmap() {
	$XFS_IO_PROG -c 'fsmap' $SCRATCH_MNT | \
		grep -v 'EXT:' | \
		tr ':.[]' '    ' | \
		while read junk major minor physoff physend junk; do
			echo "$major:$minor $physoff $physend"
		done
}

# Check the fsmap output contains a record for the realtime device at a
# physical offset higher than end of the data device and corresponding to the
# beginning of the non-punched area.  The "found_end" check uses >= because
# rtfile can be larger than the number of rtextents if the size of the rtfile
# is not congruent with the rt extent size.
fsmap | $AWK_PROG -v dev="$rtmajor:$rtminor" -v offset=$expected_offset -v end=$expected_end '
BEGIN {
	found_start = 0;
	found_end = 0;
}
{
	if ($1 == dev && $2 >= offset) {
		found_start = 1;
		if ($3 >= end) {
			found_end = 1;
		}
	}
}
END {
	if (found_start == 0)
		printf("No fsmap record for rtdev %s above sector %d\n", dev, offset);
	if (found_end == 0)
		printf("No fsmap record for rtdev %s ending at sector %d\n", dev, end);
}'

echo Silence is golden
# success, all done
status=0
exit
