#!/bin/bash

shopt -s extglob

# generated from util-linux source: libmount/src/utils.c
declare -A pseudofs_types=([anon_inodefs]=1
                           [autofs]=1
                           [bdev]=1
                           [binfmt_misc]=1
                           [cgroup]=1
                           [configfs]=1
                           [cpuset]=1
                           [debugfs]=1
                           [devfs]=1
                           [devpts]=1
                           [devtmpfs]=1
                           [dlmfs]=1
                           [fuse.gvfs-fuse-daemon]=1
                           [fusectl]=1
                           [hugetlbfs]=1
                           [mqueue]=1
                           [nfsd]=1
                           [none]=1
                           [pipefs]=1
                           [proc]=1
                           [pstore]=1
                           [ramfs]=1
                           [rootfs]=1
                           [rpc_pipefs]=1
                           [securityfs]=1
                           [sockfs]=1
                           [spufs]=1
                           [sysfs]=1
                           [tmpfs]=1
                           [overlayfs]=1
                           [overlay]=1
                           [iso9660]=1)
# Also remove overlay which points to /deepinhost

# generated from: pkgfile -vbr '/fsck\..+' | awk -F. '{ print $NF }' | sort
declare -A fsck_types=([cramfs]=1
                       [exfat]=1
                       [ext2]=1
                       [ext3]=1
                       [ext4]=1
                       [ext4dev]=1
                       [jfs]=1
                       [minix]=1
                       [msdos]=1
                       [reiserfs]=1
                       [vfat]=1
                       [xfs]=1)

out() { printf "$1 $2\n" "${@:3}"; }
error() { out "==> ERROR:" "$@"; } >&2
msg() { out "==>" "$@"; }
msg2() { out "  ->" "$@";}
die() { error "$@"; exit 1; }

ignore_error() {
  "$@" 2>/dev/null
  return 0
}

track_mount() {
  if [[ -z $CHROOT_ACTIVE_MOUNTS ]]; then
    CHROOT_ACTIVE_MOUNTS=()
    trap 'chroot_umount' EXIT
  fi

  mount "$@" && CHROOT_ACTIVE_MOUNTS=("$2" "${CHROOT_ACTIVE_MOUNTS[@]}")
}

mount_conditionally() {
  local cond=$1; shift
  if eval "$cond"; then
    track_mount "$@"
  fi
}

api_fs_mount() {
  mount_conditionally "! mountpoint -q '$1'" "$1" "$1" --bind &&
  track_mount proc "$1/proc" -t proc -o nosuid,noexec,nodev &&
  track_mount sys "$1/sys" -t sysfs -o nosuid,noexec,nodev,ro &&
  ignore_error mount_conditionally "[[ -d '$1/sys/firmware/efi/efivars' ]]" \
      efivarfs "$1/sys/firmware/efi/efivars" -t efivarfs -o nosuid,noexec,nodev &&
  track_mount udev "$1/dev" -t devtmpfs -o mode=0755,nosuid &&
  track_mount devpts "$1/dev/pts" -t devpts -o mode=0620,gid=5,nosuid,noexec &&
  track_mount shm "$1/dev/shm" -t tmpfs -o mode=1777,nosuid,nodev &&
  track_mount run "$1/run" -t tmpfs -o nosuid,nodev,mode=0755 &&
  track_mount tmp "$1/tmp" -t tmpfs -o mode=1777,strictatime,nodev,nosuid
}

chroot_umount() {
  umount "${CHROOT_ACTIVE_MOUNTS[@]}"
}

