#! /bin/bash
# SPDX-License-Identifier: GPL-2.0
# Copyright (c) 2023 Oracle.  All Rights Reserved.
#
# FS QA Test No. 603
#
# Functional test of using online repair to fix unlinked inodes on a clean
# filesystem that never got cleaned up.
#
. ./common/preamble
_begin_fstest auto online_repair

. ./common/filter
. ./common/fuzzy
. ./common/quota


_require_xfs_db_command iunlink
# The iunlink bucket repair code wasn't added to the AGI repair code
# until after the directory repair code was merged
_require_xfs_io_command repair -R directory
_require_scratch_nocheck	# repair doesn't like single-AG fs

# From the AGI definition
XFS_AGI_UNLINKED_BUCKETS=64

# Try to make each iunlink bucket have this many inodes in it.
IUNLINK_BUCKETLEN=5

# Disable quota since quotacheck will break this test
_qmount_option 'noquota'

format_scratch() {
	_scratch_mkfs -d agcount=1 | _filter_mkfs 2> "${tmp}.mkfs" >> $seqres.full
	source "${tmp}.mkfs"
	test "${agcount}" -eq 1 || _notrun "test requires 1 AG for error injection"

	local nr_iunlinks="$((IUNLINK_BUCKETLEN * XFS_AGI_UNLINKED_BUCKETS))"
	readarray -t BADINODES < <(_scratch_xfs_db -x -c "iunlink -n $nr_iunlinks" | awk '{print $4}')
}

__repair_check_scratch() {
	_scratch_xfs_repair -o force_geometry -n 2>&1 | \
		tee -a $seqres.full | \
		grep -E '(disconnected inode.*would move|next_unlinked in inode|unlinked bucket.*is.*in ag)'
	return "${PIPESTATUS[0]}"
}

corrupt_scratch() {
	# How far into the iunlink bucket chain do we target inodes for corruption?
	# 1 = target the inode pointed to by the AGI
	# 3 = middle of bucket list
	# 5 = last element in bucket
	local corruption_bucket_depth="$1"
	if ((corruption_bucket_depth < 1 || corruption_bucket_depth > IUNLINK_BUCKETLEN)); then
		echo "${corruption_bucket_depth}: Value must be between 1 and ${IUNLINK_BUCKETLEN}."
		return 1
	fi

	# Index of the inode numbers within BADINODES
	local bad_ino1_idx=$(( (IUNLINK_BUCKETLEN - corruption_bucket_depth) * XFS_AGI_UNLINKED_BUCKETS))
	local bad_ino2_idx=$((bad_ino1_idx + 1))

	# Inode numbers to target
	local bad_ino1="${BADINODES[bad_ino1_idx]}"
	local bad_ino2="${BADINODES[bad_ino2_idx]}"
	printf "bad: 0x%x 0x%x\n" "${bad_ino1}" "${bad_ino2}" | _tee_kernlog >> $seqres.full

	# Bucket within AGI 0's iunlinked array.
	local ino1_bucket="$((bad_ino1 % XFS_AGI_UNLINKED_BUCKETS))"
	local ino2_bucket="$((bad_ino2 % XFS_AGI_UNLINKED_BUCKETS))"

	# The first bad inode stays on the unlinked list but gets a nonzero
	# nlink; the second bad inode is removed from the unlinked list but
	# keeps its zero nlink
	_scratch_xfs_db -x \
		-c "inode ${bad_ino1}" -c "write -d core.nlinkv2 5555" \
		-c "agi 0" -c "fuzz -d unlinked[${ino2_bucket}] ones" -c "print unlinked" >> $seqres.full

	local iwatch=()
	local idx

	# Make a list of the adjacent iunlink bucket inodes for the first inode
	# that we targeted.
	if [ "${corruption_bucket_depth}" -gt 1 ]; then
		# Previous ino in bucket
		idx=$(( (IUNLINK_BUCKETLEN - corruption_bucket_depth + 1) * XFS_AGI_UNLINKED_BUCKETS))
		iwatch+=("${BADINODES[idx]}")
	fi
	iwatch+=("${bad_ino1}")
	if [ "$((corruption_bucket_depth + 1))" -lt "${IUNLINK_BUCKETLEN}" ]; then
		# Next ino in bucket
		idx=$(( (IUNLINK_BUCKETLEN - corruption_bucket_depth - 1) * XFS_AGI_UNLINKED_BUCKETS))
		iwatch+=("${BADINODES[idx]}")
	fi

	# Make a list of the adjacent iunlink bucket inodes for the second
	# inode that we targeted.
	if [ "${corruption_bucket_depth}" -gt 1 ]; then
		# Previous ino in bucket
		idx=$(( (IUNLINK_BUCKETLEN - corruption_bucket_depth + 1) * XFS_AGI_UNLINKED_BUCKETS))
		iwatch+=("${BADINODES[idx + 1]}")
	fi
	iwatch+=("${bad_ino2}")
	if [ "$((corruption_bucket_depth + 1))" -lt "${IUNLINK_BUCKETLEN}" ]; then
		# Next ino in bucket
		idx=$(( (IUNLINK_BUCKETLEN - corruption_bucket_depth - 1) * XFS_AGI_UNLINKED_BUCKETS))
		iwatch+=("${BADINODES[idx + 1]}")
	fi

	# Construct a grep string for tracepoints.
	GREP_STR="(xrep_attempt|xrep_done|bucket ${ino1_bucket} |bucket ${ino2_bucket} |bucket ${fuzz_bucket} "
	GREP_STR="(xrep_attempt|xrep_done|bucket ${ino1_bucket} |bucket ${ino2_bucket} "
	for ino in "${iwatch[@]}"; do
		f="$(printf "|ino 0x%x" "${ino}")"
		GREP_STR="${GREP_STR}${f}"
	done
	GREP_STR="${GREP_STR})"
	echo "grep -E \"${GREP_STR}\"" >> $seqres.full

	# Dump everything we did to to the full file.
	local db_dump=(-c 'agi 0' -c 'print unlinked')
	db_dump+=(-c 'addr root' -c 'print')
	test "${ino1_bucket}" -gt 0 && \
		db_dump+=(-c "dump_iunlinked -a 0 -b $((ino1_bucket - 1))")
	db_dump+=(-c "dump_iunlinked -a 0 -b ${ino1_bucket}")
	db_dump+=(-c "dump_iunlinked -a 0 -b ${ino2_bucket}")
	test "${ino2_bucket}" -lt 63 && \
		db_dump+=(-c "dump_iunlinked -a 0 -b $((ino2_bucket + 1))")
	db_dump+=(-c "inode $bad_ino1" -c 'print core.nlinkv2 v3.inumber next_unlinked')
	db_dump+=(-c "inode $bad_ino2" -c 'print core.nlinkv2 v3.inumber next_unlinked')
	_scratch_xfs_db "${db_dump[@]}" >> $seqres.full

	# Test run of repair to make sure we find disconnected inodes
	__repair_check_scratch | \
		sed -e 's/disconnected inode \([0-9]*\)/disconnected inode XXXXXX/g' \
		    -e 's/next_unlinked in inode \([0-9]*\)/next_unlinked in inode XXXXXX/g' \
		    -e 's/unlinked bucket \([0-9]*\) is \([0-9]*\) in ag \([0-9]*\) .inode=\([0-9]*\)/unlinked bucket YY is XXXXXX in ag Z (inode=AAAAAA/g' | \
		uniq -c >> $seqres.full
	res=${PIPESTATUS[0]}
	test "$res" -ne 0 || echo "repair returned $res after corruption?"
}

