##/bin/bash

# Routines for fuzzing and scrubbing a filesystem.
#
#-----------------------------------------------------------------------
#  Copyright (c) 2017 Oracle.  All Rights Reserved.
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
#  USA
#-----------------------------------------------------------------------

# Modify various files after a fuzzing operation
_scratch_fuzz_modify() {
	nr="$1"

	test -z "${nr}" && nr=50000
	echo "+++ touch ${nr} files"
	blk_sz=$(stat -f -c '%s' ${SCRATCH_MNT})
	$XFS_IO_PROG -f -c "pwrite -S 0x63 0 ${blk_sz}" "/tmp/afile" > /dev/null
	date="$(date)"
	find "${SCRATCH_MNT}/" -type f 2> /dev/null | head -n "${nr}" | while read f; do
		setfattr -n "user.date" -v "${date}" "$f"
		cat "/tmp/afile" >> "$f"
		mv "$f" "$f.longer"
	done
	sync
	rm -rf "/tmp/afile"

	echo "+++ create files"
	mkdir -p "${SCRATCH_MNT}/test.moo"
	$XFS_IO_PROG -f -c 'pwrite -S 0x80 0 65536' "${SCRATCH_MNT}/test.moo/urk" > /dev/null
	sync

	echo "+++ remove files"
	rm -rf "${SCRATCH_MNT}/test.moo"
}

# Try to access files after fuzzing
_scratch_fuzz_test() {
	echo "+++ ls -laR" >> $seqres.full
	ls -laR "${SCRATCH_MNT}/test.1/" >/dev/null 2>&1

	echo "+++ cat files" >> $seqres.full
	(find "${SCRATCH_MNT}/test.1/" -type f -size -1048576k -print0 | xargs -0 cat) >/dev/null 2>&1
}

# Do we have an online scrub program?
_require_scrub() {
	case "${FSTYP}" in
	"xfs"|"ext4")
		test -x "$XFS_SCRUB_PROG" || _notrun "xfs_scrub not found"
		;;
	*)
		_notrun "No online scrub program for ${FSTYP}."
		;;
	esac
}

# Scrub the scratch filesystem metadata (online)
_scratch_scrub() {
	case "${FSTYP}" in
	"xfs"|"ext4"|"ext3"|"ext2")
		$XFS_SCRUB_PROG -d -T -v "$@" $SCRATCH_MNT
		;;
	*)
		_fail "No online scrub program for ${FSTYP}."
		;;
	esac
}

# Filter the xfs_db print command's field debug information
# into field name and type.
__filter_xfs_db_print_fields() {
	filter="$1"
	if [ -z "${filter}" ] || [ "${filter}" = "nofilter" ]; then
		filter='^'
	fi
	grep ' = ' | while read key equals value; do
		# Filter out any keys with an array index >= 10, and
		# collapse any array range ("[1-195]") to the first item.
		fuzzkey="$(echo "${key}" | sed -e '/\([a-z]*\)\[\([0-9][0-9]\+\)\].*/d' -e 's/\([a-zA-Z0-9_]*\)\[\([0-9]*\)-[0-9]*\]/\1[\2]/g')"
		if [ -z "${fuzzkey}" ]; then
			continue
		elif [[ "${value}" == "["* ]]; then
			echo "${value}" | sed -e 's/^.//g' -e 's/.$//g' -e 's/,/\n/g' | while read subfield; do
				echo "${fuzzkey}.${subfield}"
			done
		else
			echo "${fuzzkey}"
		fi
	done | egrep "${filter}"
}

# Navigate to some part of the filesystem and print the field info.
# The first argument is an egrep filter for the fields
# The rest of the arguments are xfs_db commands to locate the metadata.
_scratch_xfs_list_metadata_fields() {
	filter="$1"
	shift
	if [ -n "${SCRATCH_XFS_LIST_METADATA_FIELDS}" ]; then
		echo "${SCRATCH_XFS_LIST_METADATA_FIELDS}" | tr '[ ,]' '[\n\n]'
		return;
	fi

	local cmds=()
	for arg in "$@"; do
		cmds+=("-c" "${arg}")
	done
	_scratch_xfs_db "${cmds[@]}" -c print | __filter_xfs_db_print_fields "${filter}"
}

# Get a metadata field
# The first arg is the field name
# The rest of the arguments are xfs_db commands to find the metadata.
_scratch_xfs_get_metadata_field() {
	key="$1"
	shift

	grep_key="$(echo "${key}" | tr '[]()' '....')"
	local cmds=()
	for arg in "$@"; do
		cmds+=("-c" "${arg}")
	done
	_scratch_xfs_db "${cmds[@]}" -c "print ${key}" | grep "^${grep_key}" | \
		sed -e 's/^.* = //g'
}

# Set a metadata field
# The first arg is the field name
# The second arg is the new value
# The rest of the arguments are xfs_db commands to find the metadata.
_scratch_xfs_set_metadata_field() {
	key="$1"
	value="$2"
	shift; shift

	local cmds=()
	for arg in "$@"; do
		cmds+=("-c" "${arg}")
	done
	_scratch_xfs_db -x "${cmds[@]}" -c "write -d ${key} ${value}"
	echo
}

# Fuzz a metadata field
# The first arg is the field name
# The second arg is the xfs_db fuzz verb
# The rest of the arguments are xfs_db commands to find the metadata.
_scratch_xfs_fuzz_metadata_field() {
	key="$1"
	value="$2"
	shift; shift

	if [[ "${key}" == *crc ]]; then
		fuzz_arg="-c"
	else
		fuzz_arg="-d"
	fi
	oldval="$(_scratch_xfs_get_metadata_field "${key}" "$@")"

	local cmds=()
	for arg in "$@"; do
		cmds+=("-c" "${arg}")
	done
	_scratch_xfs_db -x "${cmds[@]}" -c "fuzz ${fuzz_arg} ${key} ${value}"
	echo
	newval="$(_scratch_xfs_get_metadata_field "${key}" "$@" 2> /dev/null)"
	if [ "${oldval}" = "${newval}" ]; then
		echo "Field ${key} already set to ${newval}, skipping test."
		return 1
	fi
	return 0
}

# Try to forcibly unmount the scratch fs
__scratch_xfs_fuzz_unmount()
{
	while _scratch_unmount 2>/dev/null; do sleep 0.2; done
}

# Restore metadata to scratch device prior to field-fuzzing.
__scratch_xfs_fuzz_mdrestore()
{
	test -e "${POPULATE_METADUMP}" || _fail "Need to set POPULATE_METADUMP"

	__scratch_xfs_fuzz_unmount
	xfs_mdrestore "${POPULATE_METADUMP}" "${SCRATCH_DEV}"
}

__fuzz_notify() {
	echo "$@"
	test -w /dev/ttyprintk && echo "$@" >> /dev/ttyprintk
}

