#!/bin/bash

die() {
	printf "$PROG: %s\n" "$*" >&2
	exit 1
}

usage() {
	cat <<-EOH
	Usage: $PROG [OPTIONS] <root>

	Generates an fstab(5) for a given root.

	OPTIONS
	  -p      Exclude pseudo-filesystems (default behavior)
	  -P      Include pseudo-filesystems
	  -t TAG  Use TAG for identifiers (LABEL, UUID, PARTLABEL, PARTUUID)
	  -L      Use labels for identifiers (same as -t LABEL)
	  -U      Use UUIDs for identifiers (same as -t UUID)
	  -h      Print this message
	EOH
}

write_header() {
	cat <<- EOF
	# Generated by xgenfstab(1).
	#
	# See fstab(5).
	#
	# <file system>       	<dir>         	<type>    	<options> 	<dump> <pass>
	EOF
}

write_line() {
	local source="$1" target="$2" fstype="$3" options="$4" passno="$5"
	printf '%-20s\t%-15s\t%-10s\t%-10s\t0 %d\n' "$source" "$target" "$fstype" "$options" "$passno"
}

array_contains() {
	local -n array="$1"
	printf '%s\0' "${array[@]}" | grep -Fxqz "$2"
}

is_pseudofs() {
	array_contains PSEUDO_FS "$1"
}

is_fsckable() {
	array_contains FSCK_FS "$1"
}

mapper_name() {
	local name
	read -r name < "/sys/class/block/${1#/dev/}/dm/name"
	if [ -n "$name" ]; then
		echo "/dev/mapper/$name"
	fi
}

dev_name() {
	local name
	if [ -n "$TAG" ]; then
		name="$(lsblk -nro "$TAG" "$1")"
	fi

	if [ -n "$name" ]; then
		echo "$TAG=$name"
	else
		echo "$1"
	fi
}

clean_mntopts() {
	local fstype="$1" firstopt=1 opt
	local -a mntopts
	IFS=',' read -r -a mntopts <<< "$2"

	for opt in "${mntopts[@]}"; do
		case "$opt" in
			relatime) continue ;; # default
			seclabel) continue ;; # might not be supported
		esac

		case "$fstype" in
			btrfs)
				case "$opt" in
					subvolid=*)
						# having both subvol= and subvolid= prevents some btrfs tools from working
						if [[ "${mntopts[*]}" = *' subvol='* ]]; then
							continue
						fi
						;;
				esac
				;;
			f2fs)
				case "$opt" in
					acl|noacl|user_xattr|nouserxattr) continue ;; # controlled by kconfig
					atgc) continue ;; # requires kernel cmdline parameters to remount
				esac
				;;
		esac

		[ -n "$firstopt" ] && firstopt='' || printf ','
		printf '%s' "$opt"
	done
}

PROG="${0##*/}"
INC_PSEUDO=
TAG=

if [ "$#" -eq 0 ]; then
	usage >&2
	exit 1
fi

while getopts "pPLUt:h" flag; do
	case "$flag" in
		p) INC_PSEUDO= ;;
		P) INC_PSEUDO=1 ;;
		L) TAG=LABEL ;;
		U) TAG=UUID ;;
		t) TAG="${OPTARG^^}" ;;
		h) usage; exit 0 ;;
		*) usage >&2; exit 1 ;;
	esac
done

shift $(( OPTIND - 1 ))

if ! command -v mountpoint &>/dev/null; then
	die "cannot find program mountpoint"
fi

if ! command -v findmnt &>/dev/null; then
	die "cannot find program findmnt"
fi

ROOT="$1"

[ -n "$ROOT" ] || die "missing root argument"
mountpoint -q "$ROOT" > /dev/null 2>&1 || die "given root is not a mountpoint"

# make absolute and canonical once we know it exists
ROOT="$(realpath "$ROOT")"

# known pseudo-filesystems
# used in is_pseudofs via nameref
# shellcheck disable=SC2034
mapfile -t PSEUDO_FS < <(findmnt --pseudo -Uno fstype | sort -u)

# known fsckable filesystems
# used in is_fsckable via nameref
# shellcheck disable=SC2034
FSCK_FS=(cramfs exfat ext2 ext3 ext4 jfs minix msdos reiserfs vfat xfs)

write_header

findmnt -Rcenruv -o source,target,fstype,fsroot,options "$ROOT" | \
	while read -r source target fstype fsroot options; do
		if [ -z "$INC_PSEUDO" ] && is_pseudofs "$fstype"; then
			continue
		fi

		# filetypes that should probably never be in fstab
		case "$fstype" in
			autofs|binfmt_misc|fuseblk|fuse-overlayfs|zfs) continue ;;
		esac

		if [ "$ROOT" != '/' ]; then
			if [ "$target" = "$ROOT" ]; then
				target='/'
			else
				target="${target#"$ROOT"}"
			fi
		fi

		case "$target" in
			/run/*) continue ;;
			/) passno=1 ;;
			*) passno=2 ;;
		esac

		if ! is_fsckable "$fstype"; then
			passno=0
		fi

		# exclude bind mounts
		if [ "$fsroot" != '/' ] && [ "$fstype" != 'btrfs' ]; then
			continue
		fi

		options="$(clean_mntopts "$fstype" "$options")"

		source="$(dev_name "$source")"
		write_line "$source" "$target" "$fstype" "$options" "$passno"
	done

# add swap
{
	# skip header
	read -r

	while read -r dev _type _sz _used priority; do
		options='defaults'

		if [ "$priority" -ge 0 ]; then
			opts+=",$priority"
		fi

		case "$dev" in
			*'(deleted)') continue ;; # deleted by kernel
			/dev/dm-*)
				dev="$(mapper_name "$dev")"
				if [ -z "$dev" ]; then
					die "could not find device mapper name of $dev"
				fi
				;;
			/dev/zram*) continue ;; # may not be available at mount -a; should be added manually if using e.g. udev to mkswap
			*)
				if [ -f "$dev" ]; then
					if [ "$ROOT" != '/' ]; then
						dev="${dev#"$ROOT"}"
					fi
				fi
				;;
		esac

		dev="$(dev_name "$dev")"
		write_line "$dev" none swap "$options" 0
	done
} < /proc/swaps

if [ -z "$INC_PSEUDO" ]; then
	# add /tmp
	write_line tmpfs /tmp tmpfs defaults,nosuid,nodev 0
fi