exercise_scratch() {
	# Create a bunch of files...
	declare -A inums
	for ((i = 0; i < (XFS_AGI_UNLINKED_BUCKETS * 2); i++)); do
		touch "${SCRATCH_MNT}/${i}" || break
		inums["${i}"]="$(stat -c %i "${SCRATCH_MNT}/${i}")"
	done

	# ...then delete them to exercise the unlinked buckets
	for ((i = 0; i < (XFS_AGI_UNLINKED_BUCKETS * 2); i++)); do
		if ! rm -f "${SCRATCH_MNT}/${i}"; then
			echo "rm failed on inum ${inums[$i]}"
			break
		fi
	done
}

# Offline repair should not find anything
final_check_scratch() {
	__repair_check_scratch
	res=$?
	if [ $res -eq 2 ]; then
		echo "scratch fs went offline?"
		_scratch_mount
		_scratch_unmount
		__repair_check_scratch
	fi
	test "$res" -ne 0 && echo "repair returned $res?"
}

echo "+ Part 1: See if scrub can recover the unlinked list" | tee -a $seqres.full
format_scratch
_kernlog "no bad inodes"
_scratch_mount
_scratch_scrub >> $seqres.full
exercise_scratch
_scratch_unmount
final_check_scratch

echo "+ Part 2: Corrupt the first inode in the bucket" | tee -a $seqres.full
format_scratch
corrupt_scratch 1
_scratch_mount
_scratch_scrub >> $seqres.full
exercise_scratch
_scratch_unmount
final_check_scratch

echo "+ Part 3: Corrupt the middle inode in the bucket" | tee -a $seqres.full
format_scratch
corrupt_scratch 3
_scratch_mount
_scratch_scrub >> $seqres.full
exercise_scratch
_scratch_unmount
final_check_scratch

echo "+ Part 4: Corrupt the last inode in the bucket" | tee -a $seqres.full
format_scratch
corrupt_scratch 5
_scratch_mount
_scratch_scrub >> $seqres.full
exercise_scratch
_scratch_unmount
final_check_scratch

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