# Fuzz one field of some piece of metadata.
# First arg is the field name
# Second arg is the fuzz verb (ones, zeroes, random, add, sub...)
# Third arg is the repair mode (online, offline, both)
__scratch_xfs_fuzz_field_test() {
	field="$1"
	fuzzverb="$2"
	repair="$3"
	shift; shift; shift

	# Set the new field value
	__fuzz_notify "+ Fuzz ${field} = ${fuzzverb}"
	echo "========================"
	_scratch_xfs_fuzz_metadata_field "${field}" ${fuzzverb} "$@"
	res=$?
	test $res -ne 0 && return

	# Try to catch the error with scrub
	echo "+ Try to catch the error"
	_scratch_mount 2>&1
	res=$?
	if [ $res -eq 0 ]; then
		# Try an online scrub unless we're fuzzing ag 0's sb,
		# which scrub doesn't know how to fix.
		echo "++ Online scrub"
		if [ "$1" != "sb 0" ]; then
			_scratch_scrub -n -a 1 -e continue 2>&1
			res=$?
			test $res -eq 0 && \
				(>&2 echo "scrub didn't fail with ${field} = ${fuzzverb}.")
		fi

		# Try fixing the filesystem online?!
		if [ "${repair}" = "online" ] || [ "${repair}" = "both" ]; then
			__fuzz_notify "++ Try to repair filesystem online"
			_scratch_scrub -y 2>&1
			res=$?
			test $res -ne 0 && \
				(>&2 echo "online repair failed ($res) with ${field} = ${fuzzverb}.")
		fi

		__scratch_xfs_fuzz_unmount
	elif [ "${repair}" = "online" ] || [ "${repair}" = "both" ]; then
		(>&2 echo "mount failed ($res) with ${field} = ${fuzzverb}.")
	fi

	# Repair the filesystem offline?
	if [ "${repair}" = "offline" ] || [ "${repair}" = "both" ]; then
		echo "+ Try to repair the filesystem offline"
		_repair_scratch_fs 2>&1
		res=$?
		test $res -ne 0 && \
			(>&2 echo "offline repair failed ($res) with ${field} = ${fuzzverb}.")
	fi

	# See if repair finds a clean fs
	echo "+ Make sure error is gone (offline)"
        _scratch_xfs_repair -n 2>&1
	res=$?
	test $res -ne 0 && \
		(>&2 echo "offline re-scrub ($res) with ${field} = ${fuzzverb}.")

	# See if scrub finds a clean fs
	echo "+ Make sure error is gone (online)"
	_scratch_mount 2>&1
	res=$?
	if [ $res -eq 0 ]; then
		# Try an online scrub unless we're fuzzing ag 0's sb,
		# which scrub doesn't know how to fix.
		echo "++ Online scrub"
		if [ "$1" != "sb 0" ]; then
			_scratch_scrub -e continue 2>&1
			res=$?
			test $res -ne 0 && \
				(>&2 echo "online re-scrub ($res) with ${field} = ${fuzzverb}.")
		fi

		# Try modifying the filesystem again!
		__fuzz_notify "++ Try to write filesystem again"
		_scratch_fuzz_modify 100 2>&1
		__scratch_xfs_fuzz_unmount
	else
		(>&2 echo "re-mount failed ($res) with ${field} = ${fuzzverb}.")
	fi

	# See if repair finds a clean fs
	echo "+ Re-check the filesystem (offline)"
	_scratch_xfs_repair -n 2>&1
	res=$?
	test $res -ne 0 && \
		(>&2 echo "re-repair failed ($res) with ${field} = ${fuzzverb}.")
}

# Make sure we have all the pieces we need for field fuzzing
_require_scratch_xfs_fuzz_fields()
{
	_require_scratch_nocheck
	_require_scrub
	_require_populate_commands
	_scratch_mkfs_xfs >/dev/null 2>&1
	_require_xfs_db_command "fuzz"
}

# Grab the list of available fuzzing verbs
_scratch_xfs_list_fuzz_verbs() {
	if [ -n "${SCRATCH_XFS_LIST_FUZZ_VERBS}" ]; then
		echo "${SCRATCH_XFS_LIST_FUZZ_VERBS}" | tr '[ ,]' '[\n\n]'
		return;
	fi
	_scratch_xfs_db -x -c 'sb 0' -c 'fuzz' | grep '^Verbs:' | \
		sed -e 's/[,.]//g' -e 's/Verbs: //g' -e 's/ /\n/g'
}

# Fuzz some of the fields of some piece of metadata
# The first argument is an egrep filter for the field names
# The second argument is the repair mode (online, offline, both)
# The rest of the arguments are xfs_db commands to locate the metadata.
#
# Users can specify the fuzz verbs via SCRATCH_XFS_LIST_FUZZ_VERBS
# They can specify the fields via SCRATCH_XFS_LIST_METADATA_FIELDS
_scratch_xfs_fuzz_metadata() {
	filter="$1"
	repair="$2"
	shift; shift

	fields="$(_scratch_xfs_list_metadata_fields "${filter}" "$@")"
	verbs="$(_scratch_xfs_list_fuzz_verbs)"
	echo "Fields we propose to fuzz under: $@"
	echo $(echo "${fields}")
	echo "Verbs we propose to fuzz with:"
	echo $(echo "${verbs}")

	echo "${fields}" | while read field; do
		echo "${verbs}" | while read fuzzverb; do
			__scratch_xfs_fuzz_mdrestore
			__scratch_xfs_fuzz_field_test "${field}" "${fuzzverb}" "${repair}" "$@"
		done
	done
}