valid_number_of_base() {
  local base=$1 len=${#2} i=

  for (( i = 0; i < len; i++ )); do
    { _=$(( $base#${2:i:1} )) || return 1; } 2>/dev/null
  done

  return 0
}

mangle() {
  local i= chr= out=

  unset {a..f} {A..F}

  for (( i = 0; i < ${#1}; i++ )); do
    chr=${1:i:1}
    case $chr in
      [[:space:]\\])
        printf -v chr '%03o' "'$chr"
        out+=\\
        ;;
    esac
    out+=$chr
  done

  printf '%s' "$out"
}

unmangle() {
  local i= chr= out= len=$(( ${#1} - 4 ))

  unset {a..f} {A..F}

  for (( i = 0; i < len; i++ )); do
    chr=${1:i:1}
    case $chr in
      \\)
        if valid_number_of_base 8 "${1:i+1:3}" ||
            valid_number_of_base 16 "${1:i+1:3}"; then
          printf -v chr '%b' "${1:i:4}"
          (( i += 3 ))
        fi
        ;;
    esac
    out+=$chr
  done

  printf '%s' "$out${1:i}"
}

dm_name_for_devnode() {
  read dm_name <"/sys/class/block/${1#/dev/}/dm/name"
  if [[ $dm_name ]]; then
    printf '/dev/mapper/%s' "$dm_name"
  else
    # don't leave the caller hanging, just print the original name
    # along with the failure.
    print '%s' "$1"
    error 'Failed to resolve device mapper name for: %s' "$1"
  fi
}

fstype_is_pseudofs() {
  (( pseudofs_types["$1"] ))
}

fstype_has_fsck() {
  (( fsck_types["$1"] ))
}


write_source() {
  local src=$1 spec= label= uuid= comment=()

  label=$(blkid -s PARTLABEL -o value "$1" 2>/dev/null)
  uuid=$(blkid -s UUID -o value "$1" 2>/dev/null)

  # bind mounts do not have a UUID!

  case $bytag in
    '')
      [[ $uuid ]] && comment=("UUID=$uuid")
      [[ $label ]] && comment+=("LABEL=$(mangle "$label")")
      ;;
    LABEL)
      spec=$label
      [[ $uuid ]] && comment=("$src" "UUID=$uuid")
      ;;
    UUID)
      spec=$uuid
      comment=("$src")
      [[ $label ]] && comment+=("LABEL=$(mangle "$label")")
      ;;
    *)
      [[ $uuid ]] && comment=("$1" "UUID=$uuid")
      [[ $label ]] && comment+=("LABEL=$(mangle "$label")")
      [[ $bytag ]] && spec=$(lsblk -rno "$bytag" "$1" 2>/dev/null)
      ;;
  esac

  [[ $comment ]] && printf '# %s\n' "${comment[*]}"

  if [[ $spec ]]; then
    printf '%-20s' "$bytag=$(mangle "$spec")"
  else
    printf '%-20s' "$(mangle "$src")"
  fi
}

usage() {
  cat <<EOF
usage: ${0##*/} [options] root

  Options:
    -L             Use labels for source identifiers (shortcut for -t LABEL)
    -p             Avoid printing pseudofs mounts
    -t TAG         Use TAG for source identifiers
    -U             Use UUIDs for source identifiers (shortcut for -t UUID)

    -h             Print this help message

genfstab generates output suitable for addition to an fstab file based on the
devices mounted under the mountpoint specified by the given root.

EOF
}

if [[ -z $1 || $1 = @(-h|--help) ]]; then
  usage
  exit $(( $# ? 0 : 1 ))
fi

while getopts ':Lpt:U' flag; do
  case $flag in
    L)
      bytag=LABEL
      ;;
    U)
      bytag=UUID
      ;;
    p)
      nopseudofs=1
      ;;
    t)
      bytag=${OPTARG^^}
      ;;
    :)
      die '%s: option requires an argument -- '\''%s'\' "${0##*/}" "$OPTARG"
      ;;
    ?)
      die '%s: invalid option -- '\''%s'\' "${0##*/}" "$OPTARG"
      ;;
  esac
done
shift $(( OPTIND - 1 ))

(( $# )) || die "No root directory specified"
root=$1; shift

if ! mountpoint -q "$root"; then
  die "$root is not a mountpoint"
fi

# handle block devices
findmnt -Recvruno SOURCE,TARGET,FSTYPE,OPTIONS "$root" |
    while read -r src target fstype opts; do
  if (( nopseudofs )) && fstype_is_pseudofs "$fstype"; then
    continue
  fi

  # default 5th and 6th columns
  dump=0 pass=2

  src=$(unmangle "$src")
  target=$(unmangle "$target")
  target=${target#$root}

  if (( !foundroot )) && findmnt "$src" "$root" >/dev/null; then
    # this is root. we can't possibly have more than one...
    pass=1 foundroot=1
  fi

  # if there's no fsck tool available, then only pass=0 makes sense.
  if ! fstype_has_fsck "$fstype"; then
    pass=0
  fi

  # filesystem quirks
  case $fstype in
    fuseblk)
      # well-behaved FUSE filesystems will report themselves as fuse.$fstype.
      # this is probably NTFS-3g, but let's just make sure.
      if ! newtype=$(lsblk -no FSTYPE "$src") || [[ -z $newtype ]]; then
        # avoid blanking out fstype, leading to an invalid fstab
        error 'Failed to derive real filesystem type for FUSE device on %s' "$target"
      else
        fstype=$newtype
      fi
      ;;
  esac

  # write one line
  write_source "$src"
  printf '\t%-10s' "/$(mangle "${target#/}")" "$fstype" "$opts"
  printf '\t%s %s' "$dump" "$pass"
  printf '\n\n'
done

# handle swaps devices
{
  # ignore header
  read

  while read -r device type _ _ prio; do
    options=defaults
    if [[ $prio != -1 ]]; then
      options+=,pri=$prio
    fi

    # skip files marked deleted by the kernel
    [[ $device = *'\040(deleted)' ]] && continue

    if [[ $type = file ]]; then
      printf '%-20s' "$device"
    elif [[ $device = /dev/dm-+([0-9]) ]]; then
      # device mapper doesn't allow characters we need to worry
      # about being mangled, and it does the escaping of dashes
      # for us in sysfs.
      write_source "$(dm_name_for_devnode "$device")"
    else
      write_source "$(unmangle "$device")"
    fi

    printf '\t%-10s\t%-10s\t%-10s\t0 0\n\n' 'none' 'swap' "$options"
  done
} </proc/swaps

# vim: et ts=2 sw=2 ft=sh:
